@hew-lang/sandbox-vm 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +43 -4
  2. package/cli/test-runner.mjs +4 -1
  3. package/dist/interpreter/interpreter.d.ts +1 -1
  4. package/dist/interpreter/interpreter.js +1 -1
  5. package/dist/interpreter/parity-runner.js +26 -1
  6. package/dist/interpreter/run-program.d.ts +3 -2
  7. package/dist/interpreter/run-program.js +5 -3
  8. package/dist/interpreter/schema-validator.d.ts +9 -4
  9. package/dist/interpreter/schema-validator.js +15 -49
  10. package/dist/interpreter/types.d.ts +4 -0
  11. package/dist/interpreter/v1/browser-directory.d.ts +107 -0
  12. package/dist/interpreter/v1/browser-directory.js +288 -0
  13. package/dist/interpreter/v1/calendar.d.ts +4 -0
  14. package/dist/interpreter/v1/calendar.js +64 -0
  15. package/dist/interpreter/v1/codecs.js +2 -2
  16. package/dist/interpreter/v1/exec.d.ts +2 -2
  17. package/dist/interpreter/v1/exec.js +439 -183
  18. package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
  19. package/dist/interpreter/v1/instructions.generated.js +106 -0
  20. package/dist/interpreter/v1/package-graph.d.ts +2 -0
  21. package/dist/interpreter/v1/package-graph.js +729 -0
  22. package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
  23. package/dist/interpreter/v1/package-validator.generated.js +75846 -0
  24. package/dist/interpreter/v1/package.d.ts +5 -680
  25. package/dist/interpreter/v1/package.js +0 -12
  26. package/dist/interpreter/v1/scheduler.d.ts +5 -1
  27. package/dist/interpreter/v1/scheduler.js +32 -3
  28. package/dist/interpreter/v1/shims.d.ts +250 -7
  29. package/dist/interpreter/v1/shims.js +429 -150
  30. package/dist/interpreter/v1/structural.d.ts +2 -28
  31. package/dist/interpreter/v1/timestamp.d.ts +7 -0
  32. package/dist/interpreter/v1/timestamp.js +54 -0
  33. package/dist/interpreter/v1/validate.d.ts +8 -2
  34. package/dist/interpreter/v1/validate.js +190 -202
  35. package/dist/interpreter/v1/value-operations.d.ts +7 -2
  36. package/dist/interpreter/v1/value-operations.js +13 -6
  37. package/dist/interpreter/values.d.ts +4 -0
  38. package/dist/interpreter/values.js +6 -0
  39. package/dist/interpreter/worker.js +4 -3
  40. package/package.json +4 -4
package/README.md CHANGED
@@ -5,8 +5,9 @@
5
5
  borrows, cleanup, checked failures, actor protocols and suspension edges.
6
6
 
7
7
  Actors, tasks and pipes use resumable frames on a deterministic scheduler.
8
- Timers advance virtual time. Native filesystem and network operations are
9
- refused before execution; other missing operations are reported separately as
8
+ Timers advance virtual time. Hosts can supply real origin-private filesystem
9
+ (OPFS) directory handles; network and ambient filesystem operations are refused
10
+ before execution. Other missing operations are reported separately as
10
11
  `not_implemented`. The loader is the capability admission authority.
11
12
 
12
13
  ## Browser API
@@ -17,16 +18,43 @@ import { runBytecode } from "@hew-lang/sandbox-vm";
17
18
 
18
19
  const { diagnostics, bytecode } = JSON.parse(compileToSandboxBytecode(source));
19
20
  if (bytecode) {
20
- const trace = runBytecode(bytecode, { replay: { step_budget: 100000 } });
21
+ const trace = await runBytecode(bytecode, {
22
+ replay: { step_budget: 100000 },
23
+ });
21
24
  }
