sandbox: the network grant, with the VM's tasks (NETWORK slice 10) #7

Closed
opened 2026-10-05 21:07:09 +00:00 by orion · 5 comments
Collaborator

Goal

A host can grant a sandboxed receiver script the network: Sandbox.load<T>(path) { modules "std/network"; network connect: "api.example.test:443", listen: "127.0.0.1:8080" }. Inside a sandbox the runtime then lets a script resolve, connect and listen only as the grant names, and stops the script with a SandboxError for anything else. VM tasks run inside a sandbox now (see below), so the check can be tested by a script that tries, which is the reason NETWORK.md put the grant off until then.

Source

Pinned at a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b (main):

  • NETWORK.md section 10, "Capabilities": the two grants, the pattern rule, "TLS and HTTP add no capability", "the default is none".
  • NETWORK.md section 11, slice 10 (state "Open"; this issue closes it).
  • SCRIPTS.md section 4: the capability table already has the network row, "built with the VM's tasks"; section 6 has the error table.
  • VM.md section 10: a script's tasks belong to its sandbox, tested by tests/conformance/vm-only/sandbox-tasks.trb. This is the "VM's tasks" the slice waited for, and it is built.
  • ROADMAP.md Milestone 7 lists the sandbox's network grant as open work.
  • Fully settled by the records; no entry of CONCEPT "Open Questions" is involved. Nothing in flight collides: #5 (IO gates), #6 (pool benchmark) and #155/#156 (slice F) do not touch runtime/sandbox.c, std/sandbox or the network natives' entry points. #119 (Unix domain sockets) and #129 (OS network addresses) are open: whoever lands second guards the new entry point the same way (see the last criterion).

What to build

The grant is text, one setting per line (runtime/sandbox.c, SandboxCapabilities.grantText in std/sandbox/src/lib.trb), and the native script host passes the same text across, so no new plumbing:

  • std/sandbox: var fn network(connect: String = "", listen: String = "") on SandboxCapabilities, mirroring files; each call appends patterns, written as connect\t<pattern> and listen\t<pattern> lines. Several patterns per side are allowed by calling network again.
  • runtime/sandbox.c: parse the two words, keep the patterns, and add the checks the network natives consult while a sandbox is open (and not set aside for a program's task, VM.md section 10): resolve by host pattern, connect by address, listen/bind by address.
  • the torb_network_* entry points of runtime/io.c call the checks; a refusal is TORB_SANDBOX_REFUSED like a refused path, i.e. a stop, never a NetworkError the script could catch.
  • The toolchain's own grant for project.trb (compiler/src/vm/sandbox.trb) is unchanged: no network.

Acceptance criteria

Semantics, per NETWORK.md section 10 (the implementer writes the exact grammar below into that section, so the record stays the specification):

  • A pattern is host:port; the port is a decimal number or *; the host is an exact name (compared case-insensitively, a trailing dot ignored), * alone, a *.-prefixed suffix (*.example.test matches a.example.test and a.b.example.test, not example.test), or a literal IPv4/IPv6 address (IPv6 as [::1]:8080). A malformed pattern is a SandboxError of load (line 0), not a silent no-match.
  • resolve of a host that no connect pattern names stops the script. The addresses a granted host resolved to become connectable for the ports its pattern allows, for the rest of that sandbox opening.
  • connect to a literal address needs a connect pattern for that address and port, or a prior resolution from a granted host; so a script granted api.example.test:443 cannot reach 10.0.0.1 or api.example.test:22.
  • listen on an address and port that no listen pattern names stops the script. UDP follows the same two grants (this is not in section 10 and the implementer adds it there): UdpSocket bind is a listen, send(to:) and connect are a connect. Operations on a socket already granted (accept, receive, send, close, address) are not checked again.
  • The DNS client of std/network (lookup, Resolver) is not resolve: it talks UDP/TCP to name servers, so under the rules above it is held by connect patterns for the name server's address and port, and systemNameServers (the host's resolver configuration, torb_network_name_servers) stops the script in a sandbox. Both fail closed and are written into section 10; a host that wants a script to look names up grants resolve through a host pattern.
  • No grant means no network: with modules "std/network" and no network, every resolve, connect and listen stops the script; without modules "std/network" (or std/http/std/tls) the script does not load, with the existing message naming the module (SCRIPTS.md section 6). TLS and HTTP add no capability: an https request is a resolve and a connect and is held to the same patterns. Add the matching rows to the error table of SCRIPTS.md section 6 (The script may not connect to `10.0.0.1:80`: no `network connect` pattern names it, same shape for resolve and listen), with the line of the call.
  • A task of the program that runs while a script's sandbox is open (VM.md section 10) is not held to the script's grant: covered by a test like vm-only/sandbox-tasks.trb.

