@namzu/sandbox 1.1.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.
- package/CHANGELOG.md +474 -0
- package/LICENSE.md +110 -0
- package/README.md +148 -0
- package/dist/backends/aci-standby-pool/index.d.ts +104 -0
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -0
- package/dist/backends/aci-standby-pool/index.js +425 -0
- package/dist/backends/aci-standby-pool/index.js.map +1 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +40 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts.map +1 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +157 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js.map +1 -0
- package/dist/backends/docker/index.d.ts +118 -0
- package/dist/backends/docker/index.d.ts.map +1 -0
- package/dist/backends/docker/index.js +645 -0
- package/dist/backends/docker/index.js.map +1 -0
- package/dist/backends/firecracker/__tests__/backend.test.d.ts +13 -0
- package/dist/backends/firecracker/__tests__/backend.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/backend.test.js +353 -0
- package/dist/backends/firecracker/__tests__/backend.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts +19 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +201 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts +39 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js +149 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js.map +1 -0
- package/dist/backends/firecracker/__tests__/protocol.test.d.ts +6 -0
- package/dist/backends/firecracker/__tests__/protocol.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/protocol.test.js +77 -0
- package/dist/backends/firecracker/__tests__/protocol.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/transport.test.d.ts +20 -0
- package/dist/backends/firecracker/__tests__/transport.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/transport.test.js +449 -0
- package/dist/backends/firecracker/__tests__/transport.test.js.map +1 -0
- package/dist/backends/firecracker/index.d.ts +124 -0
- package/dist/backends/firecracker/index.d.ts.map +1 -0
- package/dist/backends/firecracker/index.js +334 -0
- package/dist/backends/firecracker/index.js.map +1 -0
- package/dist/backends/firecracker/protocol.d.ts +132 -0
- package/dist/backends/firecracker/protocol.d.ts.map +1 -0
- package/dist/backends/firecracker/protocol.js +112 -0
- package/dist/backends/firecracker/protocol.js.map +1 -0
- package/dist/backends/firecracker/transport.d.ts +251 -0
- package/dist/backends/firecracker/transport.d.ts.map +1 -0
- package/dist/backends/firecracker/transport.js +524 -0
- package/dist/backends/firecracker/transport.js.map +1 -0
- package/dist/index.d.ts +611 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +376 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +28 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +670 -0
- package/dist/index.test.js.map +1 -0
- package/package.json +54 -0
- package/src/backends/aci-standby-pool/index.ts +602 -0
- package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +169 -0
- package/src/backends/docker/index.ts +826 -0
- package/src/backends/firecracker/__tests__/backend.test.ts +418 -0
- package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +253 -0
- package/src/backends/firecracker/__tests__/fixtures/mtls-pki.ts +166 -0
- package/src/backends/firecracker/__tests__/protocol.test.ts +90 -0
- package/src/backends/firecracker/__tests__/transport.test.ts +526 -0
- package/src/backends/firecracker/index.ts +528 -0
- package/src/backends/firecracker/protocol.ts +191 -0
- package/src/backends/firecracker/transport.ts +667 -0
- package/src/index.test.ts +731 -0
- package/src/index.ts +930 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE wire contract, shared across both transports.
|
|
3
|
+
*
|
|
4
|
+
* The docker (`backends/docker/`) and ACI (`backends/aci-standby-pool/`)
|
|
5
|
+
* backends speak this contract over **HTTP**: a streaming NDJSON
|
|
6
|
+
* `/execute` response and base64-bodied `/read-file` / `/write-file`
|
|
7
|
+
* JSON requests, served by `worker/server.js`. The Firecracker
|
|
8
|
+
* backend speaks the **same NDJSON shapes and the same base64 file-IO
|
|
9
|
+
* shapes** — only the transport changes from HTTP-over-TCP to
|
|
10
|
+
* framed-over-vsock (see `transport.ts`).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for the message shapes
|
|
13
|
+
* and the streaming exec-line parser, so the two transports cannot
|
|
14
|
+
* drift. It deliberately carries NO transport concern (no `fetch`, no
|
|
15
|
+
* socket) — it is pure codec. The HTTP backends keep their own inline
|
|
16
|
+
* copies (they predate this module and stay UNTOUCHED per the P0
|
|
17
|
+
* scope); this module mirrors those shapes verbatim and is the
|
|
18
|
+
* canonical definition the vsock path consumes.
|
|
19
|
+
*
|
|
20
|
+
* Why mirror rather than refactor docker/aci onto it: the P0 scope is
|
|
21
|
+
* "do NOT touch docker or aci". Centralising the shapes here, with the
|
|
22
|
+
* inline docker copy pinned by `backends/docker/__tests__` and this
|
|
23
|
+
* module pinned by `backends/firecracker/__tests__`, keeps both honest
|
|
24
|
+
* without editing the frozen HTTP path. A later cleanup pass can fold
|
|
25
|
+
* docker/aci onto this module once it is no longer release-frozen.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { SandboxExecResult } from '@namzu/sdk'
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Exec — request + the NDJSON event shapes (verbatim from worker/server.js)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `/execute` request body. Identical field set to the HTTP worker's
|
|
36
|
+
* `handleExecute` body (`command`, `args`, `cwd`, `env`, `stdin`,
|
|
37
|
+
* `timeoutMs`, `maxOutputBytes`). `timeoutMs` maps from the SDK's
|
|
38
|
+
* `SandboxExecOptions.timeout`.
|
|
39
|
+
*/
|
|
40
|
+
export interface ExecRequest {
|
|
41
|
+
readonly command: string
|
|
42
|
+
readonly args?: readonly string[]
|
|
43
|
+
readonly cwd?: string
|
|
44
|
+
readonly env?: Record<string, string>
|
|
45
|
+
readonly stdin?: string
|
|
46
|
+
readonly timeoutMs?: number
|
|
47
|
+
readonly maxOutputBytes?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One NDJSON event the agent emits while streaming an `/execute`. The
|
|
52
|
+
* exact union the HTTP worker writes via `writeEvent`:
|
|
53
|
+
* { type: 'stdout_delta', data }
|
|
54
|
+
* { type: 'stderr_delta', data }
|
|
55
|
+
* { type: 'result', exitCode, timedOut, durationMs, stdoutTruncated?, stderrTruncated? }
|
|
56
|
+
* { type: 'error', error }
|
|
57
|
+
*/
|
|
58
|
+
export type ExecEvent =
|
|
59
|
+
| { readonly type: 'stdout_delta'; readonly data: string }
|
|
60
|
+
| { readonly type: 'stderr_delta'; readonly data: string }
|
|
61
|
+
| {
|
|
62
|
+
readonly type: 'result'
|
|
63
|
+
readonly exitCode: number
|
|
64
|
+
readonly timedOut: boolean
|
|
65
|
+
readonly durationMs?: number
|
|
66
|
+
readonly stdoutTruncated?: boolean
|
|
67
|
+
readonly stderrTruncated?: boolean
|
|
68
|
+
}
|
|
69
|
+
| { readonly type: 'error'; readonly error: string }
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// File-IO — base64 request + response shapes (verbatim from server.js)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** `/write-file` request body. `content` is base64. */
|
|
76
|
+
export interface WriteFileRequest {
|
|
77
|
+
readonly path: string
|
|
78
|
+
readonly content: string
|
|
79
|
+
readonly encoding: 'base64'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `/write-file` success response. */
|
|
83
|
+
export interface WriteFileResponse {
|
|
84
|
+
readonly ok: boolean
|
|
85
|
+
readonly bytesWritten?: number
|
|
86
|
+
readonly error?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `/read-file` request body. */
|
|
90
|
+
export interface ReadFileRequest {
|
|
91
|
+
readonly path: string
|
|
92
|
+
readonly encoding: 'base64'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `/read-file` response. `content` is base64 on success. */
|
|
96
|
+
export interface ReadFileResponse {
|
|
97
|
+
readonly ok: boolean
|
|
98
|
+
readonly content?: string
|
|
99
|
+
readonly sizeBytes?: number
|
|
100
|
+
readonly encoding?: string
|
|
101
|
+
readonly error?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Streaming exec-line accumulator — the parser docker/aci inline today,
|
|
106
|
+
// lifted out so the vsock transport reuses it byte-for-byte.
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Accumulates the streamed `/execute` NDJSON into a single
|
|
111
|
+
* {@link SandboxExecResult}, exactly as the docker/aci `execViaWorker`
|
|
112
|
+
* loops do: concatenate `stdout_delta` / `stderr_delta`, capture the
|
|
113
|
+
* terminal `result`, and **throw** on an `error` event.
|
|
114
|
+
*
|
|
115
|
+
* Transport-agnostic: feed it whole parsed {@link ExecEvent}s (the
|
|
116
|
+
* transport owns newline-framing → JSON.parse → here). Malformed lines
|
|
117
|
+
* are dropped by the transport's `JSON.parse` guard before they reach
|
|
118
|
+
* this accumulator, matching the docker loop's `SyntaxError` swallow.
|
|
119
|
+
*/
|
|
120
|
+
export class ExecResultAccumulator {
|
|
121
|
+
private stdout = ''
|
|
122
|
+
private stderr = ''
|
|
123
|
+
private exitCode = -1
|
|
124
|
+
private timedOut = false
|
|
125
|
+
private signal: string | undefined
|
|
126
|
+
private settled = false
|
|
127
|
+
private readonly start: number
|
|
128
|
+
|
|
129
|
+
constructor(start: number = Date.now()) {
|
|
130
|
+
this.start = start
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Apply one event. Returns `true` once a terminal `result` has been
|
|
135
|
+
* seen (so the transport can stop reading early if it wants).
|
|
136
|
+
* Throws if the event is an `error` — the same control flow the
|
|
137
|
+
* docker loop uses (`throw new Error(event.error)`).
|
|
138
|
+
*/
|
|
139
|
+
push(event: ExecEvent): boolean {
|
|
140
|
+
if (event.type === 'stdout_delta') {
|
|
141
|
+
this.stdout += event.data
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
if (event.type === 'stderr_delta') {
|
|
145
|
+
this.stderr += event.data
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
if (event.type === 'result') {
|
|
149
|
+
this.exitCode = event.exitCode
|
|
150
|
+
this.timedOut = event.timedOut
|
|
151
|
+
this.settled = true
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
// event.type === 'error'
|
|
155
|
+
throw new Error(event.error)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get done(): boolean {
|
|
159
|
+
return this.settled
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build the SDK-shaped result. `durationMs` measured host-side. */
|
|
163
|
+
finish(): SandboxExecResult {
|
|
164
|
+
return {
|
|
165
|
+
exitCode: this.exitCode,
|
|
166
|
+
stdout: this.stdout,
|
|
167
|
+
stderr: this.stderr,
|
|
168
|
+
...(this.signal ? { signal: this.signal } : {}),
|
|
169
|
+
timedOut: this.timedOut,
|
|
170
|
+
durationMs: Date.now() - this.start,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse a single NDJSON line into an {@link ExecEvent}, or `undefined`
|
|
177
|
+
* if the line is blank or not valid JSON (the docker loop's
|
|
178
|
+
* `SyntaxError` swallow). Non-`SyntaxError` problems are impossible
|
|
179
|
+
* here because we only `JSON.parse`; structural validation is by the
|
|
180
|
+
* `type` discriminator at the call site.
|
|
181
|
+
*/
|
|
182
|
+
export function parseExecLine(line: string): ExecEvent | undefined {
|
|
183
|
+
const trimmed = line.trim()
|
|
184
|
+
if (!trimmed) return undefined
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(trimmed) as ExecEvent
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof SyntaxError) return undefined
|
|
189
|
+
throw err
|
|
190
|
+
}
|
|
191
|
+
}
|