@namzu/sandbox 13.0.0 → 14.0.0

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 (67) hide show
  1. package/CHANGELOG.md +309 -0
  2. package/README.md +151 -0
  3. package/dist/backends/firecracker/protocol.d.ts +22 -0
  4. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  5. package/dist/backends/firecracker/protocol.js.map +1 -1
  6. package/dist/backends/firecracker/transport.d.ts +104 -9
  7. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  8. package/dist/backends/firecracker/transport.js +139 -13
  9. package/dist/backends/firecracker/transport.js.map +1 -1
  10. package/dist/backends/kubernetes/egress-policy.d.ts +219 -0
  11. package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -0
  12. package/dist/backends/kubernetes/egress-policy.js +314 -0
  13. package/dist/backends/kubernetes/egress-policy.js.map +1 -0
  14. package/dist/backends/kubernetes/index.d.ts +374 -0
  15. package/dist/backends/kubernetes/index.d.ts.map +1 -0
  16. package/dist/backends/kubernetes/index.js +671 -0
  17. package/dist/backends/kubernetes/index.js.map +1 -0
  18. package/dist/backends/kubernetes/k8s-client.d.ts +125 -0
  19. package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -0
  20. package/dist/backends/kubernetes/k8s-client.js +246 -0
  21. package/dist/backends/kubernetes/k8s-client.js.map +1 -0
  22. package/dist/backends/kubernetes/lease.d.ts +119 -0
  23. package/dist/backends/kubernetes/lease.d.ts.map +1 -0
  24. package/dist/backends/kubernetes/lease.js +151 -0
  25. package/dist/backends/kubernetes/lease.js.map +1 -0
  26. package/dist/backends/kubernetes/objects.d.ts +282 -0
  27. package/dist/backends/kubernetes/objects.d.ts.map +1 -0
  28. package/dist/backends/kubernetes/objects.js +156 -0
  29. package/dist/backends/kubernetes/objects.js.map +1 -0
  30. package/dist/backends/kubernetes/privilege-probe.d.ts +136 -0
  31. package/dist/backends/kubernetes/privilege-probe.d.ts.map +1 -0
  32. package/dist/backends/kubernetes/privilege-probe.js +185 -0
  33. package/dist/backends/kubernetes/privilege-probe.js.map +1 -0
  34. package/dist/backends/kubernetes/sandbox.d.ts +123 -0
  35. package/dist/backends/kubernetes/sandbox.d.ts.map +1 -0
  36. package/dist/backends/kubernetes/sandbox.js +299 -0
  37. package/dist/backends/kubernetes/sandbox.js.map +1 -0
  38. package/dist/backends/kubernetes/transport.d.ts +122 -0
  39. package/dist/backends/kubernetes/transport.d.ts.map +1 -0
  40. package/dist/backends/kubernetes/transport.js +197 -0
  41. package/dist/backends/kubernetes/transport.js.map +1 -0
  42. package/dist/backends/kubernetes/workspace.d.ts +381 -0
  43. package/dist/backends/kubernetes/workspace.d.ts.map +1 -0
  44. package/dist/backends/kubernetes/workspace.js +1064 -0
  45. package/dist/backends/kubernetes/workspace.js.map +1 -0
  46. package/dist/index.d.ts +132 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +102 -34
  49. package/dist/index.js.map +1 -1
  50. package/dist/testing/sandbox-conformance.d.ts +193 -0
  51. package/dist/testing/sandbox-conformance.d.ts.map +1 -0
  52. package/dist/testing/sandbox-conformance.js +465 -0
  53. package/dist/testing/sandbox-conformance.js.map +1 -0
  54. package/package.json +5 -4
  55. package/src/backends/firecracker/protocol.ts +27 -0
  56. package/src/backends/firecracker/transport.ts +199 -28
  57. package/src/backends/kubernetes/egress-policy.ts +437 -0
  58. package/src/backends/kubernetes/index.ts +1012 -0
  59. package/src/backends/kubernetes/k8s-client.ts +352 -0
  60. package/src/backends/kubernetes/lease.ts +198 -0
  61. package/src/backends/kubernetes/objects.ts +363 -0
  62. package/src/backends/kubernetes/privilege-probe.ts +261 -0
  63. package/src/backends/kubernetes/sandbox.ts +395 -0
  64. package/src/backends/kubernetes/transport.ts +286 -0
  65. package/src/backends/kubernetes/workspace.ts +1386 -0
  66. package/src/index.ts +257 -35
  67. package/src/testing/sandbox-conformance.ts +667 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,314 @@
1
1
  # @namzu/sandbox
2
2
 