Tests (gates a, plus b as CLAUDE.md asks for runtime changes; the worker states which gates ran):

  • runtime/tests/sandbox_test.c: pattern parsing and matching (exact, *, *.suffix, port *, IPv6 literal, malformed), and the connectable set after a resolution.
  • A conformance program under tests/conformance/vm-only/ that loads scripts over loopback (no outside network): granted listen plus connect on 127.0.0.1 works and exchanges a message through tasks; an ungranted connect, listen and resolve each end the script with the expected SandboxError text and line; the program goes on afterwards and the live-block counter is zero.
  • The same through the native script host (an --embed-vm binary), for example by extending tests/conformance/sandbox-crossing.trb or a sibling, so the grant crossing the boundary is exercised.
  • docs/design/NETWORK.md section 10 and slice 10 ("Done"), docs/design/SCRIPTS.md section 4 (the table row no longer says "built with the VM's tasks") and section 6, docs/standard-library/sandbox.md and docs/language/configuration/the-sandbox.md updated; skills/ regenerated (tools/check-commits.sh and the docs gate clean).
  • New entry points that open or address a socket must fail closed in a sandbox: a short comment at the single place that lists the guarded natives, so #119 and later slices find it.

Seed

No. No syntax change, no new native (the existing torb_network_* natives gain a check inside the runtime), no driver flag, no std name the compiler looks up by string (SandboxCapabilities gains a method; the compiler references the type through the generated script table, not the member). If the work turns out to need one of those, stop and file an owner decision instead of refreshing a seed.

Size and plan

One PR, two Conventional Commits:

  1. feat(runtime): hold the network natives to a sandbox's network grant (runtime/sandbox.c, the network entry points, runtime/tests/sandbox_test.c).
  2. feat(std/sandbox): grant the network to a script (SandboxCapabilities.network, conformance programs, docs and regenerated skills/).

Suggested implementer: opus, high effort (runtime and VM boundary, security-relevant lock). Dependencies: none open; the VM's tasks it waited for are in.

