@hew-lang/sandbox-vm 0.6.1 → 0.6.3

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 (52) hide show
  1. package/README.md +48 -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 +1 -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/strings.d.ts +21 -0
  11. package/dist/interpreter/strings.js +191 -0
  12. package/dist/interpreter/types.d.ts +6 -0
  13. package/dist/interpreter/unicode-data.d.ts +4 -0
  14. package/dist/interpreter/unicode-data.js +1673 -0
  15. package/dist/interpreter/unicode.d.ts +3 -0
  16. package/dist/interpreter/unicode.js +35 -0
  17. package/dist/interpreter/v1/actor-identity.d.ts +3 -0
  18. package/dist/interpreter/v1/actor-identity.js +7 -0
  19. package/dist/interpreter/v1/actor-waits.d.ts +6 -0
  20. package/dist/interpreter/v1/actor-waits.js +33 -0
  21. package/dist/interpreter/v1/browser-directory.d.ts +107 -0
  22. package/dist/interpreter/v1/browser-directory.js +288 -0
  23. package/dist/interpreter/v1/calendar.d.ts +4 -0
  24. package/dist/interpreter/v1/calendar.js +64 -0
  25. package/dist/interpreter/v1/codecs.js +8 -11
  26. package/dist/interpreter/v1/exec.d.ts +2 -2
  27. package/dist/interpreter/v1/exec.js +1761 -489
  28. package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
  29. package/dist/interpreter/v1/instructions.generated.js +106 -0
  30. package/dist/interpreter/v1/package-graph.d.ts +2 -0
  31. package/dist/interpreter/v1/package-graph.js +729 -0
  32. package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
  33. package/dist/interpreter/v1/package-validator.generated.js +75902 -0
  34. package/dist/interpreter/v1/package.d.ts +5 -645
  35. package/dist/interpreter/v1/package.js +0 -12
  36. package/dist/interpreter/v1/pipes.d.ts +1 -0
  37. package/dist/interpreter/v1/pipes.js +9 -6
  38. package/dist/interpreter/v1/scheduler.d.ts +8 -1
  39. package/dist/interpreter/v1/scheduler.js +50 -11
  40. package/dist/interpreter/v1/shims.d.ts +260 -7
  41. package/dist/interpreter/v1/shims.js +647 -175
  42. package/dist/interpreter/v1/structural.d.ts +2 -28
  43. package/dist/interpreter/v1/timestamp.d.ts +7 -0
  44. package/dist/interpreter/v1/timestamp.js +54 -0
  45. package/dist/interpreter/v1/validate.d.ts +8 -2
  46. package/dist/interpreter/v1/validate.js +284 -200
  47. package/dist/interpreter/v1/value-operations.d.ts +11 -3
  48. package/dist/interpreter/v1/value-operations.js +70 -18
  49. package/dist/interpreter/values.d.ts +30 -8
  50. package/dist/interpreter/values.js +31 -5
  51. package/dist/interpreter/worker.js +4 -3
  52. package/package.json +4 -4
@@ -1,31 +1,6 @@
1
1
  import { type VmValue } from "../values.js";
2
- interface Field {
3
- name: string;
4
- recipe: number;
5
- }
6
- export type StructuralRecipe = {
7
- kind: "scalar" | "unit";
8
- } | {
9
- kind: "display";
10
- callee: number;
11
- } | {
12
- kind: "identity";
13
- name: string;
14
- } | {
15
- kind: "tuple" | "vector" | "map";
16
- members: number[];
17
- } | {
18
- kind: "record";
19
- name: string;
20
- fields: Field[];
21
- } | {
22
- kind: "enum";
23
- cases: Array<{
24
- name: string;
25
- kind: "unit" | "tuple" | "struct";
26
- fields: Field[];
27
- }>;
28
- };
2
+ import type { StructuralRecipe } from "./package.js";
3
+ export type { StructuralRecipe } from "./package.js";
29
4
  /** A borrowed traversal. Display callbacks can suspend, so their callers retain
30
5
  * this stack and resume it with the returned string. No later sibling is read
31
6
  * after a callback fails. */
@@ -41,4 +16,3 @@ export declare class StructuralRenderer {
41
16
  };
42
17
  private sequence;
43
18
  }
