@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/LICENSE.md +110 -0
  3. package/README.md +148 -0
  4. package/dist/backends/aci-standby-pool/index.d.ts +104 -0
  5. package/dist/backends/aci-standby-pool/index.d.ts.map +1 -0
  6. package/dist/backends/aci-standby-pool/index.js +425 -0
  7. package/dist/backends/aci-standby-pool/index.js.map +1 -0
  8. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +40 -0
  9. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts.map +1 -0
  10. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +157 -0
  11. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js.map +1 -0
  12. package/dist/backends/docker/index.d.ts +118 -0
  13. package/dist/backends/docker/index.d.ts.map +1 -0
  14. package/dist/backends/docker/index.js +645 -0
  15. package/dist/backends/docker/index.js.map +1 -0
  16. package/dist/backends/firecracker/__tests__/backend.test.d.ts +13 -0
  17. package/dist/backends/firecracker/__tests__/backend.test.d.ts.map +1 -0
  18. package/dist/backends/firecracker/__tests__/backend.test.js +353 -0
  19. package/dist/backends/firecracker/__tests__/backend.test.js.map +1 -0
  20. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts +19 -0
  21. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts.map +1 -0
  22. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +201 -0
  23. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +1 -0
  24. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts +39 -0
  25. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts.map +1 -0
  26. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js +149 -0
  27. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js.map +1 -0
  28. package/dist/backends/firecracker/__tests__/protocol.test.d.ts +6 -0
  29. package/dist/backends/firecracker/__tests__/protocol.test.d.ts.map +1 -0
  30. package/dist/backends/firecracker/__tests__/protocol.test.js +77 -0
  31. package/dist/backends/firecracker/__tests__/protocol.test.js.map +1 -0
  32. package/dist/backends/firecracker/__tests__/transport.test.d.ts +20 -0
  33. package/dist/backends/firecracker/__tests__/transport.test.d.ts.map +1 -0
  34. package/dist/backends/firecracker/__tests__/transport.test.js +449 -0
  35. package/dist/backends/firecracker/__tests__/transport.test.js.map +1 -0
  36. package/dist/backends/firecracker/index.d.ts +124 -0
  37. package/dist/backends/firecracker/index.d.ts.map +1 -0
  38. package/dist/backends/firecracker/index.js +334 -0
  39. package/dist/backends/firecracker/index.js.map +1 -0
  40. package/dist/backends/firecracker/protocol.d.ts +132 -0
  41. package/dist/backends/firecracker/protocol.d.ts.map +1 -0
  42. package/dist/backends/firecracker/protocol.js +112 -0
  43. package/dist/backends/firecracker/protocol.js.map +1 -0
  44. package/dist/backends/firecracker/transport.d.ts +251 -0
  45. package/dist/backends/firecracker/transport.d.ts.map +1 -0
  46. package/dist/backends/firecracker/transport.js +524 -0
  47. package/dist/backends/firecracker/transport.js.map +1 -0
  48. package/dist/index.d.ts +611 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +376 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/index.test.d.ts +28 -0
  53. package/dist/index.test.d.ts.map +1 -0
  54. package/dist/index.test.js +670 -0
  55. package/dist/index.test.js.map +1 -0
  56. package/package.json +54 -0
  57. package/src/backends/aci-standby-pool/index.ts +602 -0
  58. package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +169 -0
  59. package/src/backends/docker/index.ts +826 -0
  60. package/src/backends/firecracker/__tests__/backend.test.ts +418 -0
  61. package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +253 -0
  62. package/src/backends/firecracker/__tests__/fixtures/mtls-pki.ts +166 -0
  63. package/src/backends/firecracker/__tests__/protocol.test.ts +90 -0
  64. package/src/backends/firecracker/__tests__/transport.test.ts +526 -0
  65. package/src/backends/firecracker/index.ts +528 -0
  66. package/src/backends/firecracker/protocol.ts +191 -0
  67. package/src/backends/firecracker/transport.ts +667 -0
  68. package/src/index.test.ts +731 -0
  69. 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
+ }