## Goal A host can grant a sandboxed receiver script the network: `Sandbox.load<T>(path) { modules "std/network"; network connect: "api.example.test:443", listen: "127.0.0.1:8080" }`. Inside a sandbox the runtime then lets a script resolve, connect and listen only as the grant names, and stops the script with a `SandboxError` for anything else. VM tasks run inside a sandbox now (see below), so the check can be tested by a script that tries, which is the reason NETWORK.md put the grant off until then. ## Source Pinned at `a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b` (`main`): - [NETWORK.md section 10, "Capabilities"](https://git.torb.dev/torbscript/language/src/commit/a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b/docs/design/NETWORK.md#L696): the two grants, the pattern rule, "TLS and HTTP add no capability", "the default is none". - [NETWORK.md section 11, slice 10](https://git.torb.dev/torbscript/language/src/commit/a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b/docs/design/NETWORK.md#L749) (state "Open"; this issue closes it). - [SCRIPTS.md section 4](https://git.torb.dev/torbscript/language/src/commit/a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b/docs/design/SCRIPTS.md#L149): the capability table already has the network row, "built with the VM's tasks"; section 6 has the error table. - [VM.md section 10](https://git.torb.dev/torbscript/language/src/commit/a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b/docs/design/VM.md#L453): a script's tasks belong to its sandbox, tested by `tests/conformance/vm-only/sandbox-tasks.trb`. This is the "VM's tasks" the slice waited for, and it is built. - [ROADMAP.md Milestone 7](https://git.torb.dev/torbscript/language/src/commit/a022d2a8af1d27cb5a07f2a79be68a2ee5f7d12b/docs/ROADMAP.md#L49) lists the sandbox's network grant as open work. - Fully settled by the records; no entry of CONCEPT "Open Questions" is involved. Nothing in flight collides: #5 (IO gates), #6 (pool benchmark) and #155/#156 (slice F) do not touch `runtime/sandbox.c`, `std/sandbox` or the network natives' entry points. #119 (Unix domain sockets) and #129 (OS network addresses) are open: whoever lands second guards the new entry point the same way (see the last criterion). ## What to build The grant is text, one setting per line (`runtime/sandbox.c`, `SandboxCapabilities.grantText` in `std/sandbox/src/lib.trb`), and the native script host passes the same text across, so no new plumbing: - `std/sandbox`: `var fn network(connect: String = "", listen: String = "")` on `SandboxCapabilities`, mirroring `files`; each call appends patterns, written as `connect\t<pattern>` and `listen\t<pattern>` lines. Several patterns per side are allowed by calling `network` again. - `runtime/sandbox.c`: parse the two words, keep the patterns, and add the checks the network natives consult while a sandbox is open (and not set aside for a program's task, VM.md section 10): resolve by host pattern, connect by address, listen/bind by address. - the `torb_network_*` entry points of `runtime/io.c` call the checks; a refusal is `TORB_SANDBOX_REFUSED` like a refused path, i.e. a stop, never a `NetworkError` the script could catch. - The toolchain's own grant for `project.trb` (`compiler/src/vm/sandbox.trb`) is unchanged: no network. ## Acceptance criteria Semantics, per NETWORK.md section 10 (the implementer writes the exact grammar below into that section, so the record stays the specification): - [ ] A pattern is `host:port`; the port is a decimal number or `*`; the host is an exact name (compared case-insensitively, a trailing dot ignored), `*` alone, a `*.`-prefixed suffix (`*.example.test` matches `a.example.test` and `a.b.example.test`, not `example.test`), or a literal IPv4/IPv6 address (IPv6 as `[::1]:8080`). A malformed pattern is a `SandboxError` of `load` (line 0), not a silent no-match. - [ ] `resolve` of a host that no `connect` pattern names stops the script. The addresses a granted host resolved to become connectable for the ports its pattern allows, for the rest of that sandbox opening. - [ ] `connect` to a literal address needs a `connect` pattern for that address and port, or a prior resolution from a granted host; so a script granted `api.example.test:443` cannot reach `10.0.0.1` or `api.example.test:22`. - [ ] `listen` on an address and port that no `listen` pattern names stops the script. UDP follows the same two grants (this is not in section 10 and the implementer adds it there): `UdpSocket` bind is a `listen`, `send(to:)` and `connect` are a `connect`. Operations on a socket already granted (accept, receive, send, close, `address`) are not checked again. - [ ] The DNS client of `std/network` (`lookup`, `Resolver`) is not `resolve`: it talks UDP/TCP to name servers, so under the rules above it is held by `connect` patterns for the name server's address and port, and `systemNameServers` (the host's resolver configuration, `torb_network_name_servers`) stops the script in a sandbox. Both fail closed and are written into section 10; a host that wants a script to look names up grants `resolve` through a host pattern. - [ ] No grant means no network: with `modules "std/network"` and no `network`, every resolve, connect and listen stops the script; without `modules "std/network"` (or `std/http`/`std/tls`) the script does not load, with the existing message naming the module (SCRIPTS.md section 6). TLS and HTTP add no capability: an `https` request is a resolve and a connect and is held to the same patterns. Add the matching rows to the error table of SCRIPTS.md section 6 (``The script may not connect to `10.0.0.1:80`: no `network connect` pattern names it``, same shape for resolve and listen), with the line of the call. - [ ] A task of the program that runs while a script's sandbox is open (VM.md section 10) is not held to the script's grant: covered by a test like `vm-only/sandbox-tasks.trb`. Tests (gates `a`, plus `b` as CLAUDE.md asks for runtime changes; the worker states which gates ran): - [ ] `runtime/tests/sandbox_test.c`: pattern parsing and matching (exact, `*`, `*.suffix`, port `*`, IPv6 literal, malformed), and the connectable set after a resolution. - [ ] A conformance program under `tests/conformance/vm-only/` that loads scripts over loopback (no outside network): granted `listen` plus `connect` on `127.0.0.1` works and exchanges a message through tasks; an ungranted connect, listen and resolve each end the script with the expected `SandboxError` text and line; the program goes on afterwards and the live-block counter is zero. - [ ] The same through the native script host (an `--embed-vm` binary), for example by extending `tests/conformance/sandbox-crossing.trb` or a sibling, so the grant crossing the boundary is exercised. - [ ] `docs/design/NETWORK.md` section 10 and slice 10 ("Done"), `docs/design/SCRIPTS.md` section 4 (the table row no longer says "built with the VM's tasks") and section 6, `docs/standard-library/sandbox.md` and `docs/language/configuration/the-sandbox.md` updated; `skills/` regenerated (`tools/check-commits.sh` and the docs gate clean). - [ ] New entry points that open or address a socket must fail closed in a sandbox: a short comment at the single place that lists the guarded natives, so #119 and later slices find it. ## Seed No. No syntax change, no new native (the existing `torb_network_*` natives gain a check inside the runtime), no driver flag, no std name the compiler looks up by string (`SandboxCapabilities` gains a method; the compiler references the type through the generated script table, not the member). If the work turns out to need one of those, stop and file an owner decision instead of refreshing a seed. ## Size and plan One PR, two Conventional Commits: 1. `feat(runtime): hold the network natives to a sandbox's network grant` (`runtime/sandbox.c`, the network entry points, `runtime/tests/sandbox_test.c`). 2. `feat(std/sandbox): grant the network to a script` (`SandboxCapabilities.network`, conformance programs, docs and regenerated `skills/`). Suggested implementer: opus, high effort (runtime and VM boundary, security-relevant lock). Dependencies: none open; the VM's tasks it waited for are in.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop). Implements NETWORK.md slice 10; design record is the specification, no collision with CONCEPT "Open Questions". Touches the sandbox and VM tasks (opus/high worker when implemented); seed impact to settle in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued, buffer >= 2 ready)

