@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,166 @@
1
+ /**
2
+ * Shared Ring-0 mTLS test PKI + a loopback relay stand-in.
3
+ *
4
+ * A throwaway P-256 test PKI (CA + server leaf + client leaf + an unrelated
5
+ * rogue client) baked in as PEM constants — no openssl/forge at test time, so
6
+ * the package stays dependency-light. Minted offline:
7
+ * openssl ec/x509 self-signed CA + leaves
8
+ * (server SAN = DNS:sandbox.fc.internal, IP:127.0.0.1), far-future expiry.
9
+ * `ROGUE_*` chains to a DIFFERENT CA to prove `rejectUnauthorized` blocks it.
10
+ *
11
+ * Used by both transport.test.ts (the raw `kind:'mtls'` dialer arm) and
12
+ * backend.test.ts (the cert-INJECTION seam through `buildFirecrackerBackend`),
13
+ * so the PKI + relay harness lives in one place instead of per-test copies.
14
+ */
15
+
16
+ import { connect as netConnect } from 'node:net'
17
+ import { type TLSSocket, type Server as TlsServer, createServer as createTlsServer } from 'node:tls'
18
+
19
+ export const CA_CRT = `-----BEGIN CERTIFICATE-----
20
+ MIIBjjCCATWgAwIBAgIUGXUtG14KQr4spNGW7rpfjZGoBIIwCgYIKoZIzj0EAwIw
21
+ HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
22
+ MjEyNjA1MjMwODMyNTJaMBwxGjAYBgNVBAMMEXZhbmRhbC1mYy10ZXN0LWNhMFkw
23
+ EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5l+vjhjm/PX02D55n56EwelgvM1jrqQM
24
+ Z+ezzvMmrZEBGWInvBND3z3kc6nv7snu6fHT2HHOyhwUNoLqmrIg/qNTMFEwHQYD
25
+ VR0OBBYEFP0M8/KZY1ovxyGDci5iuoYXcmZgMB8GA1UdIwQYMBaAFP0M8/KZY1ov
26
+ xyGDci5iuoYXcmZgMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIg
27
+ C0xtFDASnwsOYT0GiZwzYWTOBfgmjDWK7ANXQQFNTh4CICVLQwq5eLy3424drEHk
28
+ 5Ol/FNKVZcGRjE96rB0mRUOp
29
+ -----END CERTIFICATE-----
30
+ `
31
+
32
+ export const SERVER_CRT = `-----BEGIN CERTIFICATE-----
33
+ MIIBsDCCAVWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1MwCgYIKoZIzj0EAwIw
34
+ HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
35
+ MjEyNjA1MjMwODMyNTJaMBIxEDAOBgNVBAMMB2ZjLWhvc3QwWTATBgcqhkjOPQIB
36
+ BggqhkjOPQMBBwNCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMBfmonVb7dUhM0ZWpo
37
+ ePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6o30wezAkBgNVHREEHTAbghNz
38
+ YW5kYm94LmZjLmludGVybmFshwR/AAABMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0G
39
+ A1UdDgQWBBRXHU1vlX7WvqzcbaNxRxV4f3gfDDAfBgNVHSMEGDAWgBT9DPPymWNa
40
+ L8chg3IuYrqGF3JmYDAKBggqhkjOPQQDAgNJADBGAiEAlqzKqgBf5hxJqd26QwcY
41
+ n3cvEKi4f2BSLe1Rfzr5oSoCIQD0izUOJXLNvmac9cMgV0HvkftBerDKFOcGeGTN
42
+ g3Eunw==
43
+ -----END CERTIFICATE-----
44
+ `
45
+
46
+ export const SERVER_KEY = `-----BEGIN PRIVATE KEY-----
47
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/ROnYXIkMKnipbLt
48
+ JnFEwXAifgeOv9TGLcK2RojE7jmhRANCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMB
49
+ fmonVb7dUhM0ZWpoePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6
50
+ -----END PRIVATE KEY-----
51
+ `
52
+
53
+ export const CLIENT_CRT = `-----BEGIN CERTIFICATE-----
54
+ MIIBjjCCATWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1QwCgYIKoZIzj0EAwIw
55
+ HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
56
+ MjEyNjA1MjMwODMyNTJaMBgxFjAUBgNVBAMMDWNhLXZhbmRhbC1hcHAwWTATBgcq
57
+ hkjOPQIBBggqhkjOPQMBBwNCAASlCMCwjtrQUicWcWsO29R5S7fzjMbbxXvDh8/K
58
+ w57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXko1cwVTATBgNVHSUE
59
+ DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU2cxDCNrlVkk4ODs6ZPrkC6GzHjkwHwYD
60
+ VR0jBBgwFoAU/Qzz8pljWi/HIYNyLmK6hhdyZmAwCgYIKoZIzj0EAwIDRwAwRAIg
61
+ RghqArNm3bXnWnRi+jkEEJa8mRb2z0Wj4G7SIhtMurUCIBQSllVtICgZB9hvcM0D
62
+ d24fcW4nlfvaZJMJ6dRC1Wle
63
+ -----END CERTIFICATE-----
64
+ `
65
+
66
+ export const CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
67
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaamj/ycrEuPqpiC2
68
+ IhAYx/M5iN4t4P+B17V4GO54Nb2hRANCAASlCMCwjtrQUicWcWsO29R5S7fzjMbb
69
+ xXvDh8/Kw57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXk
70
+ -----END PRIVATE KEY-----
71
+ `
72
+
73
+ export const ROGUE_CLIENT_CRT = `-----BEGIN CERTIFICATE-----
74
+ MIIBhjCCASugAwIBAgIUbAYOlW4PQGyXjjRsjCPy62EHUmIwCgYIKoZIzj0EAwIw
75
+ EzERMA8GA1UEAwwIcm9ndWUtY2EwIBcNMjYwNjE2MDgzMjUyWhgPMjEyNjA1MjMw
76
+ ODMyNTJaMBcxFTATBgNVBAMMDHJvZ3VlLWNsaWVudDBZMBMGByqGSM49AgEGCCqG
77
+ SM49AwEHA0IABOPSzdzQTQz3M5CDHLVPguvHd10ncaDV9t4zKUy/PCE+U7GTJwN2
78
+ TtypGnmYEahbcl45j94hwu495P5VbR5ohb6jVzBVMBMGA1UdJQQMMAoGCCsGAQUF
79
+ BwMCMB0GA1UdDgQWBBTf0woauniche3/ps77+Kd0dsbbSDAfBgNVHSMEGDAWgBSX
80
+ Yu0IJlPxi79Gyi5hR1IfS/sqyzAKBggqhkjOPQQDAgNJADBGAiEAxD4N4XWtaHPJ
81
+ yaCOzQP0e5RRyNst3QrhH0NSMyPw89wCIQC+bezLX97TuV1sId53Bo1l9j1TSOMa
82
+ EZotN/Wsq6L+qw==
83
+ -----END CERTIFICATE-----
84
+ `
85
+
86
+ export const ROGUE_CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
87
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR6fGq27O9eT/hi0Q
88
+ c+L5+lSm5fbDEghVJCo6k7HrG62hRANCAATj0s3c0E0M9zOQgxy1T4Lrx3ddJ3Gg
89
+ 1fbeMylMvzwhPlOxkycDdk7cqRp5mBGoW3JeOY/eIcLuPeT+VW0eaIW+
90
+ -----END PRIVATE KEY-----
91
+ `
92
+
93
+ export interface RelayHandle {
94
+ readonly server: TlsServer
95
+ readonly port: number
96
+ /** sandboxId from the first connection's `SANDBOX <id>` preamble. */
97
+ preamble(): Promise<string>
98
+ }
99
+
100
+ /**
101
+ * A loopback stand-in for the per-FC-host mTLS relay.
102
+ *
103
+ * Terminates mTLS (requestCert + rejectUnauthorized + CA pin), reads the
104
+ * `SANDBOX <id>\n` routing preamble line, then bridges the rest of the TLS
105
+ * stream verbatim to a FRESH `net.connect` to `agentSockPath` (the
106
+ * unix-socket-backed real agent) — exactly the dumb byte-pump the production
107
+ * relay is. It does NOT speak framing itself and does NOT send an ack line.
108
+ */
109
+ export function startMtlsRelay(agentSockPath: string, listenPort = 0): Promise<RelayHandle> {
110
+ let resolvePreamble: (id: string) => void
111
+ const preamble = new Promise<string>((r) => {
112
+ resolvePreamble = r
113
+ })
114
+
115
+ return new Promise<RelayHandle>((resolve, reject) => {
116
+ const server = createTlsServer(
117
+ {
118
+ requestCert: true,
119
+ rejectUnauthorized: true,
120
+ ca: CA_CRT,
121
+ cert: SERVER_CRT,
122
+ key: SERVER_KEY,
123
+ minVersion: 'TLSv1.3',
124
+ },
125
+ (tlsSock: TLSSocket) => {
126
+ let pre = Buffer.alloc(0)
127
+ const onPreambleData = (chunk: Buffer) => {
128
+ pre = Buffer.concat([pre, chunk])
129
+ const nl = pre.indexOf(0x0a)
130
+ if (nl < 0) return
131
+ tlsSock.removeListener('data', onPreambleData)
132
+ tlsSock.pause()
133
+ const line = pre.subarray(0, nl).toString('utf8')
134
+ const rest = pre.subarray(nl + 1)
135
+ if (rest.length > 0) tlsSock.unshift(rest)
136
+ const m = /^SANDBOX (.+)$/.exec(line)
137
+ const id = m?.[1]
138
+ if (id === undefined) {
139
+ tlsSock.destroy()
140
+ return
141
+ }
142
+ resolvePreamble(id)
143
+ // 1 inbound mTLS conn -> 1 FRESH local agent connect (the
144
+ // resume-survival invariant the relay must preserve).
145
+ const upstream = netConnect({ path: agentSockPath })
146
+ upstream.on('error', () => tlsSock.destroy())
147
+ tlsSock.on('error', () => upstream.destroy())
148
+ upstream.once('connect', () => {
149
+ tlsSock.pipe(upstream)
150
+ upstream.pipe(tlsSock)
151
+ })
152
+ }
153
+ tlsSock.on('data', onPreambleData)
154
+ },
155
+ )
156
+ server.on('error', reject)
157
+ server.listen(listenPort, '127.0.0.1', () => {
158
+ const addr = server.address()
159
+ if (addr === null || typeof addr === 'string') {
160
+ reject(new Error('relay: no TCP port'))
161
+ return
162
+ }
163
+ resolve({ server, port: addr.port, preamble: () => preamble })
164
+ })
165
+ })
166
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Unit-pins the shared NDJSON codec the vsock + HTTP transports both
3
+ * speak, and the `pickBackend` routing for `microvm:self-hosted`.
4
+ */
5
+
6
+ import { describe, expect, it } from 'vitest'
7
+
8
+ import { SandboxBackendNotImplementedError, createSandboxProvider } from '../../../index.js'
9
+ import { ExecResultAccumulator, parseExecLine } from '../protocol.js'
10
+
11
+ describe('ExecResultAccumulator', () => {
12
+ it('accumulates stdout/stderr deltas and captures the terminal result', () => {
13
+ const acc = new ExecResultAccumulator(0)
14
+ expect(acc.push({ type: 'stdout_delta', data: 'a' })).toBe(false)
15
+ expect(acc.push({ type: 'stdout_delta', data: 'b' })).toBe(false)
16
+ expect(acc.push({ type: 'stderr_delta', data: 'E' })).toBe(false)
17
+ expect(acc.done).toBe(false)
18
+ expect(acc.push({ type: 'result', exitCode: 0, timedOut: false })).toBe(true)
19
+ expect(acc.done).toBe(true)
20
+ const r = acc.finish()
21
+ expect(r.stdout).toBe('ab')
22
+ expect(r.stderr).toBe('E')
23
+ expect(r.exitCode).toBe(0)
24
+ expect(r.timedOut).toBe(false)
25
+ })
26
+
27
+ it('throws on an error event (docker loop parity)', () => {
28
+ const acc = new ExecResultAccumulator(0)
29
+ expect(() => acc.push({ type: 'error', error: 'boom' })).toThrow('boom')
30
+ })
31
+ })
32
+
33
+ describe('parseExecLine', () => {
34
+ it('parses a valid NDJSON line', () => {
35
+ expect(parseExecLine('{"type":"stdout_delta","data":"x"}')).toEqual({
36
+ type: 'stdout_delta',
37
+ data: 'x',
38
+ })
39
+ })
40
+ it('returns undefined for blank or malformed lines (SyntaxError swallow)', () => {
41
+ expect(parseExecLine('')).toBeUndefined()
42
+ expect(parseExecLine(' ')).toBeUndefined()
43
+ expect(parseExecLine('{not json')).toBeUndefined()
44
+ })
45
+ })
46
+
47
+ describe('pickBackend — microvm:self-hosted', () => {
48
+ it('builds the firecracker backend when orchestratorEndpoint + getToken are present', () => {
49
+ const provider = createSandboxProvider({
50
+ backend: {
51
+ tier: 'microvm',
52
+ service: 'self-hosted',
53
+ firecrackerBinary: '/usr/bin/firecracker',
54
+ kernelImage: '/golden/vmlinux',
55
+ rootfsImage: '/golden/rootfs.ext4',
56
+ orchestratorEndpoint: 'https://orchestrator.test',
57
+ getToken: async () => 'tok',
58
+ template: 'golden-rev-1',
59
+ },
60
+ })
61
+ expect(provider.id).toContain('microvm')
62
+ expect(provider.id).toContain('firecracker')
63
+ expect(provider.name).toContain('microvm:self-hosted')
64
+ })
65
+
66
+ it('still throws for the legacy local-containerd self-hosted shape (no orchestrator)', () => {
67
+ expect(() =>
68
+ createSandboxProvider({
69
+ backend: {
70
+ tier: 'microvm',
71
+ service: 'self-hosted',
72
+ firecrackerBinary: '/usr/bin/firecracker',
73
+ kernelImage: '/k',
74
+ rootfsImage: '/r',
75
+ },
76
+ }),
77
+ ).toThrow(SandboxBackendNotImplementedError)
78
+ })
79
+
80
+ it('still throws for microvm:e2b and microvm:fly-machines', () => {
81
+ expect(() =>
82
+ createSandboxProvider({ backend: { tier: 'microvm', service: 'e2b', apiKey: 'k' } }),
83
+ ).toThrow(SandboxBackendNotImplementedError)
84
+ expect(() =>
85
+ createSandboxProvider({
86
+ backend: { tier: 'microvm', service: 'fly-machines', apiToken: 't', app: 'a', image: 'i' },
87
+ }),
88
+ ).toThrow(SandboxBackendNotImplementedError)
89
+ })
90
+ })