3
+ ## 14.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cd43cfe: `SandboxProviderConfig` gains a real arm for `ACIStandbyPoolBackendConfig` (paired with the `ContainerSandboxLayout` it requires, same as the plain container arm). Previously the exported union only covered `ContainerBackendConfig`, `MicroVMBackendConfig` and `KubernetesBackendConfig`, so `createSandboxProvider({ backend: { tier: 'container', runtime: 'aci-standby-pool', … } })` did not type-check even though the backend was fully implemented and `pickBackend` already dispatched to it internally through two `as unknown as` casts. That call now type-checks with no cast.
8
+
9
+ **Minor, not patch:** this is a backward-compatible widening of an exported input type — every config that type-checked before still does, and the only change is that a config shape the runtime already accepted is now also accepted by the type checker. That is additive public surface (a new union arm a consumer's own type-level code can observe), not an implementation-only correction, so it does not qualify for patch under this repo's rule that patch is reserved for changes that leave the public surface untouched.
10
+
11
+ No runtime behavior changed: the ACI backend's construction, options and defaults are exactly what they were.
12
+
13
+ - f54f6f1: The microVM guest agent can now listen on a TCP port and require a per-instance
14
+ token. Both are opt-in through the environment and both are absent from every
15
+ shipped backend's configuration today, so an existing Firecracker deployment
16
+ behaves exactly as it did: with neither variable set, the agent authenticates
17
+ nothing and listens exactly where it listened before.
18
+
19
+ `NAMZU_AGENT_TCP_PORT` is a third listen mode, after `NAMZU_AGENT_UNIX_PATH`
20
+ and the inherited vsock descriptor and in that order, binding `0.0.0.0` for a
21
+ deployment that reaches the guest over a routed network rather than a
22
+ host-local socket. It fails closed: set with neither token variable it is
23
+ refused at startup, naming both, instead of binding an unauthenticated
24
+ listener. Framing, ops, execution leases, terminals, loopback TCP and
25
+ file IO are unchanged — only the listen address differs. A guest configured
26
+ with none of the three still refuses to start, and the message now names all
27
+ three.
28
+
29
+ `NAMZU_AGENT_BIND_TOKEN` makes every op except `healthz` present that exact
30
+ token in the request envelope, from the first frame of a connection; anything
31
+ else is answered `unauthorized` and the connection is closed before a handler
32
+ runs. Comparison is constant-time over a fixed-width digest, so neither the
33
+ value nor its length is learnable by probing. `NAMZU_AGENT_REQUIRE_TOKEN`
34
+ without a preset token is a fallback that binds to the first token seen and
35
+ refuses every other for the life of the process. `healthz` never requires a
36
+ token and never echoes one, so readiness probing needs no secret. An empty
37
+ `NAMZU_AGENT_BIND_TOKEN` is refused at startup in every mode — that is what a
38
+ downward-API injection looks like when it resolved to nothing.
39
+
40
+ A refused connection is destroyed rather than `end()`ed, so a refused peer can
41
+ no longer keep streaming into the agent's frame buffer over the readable half
42
+ that `end()` leaves open. Alongside it, bounds on what an unauthenticated peer
43
+ may spend before the gate — which cannot run until a whole frame is parsed,
44
+ because the credential rides inside the envelope.
45
+ `NAMZU_AGENT_MAX_FRAME_BYTES` (default 256 MiB) caps the length ANY frame
46
+ header may announce, on every listen mode, where the 8-hex prefix used to allow
47
+ 4 GiB. `NAMZU_AGENT_REFUSAL_FLUSH_GRACE_MS` (default 1s) likewise applies
48
+ everywhere: it is how long a refusal frame may take to reach the wire before
49
+ the socket is destroyed regardless, so a peer that stops reading cannot hold a
50
+ refusal open. `NAMZU_AGENT_MAX_PREAUTH_FRAME_BYTES` (default 8 MiB),
51
+ `NAMZU_AGENT_MAX_PREAUTH_CONNECTIONS` (default 64),
52
+ `NAMZU_AGENT_MAX_PREAUTH_BUFFER_BYTES` (default 32 MiB),
53
+ `NAMZU_AGENT_PREAUTH_IDLE_TIMEOUT_MS` (default 10s) and
54
+ `NAMZU_AGENT_PREAUTH_DEADLINE_MS` (default 10s) apply only in the token
55
+ modes, so the Firecracker path sees none of them; together they bound what an
56
+ unauthenticated peer can make the agent hold to one number rather than to a
57
+ number per connection. One bound needs no variable and applies everywhere: a
58
+ frame header is exactly nine bytes, so a peer streaming bytes with no newline
59
+ in them is refused on the ninth rather than buffered against a newline that
60
+ never arrives. Note what the pre-auth cap
61
+ costs in a token mode: a `write-file` body shares the first frame with the
62
+ token, so that cap is the ceiling on the body — about 6 MiB of file content at
63
+ the default — and a body above it is refused `frame_too_large`, naming the
64
+ limit and the variable to raise.
65
+
66
+ Two of those pre-auth bounds are shaped by a slow loris rather than by a flood,
67
+ and an operator who tunes them should know which is which.
68
+ `NAMZU_AGENT_PREAUTH_IDLE_TIMEOUT_MS` is an idle timer that every byte resets,
69
+ so on its own it retires only a silent connection;
70
+ `NAMZU_AGENT_PREAUTH_DEADLINE_MS` runs from accept, is reset by nothing, and is
71
+ the bound a peer dripping one byte every few seconds actually meets. And a full
72
+ pre-auth pool evicts its **oldest** unauthenticated connection — answering it
73
+ `too_many_unauthenticated_connections` — rather than refusing the arrival, so a
74
+ poolful of squatters can no longer decide that nobody else, `healthz` included,
75
+ gets served. None of this stops a peer that can reach the port from causing
76
+ churn; the NetworkPolicy ingress rule in front of that port is the boundary,
77
+ and these bounds are defence in depth behind it.
78
+
79
+ The guest protocol version is deliberately NOT bumped: `token` is an optional,
80
+ additive envelope field, and the wire is otherwise byte-for-byte what it was.
81
+ Taking this release therefore requires no coupled rollout — no golden image has
82
+ to be rebuilt and no host has to be redeployed in step with it. A deployment
83
+ that wants the new modes turns them on in its own pod or image environment.
84
+
85
+ Read `NAMZU_AGENT_BIND_TOKEN` as an instance credential, not an isolation
86
+ boundary: the agent and the workload share a uid after deprivileging, so a
87
+ workload can read the agent's own environment out of `/proc`. It is
88
+ per-instance for that reason, and the network rule in front of the agent port
89
+ is the boundary it sits behind.
90
+
91
+ One behaviour changes for every existing deployment, Firecracker included, and
92
+ it is the reason to read this entry before upgrading. The `terminal` op used to
93
+ hand its shell the agent's whole environment; it now gets the same scrubbed
94
+ environment an `execute` child has always had, with every `NAMZU_AGENT_*` and
95
+ `NAMZU_SANDBOX_*` variable removed, and so does the `stty` resize helper behind
96
+ it. That closed a hole this release would otherwise have opened — the bind
97
+ token was visible in an interactive shell — and it means a terminal session no
98
+ longer sees the agent's own settings, such as `NAMZU_SANDBOX_WORKSPACE`. A
99
+ workload that needs a value in its terminal passes it in `env` on the
100
+ `openTerminal` call, which still wins over everything else, `TERM` included.
101
+ The bump stays `minor`: nothing exported changes shape, and the environment a
102
+ terminal is handed is guest-internal behaviour rather than a typed API, but it
103
+ is a change a terminal user can observe.
104
+
105
+ - 5604f73: A Kubernetes backend that claims VM-isolated sandboxes out of an [agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) warm pool. New exported types `KubernetesBackendConfig` and `KubernetesClusterAccess`; `SandboxBackendConfig` and `SandboxProviderConfig` each gain an arm for it, so `createSandboxProvider({ backend: { tier: 'microvm', service: 'kubernetes', … } })` type-checks with no cast. Nothing existing changes shape.
106
+
107
+ **Take this upgrade for the new backend, not for a complete one.** This changeset covers acquire, readiness, address resolution and teardown; the execution surface, the acquire-time privilege probe and the lease arrive in the same release under their own changesets, and persistent workspaces (suspend/resume with a block-mode disk) and the cluster manifests follow in later ones. Every other backend is untouched.
108
+
109
+ What it does today, on a cluster running agent-sandbox v1.0.2 with a VM-isolating `RuntimeClass`:
110
+
111
+ - **Warm claim, or a direct Sandbox.** With `warmPoolName`, `create()` POSTs a `SandboxClaim` at that pool and the controller binds an already-running sandbox. Without it, it POSTs a `Sandbox` built from `sandboxTemplateName`'s pod template — necessitated rather than offered, since `SandboxClaim.spec.warmPoolRef` is required and a pool-less claim does not exist in the API.
112
+ - **The claim is pristine.** `spec.env` and `spec.volumeClaimTemplates` are never set, because a claim carrying either is forced to cold-start upstream instead of adopting a pool sandbox. It would still work; it would just stop being fast. Per-sandbox `env`, `memoryLimitMb`, `maxProcesses` and `egress` are therefore refused by name instead of accepted and dropped — set them on the `SandboxTemplate` the pool is built from.
113
+ - **The bound sandbox's own identity.** A pool sandbox keeps the name the pool generated for it, so the backend reads `status.sandbox` back rather than assuming the claim's name; the sandbox `id` is that cluster name, which makes an id in a log line a `kubectl get sandbox` argument.
114
+ - **Nothing left behind.** Every created object carries an absolute `shutdownTime` (default one hour, `claimTtlSeconds`) plus `shutdownPolicy: Delete`, so a host that dies mid-run costs one expiry rather than a leaked sandbox — `ttlSecondsAfterFinished` deliberately is not used, because its timer starts from a `Finished` condition a crashed host never reaches. Every failure on the create path deletes what it created on a separate short budget; an object already gone counts as released.
115
+ - **A per-instance agent credential.** The pod's own `metadata.uid`, read with one `GET` after readiness and delivered to the guest through the downward API. No claim mutation, so the warm path stays pristine.
116
+
117
+ Credentials arrive through `access`: `{ inCluster: true }` reads the projected ServiceAccount volume, and anything else supplies `{ server, ca?, getToken }`. There is no kubeconfig parsing in the package and no new dependency — `@namzu/sandbox` still declares no `dependencies` key.
118
+
119
+ - 437e3d3: The Kubernetes backend now ships the cluster-side half of itself: the guest image, its entrypoint, the `RuntimeClass`/`SandboxTemplate`/`SandboxWarmPool`/`NetworkPolicy`/RBAC manifests, and five scripts that measure the five acceptance criteria against a live cluster — all under `packages/sandbox/k8s/`, which is **not part of the published package** (the `files` array still packs only `dist` and `src`; `npm pack --dry-run` confirms it). None of that is a consumer-visible surface. What IS:
120
+
121
+ **The guest agent now exits cleanly on `SIGTERM` — bumped `minor`, not `patch`, for exactly this.** `k8s/entrypoint.sh` `exec`s straight into `agent/agent.cjs`, so in a real pod the agent is pid 1 of the container's own pid namespace, and Linux leaves a signal whose default action is "terminate" un-applied for pid 1 unless the process installs its own handler. With none registered, an operator deleting a sandbox watched it ride out the pod's full `terminationGracePeriodSeconds` before `SIGKILL` finally landed — every delete paid that tax, cluster-wide, whatever backend dialed the agent. The new handler closes the listener and calls `process.exit(0)` the moment `SIGTERM` arrives. It is not a graceful drain: an in-flight `exec` or an open terminal gets no grace window, the same "gone" a caller already has to handle from a pod the cluster removed out from under it. This is additive guest behavior on a file the Firecracker tier also runs in production — nothing about the vsock/unix paths changes, and `REMOTE_EXECUTION_PROTOCOL_VERSION`/`FIRECRACKER_AGENT_PROTOCOL_VERSION` are untouched — but it is a real, observable change to when a pod actually terminates, which is why this is `minor` rather than `patch`.
122
+
123
+ **Documented for the first time, no behavior change:** a confirmed `exec` cancellation on this backend resolves with the terminal signal/exit code the shared `RemoteExecutionController` observed — the same contract the Firecracker tier's `exec` already honors, now stated on `docs/sdk/kubernetes-sandbox.md` rather than left implicit.
124
+
125
+ Everything else in this change is infrastructure an operator applies by hand — see `packages/sandbox/k8s/README.md` for the apply order and how to run each acceptance script, and `docs/sdk/kubernetes-sandbox.md`'s new deployment section for what each one measures. The five acceptance numbers themselves are not yet in that table; they are gathered by running those scripts against a real Kata cluster, not by this change.
126
+
127
+ - 432db25: The Kubernetes backend's `KubernetesBackendConfig` gains an optional `egress` block: `{ policy: EgressPolicy; networkPolicyName?: string; engine?: 'core' | 'cilium' }` (`engine` defaults to `'core'`). New exported types `KubernetesEgressConfig` and `KubernetesEgressEngine`. Nothing existing changes shape — `egress` is additive and optional, and every Sandbox this backend produces now carries a `sandbox.namzu.ai/template` label it did not carry before, which is additive metadata rather than a behavior change for an existing caller.
128
+
129
+ **What it does.** `deny-all` and `allow-all` translate into a `NetworkPolicy` (egress rules that always leave the cluster's own DNS reachable, even under `deny-all`). `static` and `resolver` — hostname allowlists — are **refused at construction**, before any API call, naming the policy kind and what the cluster needs: core Kubernetes `NetworkPolicy` has no FQDN concept at all. Declaring `engine: 'cilium'` turns that refusal into an emission: a `CiliumNetworkPolicy` with a `toFQDNs` entry for every allowed host. This backend never emits `HTTP_PROXY`/`HTTPS_PROXY` as a substitute for an unenforceable policy — that is the container tier's still-open gap, not repeated here.
130
+
131
+ **Verify, never trust.** This backend never creates the `NetworkPolicy`/`CiliumNetworkPolicy` itself — like the docker backend's network, it is operator-applied. The first `create()` after construction (not `createSandboxProvider`, which still contacts nothing) `GET`s the object named by `networkPolicyName` (default `${sandboxTemplateName}-egress`) and refuses to proceed on a 404 or a shape mismatch, naming the field that is wrong. It runs once per backend and is not cached across a failure, so fixing the cluster and creating again retries it.
132
+
133
+ **Take this upgrade for the label, even without using `egress`.** Every Sandbox this backend creates directly now carries `sandbox.namzu.ai/template: <sandboxTemplateName>` on its pod — agent-sandbox's own controller-owned template label is written only on a Sandbox adopted out of a `SandboxWarmPool`, never on one this backend POSTs directly, so a `NetworkPolicy` an operator writes against a direct Sandbox should select by this new label. A `SandboxWarmPool`'s own `SandboxTemplate` needs the same label added to its `podTemplate.metadata.labels` for a pooled sandbox to match it too — this backend has no path to add it after the fact.
134
+
135
+ RBAC: when `config.egress` is set, the ServiceAccount also needs `get` on `networkpolicies` (`networking.k8s.io`), or `get` on `ciliumnetworkpolicies` (`cilium.io`) under `engine: 'cilium'`.
136
+
137
+ - a3ae83e: The Kubernetes backend now returns a working `Sandbox`, refuses to hand one back until the guest has proved it is deprivileged, and keeps a long run's pod from expiring underneath it.
138
+
139
+ **The execution surface exists.** `exec`, `writeFile`, `readFile`, `listFiles`, `openTerminal` and `openTcpConnection` no longer throw `KubernetesAgentTransportPendingError` — that error is gone, and so is the reason for it. `exec` runs through the same reserve-before-admission controller every other remote backend uses, so an `AbortSignal` terminates the guest process and the peer confirms the termination rather than the host abandoning the wait. `destroy()` kills and awaits every terminal it handed out before releasing the object, which is what makes offering `openTerminal` compliant at all; it is idempotent, and an object something else already reaped counts as released. Every call after it throws `KubernetesSandboxDestroyedError` naming the operation.
140
+
141
+ **`setNetworkPolicy`, `spawnDetached` and `walkFiles` are absent, deliberately.** Egress here is a `NetworkPolicy` on the pool's `SandboxTemplate` and there is no per-running-pod knob, the guest agent has no detached-spawn op, and bounded search is not in this batch. The SDK's contract says a backend that cannot honour an optional method must omit it rather than accept it and quietly do nothing; a test asserts each stays absent.
142
+
143
+ **Every acquire now proves the guest is deprivileged.** Before `create()` resolves, the backend reads `/proc/self/status` through the agent's `execute` op and refuses unless `CapInh`, `CapPrm`, `CapEff` and `CapBnd` are ALL zero and `NoNewPrivs` is 1. Checking `CapEff` alone would pass a container running as uid 0 with the full bounding set. A refusal destroys the instance and rejects, so no handle to an under-hardened sandbox escapes, and the error distinguishes "the probe could not run" (a minimal image with no `cat`) from "the process is privileged". **There is no configuration that turns this off.** If your image's entrypoint does not end with `exec setpriv --reuid --regid --clear-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- node agent.cjs`, or equivalent, `create()` will now reject where it previously returned. The probe carries its own deadline — `min(readyTimeoutMs, 15s)` — so a pod whose agent has wedged (out of memory, an event loop the workload blocked) is refused on your acquire budget rather than held open for the execution controller's five-minute default; budget for a `create()` that can take `readyTimeoutMs` plus that again in the worst case. The probe is a check that the deprivileging happened, not a boundary against a guest that is already compromised — it asks the agent to report its own `/proc/self/status`. The boundary is still the VM and the `NetworkPolicy`.
144
+
145
+ **A handle renews its own lease.** The absolute `shutdownTime` this backend stamps on every object it creates bounds a leak; unrenewed it also bounded the RUN, so a session outliving `claimTtlSeconds` (default one hour) had its pod deleted mid-command. The handle now merge-PATCHes that expiry a full TTL forward every half TTL, jittered ±10%, and `destroy()` stops it. **This adds an RBAC requirement**: the ServiceAccount needs `patch` on `sandboxclaims` and `sandboxes` in the sandbox namespace, alongside the verbs it already needed. A renewal that fails is reported to the new optional `KubernetesBackendConfig.onLeaseRenewalError` and retried on the next tick; each PATCH is bounded on its own clock (a quarter of the interval, capped at 30 seconds), so an API server that accepts a renewal and never answers it is abandoned and retried rather than parking the loop and letting the lease expire in silence; and one that finds the object already deleted stops the loop and marks the handle gone, so later calls throw `KubernetesSandboxGoneError` instead of dialing a pod that no longer exists. A handle you drop without calling `destroy()` keeps renewing for as long as the process lives, so `destroy()` is now load-bearing for cleanup inside a long-lived host.
146
+
147
+ New on the public surface: `KubernetesPrivilegeProbeError` (with `reason`, `PrivilegeProbeFailure` and `ProcStatusPrivileges`), `KubernetesSandboxDestroyedError`, `KubernetesSandboxGoneError`, `KubernetesAgentUnauthorizedError`, `AgentPreauthFrameTooLargeError`, `TCP_PREAUTH_FRAME_LIMIT_BYTES`, and `KubernetesBackendConfig.onLeaseRenewalError`. Removed: `KubernetesAgentTransportPendingError`, which was never exported from the package entry point and could only ever be thrown by a method that now works.
148
+
149
+ One limit worth knowing before you write a large file: every request to the guest dials a fresh connection, so every request is that connection's first — not-yet-authenticated — frame and is bounded by the guest's pre-auth ceiling (8 MiB by default) on every call, not once. A `writeFile` whose base64 body would exceed it throws `AgentPreauthFrameTooLargeError` before dialing, naming the limit; in practice bodies above about 5.9 MiB raw do not fit. Chunking is not implemented — raise `NAMZU_AGENT_MAX_PREAUTH_FRAME_BYTES` in the deployment, or split the write.
150
+
151
+ Every other backend is untouched.
152
+
153
+ - 53c526c: `SandboxAgentHandle` (re-exported from the package's public entry point)
154
+ gains a fourth arm: `{ kind: 'tcp', host, port, token }`. `VsockAgentTransport`
155
+ (also public) gains a new `executeStreamed()` method, and its
156
+ `VsockTransportOptions` gain an optional `onDial` callback. All three
157
+ changes are additive and backward compatible — existing `unix`/`vsock`/`mtls`
158
+ handles, existing `VsockAgentTransport` callers, and the guest protocol are
159
+ untouched (`firecracker/__tests__/transport.test.ts` passes unmodified) — but
160
+ they are genuinely new surface in the compiled `.d.ts`, not yet constructible
161
+ by anything outside `packages/sandbox/src/backends/` until a later workstream
162
+ wires the kubernetes backend up to them.
163
+
164
+ Alongside this, a new (package-internal, not yet exported) `KubernetesAgentTransport`
165
+ in `src/backends/kubernetes/transport.ts` dials the guest agent (`agent/agent.cjs`)
166
+ directly over a routed pod network for the upcoming kubernetes backend.
167
+
168
+ The `tcp` dialer is a plain `net.connect({ host, port })` per call — no
169
+ routing preamble, no ack, no cached socket or IP — so a `host` that is a
170
+ Kubernetes Service FQDN is re-resolved on every request and a resumed
171
+ pod's new address costs nothing extra. The handle's optional `token`
172
+ rides in each request envelope (the credential field the guest agent
173
+ already accepts); a wrong token surfaces as a named
174
+ `KubernetesAgentUnauthorizedError` rather than a generic protocol error.
175
+
176
+ Because every `tcp` request dials a fresh connection, that connection's
177
+ first frame is also the one the guest agent has not authenticated yet,
178
+ so it is bound by the agent's pre-auth frame ceiling (8 MiB by default)
179
+ on every call, not just on first use. This transport now checks an
180
+ outgoing envelope's size against that ceiling BEFORE dialing and throws
181
+ a named `AgentPreauthFrameTooLargeError` naming the limit, instead of
182
+ opening a connection the agent would refuse anyway. Chunking a large
183
+ `write-file` body across multiple frames is a documented follow-up, not
184
+ implemented here.
185
+
186
+ `KubernetesAgentTransport` also accepts an optional `onTiming` callback
187
+ reporting a completed `exec()` call's dial/reserve/execute/drain
188
+ durations (never the token, command, or output), so the kubernetes
189
+ backend's sub-second warm-acquire target can be measured rather than
190
+ assumed.
191
+
192
+ - 50de52b: The Kubernetes backend can now keep a workspace: a sandbox with a block-mode disk that survives being suspended.
193
+
194
+ **New verb, not a new provider.** `createKubernetesWorkspace(config, options)` returns a `KubernetesWorkspace` — the SDK's `Sandbox`, plus `suspend()`, `resume()`, a `suspended` flag, and a `destroy()` that takes `deleteDisk`. It is separate from `createSandboxProvider` because a `SandboxProvider` promises an ephemeral sandbox per run and this promises the opposite; `warmPoolName` is ignored, since a workspace is always a `Sandbox` POSTed directly. `@namzu/sdk`'s `Sandbox` is untouched: no new `SandboxStatus` member, no `suspend?()`/`resume?()` on the shared contract, no `deleteDisk` on the shared `SandboxDestroyOptions`.
195
+
196
+ **`destroy()` keeps the disk.** The API has `operatingMode` and it has DELETE, and nothing in between, so there is no delete-compute-keep-disk verb to offer. `destroy()` and `destroy({ deleteDisk: false })` SUSPEND and leave the object standing; only `destroy({ deleteDisk: true })` DELETEs the Sandbox and cascades to its Pod, Service and PVC. The default is the non-destructive one because `destroy()` is what a `finally` block calls. No failure path ever deletes: a create or resume that fails after the object exists suspends it and rethrows — including a create that POSTed the object itself, because two processes can be coming up on one name at once and the one that got the `201` would otherwise delete the disk the other just adopted. The named cost: a failed create can leave one suspended `Sandbox` and its PVC standing, which nothing reaps and which the caller finds again under the same name.
197
+
198
+ **A workspace carries no lease.** Unlike a task sandbox it gets no `shutdownTime`, no `shutdownPolicy: Delete` and no renewal loop. An expiry on a workspace is a timer that deletes your files, and a renewal loop makes keeping them conditional on a host process staying up. The trade is explicit: **nothing reaps a workspace you abandon** — the PVC stands until someone calls `destroy({ deleteDisk: true })` or deletes the Sandbox by hand.
199
+
200
+ **The disk is fixed at creation and must be `volumeMode: Block` — every entry of it.** `Sandbox.spec.volumeClaimTemplates` is CEL-immutable and a `SandboxClaim` carrying one is forced to cold-start, so "claim a warm diskless sandbox and attach a disk later" is not expressible in this API; resizing is out of scope for the same reason. The `SandboxTemplate` a workspace is built from must declare at least one `volumeClaimTemplates` entry, every entry must be `Block`, and every entry must be claimed through a container's `volumeDevices` rather than `volumeMounts`. Anything else throws the new `KubernetesWorkspaceDiskError` before anything is created — each refused shape otherwise WORKS: no disk gives you a sandbox whose files vanish on the first suspend, and a `Filesystem` PVC under a VM-isolating RuntimeClass reaches the guest over a filesystem passthrough that pays a round trip per file operation, so a dependency-tree walk is several times slower and nothing fails.
201
+
202
+ **`config.egress` applies to a workspace too.** `createKubernetesWorkspace` runs the same two steps a provider `create()` runs: a `static`/`resolver` hostname allowlist with no FQDN-capable `engine` is refused synchronously, before any request, and the `NetworkPolicy` an operator applied is fetched and matched against the translation before anything is created. It is checked against the template the workspace is built from (`options.sandboxTemplateName`, falling back to `config.sandboxTemplateName`), because that name is the pod label the policy's `podSelector` matches — a deployment with a separate workspace template needs a policy object for it (`<workspace template>-egress` by default), and the task template's does not cover a workspace pod. Unlike the provider's once-per-backend check, this one runs on every call.
203
+
204
+ **A suspend waits for the pod, and only the pod.** `suspend()` resolves once the pod is gone or in a terminal phase — not on the Sandbox's `Suspended` condition, which upstream documents as lingering True after a resume, and not on a `deletionTimestamp`, which is set while the guest is still running and still writing to the disk. A pod that outlives `readyTimeoutMs` rejects with the new `KubernetesWorkspaceSuspendTimeoutError` rather than resolving early. A call already in flight when `suspend()` starts is not cancelled: it fails at the transport, not with the suspended error.
205
+
206
+ **A state is recorded when the cluster confirms it, never before.** Both verbs are idempotent by early-returning on a recorded state, so the moment that record is written decides what happens to a failure. `suspended` is written after the patch lands AND the pod is observed stopped; `deleted` after the DELETE resolves, or reports the object already gone. A request that fails leaves the state it found and rethrows, so you can retry: a refused suspend patch leaves the workspace running and still serving calls, a suspend whose pod outlived the wait admits no call but is not recorded as finished, and a failed DELETE does not answer your retry "already deleted" while the `Sandbox`, its pod and its PVC stand on the cluster with nothing left that would remove them. Concurrency is covered the other way round, with a single flight per verb: a second `suspend()` or `destroy()` arriving mid-transition awaits the one in progress instead of sending a second request into the window the deferred record opens, and `destroy()` with no options shares the suspend's flight because it is a suspend — the shared request running under the FIRST caller's `signal`, since that is what sharing one request means. `destroy()` is idempotent across the two shapes as well as within each: a plain `destroy()` on a workspace already removed by `destroy({ deleteDisk: true })` is a no-op in either admission order, because `destroy()` is what a `finally` block calls and what it asks for has happened; an explicit `suspend()` on a deleted workspace still throws.
207
+
208
+ **Nothing but `deleteDisk: true` deletes — including the path you never call.** When an execution's cancellation cannot be confirmed (a wedged agent, a partitioned pod, the `cancel-execution` window closing with no answer), the shared execution controller retires the pod that command was left in. On a task sandbox retiring means DELETEing the object, correctly — it is disposable and its disk is scratch. A workspace is retired by the same `operatingMode: Suspended` patch `suspend()` sends: the `exec()` still rejects, carrying `retirement: { accepted: true }` once that patch lands (`accepted: false`, with the error, when it does not), the workspace then reads `suspended: true` and admits nothing, and `resume()` brings up a fresh pod on the same disk. A `destroy({ deleteDisk: true })` whose DELETE fails leaves the same shape — session torn down, nothing admitted, `suspended: true` — because neither the delete nor a suspend reached the cluster, so both `resume()` and a retried delete are open to you.
209
+
210
+ **A resume rebuilds the address and the token.** A resumed pod keeps the sandbox's name and gets a new uid and a new IP, so `resume()` re-resolves the address, re-reads the bind token and rebuilds the transport, skipping any pod carrying a `deletionTimestamp` or in a terminal phase — while the outgoing pod terminates, a `GET` by name can still answer with it and a selector list can return it beside the new one. `Ready` is not a transition signal either — the controller leaves it standing across a resume the way it leaves `Suspended` standing — so the uid is POLLED under `readyTimeoutMs` until a live pod with a uid different from the one the last landed suspend patch retired is found, rather than read once from a status that has not caught up. That covers a resume issued straight after a suspend whose pod outlived its own wait, which arrives mid-drain with no live pod of that name to read at all. Every patch that lands is recorded the same way — an explicit `suspend()`, the retirement above, and the cleanup after a failed create or resume, which swallows its own failure and therefore records only when the request actually came back; a pod nobody asked the controller to remove has no replacement to wait for, and excluding it would time out a resume whose workspace was perfectly usable. What visibly moves depends on the Service: the token is always new, the pod IP always changes, and a `status.serviceFQDN` address does not, because the Service outlives the pod. The acquire-time privilege probe runs again on every resume. Between a suspend and a resume every call throws the new `KubernetesWorkspaceSuspendedError` and issues no dial, because the Service outlives the pod and a dial would hang on a connect timeout that names nothing. `status` reports `destroyed` while suspended — `SandboxStatus` has no suspended member — and `suspended` is what tells the recoverable state from the final one.
211
+
212
+ **Calling it twice reattaches — and what is adopted is checked.** The Sandbox is named `namzu-ws-<workspaceId>`, so a second process finds the same workspace; a create that collides adopts the existing object and resumes it if it was asleep. Because an adopt is handed an object this call did not build, the object is checked against the configuration before it is woken: it must carry a block disk, its pod template must carry `sandbox.namzu.ai/template` for the template this call builds from, and — when `runtimeClassName` is configured — it must already run under that class. A disagreement throws the new `KubernetesWorkspaceMismatchError` and nothing is patched, woken or dialed. The label check runs whether or not `config.egress` is set, since that label is what a policy's `podSelector` matches and adopting an object built from another template would hand back a pod the verified policy does not select; the RuntimeClass check is there because the privilege probe cannot see a missing VM boundary (`/proc/self/status` reads the same under Kata and under runc). The fix is to point the workspace at the template it was built from, or to delete the Sandbox — which takes its disk with it — and create it again.
213
+
214
+ A `workspaceId` that is not already a legal DNS-1123 label is refused rather than sanitised, because two ids that sanitise to one name would silently share one disk. It is a name and not a lock: two host processes can adopt one running workspace, and either one's `destroy()` suspends the pod the other is executing in.
215
+
216
+ **Fixed on the task path, in the same change:** a pool-less `create()` copied the `SandboxTemplate`'s `podTemplate` and dropped its `volumeClaimTemplates`, so a task template declaring a disk produced a healthy Sandbox with no disk and a container naming a volume that did not exist. Both paths now copy the template's `volumeClaimTemplates` verbatim. If you have been working around that by declaring no disk on a task template, nothing changes; if you declared one and wondered where it went, it now arrives.
217
+
218
+ New on the public surface: `createKubernetesWorkspace`, `KubernetesWorkspace`, `KubernetesWorkspaceOptions`, `KubernetesWorkspaceDestroyOptions`, `KubernetesWorkspaceTransitionOptions`, `KubernetesWorkspaceDiskError`, `KubernetesWorkspaceMismatchError`, `KubernetesWorkspaceSuspendTimeoutError`, `KubernetesWorkspaceSuspendedError`. RBAC is unchanged — a workspace uses verbs the task path already needed. Every other backend is untouched.
219
+
220
+ - a70c936: A `Sandbox` contract conformance suite: `defineSandboxConformance` (`packages/sandbox/src/testing/sandbox-conformance.ts`) asserts `exec`'s exit codes and streamed output, the `AbortSignal` contract (the process is genuinely terminated, never a resolved result that reads as an unaborted success), a `writeFile`/`readFile` round trip including binary content, `listFiles`, `openTerminal` ownership on `destroy()`, `openTcpConnection` to guest loopback and its refusal of a non-loopback host, destroy idempotence, and every call failing once destroyed. It takes its `describe`/`it`/`expect` and a factory producing a fresh `Sandbox` as arguments, the same shape `@namzu/sdk/testing`'s checkpoint-store and provider-driver suites already use, so `@namzu/sandbox` gains no test dependency from shipping it and a caller can run it against a recording harness.
221
+
222
+ **New exported surface, within the package only.** `@namzu/sandbox` has no `testing` subpath in its published `exports` map, and this change does not add one — that is a deliberate, separate decision. A caller inside this monorepo imports `defineSandboxConformance` by relative path (`src/testing/sandbox-conformance.js`), exactly as the two new test files below do. It is minor rather than patch because it is new, intentionally-public TypeScript surface a consumer with access to the package's source can import and depend on, even though nothing in `@namzu/sandbox`'s npm entry point changes shape.
223
+
224
+ **Proven against two backends, not one.** `packages/sandbox/src/backends/kubernetes/__tests__/conformance.test.ts` and `packages/sandbox/src/backends/firecracker/__tests__/conformance.test.ts` both run the identical suite — the kubernetes backend over a real `agent/agent.cjs` on a loopback TCP socket, Firecracker over the same agent on its existing unix-domain-socket fixture — which is what makes it a contract suite rather than one backend's tests wearing a new name. `packages/sandbox/src/testing/__tests__/conformance-fails-a-broken-sandbox.test.ts` is the suite's own negative test: three deliberately broken `Sandbox`s (resolves `exec` after abort, `destroy()` leaves a terminal running, `readFile` returns corrupted bytes) each fail it by name.
225
+
226
+ Nothing existing changes shape — every other export, every shipped backend's behavior, is untouched.
227
+
228
+ ### Patch Changes
229
+
230
+ - 4b0e7ad: `defineSandboxConformance`'s `openTcpConnection` positive case now starts its echo listener INSIDE the guest, through `openTerminal`, instead of on the orchestrator/test process's own loopback. The old fixture only ever proved anything for a backend whose "guest" happened to share that loopback with the test process (a Firecracker unit test over a local socket, a fake-agent-in-process kubernetes test) — it could never pass against a real remote sandbox, which cannot dial the orchestrator's loopback at all. Confirmed in-cluster: this case now passes against a live kubernetes backend acquisition on a real kind cluster, where it previously failed with `connect ECONNREFUSED`.
231
+
232
+ Two new optional fields on `SandboxConformanceOptions` — `guestCanRunNode` and `guestListenerCommand` — let a backend whose guest cannot run a listener this way skip the case with a stated reason (its own title) rather than fail spuriously; both default to the existing behavior (node is assumed available, since every shipped backend's guest agent already runs on node), so no existing caller of `defineSandboxConformance` needs to change anything.
233
+
234
+ Patch, not minor: this module has no `testing` subpath in `@namzu/sandbox`'s own `exports` map (see the file's own doc comment) — a caller reaches it only by relative path within the monorepo, as `backends/kubernetes/__tests__/conformance.test.ts` and `backends/firecracker/__tests__/conformance.test.ts` already do — so this is not yet public surface, and the added options are additive and optional regardless.
235
+
236
+ Also corrects a self-contradictory doc comment on `Sandbox.openTerminal` (`@namzu/sdk`): it told an implementer to both "throw" and "omit" for a guest that cannot provide one. It now says only "omit", matching `Sandbox.openTcpConnection`'s own wording and this suite's documented skip-if-unavailable convention. Comment-only; no type or behavior changed.
237
+
238
+ - 13d01db: Adds an internal Kubernetes API client (`backends/kubernetes/k8s-client.ts`, not yet exported from the package entrypoint) for an in-progress Kubernetes/Kata sandbox backend. It speaks the API server with bare `fetch`, falling back to `node:https` only when a custom cluster CA is supplied, and bootstraps in-cluster credentials straight from the projected ServiceAccount volume — the same zero-dependency pattern the ACI and Firecracker backends already use. `@namzu/sandbox` still declares no `dependencies` key.
239
+ - 77adb50: Fix both the kubernetes/Firecracker guest agent (`agent/agent.cjs`) and the
240
+ container-tier HTTP worker (`worker/server.js`) reporting a cancelled
241
+ `exec()` as a clean, unaborted-looking success when the target process
242
+ ignores `SIGTERM` but happens to finish on its own before the cancel grace
243
+ window elapses. Both peers' `terminateAndConfirm` only escalated to
244
+ `SIGKILL` if the owned process group was still alive at the end of that
245
+ window, with nothing checking that the exit was actually caused by the
246
+ signal — so an ignoring process whose natural runtime was under the grace
247
+ period ran to completion untouched, in violation of
248
+ `SandboxExecOptions.signal`'s contract ("must terminate the owned process
249
+ ... never silently ignore the signal and let the command run to
250
+ completion"). This is the same mechanism on both transports, found on the
251
+ guest agent first (issue #469's kind conformance run) and confirmed to
252
+ exist verbatim in the worker once looked for.
253
+
254
+ The default (previously `2000`ms on both) is now `250`ms for
255
+ `NAMZU_AGENT_CANCEL_GRACE_MS` (agent) and `NAMZU_SANDBOX_CANCEL_GRACE_MS`
256
+ (worker) — comfortably under the shared conformance suite's adversarial
257
+ fixture (a command that finishes on its own in ~400ms) while still enough
258
+ for a fast, well-behaved SIGTERM handler's cleanup. A deployment that
259
+ genuinely needs a longer window for cooperative shutdown sets either
260
+ variable explicitly; both were already, and remain, overridable. A new
261
+ regression test on each transport deliberately leaves its own grace
262
+ variable unset — the one thing every other suite on that transport
263
+ overrides — and fails against the old default, passes against the new one.
264
+
265
+ **Defect 2, found alongside the agent fix, is now MITIGATED for the
266
+ Kubernetes backend's shipped image, not merely documented:** in a real pod
267
+ the agent used to run as the container's PID 1 with no subreaper
268
+ (`packages/sandbox/k8s/entrypoint.sh` `exec`ed straight into it), so a
269
+ background job forked by a cancelled `sh -c` command (`... &`) reparented
270
+ to the agent on `SIGKILL` and was never reaped — Node's `child_process`
271
+ only `waitpid()`s the children it spawned itself — running that
272
+ cancellation out the full `RemoteExecutionController` cancel-confirm window
273
+ (8s by default) and tearing the sandbox down instead of confirming.
274
+ `k8s/entrypoint.sh` now execs into `tini` (installed in `k8s/Dockerfile`)
275
+ as the container's real PID 1 and subreaper, with the guest agent as its
276
+ child; `tini` reaps the orphan and forwards `SIGTERM` to the agent exactly
277
+ as before. **A host building its own image from `agent.cjs` directly rather
278
+ than from `k8s/Dockerfile` must still provide its own subreaper as PID 1**
279
+ — this fix lives in the shipped image, not in the agent itself, since
280
+ `agent.cjs` cannot know what pid it is. Root-caused with live evidence in
281
+ `research/k8s-sandbox/kind-e2e-results.md` ("Defect 2", `2026-09-16`) and
282
+ re-verified in-cluster against the new image in
283
+ `research/k8s-sandbox/abort-case-recheck-results.json`.
284
+
285
+ **Patch, not minor or major:** `agent/agent.cjs` and `worker/server.js` are
286
+ guest/worker files that run INSIDE a sandbox or container, never imported
287
+ by a consumer of the published `@namzu/sandbox` tarball (`npm pack
288
+ --dry-run` packs only `dist` and `src`); the k8s image's `Dockerfile` and
289
+ `entrypoint.sh` are likewise deployment artifacts, not the package's own
290
+ `exports`. Both defaults that changed are guest-/worker-internal timings
291
+ with no public type or exported symbol affected, and both changes narrow
292
+ when a cancellation escalates to `SIGKILL` and add a real subreaper —
293
+ strictly tightening what `SandboxExecOptions.signal`'s contract already
294
+ promised, never loosening it, so no caller-visible behavior a consumer
295
+ could have depended on gets worse.
296
+
297
+ - Updated dependencies [68e535b]
298
+ - Updated dependencies [a9e4b19]
299
+ - Updated dependencies [a54dc71]
300
+ - Updated dependencies [86a3818]
301
+ - Updated dependencies [03630cd]
302
+ - Updated dependencies [f33c62b]
303
+ - Updated dependencies [a8df193]
304
+ - Updated dependencies [8bfe291]
305
+ - Updated dependencies [6ae4072]
306
+ - Updated dependencies [dd8702d]
307
+ - Updated dependencies [92ab1d9]
308
+ - Updated dependencies [e6d6d1e]
309
+ - Updated dependencies [7ca8c7d]
310
+ - @namzu/sdk@40.0.0
311
+
3
312
  ## 13.0.0
4
313
 
5
314
  ### Patch Changes
package/README.md CHANGED
@@ -84,6 +84,24 @@ Concurrent destroy and automatic-retirement calls share one checked teardown;
84
84
  Docker removal is never reported as accepted after a non-zero or aborted
85
85
  `docker rm -f`.
86
86
 
87
+ **The SIGTERM → SIGKILL grace window.** Both peers implement the same
88
+ mechanism, deliberately kept textually parallel so a future reader sees they
89
+ are one design, not two: `SIGTERM` the owned process group, wait for it to go
90
+ quiet, and escalate to `SIGKILL` only if it is still alive at the end of a
91
+ grace window — with nothing checking that a quiet group went quiet BECAUSE of
92
+ the signal rather than by finishing on its own. A command that ignores
93
+ `SIGTERM` but happens to complete within the window therefore runs to
94
+ completion untouched and reports back as a clean, unaborted-looking success.
95
+ The container worker's `NAMZU_SANDBOX_CANCEL_GRACE_MS` and the Firecracker/
96
+ kubernetes guest agent's `NAMZU_AGENT_CANCEL_GRACE_MS` both default to `250`
97
+ (previously `2000` on both — issue #469's kind conformance run caught this on
98
+ the agent transport first; the identical worker-side race was fixed in the
99
+ same change once found). 250ms is comfortably under the shared conformance
100
+ suite's adversarial fixture (a command that finishes on its own in ~400ms)
101
+ while still enough for a fast, well-behaved `SIGTERM` handler's own cleanup;
102
+ a deployment that genuinely needs a longer cooperative-shutdown window sets
103
+ either variable explicitly.
104
+
87
105
  Every worker and microVM guest publishes its wire-protocol version in the
88
106
  readiness response. The host admits only the exact version implemented by its
89
107
  release; missing, older, and newer versions fail before a sandbox handle or
@@ -101,6 +119,139 @@ previous host and golden-image pair available together for rollback. Rolling
101
119
  back only one side is intentionally rejected at readiness, so a mismatched
102
120
  guest never accepts work under an unverified wire contract.
103
121
 
122
+ ## Guest agent transports and the per-instance token
123
+
124
+ The microVM guest agent picks its listen socket from the environment, in a
125
+ fixed order: `NAMZU_AGENT_UNIX_PATH` for a unix-domain socket, then an
126
+ inherited descriptor for the vsock bridge named by `NAMZU_AGENT_VSOCK_PORT`,
127
+ then `NAMZU_AGENT_TCP_PORT` for a TCP listener on `0.0.0.0`. The third mode is
128
+ for a deployment that reaches the guest over a routed network — one sandbox per
129
+ pod on a container orchestrator — instead of over a host-local socket. Framing,
130
+ ops, execution leases, terminals, loopback TCP and file IO are identical on all
131
+ three; only the listen address differs. A guest whose environment configures
132
+ none of them still refuses to start, naming all three. `NAMZU_AGENT_TCP_PORT=0`
133
+ binds an ephemeral port, which is what the suites use; a deployment names a
134
+ fixed port, because nothing in front of the guest can be configured to reach a
135
+ port that is only chosen at startup.
136
+
137
+ A routed listener is reachable by whatever the network admits, so that
138
+ deployment also gives the agent a per-instance credential. With
139
+ `NAMZU_AGENT_BIND_TOKEN` set, every op except `healthz` must present exactly
140
+ that token in its request envelope, from the first frame of the connection; a
141
+ missing, empty or different token is answered `unauthorized` and the connection
142
+ is closed before any handler runs. The token is compared in constant time
143
+ against a fixed-width digest, so neither its value nor its length is learnable
144
+ by probing. `NAMZU_AGENT_REQUIRE_TOKEN` is the fallback for a deployment that
145
+ cannot inject a token: the agent binds to the first token it is shown and
146
+ refuses every other one for the life of the process. With neither variable set
147
+ the agent authenticates nothing, which is what the host-local vsock and unix
148
+ transports have always done and what they keep doing. `healthz` never requires
149
+ a token and never echoes one, so readiness probing needs no secret and leaks
150
+ nothing beyond liveness and the protocol version.
151
+
152
+ The TCP mode fails closed. `NAMZU_AGENT_TCP_PORT` set with neither
153
+ `NAMZU_AGENT_BIND_TOKEN` nor `NAMZU_AGENT_REQUIRE_TOKEN` is refused at startup,
154
+ naming both, rather than binding an unauthenticated listener on `0.0.0.0`; the
155
+ unix and inherited-descriptor modes keep requiring no token, because their
156
+ control channel is host↔guest only. `NAMZU_AGENT_BIND_TOKEN` set to the empty
157
+ string is refused at startup in **every** mode: that is the shape a
158
+ downward-API injection takes when it resolved to nothing, and honouring it
159
+ would open precisely the hole the variable was set to close.
160
+
161
+ Because the credential rides inside the request envelope, the gate cannot run
162
+ until a whole frame has been parsed — so what an unauthenticated peer may spend
163
+ in that window is bounded rather than trusted, and in the token modes only:
164
+
165
+ | Variable | Default | What it bounds |
166
+ |---|---|---|
167
+ | `NAMZU_AGENT_MAX_FRAME_BYTES` | 256 MiB | The largest length any frame header may announce, on every listen mode. The 8-hex prefix otherwise permits 4 GiB, which the reader used to honour. Sized for the largest frame the host legitimately writes: a `write-file` carries the whole base64 body in one envelope. |
168
+ | `NAMZU_AGENT_MAX_PREAUTH_FRAME_BYTES` | 8 MiB | The same ceiling for a connection that has not yet presented the token, clamped to the one above. Token modes only. It is also the `write-file` ceiling on a token path — see below. |
169
+ | `NAMZU_AGENT_MAX_PREAUTH_CONNECTIONS` | 64 | How many connections may be unauthenticated at once. Token modes only. Every bound above is per connection, so without this one they could be paid again on the next connection. A full pool evicts its **oldest** unauthenticated member and serves the arrival — see below for why that direction. |
170
+ | `NAMZU_AGENT_MAX_PREAUTH_BUFFER_BYTES` | 32 MiB | What all unauthenticated connections may buffer **between them**, never less than one pre-auth frame. Token modes only. The count above bounds sockets; this bounds the heap behind them, and the heap is what runs out first. |
171
+ | `NAMZU_AGENT_PREAUTH_IDLE_TIMEOUT_MS` | 10000 | How long a connection may stay unauthenticated while **quiet**. Every byte received resets it, so it retires the connection that says nothing, not the one that says too little. Token modes only, cleared the moment a connection authenticates. |
172
+ | `NAMZU_AGENT_PREAUTH_DEADLINE_MS` | 10000 | How long a connection may stay unauthenticated **at all**, measured from accept and reset by nothing. Token modes only, cleared the moment a connection authenticates, so no long-lived terminal, `tcp-connect` or streaming `execute` is ever measured against it. |
173
+ | `NAMZU_AGENT_REFUSAL_FLUSH_GRACE_MS` | 1000 | How long a refusal frame may take to reach the wire before the socket is destroyed anyway. Every listen mode. A backstop against a peer that has stopped reading, not a budget anything normally spends. |
174
+
175
+ So the most an unauthenticated peer can make the agent hold is
176
+ `NAMZU_AGENT_MAX_PREAUTH_BUFFER_BYTES`, spread over at most
177
+ `NAMZU_AGENT_MAX_PREAUTH_CONNECTIONS` sockets, and both are tunable against the
178
+ pod's memory limit. Resident memory settles somewhat above that while the
179
+ allocator catches up; what it does not do is keep climbing. Note what a shared
180
+ budget means when it is exhausted: the connection refused is whichever one asks
181
+ next, which may be a legitimate caller rather than the peer holding the budget.
182
+ That is the trade a global bound makes — a refused request is recoverable, an
183
+ exhausted pod is not.
184
+
185
+ A header above either ceiling is answered `frame_too_large`, naming the
186
+ announced length, the limit, and the variable that governs it, because a caller
187
+ told only a number cannot tell which of the two ceilings it hit. A fourth bound
188
+ needs no variable: a frame header is exactly nine bytes, eight hex digits and a
189
+ newline, so a peer streaming bytes that contain no newline at all is refused on
190
+ the ninth of them rather than buffered against a newline that is never coming.
191
+ A refused connection — for any of these, or for `unauthorized` — is
192
+ **destroyed**, not `end()`ed: ending a socket closes only its writable half, so
193
+ a refused peer used to be able to keep streaming into the agent's frame buffer
194
+ for as long as it liked.
195
+
196
+ Two of those bounds exist because the others do not answer a slow loris, and in
197
+ one earlier shape made each other worse. An idle timeout is reset by every
198
+ byte, so a peer that trickles one byte every few seconds stays unauthenticated
199
+ for as long as it cares to; `NAMZU_AGENT_PREAUTH_DEADLINE_MS` is what bounds
200
+ it, because it runs from accept and nothing resets it. And a full pool that
201
+ refused the **newest** connection handed exactly those peers the power to
202
+ decide who else got served: enough of them locked out every later caller,
203
+ including the credential-exempt `healthz` probe that a readiness check cannot
204
+ do without. So a full pool evicts its oldest unauthenticated member instead,
205
+ answers it `too_many_unauthenticated_connections`, and serves the arrival — the
206
+ oldest unauthenticated connection being, by construction, the one that has had
207
+ the longest to present a token and has not.
208
+
209
+ What none of this does is stop a peer that can reach the port from causing
210
+ churn. It can still open connections, hold slots until the deadline, and make
211
+ the agent evict and re-accept; what it cannot do is hold a slot indefinitely or
212
+ starve a probe. That residual is deliberate, and it is the division of labour
213
+ this design rests on: the NetworkPolicy ingress rule in front of the agent port
214
+ is the boundary that decides who may reach it at all, and the token and these
215
+ bounds are defence in depth behind it, for a peer already inside that rule.
216
+
217
+ One consequence is worth naming, because it is the price of putting the
218
+ credential in the envelope rather than in a handshake: a `write-file` body
219
+ travels in the same first frame as the token, so in a token mode the pre-auth
220
+ cap **is** the ceiling on that body — about 6 MiB of file content at the
221
+ default, since the body travels base64-encoded. A deployment that writes larger
222
+ files raises `NAMZU_AGENT_MAX_PREAUTH_FRAME_BYTES` to suit, trading pre-auth
223
+ buffer budget for body size. Nothing on the Firecracker path is affected: no
224
+ token mode is active there, so no pre-auth cap applies and a `write-file` frame
225
+ of any size up to `NAMZU_AGENT_MAX_FRAME_BYTES` is accepted exactly as before.
226
+
227
+ None of this is a wire change. `token` is an optional envelope field, so the
228
+ guest protocol version is deliberately unchanged and no host and no golden
229
+ image has to roll together with this release.
230
+
231
+ What the token is not: a boundary against the sandbox's own workload. Once the
232
+ image entrypoint deprivileges, the agent and the workload share a uid, so a
233
+ workload process can read the agent's own `/proc/<pid>/environ`. It is
234
+ per-instance for exactly that reason — a workload that steals its own
235
+ instance's token gains nothing it does not already have inside that instance,
236
+ and there is no shared pool secret whose theft would reach the other instances.
237
+ The boundary that keeps other tenants out is the network rule in front of the
238
+ agent port; the token is defence in depth behind it. What the guest does
239
+ guarantee is narrower and exact: every process it starts to serve a request —
240
+ an `execute` command, a `terminal` shell, the resize helper behind that
241
+ terminal — is handed an environment stripped of every `NAMZU_AGENT_*` and
242
+ `NAMZU_SANDBOX_*` variable, so the token and the agent's own configuration
243
+ never enter the workload's environment through the environment it is given.
244
+ Reading them out of `/proc` is the exposure above, and it is why the token is
245
+ per-instance.
246
+
247
+ That scrub is a **behaviour change** for the `terminal` op, not only a new
248
+ guarantee. A terminal shell used to be handed the agent's whole `process.env`;
249
+ it now gets the scrubbed environment an `execute` child has always had, and so
250
+ does the `stty` resize helper behind it. A terminal session therefore no longer
251
+ sees the agent's own settings — `NAMZU_SANDBOX_WORKSPACE` among them. A
252
+ workload that needs a value in its terminal passes it in `env` on the
253
+ `openTerminal` call, which still wins over everything else, `TERM` included.
254
+
104
255
  ## Firecracker workspace channels
105
256
 
106
257
  The Firecracker backend exposes two optional same-sandbox channels. Call
@@ -15,6 +15,28 @@
15
15
  * these data events. HTTP uses endpoints; the framed guest uses dedicated ops.
16
16
  */
17
17
  import type { SandboxExecOptions, SandboxExecResult } from '@namzu/sdk';
18
+ /**
19
+ * The per-instance credential a framed request may present, mixed into
20
+ * the request envelope alongside its `op`.
21
+ *
22
+ * Absent on the vsock and unix transports: that control channel is
23
+ * host↔guest only and never traverses guest egress, so the guest agent
24
+ * authenticates nothing there and this field is simply never written.
25
+ * A guest reached over a ROUTED network has no such boundary, so it is
26
+ * started with a per-instance token (`NAMZU_AGENT_BIND_TOKEN`, fed the
27
+ * pod's own identity by its orchestrator) and refuses every op but
28
+ * `healthz` that does not present exactly that value — from the first
29
+ * frame, since the agent dispatches a connection on its first frame and
30
+ * there is no handshake to defer the check to.
31
+ *
32
+ * The field is OPTIONAL and additive, so a host that never sets it
33
+ * speaks the same wire it always did and
34
+ * `FIRECRACKER_AGENT_PROTOCOL_VERSION` is deliberately unchanged: this
35
+ * needs no coupled golden-image and host rollout.
36
+ */
37
+ export interface AgentRequestCredential {
38
+ readonly token?: string;
39
+ }
18
40
  /**
19
41
  * `/execute` request body. Identical field set to the HTTP worker's
20
42
  * `handleExecute` body (`command`, `args`, `cwd`, `env`, `stdin`,
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/backends/firecracker/protocol.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/backends/firecracker/protocol.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/backends/firecracker/protocol.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/backends/firecracker/protocol.ts"],"names":[],"mappings":"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"}