[loop:torbscript] intake: in scope (author orion, label loop). Implements NETWORK.md slice 10; design record is the specification, no collision with CONCEPT "Open Questions". Touches the sandbox and VM tasks (opus/high worker when implemented); seed impact to settle in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued, buffer >= 2 ready)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T00:16:12Z

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T00:16:12Z
Author
Collaborator

[loop:torbscript] refine: rewrote the body to the definition of ready (goal, pinned sources in NETWORK.md section 10 / slice 10, SCRIPTS.md section 4 and 6, VM.md section 10, testable criteria incl. pattern grammar, UDP/DNS fail-closed, VM and native-host tests; no seed change; one PR as two commits, opus/high), label ready; the VM tasks it waited for are built, no collision with #5/#6/#155/#156, worker usage about 95k tokens, next: implement

[loop:torbscript] refine: rewrote the body to the definition of ready (goal, pinned sources in NETWORK.md section 10 / slice 10, SCRIPTS.md section 4 and 6, VM.md section 10, testable criteria incl. pattern grammar, UDP/DNS fail-closed, VM and native-host tests; no seed change; one PR as two commits, opus/high), label ready; the VM tasks it waited for are built, no collision with #5/#6/#155/#156, worker usage about 95k tokens, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T06:31:58Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T06:31:58Z
argus closed this issue 2026-10-08 02:17:59 +00:00
Author
Collaborator

[loop:torbscript] implement: resumed after a gateway restart; the last push (af21560c) already finished the plan (two commits, leak-holding refusals moved to sandbox-network-refused.trb with a .leaks file, every acceptance criterion addressed in the PR body); no further change needed, check-commits clean, gates a and b ran in forge CI only. PR #167, CI green (success on all jobs, tier A, tier B, all hosts), worker usage ~60k tokens, next: review

[loop:torbscript] implement: resumed after a gateway restart; the last push (af21560c) already finished the plan (two commits, leak-holding refusals moved to sandbox-network-refused.trb with a .leaks file, every acceptance criterion addressed in the PR body); no further change needed, check-commits clean, gates a and b ran in forge CI only. PR #167, CI green (success on all jobs, tier A, tier B, all hosts), worker usage ~60k tokens, next: review
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#7
No description provided.