44
- export {};
@@ -0,0 +1,7 @@
1
+ import type { VmValue } from "../values.js";
2
+ import type { PackageV1, TermV1 } from "./package.js";
3
+ type TimestampBinary = Extract<TermV1, {
4
+ op: "timestamp.binary";
5
+ }>;
6
+ export declare function timestampBinary(pkg: PackageV1, term: TimestampBinary, lhs: VmValue, rhs: VmValue): VmValue | null;
7
+ export {};
@@ -0,0 +1,54 @@
1
+ const BILLION = 1000000000n;
2
+ const MIN_I64 = -(1n << 63n);
3
+ const MAX_I64 = (1n << 63n) - 1n;
4
+ function exactNanos(pkg, value, timestamp) {
5
+ if (!timestamp) {
6
+ if (value.kind !== "i64" || value.value < MIN_I64 || value.value > MAX_I64)
7
+ throw new TypeError("timestamp offset is not a duration");
8
+ return value.value;
9
+ }
10
+ if (value.kind !== "record" ||
11
+ value.fields.length !== 2 ||
12
+ value.shape === undefined ||
13
+ pkg.aggregates[value.shape]?.builtin !== "Timestamp")
14
+ throw new TypeError("timestamp lacks its normalized pair");
15
+ const [seconds, nanos] = value.fields;
16
+ if (seconds?.kind !== "i64" ||
17
+ nanos?.kind !== "i64" ||
18
+ seconds.value < MIN_I64 ||
19
+ seconds.value > MAX_I64 ||
20
+ nanos.value < 0n ||
21
+ nanos.value >= BILLION)
22
+ throw new TypeError("invalid normalized timestamp");
23
+ return seconds.value * BILLION + nanos.value;
24
+ }
25
+ export function timestampBinary(pkg, term, lhs, rhs) {
26
+ const left = exactNanos(pkg, lhs, term.lhs_timestamp);
27
+ const right = exactNanos(pkg, rhs, term.rhs_timestamp);
28
+ const exact = term.binary_op === "Add" ? left + right : left - right;
29
+ if (term.shape === null) {
30
+ return exact < MIN_I64 || exact > MAX_I64
31
+ ? null
32
+ : { kind: "i64", value: exact };
33
+ }
34
+ if (exact < MIN_I64 * BILLION || exact > MAX_I64 * BILLION + BILLION - 1n)
35
+ return null;
36
+ let seconds = exact / BILLION;
37
+ let nanos = exact % BILLION;
38
+ if (nanos < 0n) {
39
+ seconds -= 1n;
40
+ nanos += BILLION;
41
+ }
42
+ const shape = pkg.aggregates[term.shape];
43
+ if (!shape)
44
+ throw new TypeError("timestamp result shape is absent");
45
+ return {
46
+ kind: "record",
47
+ typeId: shape.name,
48
+ shape: term.shape,
49
+ fields: [
50
+ { kind: "i64", value: seconds },
51
+ { kind: "i64", value: nanos },
52
+ ],
53
+ };
54
+ }
@@ -1,3 +1,9 @@
1
1
  import type { SandboxRejection } from "../types.js";
