Version Packages - #35
Merged
Merged
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
July 25, 2026 20:05
a649f26 to
7f6d1ea
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 25, 2026 20:09
7f6d1ea to
0d7a269
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@iso4/sandbox@0.4.0
Minor Changes
4126420: Every
RunResultnow reports what the run did, measured inside the Rust runtime:cpuTimeMs(active V8 execution time, bridge waits excluded —durationMsremains wall-clock time) andbridgeCalls, one metadata record per bridge call attempt in attempt order — resolved name (fetch,tools:search.query), start offset and round-trip duration on the run's clock, argument/response byte sizes, and outcome. Attempts blocked bymaxBridgeCalls/maxBridgeCallBytesare included withblocked: true, so runs that hit a limit show exactly which attempt crossed it. Payloads are never captured, keeping the report cheap enough to stay always-on. Aborted runs report zeros and an empty list for now (the abort teardown precedes the runtime's result frame; see Graceful terminate: aborted runs should get a real result from Rust #36).9332cca: Aborting a run now terminates it gracefully whenever possible, so the aborted
RunResultcarries real telemetry instead of synthesized zeros. Previously an abort destroyed the socket and TypeScript fabricated the result (durationMs: 0,cpuTimeMs: 0,bridgeCalls: []) — which meant every durable-isolates suspension (implemented as an abort) lost its per-call bridge records. Now TypeScript sends aTerminateframe carrying the run ID; the Rust runtime, parked in its bridge-wait poll loop, stops the run and replies with a realERR_ABORTEDresult reportingdurationMs,cpuTimeMs, and the bridge-call records collected up to the abort. The connection stays healthy and is reused rather than reconnected. This covers the case that matters most — suspensions always happen while the sandbox is awaiting a bridge call, exactly when the runtime can receive the frame. A run stuck in a tight synchronous loop can't be reached this way, so after a short grace period TypeScript falls back to today's socket teardown and synthesized (zeroed) result; the busy isolate is still reclaimed by its CPU guard.0fee306: Host-import modules are now built natively by the Rust runtime from structured shape data instead of being lowered to generated JavaScript source in the client. Previously the client walked a host-module object and emitted ESM text — data leaves printed as JS literals, function leaves as
__iso4_call(<id>, …)stubs — an injection-adjacent codegen surface, and the runtime only ever saw opaque source. The module shape now crosses the wire as plain data (function-leaf markers, data leaves asWireValues, nested objects as trees); the runtime materialises data leaves with the value codec, builds function leaves as async trampolines from a fixed factory (the handle ID passed as a number, never printed into source), and hands the values to a fixed-shape module through V8'simport.metacallback. Host modules remain ordinary source-text modules, so prefix snapshots capture their bindings as plain JS exactly as before.The runtime owns the handle →
<specifier>.<path>table, soRunResult.bridgeCallsrecords now arrive with fully resolved names for host-import calls (e.g.tools:search.query) straight from the runtime — the client-side name resolver is gone — andBridgeCallframes carry the resolved import target (targetKind: import+ specifier + leaf path) instead of a dispatcher name and a numeric handle argument.Undeclared-import rebind validation moved into the runtime:
prefix.run()sends only the rebind locations, and the runtime checks them against the shape declared atprecompile(), rejecting undeclared specifiers/paths, data leaves, and source modules withERR_UNDECLARED_BINDING— the same enforcement point that guards undeclared globals, no longer bypassable by a non-TypeScript client. Compile-time enforcement viaRebindImports<M>is unchanged.Wire-protocol change (
ImportBindingis now tagged source/host with a shape tree;PrefixRuncarriesImportRebindlocations;BridgeCallRecordcarries a resolvednameand dropsrawName/importHandleId):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.2f0e296: Host globals are now installed natively by the Rust runtime instead of by prepending generated JavaScript to the user's code. Previously
processGlobalsturned string globals andBridgeWithShimwrappers into source text and pasted it in front of the run's code, which shifted every line of user code so sandbox stack traces pointed at the wrong lines, and interpolated the global's name into an identifier position in the generated wrapper.The client now sends each global as structured data — a
GlobalDeftaggedbridge,string,data, orshim— and the runtime installs it directly on the sandbox global object via the V8 API. String expressions and shim wrappers are evaluated as their own scripts with their own filenames, so user code always starts at line 1 and its stack traces are correct, and a global's name only ever travels as a string passed toobject.set(or aWireValue) — never interpolated into code.Adds a new data-valued global kind for passing a plain constant:
globals: { config: { kind: 'data', value: … } }. The value crosses the wire as aWireValue(same supported set as host-module data leaves — primitives,bigint,string,Uint8Array, plain objects/arrays) and is materialised natively, removing the need to hand-roll aJSON.stringify-into-a-string-expression global. Data globals, like string globals, are constants and cannot be rebound perprefix.run().For precompiled prefixes, string/data globals and shim wrappers are baked into the snapshot at
precompile()time exactly as before, so only bridge stubs are re-installed perprefix.run()— the repeated-run hot path is unchanged.Wire-protocol change (the
globalsfield ofRun/Precompile/PrefixRunis now aList<GlobalDef>instead of a name list):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.f1ccb9d: The Rust runtime now owns the resource-limit defaults; the client sends only the limits the caller explicitly set. Previously the default numbers (64 MB memory, 5 s CPU, 30 s wall, 10 bridge calls, 16 MiB export/bridge-payload caps, 1 MiB stdio caps) were filled in client-side before every Run/Precompile/PrefixRun and shipped as concrete values on the wire, so the same constants were documented in three places (TS code,
types.tsjsdoc, Rust doc comments) that could drift, and any non-TS client had to re-implement them to get safe behavior.Each
ResourceLimitsfield is nowOptional<u32>on the wire: absent means "apply the runtime default", an explicit0still means "no limit" (distinct from absent). Rust resolves any absent field from a single set ofDEFAULT_*constants innative/v8-runtime/src/ipc.rs— the source of truth. The publicResourceLimitsfields become optional to match (limitsisResourceLimitsrather thanPartial<ResourceLimits>at the call sites), and their@defaultjsdoc now documents the runtime defaults it mirrors. Effective behavior for existing callers is unchanged; the defaults are identical.Wire-protocol change (limits payload shape):
@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.Patch Changes
maxBridgeCalls,maxBridgeCallBytes, function arguments) now terminate V8 execution immediately and uncatchably, as DESIGN.md always specified. Previously they were thrown as catchable JS exceptions: sandbox code couldtry/catchpast a violation in the synchronous window before the next microtask checkpoint, keep making (blocked) call attempts, or even complete the run successfully despite the violation. Host handler errors are unchanged and remain catchable in the sandbox.@iso4/v8-darwin-arm64@0.4.0
Minor Changes
4126420: Every
RunResultnow reports what the run did, measured inside the Rust runtime:cpuTimeMs(active V8 execution time, bridge waits excluded —durationMsremains wall-clock time) andbridgeCalls, one metadata record per bridge call attempt in attempt order — resolved name (fetch,tools:search.query), start offset and round-trip duration on the run's clock, argument/response byte sizes, and outcome. Attempts blocked bymaxBridgeCalls/maxBridgeCallBytesare included withblocked: true, so runs that hit a limit show exactly which attempt crossed it. Payloads are never captured, keeping the report cheap enough to stay always-on. Aborted runs report zeros and an empty list for now (the abort teardown precedes the runtime's result frame; see Graceful terminate: aborted runs should get a real result from Rust #36).9332cca: Aborting a run now terminates it gracefully whenever possible, so the aborted
RunResultcarries real telemetry instead of synthesized zeros. Previously an abort destroyed the socket and TypeScript fabricated the result (durationMs: 0,cpuTimeMs: 0,bridgeCalls: []) — which meant every durable-isolates suspension (implemented as an abort) lost its per-call bridge records. Now TypeScript sends aTerminateframe carrying the run ID; the Rust runtime, parked in its bridge-wait poll loop, stops the run and replies with a realERR_ABORTEDresult reportingdurationMs,cpuTimeMs, and the bridge-call records collected up to the abort. The connection stays healthy and is reused rather than reconnected. This covers the case that matters most — suspensions always happen while the sandbox is awaiting a bridge call, exactly when the runtime can receive the frame. A run stuck in a tight synchronous loop can't be reached this way, so after a short grace period TypeScript falls back to today's socket teardown and synthesized (zeroed) result; the busy isolate is still reclaimed by its CPU guard.0fee306: Host-import modules are now built natively by the Rust runtime from structured shape data instead of being lowered to generated JavaScript source in the client. Previously the client walked a host-module object and emitted ESM text — data leaves printed as JS literals, function leaves as
__iso4_call(<id>, …)stubs — an injection-adjacent codegen surface, and the runtime only ever saw opaque source. The module shape now crosses the wire as plain data (function-leaf markers, data leaves asWireValues, nested objects as trees); the runtime materialises data leaves with the value codec, builds function leaves as async trampolines from a fixed factory (the handle ID passed as a number, never printed into source), and hands the values to a fixed-shape module through V8'simport.metacallback. Host modules remain ordinary source-text modules, so prefix snapshots capture their bindings as plain JS exactly as before.The runtime owns the handle →
<specifier>.<path>table, soRunResult.bridgeCallsrecords now arrive with fully resolved names for host-import calls (e.g.tools:search.query) straight from the runtime — the client-side name resolver is gone — andBridgeCallframes carry the resolved import target (targetKind: import+ specifier + leaf path) instead of a dispatcher name and a numeric handle argument.Undeclared-import rebind validation moved into the runtime:
prefix.run()sends only the rebind locations, and the runtime checks them against the shape declared atprecompile(), rejecting undeclared specifiers/paths, data leaves, and source modules withERR_UNDECLARED_BINDING— the same enforcement point that guards undeclared globals, no longer bypassable by a non-TypeScript client. Compile-time enforcement viaRebindImports<M>is unchanged.Wire-protocol change (
ImportBindingis now tagged source/host with a shape tree;PrefixRuncarriesImportRebindlocations;BridgeCallRecordcarries a resolvednameand dropsrawName/importHandleId):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.2f0e296: Host globals are now installed natively by the Rust runtime instead of by prepending generated JavaScript to the user's code. Previously
processGlobalsturned string globals andBridgeWithShimwrappers into source text and pasted it in front of the run's code, which shifted every line of user code so sandbox stack traces pointed at the wrong lines, and interpolated the global's name into an identifier position in the generated wrapper.The client now sends each global as structured data — a
GlobalDeftaggedbridge,string,data, orshim— and the runtime installs it directly on the sandbox global object via the V8 API. String expressions and shim wrappers are evaluated as their own scripts with their own filenames, so user code always starts at line 1 and its stack traces are correct, and a global's name only ever travels as a string passed toobject.set(or aWireValue) — never interpolated into code.Adds a new data-valued global kind for passing a plain constant:
globals: { config: { kind: 'data', value: … } }. The value crosses the wire as aWireValue(same supported set as host-module data leaves — primitives,bigint,string,Uint8Array, plain objects/arrays) and is materialised natively, removing the need to hand-roll aJSON.stringify-into-a-string-expression global. Data globals, like string globals, are constants and cannot be rebound perprefix.run().For precompiled prefixes, string/data globals and shim wrappers are baked into the snapshot at
precompile()time exactly as before, so only bridge stubs are re-installed perprefix.run()— the repeated-run hot path is unchanged.Wire-protocol change (the
globalsfield ofRun/Precompile/PrefixRunis now aList<GlobalDef>instead of a name list):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.f1ccb9d: The Rust runtime now owns the resource-limit defaults; the client sends only the limits the caller explicitly set. Previously the default numbers (64 MB memory, 5 s CPU, 30 s wall, 10 bridge calls, 16 MiB export/bridge-payload caps, 1 MiB stdio caps) were filled in client-side before every Run/Precompile/PrefixRun and shipped as concrete values on the wire, so the same constants were documented in three places (TS code,
types.tsjsdoc, Rust doc comments) that could drift, and any non-TS client had to re-implement them to get safe behavior.Each
ResourceLimitsfield is nowOptional<u32>on the wire: absent means "apply the runtime default", an explicit0still means "no limit" (distinct from absent). Rust resolves any absent field from a single set ofDEFAULT_*constants innative/v8-runtime/src/ipc.rs— the source of truth. The publicResourceLimitsfields become optional to match (limitsisResourceLimitsrather thanPartial<ResourceLimits>at the call sites), and their@defaultjsdoc now documents the runtime defaults it mirrors. Effective behavior for existing callers is unchanged; the defaults are identical.Wire-protocol change (limits payload shape):
@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.Patch Changes
maxBridgeCalls,maxBridgeCallBytes, function arguments) now terminate V8 execution immediately and uncatchably, as DESIGN.md always specified. Previously they were thrown as catchable JS exceptions: sandbox code couldtry/catchpast a violation in the synchronous window before the next microtask checkpoint, keep making (blocked) call attempts, or even complete the run successfully despite the violation. Host handler errors are unchanged and remain catchable in the sandbox.@iso4/v8-darwin-x64@0.4.0
Minor Changes
4126420: Every
RunResultnow reports what the run did, measured inside the Rust runtime:cpuTimeMs(active V8 execution time, bridge waits excluded —durationMsremains wall-clock time) andbridgeCalls, one metadata record per bridge call attempt in attempt order — resolved name (fetch,tools:search.query), start offset and round-trip duration on the run's clock, argument/response byte sizes, and outcome. Attempts blocked bymaxBridgeCalls/maxBridgeCallBytesare included withblocked: true, so runs that hit a limit show exactly which attempt crossed it. Payloads are never captured, keeping the report cheap enough to stay always-on. Aborted runs report zeros and an empty list for now (the abort teardown precedes the runtime's result frame; see Graceful terminate: aborted runs should get a real result from Rust #36).9332cca: Aborting a run now terminates it gracefully whenever possible, so the aborted
RunResultcarries real telemetry instead of synthesized zeros. Previously an abort destroyed the socket and TypeScript fabricated the result (durationMs: 0,cpuTimeMs: 0,bridgeCalls: []) — which meant every durable-isolates suspension (implemented as an abort) lost its per-call bridge records. Now TypeScript sends aTerminateframe carrying the run ID; the Rust runtime, parked in its bridge-wait poll loop, stops the run and replies with a realERR_ABORTEDresult reportingdurationMs,cpuTimeMs, and the bridge-call records collected up to the abort. The connection stays healthy and is reused rather than reconnected. This covers the case that matters most — suspensions always happen while the sandbox is awaiting a bridge call, exactly when the runtime can receive the frame. A run stuck in a tight synchronous loop can't be reached this way, so after a short grace period TypeScript falls back to today's socket teardown and synthesized (zeroed) result; the busy isolate is still reclaimed by its CPU guard.0fee306: Host-import modules are now built natively by the Rust runtime from structured shape data instead of being lowered to generated JavaScript source in the client. Previously the client walked a host-module object and emitted ESM text — data leaves printed as JS literals, function leaves as
__iso4_call(<id>, …)stubs — an injection-adjacent codegen surface, and the runtime only ever saw opaque source. The module shape now crosses the wire as plain data (function-leaf markers, data leaves asWireValues, nested objects as trees); the runtime materialises data leaves with the value codec, builds function leaves as async trampolines from a fixed factory (the handle ID passed as a number, never printed into source), and hands the values to a fixed-shape module through V8'simport.metacallback. Host modules remain ordinary source-text modules, so prefix snapshots capture their bindings as plain JS exactly as before.The runtime owns the handle →
<specifier>.<path>table, soRunResult.bridgeCallsrecords now arrive with fully resolved names for host-import calls (e.g.tools:search.query) straight from the runtime — the client-side name resolver is gone — andBridgeCallframes carry the resolved import target (targetKind: import+ specifier + leaf path) instead of a dispatcher name and a numeric handle argument.Undeclared-import rebind validation moved into the runtime:
prefix.run()sends only the rebind locations, and the runtime checks them against the shape declared atprecompile(), rejecting undeclared specifiers/paths, data leaves, and source modules withERR_UNDECLARED_BINDING— the same enforcement point that guards undeclared globals, no longer bypassable by a non-TypeScript client. Compile-time enforcement viaRebindImports<M>is unchanged.Wire-protocol change (
ImportBindingis now tagged source/host with a shape tree;PrefixRuncarriesImportRebindlocations;BridgeCallRecordcarries a resolvednameand dropsrawName/importHandleId):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.2f0e296: Host globals are now installed natively by the Rust runtime instead of by prepending generated JavaScript to the user's code. Previously
processGlobalsturned string globals andBridgeWithShimwrappers into source text and pasted it in front of the run's code, which shifted every line of user code so sandbox stack traces pointed at the wrong lines, and interpolated the global's name into an identifier position in the generated wrapper.The client now sends each global as structured data — a
GlobalDeftaggedbridge,string,data, orshim— and the runtime installs it directly on the sandbox global object via the V8 API. String expressions and shim wrappers are evaluated as their own scripts with their own filenames, so user code always starts at line 1 and its stack traces are correct, and a global's name only ever travels as a string passed toobject.set(or aWireValue) — never interpolated into code.Adds a new data-valued global kind for passing a plain constant:
globals: { config: { kind: 'data', value: … } }. The value crosses the wire as aWireValue(same supported set as host-module data leaves — primitives,bigint,string,Uint8Array, plain objects/arrays) and is materialised natively, removing the need to hand-roll aJSON.stringify-into-a-string-expression global. Data globals, like string globals, are constants and cannot be rebound perprefix.run().For precompiled prefixes, string/data globals and shim wrappers are baked into the snapshot at
precompile()time exactly as before, so only bridge stubs are re-installed perprefix.run()— the repeated-run hot path is unchanged.Wire-protocol change (the
globalsfield ofRun/Precompile/PrefixRunis now aList<GlobalDef>instead of a name list):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.f1ccb9d: The Rust runtime now owns the resource-limit defaults; the client sends only the limits the caller explicitly set. Previously the default numbers (64 MB memory, 5 s CPU, 30 s wall, 10 bridge calls, 16 MiB export/bridge-payload caps, 1 MiB stdio caps) were filled in client-side before every Run/Precompile/PrefixRun and shipped as concrete values on the wire, so the same constants were documented in three places (TS code,
types.tsjsdoc, Rust doc comments) that could drift, and any non-TS client had to re-implement them to get safe behavior.Each
ResourceLimitsfield is nowOptional<u32>on the wire: absent means "apply the runtime default", an explicit0still means "no limit" (distinct from absent). Rust resolves any absent field from a single set ofDEFAULT_*constants innative/v8-runtime/src/ipc.rs— the source of truth. The publicResourceLimitsfields become optional to match (limitsisResourceLimitsrather thanPartial<ResourceLimits>at the call sites), and their@defaultjsdoc now documents the runtime defaults it mirrors. Effective behavior for existing callers is unchanged; the defaults are identical.Wire-protocol change (limits payload shape):
@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.Patch Changes
maxBridgeCalls,maxBridgeCallBytes, function arguments) now terminate V8 execution immediately and uncatchably, as DESIGN.md always specified. Previously they were thrown as catchable JS exceptions: sandbox code couldtry/catchpast a violation in the synchronous window before the next microtask checkpoint, keep making (blocked) call attempts, or even complete the run successfully despite the violation. Host handler errors are unchanged and remain catchable in the sandbox.@iso4/v8-linux-arm64-gnu@0.4.0
Minor Changes
4126420: Every
RunResultnow reports what the run did, measured inside the Rust runtime:cpuTimeMs(active V8 execution time, bridge waits excluded —durationMsremains wall-clock time) andbridgeCalls, one metadata record per bridge call attempt in attempt order — resolved name (fetch,tools:search.query), start offset and round-trip duration on the run's clock, argument/response byte sizes, and outcome. Attempts blocked bymaxBridgeCalls/maxBridgeCallBytesare included withblocked: true, so runs that hit a limit show exactly which attempt crossed it. Payloads are never captured, keeping the report cheap enough to stay always-on. Aborted runs report zeros and an empty list for now (the abort teardown precedes the runtime's result frame; see Graceful terminate: aborted runs should get a real result from Rust #36).9332cca: Aborting a run now terminates it gracefully whenever possible, so the aborted
RunResultcarries real telemetry instead of synthesized zeros. Previously an abort destroyed the socket and TypeScript fabricated the result (durationMs: 0,cpuTimeMs: 0,bridgeCalls: []) — which meant every durable-isolates suspension (implemented as an abort) lost its per-call bridge records. Now TypeScript sends aTerminateframe carrying the run ID; the Rust runtime, parked in its bridge-wait poll loop, stops the run and replies with a realERR_ABORTEDresult reportingdurationMs,cpuTimeMs, and the bridge-call records collected up to the abort. The connection stays healthy and is reused rather than reconnected. This covers the case that matters most — suspensions always happen while the sandbox is awaiting a bridge call, exactly when the runtime can receive the frame. A run stuck in a tight synchronous loop can't be reached this way, so after a short grace period TypeScript falls back to today's socket teardown and synthesized (zeroed) result; the busy isolate is still reclaimed by its CPU guard.0fee306: Host-import modules are now built natively by the Rust runtime from structured shape data instead of being lowered to generated JavaScript source in the client. Previously the client walked a host-module object and emitted ESM text — data leaves printed as JS literals, function leaves as
__iso4_call(<id>, …)stubs — an injection-adjacent codegen surface, and the runtime only ever saw opaque source. The module shape now crosses the wire as plain data (function-leaf markers, data leaves asWireValues, nested objects as trees); the runtime materialises data leaves with the value codec, builds function leaves as async trampolines from a fixed factory (the handle ID passed as a number, never printed into source), and hands the values to a fixed-shape module through V8'simport.metacallback. Host modules remain ordinary source-text modules, so prefix snapshots capture their bindings as plain JS exactly as before.The runtime owns the handle →
<specifier>.<path>table, soRunResult.bridgeCallsrecords now arrive with fully resolved names for host-import calls (e.g.tools:search.query) straight from the runtime — the client-side name resolver is gone — andBridgeCallframes carry the resolved import target (targetKind: import+ specifier + leaf path) instead of a dispatcher name and a numeric handle argument.Undeclared-import rebind validation moved into the runtime:
prefix.run()sends only the rebind locations, and the runtime checks them against the shape declared atprecompile(), rejecting undeclared specifiers/paths, data leaves, and source modules withERR_UNDECLARED_BINDING— the same enforcement point that guards undeclared globals, no longer bypassable by a non-TypeScript client. Compile-time enforcement viaRebindImports<M>is unchanged.Wire-protocol change (
ImportBindingis now tagged source/host with a shape tree;PrefixRuncarriesImportRebindlocations;BridgeCallRecordcarries a resolvednameand dropsrawName/importHandleId):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.2f0e296: Host globals are now installed natively by the Rust runtime instead of by prepending generated JavaScript to the user's code. Previously
processGlobalsturned string globals andBridgeWithShimwrappers into source text and pasted it in front of the run's code, which shifted every line of user code so sandbox stack traces pointed at the wrong lines, and interpolated the global's name into an identifier position in the generated wrapper.The client now sends each global as structured data — a
GlobalDeftaggedbridge,string,data, orshim— and the runtime installs it directly on the sandbox global object via the V8 API. String expressions and shim wrappers are evaluated as their own scripts with their own filenames, so user code always starts at line 1 and its stack traces are correct, and a global's name only ever travels as a string passed toobject.set(or aWireValue) — never interpolated into code.Adds a new data-valued global kind for passing a plain constant:
globals: { config: { kind: 'data', value: … } }. The value crosses the wire as aWireValue(same supported set as host-module data leaves — primitives,bigint,string,Uint8Array, plain objects/arrays) and is materialised natively, removing the need to hand-roll aJSON.stringify-into-a-string-expression global. Data globals, like string globals, are constants and cannot be rebound perprefix.run().For precompiled prefixes, string/data globals and shim wrappers are baked into the snapshot at
precompile()time exactly as before, so only bridge stubs are re-installed perprefix.run()— the repeated-run hot path is unchanged.Wire-protocol change (the
globalsfield ofRun/Precompile/PrefixRunis now aList<GlobalDef>instead of a name list):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.f1ccb9d: The Rust runtime now owns the resource-limit defaults; the client sends only the limits the caller explicitly set. Previously the default numbers (64 MB memory, 5 s CPU, 30 s wall, 10 bridge calls, 16 MiB export/bridge-payload caps, 1 MiB stdio caps) were filled in client-side before every Run/Precompile/PrefixRun and shipped as concrete values on the wire, so the same constants were documented in three places (TS code,
types.tsjsdoc, Rust doc comments) that could drift, and any non-TS client had to re-implement them to get safe behavior.Each
ResourceLimitsfield is nowOptional<u32>on the wire: absent means "apply the runtime default", an explicit0still means "no limit" (distinct from absent). Rust resolves any absent field from a single set ofDEFAULT_*constants innative/v8-runtime/src/ipc.rs— the source of truth. The publicResourceLimitsfields become optional to match (limitsisResourceLimitsrather thanPartial<ResourceLimits>at the call sites), and their@defaultjsdoc now documents the runtime defaults it mirrors. Effective behavior for existing callers is unchanged; the defaults are identical.Wire-protocol change (limits payload shape):
@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.Patch Changes
maxBridgeCalls,maxBridgeCallBytes, function arguments) now terminate V8 execution immediately and uncatchably, as DESIGN.md always specified. Previously they were thrown as catchable JS exceptions: sandbox code couldtry/catchpast a violation in the synchronous window before the next microtask checkpoint, keep making (blocked) call attempts, or even complete the run successfully despite the violation. Host handler errors are unchanged and remain catchable in the sandbox.@iso4/v8-linux-x64-gnu@0.4.0
Minor Changes
4126420: Every
RunResultnow reports what the run did, measured inside the Rust runtime:cpuTimeMs(active V8 execution time, bridge waits excluded —durationMsremains wall-clock time) andbridgeCalls, one metadata record per bridge call attempt in attempt order — resolved name (fetch,tools:search.query), start offset and round-trip duration on the run's clock, argument/response byte sizes, and outcome. Attempts blocked bymaxBridgeCalls/maxBridgeCallBytesare included withblocked: true, so runs that hit a limit show exactly which attempt crossed it. Payloads are never captured, keeping the report cheap enough to stay always-on. Aborted runs report zeros and an empty list for now (the abort teardown precedes the runtime's result frame; see Graceful terminate: aborted runs should get a real result from Rust #36).9332cca: Aborting a run now terminates it gracefully whenever possible, so the aborted
RunResultcarries real telemetry instead of synthesized zeros. Previously an abort destroyed the socket and TypeScript fabricated the result (durationMs: 0,cpuTimeMs: 0,bridgeCalls: []) — which meant every durable-isolates suspension (implemented as an abort) lost its per-call bridge records. Now TypeScript sends aTerminateframe carrying the run ID; the Rust runtime, parked in its bridge-wait poll loop, stops the run and replies with a realERR_ABORTEDresult reportingdurationMs,cpuTimeMs, and the bridge-call records collected up to the abort. The connection stays healthy and is reused rather than reconnected. This covers the case that matters most — suspensions always happen while the sandbox is awaiting a bridge call, exactly when the runtime can receive the frame. A run stuck in a tight synchronous loop can't be reached this way, so after a short grace period TypeScript falls back to today's socket teardown and synthesized (zeroed) result; the busy isolate is still reclaimed by its CPU guard.0fee306: Host-import modules are now built natively by the Rust runtime from structured shape data instead of being lowered to generated JavaScript source in the client. Previously the client walked a host-module object and emitted ESM text — data leaves printed as JS literals, function leaves as
__iso4_call(<id>, …)stubs — an injection-adjacent codegen surface, and the runtime only ever saw opaque source. The module shape now crosses the wire as plain data (function-leaf markers, data leaves asWireValues, nested objects as trees); the runtime materialises data leaves with the value codec, builds function leaves as async trampolines from a fixed factory (the handle ID passed as a number, never printed into source), and hands the values to a fixed-shape module through V8'simport.metacallback. Host modules remain ordinary source-text modules, so prefix snapshots capture their bindings as plain JS exactly as before.The runtime owns the handle →
<specifier>.<path>table, soRunResult.bridgeCallsrecords now arrive with fully resolved names for host-import calls (e.g.tools:search.query) straight from the runtime — the client-side name resolver is gone — andBridgeCallframes carry the resolved import target (targetKind: import+ specifier + leaf path) instead of a dispatcher name and a numeric handle argument.Undeclared-import rebind validation moved into the runtime:
prefix.run()sends only the rebind locations, and the runtime checks them against the shape declared atprecompile(), rejecting undeclared specifiers/paths, data leaves, and source modules withERR_UNDECLARED_BINDING— the same enforcement point that guards undeclared globals, no longer bypassable by a non-TypeScript client. Compile-time enforcement viaRebindImports<M>is unchanged.Wire-protocol change (
ImportBindingis now tagged source/host with a shape tree;PrefixRuncarriesImportRebindlocations;BridgeCallRecordcarries a resolvednameand dropsrawName/importHandleId):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.2f0e296: Host globals are now installed natively by the Rust runtime instead of by prepending generated JavaScript to the user's code. Previously
processGlobalsturned string globals andBridgeWithShimwrappers into source text and pasted it in front of the run's code, which shifted every line of user code so sandbox stack traces pointed at the wrong lines, and interpolated the global's name into an identifier position in the generated wrapper.The client now sends each global as structured data — a
GlobalDeftaggedbridge,string,data, orshim— and the runtime installs it directly on the sandbox global object via the V8 API. String expressions and shim wrappers are evaluated as their own scripts with their own filenames, so user code always starts at line 1 and its stack traces are correct, and a global's name only ever travels as a string passed toobject.set(or aWireValue) — never interpolated into code.Adds a new data-valued global kind for passing a plain constant:
globals: { config: { kind: 'data', value: … } }. The value crosses the wire as aWireValue(same supported set as host-module data leaves — primitives,bigint,string,Uint8Array, plain objects/arrays) and is materialised natively, removing the need to hand-roll aJSON.stringify-into-a-string-expression global. Data globals, like string globals, are constants and cannot be rebound perprefix.run().For precompiled prefixes, string/data globals and shim wrappers are baked into the snapshot at
precompile()time exactly as before, so only bridge stubs are re-installed perprefix.run()— the repeated-run hot path is unchanged.Wire-protocol change (the
globalsfield ofRun/Precompile/PrefixRunis now aList<GlobalDef>instead of a name list):@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.f1ccb9d: The Rust runtime now owns the resource-limit defaults; the client sends only the limits the caller explicitly set. Previously the default numbers (64 MB memory, 5 s CPU, 30 s wall, 10 bridge calls, 16 MiB export/bridge-payload caps, 1 MiB stdio caps) were filled in client-side before every Run/Precompile/PrefixRun and shipped as concrete values on the wire, so the same constants were documented in three places (TS code,
types.tsjsdoc, Rust doc comments) that could drift, and any non-TS client had to re-implement them to get safe behavior.Each
ResourceLimitsfield is nowOptional<u32>on the wire: absent means "apply the runtime default", an explicit0still means "no limit" (distinct from absent). Rust resolves any absent field from a single set ofDEFAULT_*constants innative/v8-runtime/src/ipc.rs— the source of truth. The publicResourceLimitsfields become optional to match (limitsisResourceLimitsrather thanPartial<ResourceLimits>at the call sites), and their@defaultjsdoc now documents the runtime defaults it mirrors. Effective behavior for existing callers is unchanged; the defaults are identical.Wire-protocol change (limits payload shape):
@iso4/sandboxand the@iso4/v8-*runtime binaries must be released together.Patch Changes
maxBridgeCalls,maxBridgeCallBytes, function arguments) now terminate V8 execution immediately and uncatchably, as DESIGN.md always specified. Previously they were thrown as catchable JS exceptions: sandbox code couldtry/catchpast a violation in the synchronous window before the next microtask checkpoint, keep making (blocked) call attempts, or even complete the run successfully despite the violation. Host handler errors are unchanged and remain catchable in the sandbox.