22
25
  ```
23
26
 
24
27
  Initialize the Wasm compiler using its generated default export before calling
25
28
  it. `runProgram(source, stdin)` is also available for existing page bridges;
26
- install `globalThis.compileToSandboxBytecode` first. Its result includes
29
+ install `globalThis.compileToSandboxBytecode` first. Both entrypoints return a Promise. The page result includes
27
30
  `stdout`, `exit_code`, `diagnostics`, `status`, `sandbox_rejections`,
28
31
  `compiler_version` and `hew_version`.
29
32
 
33
+ To grant filesystem authority, pass only the directories the program needs:
34
+
35
+ ```js
36
+ const trace = await runBytecode(bytecode, {
37
+ directoryRoots: { read: inputDirectory, write: outputDirectory },
38
+ signal: controller.signal,
39
+ });
40
+ ```
41
+
42
+ Both roots are optional and default to no authority. The handles must belong to
43
+ this origin's OPFS; external picker-backed handles are refused because their
44
+ symlink confinement is not standardized. Existing Hew `fs.open_read_dir(".")`
45
+ and `fs.open_write_dir(".")` acquire the supplied roots. Relative traversal stays
46
+ beneath the current directory owner, including after narrowing with `subdir`.
47
+ Children remain usable after closing a parent. Each `file.write` publishes its
48
+ bytes before it returns, even while the file owner remains open.
49
+
50
+ `AbortSignal` requests cancellation through the source's authored cleanup edges.
51
+ Filesystem acquisition publishes its owner before observing cancellation, as on
52
+ native hosts. An in-progress file operation drains or aborts its browser writer
53
+ before cancellation releases its inputs. Pure execution retains the deterministic
54
+ virtual scheduler and runs until a host wait or completion. Pass the same options
55
+ as the fourth argument to `runProgram(source, stdin, files, options)`; signals are
56
+ for direct calls rather than worker message payloads.
57
+
30
58
  A `sandbox_rejected` result includes rejection entries with `category`, `code`,
31
59
  `capability`, `message` and `span`. Categories are `native_only`,
32
60
  `not_implemented` and `invalid_package`. Compiler errors, language panics,
@@ -48,6 +76,13 @@ The [package contract](bytecode/package-v1.md) describes the SIR projection.
48
76
  Source fixtures are compiled by the public compiler and executed to record
49
77
  deterministic traces. Tests also compare executable sources with native Hew.
50
78
 
79
+ With `hew-wasm/pkg` and `hew-sandbox-vm/dist` built, serve the repository on
80
+ localhost and open `hew-sandbox-vm/test/browser/directory-handles.html` to run the
81
+ real browser filesystem controls. They compile the ordinary Hew source fixture,
82
+ use a temporary OPFS directory, verify disk contents, confinement and cancellation,
83
+ then remove their entries. The page accepts a `compiler` module URL query parameter
84
+ to reuse another built compiler package.
85
+
51
86
  `RunOptions.argv` supplies process arguments, including the executable name at
52
87
  index zero. It defaults to `["main.hew"]` and is copied for each run. For example,
53
88
  `runBytecode(bytecode, { argv: ["greet", "--", "Zoë", ""] })` supplies those exact
@@ -58,3 +93,7 @@ Pass its initialized `executeCodec` export as `runBytecode`'s `codecBridge`
58
93
  option. The Node test host loads that package automatically; in a source
59
94
  checkout, run `make wasm` to build `hew-wasm/pkg` first. An explicit
60
95
  `HEW_WASM_MODULE` module URL can select another build of the same package.
96
+
97
+ UTC calendar operations also require the initialized `executeCalendar`
98
+ export as `calendarBridge`. Both hosts use the shared Chrono calendar engine;
99
+ the bridge preserves integer seconds and nanoseconds exactly.
@@ -22,6 +22,7 @@ try {
22
22
  const args = parseArgs(process.argv.slice(2));
23
23
  const packageValue = JSON.parse(fs.readFileSync(args.path, "utf8"));
24
24
  let codecBridge;
25
+ let calendarBridge;
25
26
  if (packageValue.codecs?.length) {
26
27
  const local = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../hew-wasm/pkg/hew_wasm.js");
27
28
  const location = process.env.HEW_WASM_MODULE ?? (fs.existsSync(local) ? pathToFileURL(local).href : "@hew-lang/wasm");
@@ -31,11 +32,13 @@ try {
31
32
  await wasm.default({ module_or_path: fs.readFileSync(new URL(resolved.replace(/\.js$/, "_bg.wasm"))) });
32
33
  }
33
34
  codecBridge = wasm.executeCodec ?? wasm.default?.executeCodec;
35
+ calendarBridge = wasm.executeCalendar ?? wasm.default?.executeCalendar;
34
36
  if (typeof codecBridge !== "function") throw new Error("hew-wasm executeCodec export is missing; rebuild the Wasm package");
35
37
  }
36
38
  const propertyPath = reportPath ? path.join(path.dirname(reportPath), "property.cbor") : null;
37
- const trace = runBytecode(packageValue, {
39
+ const trace = await runBytecode(packageValue, {
38
40
  codecBridge,
41
+ calendarBridge,
39
42
  propertyInput: propertyPath && fs.existsSync(propertyPath) ? [...fs.readFileSync(propertyPath)] : [],
40
43
  fixtureId: "hew-test",
41
44
  traceId: "trace:hew-test",
@@ -1,2 +1,2 @@
1
1
  import type { RunOptions, SandboxTrace } from "./types.js";
2
- export declare function runBytecode(input: unknown, options?: RunOptions): SandboxTrace;
2
+ export declare function runBytecode(input: unknown, options?: RunOptions): Promise<SandboxTrace>;
@@ -1,7 +1,7 @@
1
1
  import { validateBytecodePackage } from "./schema-validator.js";
2
2
  import { runPackageV1 } from "./v1/exec.js";
3
3
  import { TraceBuilder } from "./trace.js";
4
- export function runBytecode(input, options = {}) {
4
+ export async function runBytecode(input, options = {}) {
5
5
  let bytecode;
6
6
  try {
7
7
  bytecode = validateBytecodePackage(input);
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import process from "node:process";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
5
  import { runBytecode } from "./index.js";
4
6
  const DEFAULT_SEED = 42;
5
7
  const DEFAULT_STEP_BUDGET = 1_000_000;
@@ -45,7 +47,30 @@ function parseArgs(argv) {
45
47
  try {
46
48
  const args = parseArgs(process.argv.slice(2));
47
49
  const bytecode = JSON.parse(fs.readFileSync(args.bytecodePath, "utf8"));
48
- const trace = runBytecode(bytecode, {
50
+ let codecBridge;
51
+ if (typeof bytecode === "object" &&
52
+ bytecode !== null &&
53
+ "codecs" in bytecode &&
54
+ Array.isArray(bytecode.codecs) &&
55
+ bytecode.codecs.length) {
56
+ const local = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../hew-wasm/pkg/hew_wasm.js");
57
+ const location = process.env.HEW_WASM_MODULE ?? pathToFileURL(local).href;
58
+ const wasm = await import(location);
59
+ if (typeof wasm.default === "function") {
60
+ const resolved = location.startsWith("file:")
61
+ ? location
62
+ : import.meta.resolve(location);
63
+ await wasm.default({
64
+ module_or_path: fs.readFileSync(new URL(resolved.replace(/\.js$/, "_bg.wasm"))),
65
+ });
66
+ }
67
+ codecBridge = wasm.executeCodec ?? wasm.default?.executeCodec;
68
+ if (typeof codecBridge !== "function") {
69
+ throw new Error("hew-wasm executeCodec export is missing; supply a compatible HEW_WASM_MODULE");
70
+ }
71
+ }
72
+ const trace = await runBytecode(bytecode, {
73
+ codecBridge,
49
74
  replay: {
50
75
  seed: args.seed,
51
76
  step_budget: DEFAULT_STEP_BUDGET,
@@ -1,5 +1,5 @@
1
1
  import type { PackageV1 } from "./v1/package.js";
2
- import type { JsonValue, RuntimeStatus, SandboxRejection } from "./types.js";
2
+ import type { RunOptions, JsonValue, RuntimeStatus, SandboxRejection } from "./types.js";
3
3
  export interface Diagnostic {
4
4
  severity: string;
5
5
  phase: string;
@@ -24,7 +24,8 @@ type SandboxCompiler = (source: string, profile?: string, files?: Map<string, st
24
24
  declare global {
25
25
  var __hewSandboxCompileToSandboxBytecode: SandboxCompiler | undefined;
26
26
  var __hewSandboxExecuteCodec: ((request: string) => string) | undefined;
27
+ var __hewSandboxExecuteCalendar: ((request: string) => string) | undefined;
27
28
  var compileToSandboxBytecode: SandboxCompiler | undefined;
28
29
  }
29
- export declare function runProgram(source: string, stdin: string, files?: Map<string, string | Uint8Array>): RunProgramResult;
30
+ export declare function runProgram(source: string, stdin: string, files?: Map<string, string | Uint8Array>, options?: RunOptions): Promise<RunProgramResult>;
30
31
  export {};
@@ -1,6 +1,6 @@
1
1
  import { runBytecode } from "./interpreter.js";
2
2
  const SANDBOX_PROFILE = "sandbox-vm-export";
3
- export function runProgram(source, stdin, files) {
3
+ export async function runProgram(source, stdin, files, options = {}) {
4
4
  const compileOutput = compileSource(source, files);
5
5
  if (hasErrorDiagnostics(compileOutput.diagnostics) ||
6
6
  compileOutput.bytecode === null) {
@@ -15,9 +15,11 @@ export function runProgram(source, stdin, files) {
15
15
  };
16
16
  }
17
17
  const bytecode = compileOutput.bytecode;
18
- const trace = runBytecode(bytecode, {
18
+ const trace = await runBytecode(bytecode, {
19
+ ...options,
19
20
  fixtureId: "page-run",
20
- codecBridge: globalThis.__hewSandboxExecuteCodec,
21
+ codecBridge: options.codecBridge ?? globalThis.__hewSandboxExecuteCodec,
22
+ calendarBridge: options.calendarBridge ?? globalThis.__hewSandboxExecuteCalendar,
21
23
  traceId: "trace:page-run",
22
24
  replay: { inputs: [{ kind: "stdin", data: stdin }] },
23
25
  });
@@ -1,8 +1,13 @@
1
- import { type PackageV1 } from "./v1/package.js";
1
+ import type { PackageV1 } from "./v1/package.js";
2
+ declare const checkedPackage: unique symbol;
3
+ export type CheckedPackage = PackageV1 & {
4
+ readonly [checkedPackage]: true;
5
+ };
2
6
  export declare class BytecodeValidationError extends Error {
3
7
  readonly errors: string[];
4
8
  constructor(errors: string[]);
5
9
  }
6
- /** Check the package envelope before capability admission traverses its tables.
7
- * Operation support belongs to v1/validate, with one shared executor registry. */
8
- export declare function validateBytecodePackage(input: unknown): PackageV1;
10
+ /** Decode an isolated package before capability admission or any host effect.
11
+ * Cloning prevents a caller or host callback from changing the checked input. */
12
+ export declare function validateBytecodePackage(input: unknown): CheckedPackage;
13
+ export {};
@@ -1,4 +1,5 @@
1
- import { isPackageV1 } from "./v1/package.js";
1
+ import { validPackage } from "./v1/package-validator.generated.js";
2
+ import { validatePackageGraph } from "./v1/package-graph.js";
2
3
  export class BytecodeValidationError extends Error {
3
4
  errors;
4
5
  constructor(errors) {
@@ -7,55 +8,20 @@ export class BytecodeValidationError extends Error {
7
8
  this.name = "BytecodeValidationError";
8
9
  }
9
10
  }
10
- /** Check the package envelope before capability admission traverses its tables.
11
- * Operation support belongs to v1/validate, with one shared executor registry. */
11
+ /** Decode an isolated package before capability admission or any host effect.
12
+ * Cloning prevents a caller or host callback from changing the checked input. */
12
13
  export function validateBytecodePackage(input) {
13
- if (!isPackageV1(input))
14
- throw new BytecodeValidationError(["expected hew.sandbox.bytecode.v1"]);
15
- const errors = [];
16
- for (const key of ["profile", "hew_version", "compiler_version"]) {
17
- if (typeof input[key] !== "string")
18
- errors.push(`${key} must be a string`);
19
- }
20
- for (const key of [
21
- "functions",
22
- "strings",
23
- "bytes",
24
- "regex_patterns",
25
- "aggregates",
26
- "variants",
27
- "runtime_families",
28
- "externs",
29
- "suspend_kinds",
30
- "value_capabilities",
31
- "closures",
32
- "vtables",
33
- ]) {
34
- if (!Array.isArray(input[key]))
35
- errors.push(`${key} must be an array`);
36
- }
37
- if (errors.length)
38
- throw new BytecodeValidationError(errors);
39
- for (const fn of input.functions) {
40
- if (!fn ||
41
- !Number.isInteger(fn.id) ||
42
- !Array.isArray(fn.params) ||
43
- !Array.isArray(fn.places) ||
44
- !Array.isArray(fn.blocks)) {
45
- errors.push("malformed function");
46
- continue;
47
- }
48
- for (const block of fn.blocks) {
49
- if (!block ||
50
- !Number.isInteger(block.id) ||
51
- !Array.isArray(block.params) ||
52
- !Array.isArray(block.ops) ||
53
- !block.term ||
54
- typeof block.term.op !== "string")
55
- errors.push("malformed block");
14
+ try {
15
+ const decoded = structuredClone(input);
16
+ if (!validPackage(decoded)) {
17
+ const reasons = (validPackage.errors ?? []).map((issue) => `${issue.instancePath || "/"} ${issue.message ?? "invalid value"}`);
18
+ throw new Error(reasons.join("; ") || "malformed sandbox bytecode package");
56
19
  }
20
+ const pkg = decoded;
21
+ validatePackageGraph(pkg);
22
+ return pkg;
23
+ }
24
+ catch (error) {
25
+ throw new BytecodeValidationError([String(error)]);
57
26
  }
58
- if (errors.length)
59
- throw new BytecodeValidationError(errors);
60
- return input;
61
27
  }
@@ -117,9 +117,13 @@ export interface SandboxTrace {
117
117
  };
118
118
  }
119
119
  export interface RunOptions {
120
+ /** Explicit browser filesystem authority; absent roots grant no access. */
121
+ directoryRoots?: import("./v1/browser-directory.js").DirectoryRoots;
122
+ signal?: AbortSignal;
120
123
  /** Supplied process arguments including argv[0]. Defaults to ["main.hew"]. */
121
124
  argv?: readonly string[];
122
125
  codecBridge?: (request: string) => string;
126
+ calendarBridge?: (request: string) => string;
123
127
  propertyInput?: number[];
124
128
  fixtureId?: string;
125
129
  traceId?: string;
@@ -0,0 +1,107 @@
1
+ import { type VmValue } from "../values.js";
2
+ /** Authority comes only from the embedding host, never from a path or cwd. */
3
+ export interface DirectoryRoots {
4
+ read?: FileSystemDirectoryHandle;
5
+ write?: FileSystemDirectoryHandle;
6
+ }
7
+ export type BrowserFileOwner = {
8
+ access: "read" | "write";
9
+ handle: FileSystemFileHandle | null;
10
+ cursor: number;
11
+ closed: boolean;
12
+ tail: Promise<void>;
13
+ };
14
+ export type BrowserDirectoryOwner = {
15
+ access: "read" | "write";
16
+ handle: FileSystemDirectoryHandle | null;
17
+ closed: boolean;
18
+ };
19
+ export type BrowserFsOwner = BrowserFileOwner | BrowserDirectoryOwner;
20
+ export interface BrowserIoError {
21
+ kind: number;
22
+ message: string;
23
+ }
24
+ export interface BrowserIoResult {
25
+ value: VmValue;
26
+ error: BrowserIoError | null;
27
+ }
28
+ export declare const DIRECTORY_OPERATIONS: {
29
+ readonly hew_dir_open_read: {
30
+ readonly release: "hew_dir_read_close";
31
+ readonly action: "root";
32
+ readonly access: "read";
33
+ };
34
+ readonly hew_dir_open_write: {
35
+ readonly release: "hew_dir_write_close";
36
+ readonly action: "root";
37
+ readonly access: "write";
38
+ };
39
+ readonly hew_dir_read_subdir: {
40
+ readonly release: "hew_dir_read_close";
41
+ readonly action: "subdir";
42
+ readonly access: "read";
43
+ };
44
+ readonly hew_dir_write_subdir: {
45
+ readonly release: "hew_dir_write_close";
46
+ readonly action: "subdir";
47
+ readonly access: "write";
48
+ };
49
+ readonly hew_dir_read_open_file: {
50
+ readonly release: "hew_file_read_close";
51
+ readonly action: "file";
52
+ readonly access: "read";
53
+ };
54
+ readonly hew_dir_write_open_file: {
55
+ readonly release: "hew_file_write_close";
56
+ readonly action: "file";
57
+ readonly access: "write";
58
+ };
59
+ readonly hew_dir_read_is_valid: {
60
+ readonly action: "valid";
61
+ readonly access: "read";
62
+ };
63
+ readonly hew_dir_write_is_valid: {
64
+ readonly action: "valid";
65
+ readonly access: "write";
66
+ };
67
+ readonly hew_file_read_is_valid: {
68
+ readonly action: "valid";
69
+ readonly access: "read";
70
+ };
71
+ readonly hew_file_write_is_valid: {
72
+ readonly action: "valid";
73
+ readonly access: "write";
74
+ };
75
+ readonly hew_dir_read_close: {
76
+ readonly action: "close";
77
+ readonly access: "read";
78
+ };
79
+ readonly hew_dir_write_close: {
80
+ readonly action: "close";
81
+ readonly access: "write";
82
+ };
83
+ readonly hew_file_read_close: {
84
+ readonly action: "close";
85
+ readonly access: "read";
86
+ };
87
+ readonly hew_file_write_close: {
88
+ readonly action: "close";
89
+ readonly access: "write";
90
+ };
91
+ };
92
+ export type DirectoryOperation = (typeof DIRECTORY_OPERATIONS)[keyof typeof DIRECTORY_OPERATIONS];
93
+ export declare class BrowserDirectories {
94
+ private readonly roots;
95
+ private readonly owners;
96
+ retire(release: (value: Extract<VmValue, {
97
+ kind: "host_resource";
98
+ }>) => void): void;
99
+ /** OPFS has no symlink entries. Picker-backed filesystem link confinement is
100
+ * not standardized, so this adapter refuses those handles before execution. */
101
+ static admission(roots: DirectoryRoots | undefined): Promise<string | null>;
102
+ constructor(roots?: DirectoryRoots);
103
+ call(operation: DirectoryOperation, args: VmValue[], resource?: number | null): VmValue | Promise<BrowserIoResult>;
104
+ private acquire;
105
+ private owned;
106
+ io(operation: "FileRead" | "FileWrite", args: VmValue[], cancelled: () => boolean): Promise<BrowserIoResult>;
107
+ }