2
- import type { PackageV1 } from "./package.js";
3
- export declare function admitPackage(pkg: PackageV1): SandboxRejection | null;
2
+ import type { CheckedPackage } from "../schema-validator.js";
3
+ import { type HostBridges } from "./shims.js";
4
+ export declare class AdmittedPackage {
5
+ readonly bytecode: CheckedPackage;
6
+ private constructor();
7
+ static load(pkg: CheckedPackage, bridges: HostBridges): AdmittedPackage | SandboxRejection;
8
+ }
9
+ export declare function admitPackage(pkg: CheckedPackage, bridges?: HostBridges): SandboxRejection | null;
@@ -1,111 +1,17 @@
1
- /// Load-time admission for a verified SIR package.
2
- ///
3
- /// The package declares what it needs: `runtime_families` names every runtime
4
- /// call family the instruction stream reaches, and `externs` names every
5
- /// declared C-ABI symbol. This validator matches each against the shim table
6
- /// the VM implements. A family or symbol with no shim is refused before a
7
- /// single instruction runs. Unknown is rejected, never allowed.
8
- ///
9
- /// The check keys on the declared identity: it never matches a symbol prefix
10
- /// and never reads an instruction stream to guess a capability.
11
- import { SUPPORTED_NATIVE_IO, SUPPORTED_SUSPEND_KINDS, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
1
+ import { calendarResultFields } from "./calendar.js";
2
+ import { CODEC_REGISTRATION, registrationRefusal, resolveExtern, resolveRuntime, resolveSuspension, suspensionRefusal, } from "./shims.js";
12
3
  const UNSUPPORTED = "sandbox.capability.unsupported";
13
- const NATIVE_FAMILIES = new Set(["AsyncIo", "FileRead", "Tcp"]);
14
- const NATIVE_SUSPENSIONS = new Set(["NativeIo", "RemoteAsk", "Read", "Accept"]);
15
- const OPS = new Set([
16
- "coverage.hit",
17
- "const.int",
18
- "const.bool",
19
- "const.float",
20
- "const.char",
21
- "const.unit",
22
- "const.duration",
23
- "const.str",
24
- "const.bytes",
25
- "copy_value",
26
- "fork",
27
- "move",
28
- "destroy_value",
29
- "begin_borrow",
30
- "end_borrow",
31
- "finish_linear_receiver",
32
- "alloc_place",
33
- "store.init",
34
- "store.assign",
35
- "load.copy",
36
- "load.take",
37
- "load.borrow",
38
- "end_lifetime",
39
- "tuple.make",
40
- "tuple.get",
41
- "aggregate.make",
42
- "aggregate.project_copy",
43
- "aggregate.project_borrow",
44
- "destructure",
45
- "array.make",
46
- "array.repeat",
47
- "variant.make",
48
- "variant.is",
49
- "variant.project_copy",
50
- "variant.project_borrow",
51
- "variant.destructure",
52
- "unary",
53
- "binary",
54
- "cast",
55
- "str.eq",
56
- "bytes.eq",
57
- "function.make",
58
- "closure.make",
59
- "generator.make",
60
- "callable.coerce",
61
- "dyn.make",
62
- "register_defer",
63
- "stream.pipe",
64
- "task_scope.enter",
65
- "task_scope.close",
66
- "task.spawn",
67
- "task.race",
68
- ]);
69
- const TERMS = new Set([
70
- "goto",
71
- "branch",
72
- "switch.variant",
73
- "return",
74
- "checked.binary",
75
- "dyn.call",
76
- "call",
77
- "indirect.call",
78
- "actor.call",
79
- "runtime.call",
80
- "extern.call",
81
- "value.call",
82
- "wire.codec",
83
- "panic",
84
- "checked_raise",
85
- "trap",
86
- "cleanup.dispatch",
87
- "enter_defer",
88
- "finish_defer",
89
- "recover_fault",
90
- "resume_unwind",
91
- "suspend",
92
- "unreachable",
93
- ]);
94
- const ACTOR_OPS = new Set([
95
- "spawn",
96
- "self_handle",
97
- "stop",
98
- "terminate",
99
- "await_stopped",
100
- "await_restarted",
101
- "call_start",
102
- "call_take",
103
- "submit",
104
- "stream_start",
105
- "supervisor_spawn",
106
- "supervisor_child",
107
- "supervisor_pool_view",
108
- ]);
4
+ /// The executor can only be constructed after structural/reference checks and
5
+ /// the canonical host registration have both accepted the same package.
6
+ export class AdmittedPackage {
7
+ bytecode;
8
+ constructor(bytecode) {
9
+ this.bytecode = bytecode;
10
+ }
11
+ static load(pkg, bridges) {
12
+ return admitPackage(pkg, bridges) ?? new AdmittedPackage(pkg);
13
+ }
14
+ }
109
15
  function capabilityMessage(capability, native) {
110
16
  const nativeFeatures = {
111
17
  FileRead: "Filesystem access",
@@ -114,13 +20,11 @@ function capabilityMessage(capability, native) {
114
20
  NativeIo: "Host input and output",
115
21
  Read: "Host input and output",
116
22
  Accept: "Accepting network connections",
117
- RemoteAsk: "Remote actor calls",
118
23
  };
119
24
  const features = {
120
25
  Vector: "This vector operation",
121
26
  Map: "This map operation",
122
27
  Set: "This set operation",
123
- "actor.periodic": "Periodic actor handlers",
124
28
  "actor.local_observation": "Actor links and monitors",
125
29
  "actor.heap_limit": "Per-actor heap limits",
126
30
  "actor.ingress_adapter": "Actor ingress adapters",
@@ -139,8 +43,8 @@ function unavailable(capability) {
139
43
  span: null,
140
44
  };
141
45
  }
142
- /// The first refusal, or `null` when every declared capability has a shim.
143
- export function admitPackage(pkg) {
46
+ /// The first refusal, or `null` when all operations and bridges are available.
47
+ export function admitPackage(pkg, bridges = {}) {
144
48
  if (!pkg.entry) {
145
49
  return {
146
50
  category: "invalid_package",
@@ -150,6 +54,7 @@ export function admitPackage(pkg) {
150
54
  span: null,
151
55
  };
152
56
  }
57
+ const opaqueResources = new Set();
153
58
  for (const function_ of pkg.functions) {
154
59
  for (const block of function_.blocks) {
155
60
  if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
@@ -168,120 +73,299 @@ export function admitPackage(pkg) {
168
73
  span: null,
169
74
  };
170
75
  if (block.term.op === "actor.call" &&
171
- !ACTOR_OPS.has(block.term.operation.op))
76
+ block.term.operation.op === "local_observation")
172
77
  return unavailable(`actor.${block.term.operation.op}`);
173
- if (block.ops.some((op) => op.op === "coverage.hit" &&
174
- (!Number.isSafeInteger(op.counter) ||
175
- op.counter < 0 ||
176
- op.counter > 0xffff_ffff))) {
177
- return {
178
- category: "invalid_package",
179
- code: "sandbox.package.invalid_counter",
180
- capability: "coverage.hit",
181
- message: "coverage counter must be a u32",
182
- span: null,
183
- };
78
+ const term = block.term;
79
+ if (term.op === "runtime.call" &&
80
+ pkg.runtime_families[term.family]?.family === "TimestampNow") {
81
+ const shape = term.aggregate_result_shape == null
82
+ ? undefined
83
+ : pkg.aggregates[term.aggregate_result_shape];
84
+ if (!shape ||
85
+ shape.builtin !== "Timestamp" ||
86
+ shape.fields.length !== 2) {
87
+ return {
88
+ category: "invalid_package",
89
+ code: "sandbox.package.timestamp_shape",
90
+ capability: "TimestampNow",
91
+ message: "timestamp clock lacks its checked result shape",
92
+ span: null,
93
+ };
94
+ }
95
+ }
96
+ if (term.op === "extern.call") {
97
+ const symbol = pkg.externs[term.extern]?.symbol ?? "";
98
+ const fields = calendarResultFields(symbol);
99
+ if (fields) {
100
+ const shape = term.aggregate_result_shape == null
101
+ ? undefined
102
+ : pkg.aggregates[term.aggregate_result_shape];
103
+ if (!shape ||
104
+ shape.fields.length !== fields.length ||
105
+ shape.fields.some((field, index) => field !== fields[index])) {
106
+ return {
107
+ category: "invalid_package",
108
+ code: "sandbox.package.calendar_shape",
109
+ capability: symbol,
110
+ message: "calendar operation lacks its checked result shape",
111
+ span: null,
112
+ };
113
+ }
114
+ }
115
+ }
116
+ if (term.op === "timestamp.binary") {
117
+ const both = term.lhs_timestamp && term.rhs_timestamp;
118
+ const validOperation = (term.binary_op === "Add" &&
119
+ term.lhs_timestamp !== term.rhs_timestamp) ||
120
+ (term.binary_op === "Subtract" && term.lhs_timestamp);
121
+ const shape = term.shape === null ? undefined : pkg.aggregates[term.shape];
122
+ if (typeof term.lhs_timestamp !== "boolean" ||
123
+ typeof term.rhs_timestamp !== "boolean" ||
124
+ !validOperation ||
125
+ (both
126
+ ? term.shape !== null
127
+ : !shape ||
128
+ shape.fields.length !== 2 ||
129
+ shape.builtin !== "Timestamp") ||
130
+ term.failures.length !== 1 ||
131
+ term.failures[0]?.trap !== "integer_overflow") {
132
+ return {
133
+ category: "invalid_package",
134
+ code: "sandbox.package.timestamp_shape",
135
+ capability: "timestamp.binary",
136
+ message: "timestamp arithmetic lacks its checked operands, result shape or overflow edge",
137
+ span: null,
138
+ };
139
+ }
140
+ }
141
+ if (term.op === "runtime.call" &&
142
+ pkg.runtime_families[term.family]?.family === "BytesDecodeUtf8") {
143
+ const shapes = term.variant_result_shapes;
144
+ const error = shapes && pkg.aggregates[shapes.error];
145
+ const length = shapes && pkg.variants[shapes.error_len];
146
+ const result = term.result_shape === null
147
+ ? undefined
148
+ : pkg.variants[term.result_shape];
149
+ if (!error ||
150
+ error.fields.length !== 2 ||
151
+ !length ||
152
+ !result ||
153
+ length.runtime_tags?.OptionSome === undefined ||
154
+ length.runtime_tags?.OptionNone === undefined ||
155
+ result.runtime_tags?.ResultOk === undefined ||
156
+ result.runtime_tags?.ResultErr === undefined)
157
+ return {
158
+ category: "invalid_package",
159
+ code: "sandbox.package.variant_result_shapes",
160
+ capability: null,
161
+ message: "UTF-8 decode lacks its checked result carrier",
162
+ span: null,
163
+ };
164
+ }
165
+ if (term.op === "extern.call") {
166
+ const registration = resolveExtern(pkg.externs[term.extern]);
167
+ if (registration.kind === "directory" &&
168
+ "release" in registration.operation) {
169
+ const resource = term.result_resource == null
170
+ ? undefined
171
+ : pkg.resources?.[term.result_resource];
172
+ if (resource?.kind !== "nominal" ||
173
+ resource.release !== registration.operation.release ||
174
+ term.result == null ||
175
+ term.result === "never")
176
+ return {
177
+ category: "invalid_package",
178
+ code: "sandbox.package.owner_result",
179
+ capability: null,
180
+ message: "filesystem acquisition lacks its compiler-selected owner release slot",
181
+ span: null,
182
+ };
183
+ }
184
+ }
185
+ if (term.op === "actor.call" && term.operation.op === "timer_schedule") {
186
+ const operation = term.operation;
187
+ const handler = pkg.actors
188
+ ?.find((actor) => actor.id === operation.actor)
189
+ ?.handlers.find((handler) => handler.message_id === operation.message);
190
+ const resource = pkg.resources?.[operation.resource];
191
+ if (!handler ||
192
+ handler.streams ||
193
+ term.args.length !== 5 ||
194
+ !["reject", "drop_newest"].includes(operation.policy) ||
195
+ resource?.kind !== "opaque" ||
196
+ resource.close === undefined ||
197
+ !pkg.functions.some((fn) => fn.id === resource.close))
198
+ return {
199
+ category: "invalid_package",
200
+ code: "sandbox.package.invalid_timer",
201
+ capability: "actor.timer_schedule",
202
+ message: "timer schedule lacks its checked handler, timing or non-waiting admission",
203
+ span: null,
204
+ };
205
+ opaqueResources.add(operation.resource);
206
+ }
207
+ if (term.op === "actor.call" &&
208
+ term.operation.op === "supervisor_spawn") {
209
+ const id = term.operation.supervisor;
210
+ const supervisor = pkg.supervisors?.find((layout) => layout.id === id);
211
+ const config = term.args.length -
212
+ (supervisor?.children.some((child) => child.is_pool) ? 1 : 0);
213
+ if (!supervisor ||
214
+ config < 0 ||
215
+ supervisor.children.some((child) => {
216
+ const params = pkg.functions.find((fn) => fn.id === child.spawn)?.params;
217
+ return !params || ![config, config + 1].includes(params.length);
218
+ }))
219
+ return {
220
+ category: "invalid_package",
221
+ code: "sandbox.package.supervisor_constructor",
222
+ capability: null,
223
+ message: "supervisor constructor signature does not match its config",
224
+ span: null,
225
+ };
226
+ }
227
+ if (term.op === "wire.codec") {
228
+ const refusal = registrationRefusal(CODEC_REGISTRATION, bridges);
229
+ if (refusal)
230
+ return rejectRegistration(refusal);
231
+ }
232
+ if (term.op === "suspend") {
233
+ const registration = resolveSuspension(term);
234
+ if (registration.kind === "refused")
235
+ return rejectRegistration(registration);
236
+ const mandatoryClose = (pkg.resources ?? []).some((resource) => "close" in resource && resource.close === function_.id);
237
+ if (term.kind === "Safepoint"
238
+ ? (term.cancel == null) !== mandatoryClose
239
+ : term.cancel == null)
240
+ return {
241
+ category: "invalid_package",
242
+ code: "sandbox.package.cancellation_ancestry",
243
+ capability: null,
244
+ message: "safepoint cancellation ancestry must match its authored close lifecycle",
245
+ span: null,
246
+ };
184
247
  }
185
- const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
186
- (!TERMS.has(block.term.op) ? block.term.op : null);
187
- if (unknown)
188
- return {
189
- category: "invalid_package",
190
- code: "sandbox.package.unknown_opcode",
191
- capability: unknown,
192
- message: `unknown bytecode opcode ${unknown}`,
193
- span: null,
194
- };
195
248
  }
196
249
  }
197
- for (const resource of pkg.resources ?? []) {
198
- if (resource.kind === "record" &&
199
- (resource.shape === undefined ||
200
- !pkg.aggregates.some((shape) => shape.id === resource.shape)))
201
- return {
202
- category: "invalid_package",
203
- code: "sandbox.package.resource_shape_missing",
204
- capability: null,
205
- message: "record resource has no exact aggregate shape",
206
- span: null,
207
- };
208
- if (resource.kind === "opaque")
250
+ for (const [index, resource] of (pkg.resources ?? []).entries()) {
251
+ if (resource.kind === "opaque" && !opaqueResources.has(index))
209
252
  return unavailable("resource.opaque_close");
210
253
  if (resource.kind === "nominal" &&
211
- !resolveExternShim(resource.release ?? ""))
254
+ registrationRefusal(resolveExtern({ symbol: resource.release ?? "" }), bridges))
212
255
  return unavailable("resource.external_close");
213
256
  }
214
257
  for (const actor of pkg.actors ?? []) {
215
- if (actor.handlers.some((handler) => handler.every_ns != null))
216
- return unavailable("actor.periodic");
258
+ if (typeof actor.name !== "string" || actor.name.length === 0)
259
+ return {
260
+ category: "invalid_package",
261
+ code: "sandbox.package.actor_name",
262
+ capability: null,
263
+ message: "actor lacks its declared diagnostic name",
264
+ span: null,
265
+ };
266
+ if (actor.crash != null &&
267
+ (actor.crash_info == null ||
268
+ !pkg.aggregates[actor.crash_info] ||
269
+ actor.crash_kind == null ||
270
+ !pkg.variants[actor.crash_kind] ||
271
+ actor.crash_action == null ||
272
+ !pkg.variants[actor.crash_action]))
273
+ return {
274
+ category: "invalid_package",
275
+ code: "sandbox.package.crash_hook_shapes",
276
+ capability: null,
277
+ message: "crash hook lacks its exact info, kind or action shape",
278
+ span: null,
279
+ };
280
+ if (actor.handlers.some((handler) => handler.every_ns != null &&
281
+ (!Number.isInteger(handler.every_ns) ||
282
+ handler.every_ns < 1_000_000 ||
283
+ handler.params !== 0 ||
284
+ handler.streams ||
285
+ handler.fallible ||
286
+ !pkg.functions.some((fn) => fn.id === handler.callable && fn.params.length === 1))))
287
+ return {
288
+ category: "invalid_package",
289
+ code: "sandbox.package.invalid_periodic",
290
+ capability: "actor.periodic",
291
+ message: "periodic handler lacks its checked interval or zero-argument body",
292
+ span: null,
293
+ };
217
294
  if (actor.exit != null || actor.down != null)
218
295
  return unavailable("actor.local_observation");
219
296
  if (actor.max_heap_bytes != null)
220
297
  return unavailable("actor.heap_limit");
221
298
  }
222
- for (const entry of pkg.runtime_families) {
223
- if (!resolveRuntimeShim(entry)) {
299
+ for (const supervisor of pkg.supervisors ?? []) {
300
+ const order = supervisor.startup_order === undefined
301
+ ? supervisor.children.map((_child, index) => index)
302
+ : supervisor.startup_order;
303
+ if (!Array.isArray(order) ||
304
+ supervisor.children.some((child) => child.dependencies !== undefined &&
305
+ (!Array.isArray(child.dependencies) ||
306
+ new Set(child.dependencies).size !== child.dependencies.length)))
224
307
  return {
225
- category: NATIVE_FAMILIES.has(entry.family)
226
- ? "native_only"
227
- : "not_implemented",
228
- code: UNSUPPORTED,
229
- capability: familyIdentity(entry.family, entry.detail),
230
- message: capabilityMessage(entry.family, NATIVE_FAMILIES.has(entry.family)),
308
+ category: "invalid_package",
309
+ code: "sandbox.package.supervisor_startup_order",
310
+ capability: null,
311
+ message: "supervisor startup order and dependencies must be arrays of unique children",
231
312
  span: null,
232
313
  };
233
- }
234
- }
235
- for (const entry of pkg.externs) {
236
- if (!resolveExternShim(entry.symbol)) {
237
- const native = entry.native_capability;
314
+ const ranks = new Map(order.map((child, rank) => [child, rank]));
315
+ if (order.length !== supervisor.children.length ||
316
+ ranks.size !== order.length ||
317
+ order.some((child) => !Number.isInteger(child) ||
318
+ child < 0 ||
319
+ child >= supervisor.children.length) ||
320
+ supervisor.children.some((child, index) => (child.dependencies ?? []).some((dependency) => !Number.isInteger(dependency) ||
321
+ dependency < 0 ||
322
+ !ranks.has(dependency) ||
323
+ ranks.get(dependency) >= ranks.get(index))))
238
324
  return {
239
- category: native === undefined ? "not_implemented" : "native_only",
240
- code: UNSUPPORTED,
241
- capability: entry.symbol,
242
- message: native === undefined
243
- ? "This external-library operation is not available in the browser runtime yet."
244
- : `${native} require native execution.`,
325
+ category: "invalid_package",
326
+ code: "sandbox.package.supervisor_startup_order",
327
+ capability: null,
328
+ message: "supervisor startup order must precede each declared dependency",
245
329
  span: null,
246
330
  };
247
- }
248
- }
249
- for (const kind of pkg.suspend_kinds) {
250
- const refused = kind === "NativeIo"
251
- ? refusedNativeIo(pkg)
252
- : SUPPORTED_SUSPEND_KINDS.has(kind)
253
- ? null
254
- : kind;
255
- if (refused !== null) {
331
+ if (supervisor.children.some((child) => !pkg.functions.some((fn) => fn.id === child.spawn)))
256
332
  return {
257
- category: NATIVE_SUSPENSIONS.has(kind)
258
- ? "native_only"
259
- : "not_implemented",
260
- code: UNSUPPORTED,
261
- capability: refused,
262
- message: capabilityMessage(kind, NATIVE_SUSPENSIONS.has(kind)),
333
+ category: "invalid_package",
334
+ code: "sandbox.package.supervisor_constructor",
335
+ capability: null,
336
+ message: "supervisor child has no constructor callable",
263
337
  span: null,
264
338
  };
265
- }
266
339
  }
267
- return null;
268
- }
269
- /// The first native I/O suspension whose operation the VM does not serve.
270
- function refusedNativeIo(pkg) {
271
- for (const function_ of pkg.functions) {
272
- for (const block of function_.blocks) {
273
- const term = block.term;
274
- if (term.op !== "suspend" || term.kind !== "NativeIo")
275
- continue;
276
- const operation = String(term.detail?.operation);
277
- if (!SUPPORTED_NATIVE_IO.has(operation))
278
- return `NativeIo::${operation}`;
279
- }
340
+ for (const entry of pkg.runtime_families) {
341
+ const refusal = registrationRefusal(resolveRuntime(entry), bridges);
342
+ if (refusal)
343
+ return rejectRegistration(refusal);
344
+ }
345
+ for (const entry of pkg.externs) {
346
+ const refusal = registrationRefusal(resolveExtern(entry), bridges);
347
+ if (refusal)
348
+ return rejectRegistration(refusal);
349
+ }
350
+ for (const kind of pkg.suspend_kinds) {
351
+ const refusal = suspensionRefusal(kind);
352
+ if (refusal)
353
+ return rejectRegistration(refusal);
280
354
  }
281
355
  return null;
282
356
  }
283
- function familyIdentity(family, detail) {
284
- return typeof detail === "string" && detail.length > 0
285
- ? `${family}::${detail}`
286
- : family;
357
+ function rejectRegistration(refusal) {
358
+ return {
359
+ category: refusal.category,
360
+ code: UNSUPPORTED,
361
+ capability: refusal.capability,
362
+ message: refusal.message ??
363
+ (refusal.capability === "codecBridge" ||
364
+ refusal.capability === "calendarBridge"
365
+ ? `This operation requires the ${refusal.capability === "codecBridge" ? "codec" : "calendar"} Wasm bridge.`
366
+ : refusal.description !== undefined
367
+ ? `${refusal.description} require native execution.`
368
+ : capabilityMessage(refusal.capability.split("::")[0], refusal.category === "native_only")),
369
+ span: null,
370
+ };
287
371
  }