@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,169 @@
1
+ /**
2
+ * Docker smoke test — leaf-mount permission semantics.
3
+ *
4
+ * Concern Codex flagged: the Dockerfile pre-creates only the parent
5
+ * dirs (`/mnt`, `/mnt/user-data`, `/mnt/skills`) as root-owned 0555.
6
+ * The runtime claim is that:
7
+ *
8
+ * 1. When `outputs` IS bound, uid 1001 (the worker user) can write
9
+ * into it.
10
+ * 2. When a leaf path (e.g. `/mnt/user-data/uploads`) is NOT bound,
11
+ * the path simply does not exist — `stat` returns ENOENT, NOT
12
+ * a writable empty dir.
13
+ *
14
+ * Both of those are kernel-level mount-namespace properties, not
15
+ * unit-testable. This file exercises them against a real docker
16
+ * daemon. Run via:
17
+ *
18
+ * pnpm --filter @namzu/sandbox test:smoke
19
+ * # or, from the monorepo root:
20
+ * pnpm sandbox:smoke
21
+ *
22
+ * Excluded from the default `pnpm test` run by `vitest.config.ts`.
23
+ *
24
+ * Pre-requisites:
25
+ * - `docker` CLI on PATH.
26
+ * - A pre-built reference image. By default this test looks for
27
+ * `namzu-sandbox-worker:smoke`. Override with the
28
+ * `NAMZU_SANDBOX_SMOKE_IMAGE` env. Build the image once with:
29
+ * `docker build -t namzu-sandbox-worker:smoke -f packages/sandbox/worker/Dockerfile packages/sandbox`
30
+ * - A host-side scratch directory the test creates and tears down.
31
+ *
32
+ * On a developer machine without docker the test self-skips with a
33
+ * clear message. On CI (`process.env.CI === 'true'`) the same
34
+ * pre-conditions FAIL FAST instead of skipping, so a CI
35
+ * misconfiguration cannot silently mask a regression — see the
36
+ * `.github/workflows/sandbox-smoke.yml` workflow which is meant to
37
+ * always run with docker available.
38
+ */
39
+
40
+ import { spawnSync } from 'node:child_process'
41
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
42
+ import { tmpdir } from 'node:os'
43
+ import { join } from 'node:path'
44
+
45
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
46
+
47
+ import { createSandboxProvider } from '../../../index.js'
48
+
49
+ const IMAGE = process.env.NAMZU_SANDBOX_SMOKE_IMAGE ?? 'namzu-sandbox-worker:smoke'
50
+ const IS_CI = process.env.CI === 'true'
51
+
52
+ function dockerAvailable(): boolean {
53
+ const probe = spawnSync('docker', ['version', '--format', '{{.Server.Version}}'], {
54
+ stdio: 'ignore',
55
+ })
56
+ return probe.status === 0
57
+ }
58
+
59
+ function imagePresent(image: string): boolean {
60
+ const probe = spawnSync('docker', ['image', 'inspect', image], { stdio: 'ignore' })
61
+ return probe.status === 0
62
+ }
63
+
64
+ const skipReason = !dockerAvailable()
65
+ ? 'docker daemon not reachable — skipping smoke test'
66
+ : !imagePresent(IMAGE)
67
+ ? `image ${IMAGE} not present — build it with \`docker build -t ${IMAGE} -f packages/sandbox/worker/Dockerfile packages/sandbox\` and re-run`
68
+ : null
69
+
70
+ // CI fail-fast: the smoke workflow is meant to always have docker +
71
+ // the reference image. Skipping silently in that environment would
72
+ // let a regression land without the test even attempting to run.
73
+ // Locally (no `CI=true`) skipping with a hint is the right behaviour
74
+ // for a developer who hasn't built the image yet.
75
+ if (IS_CI && skipReason) {
76
+ throw new Error(
77
+ `smoke test pre-condition not satisfied on CI: ${skipReason}. Configure the workflow to install docker and build the reference image before running.`,
78
+ )
79
+ }
80
+
81
+ describe.skipIf(skipReason !== null)('docker smoke — leaf permissions', () => {
82
+ let outputsHost: string
83
+
84
+ beforeAll(() => {
85
+ outputsHost = mkdtempSync(join(tmpdir(), 'namzu-sandbox-smoke-out-'))
86
+ })
87
+
88
+ afterAll(() => {
89
+ if (outputsHost) {
90
+ rmSync(outputsHost, { recursive: true, force: true })
91
+ }
92
+ })
93
+
94
+ it('uid 1001 can write to the outputs bind, and the host sees the file', async () => {
95
+ const provider = createSandboxProvider({
96
+ backend: { tier: 'container', image: IMAGE },
97
+ layout: {
98
+ outputs: { source: { type: 'hostDir', hostPath: outputsHost } },
99
+ },
100
+ })
101
+ const sandbox = await provider.create()
102
+ try {
103
+ const result = await sandbox.exec('sh', [
104
+ '-c',
105
+ 'id -u && echo "smoke" > /mnt/user-data/outputs/hello.txt',
106
+ ])
107
+ expect(result.exitCode).toBe(0)
108
+ expect(result.stdout.trim()).toBe('1001')
109
+ // Host-side read confirms the bind round-trips.
110
+ const hostContent = readFileSync(join(outputsHost, 'hello.txt'), 'utf8')
111
+ expect(hostContent.trim()).toBe('smoke')
112
+ } finally {
113
+ await sandbox.destroy()
114
+ }
115
+ }, 60_000)
116
+
117
+ it('unbound leaves do not exist — uploads/transcripts/tool_results are ENOENT, not empty', async () => {
118
+ const provider = createSandboxProvider({
119
+ backend: { tier: 'container', image: IMAGE },
120
+ layout: {
121
+ outputs: { source: { type: 'hostDir', hostPath: outputsHost } },
122
+ // Intentionally leave uploads / toolResults / transcripts
123
+ // unbound. The Dockerfile pre-creates only the parents
124
+ // (`/mnt/user-data`, `/mnt/skills`); leaves should not
125
+ // exist and `stat` should fail.
126
+ },
127
+ })
128
+ const sandbox = await provider.create()
129
+ try {
130
+ for (const leafPath of [
131
+ '/mnt/user-data/uploads',
132
+ '/mnt/user-data/tool_results',
133
+ '/mnt/transcripts',
134
+ ]) {
135
+ const result = await sandbox.exec('sh', ['-c', `stat ${leafPath} 2>&1; echo --rc=$?`])
136
+ // `stat` returns non-zero on ENOENT; the rc line carries
137
+ // the failure. Either is the contract; we accept either
138
+ // "No such file" or "cannot stat" wording across distros.
139
+ expect(result.stdout).toMatch(/--rc=[1-9]/)
140
+ expect(result.stdout.toLowerCase()).toMatch(/no such file|cannot stat|does not exist/)
141
+ }
142
+ } finally {
143
+ await sandbox.destroy()
144
+ }
145
+ }, 60_000)
146
+
147
+ it('uid 1001 cannot mkdir into the root-owned 0555 parent /mnt/user-data', async () => {
148
+ const provider = createSandboxProvider({
149
+ backend: { tier: 'container', image: IMAGE },
150
+ layout: {
151
+ outputs: { source: { type: 'hostDir', hostPath: outputsHost } },
152
+ },
153
+ })
154
+ const sandbox = await provider.create()
155
+ try {
156
+ // The model trying to "create the missing leaf dir" hits
157
+ // the 0555 root-owned parent and is denied — exactly what
158
+ // distinguishes "not bound" from "writable empty dir".
159
+ const result = await sandbox.exec('sh', [
160
+ '-c',
161
+ 'mkdir /mnt/user-data/fake 2>&1; echo --rc=$?',
162
+ ])
163
+ expect(result.stdout).toMatch(/--rc=[1-9]/)
164
+ expect(result.stdout.toLowerCase()).toMatch(/permission denied/)
165
+ } finally {
166
+ await sandbox.destroy()
167
+ }
168
+ }, 60_000)
169
+ })