@namzu/sandbox 15.0.0 → 17.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.
- package/CHANGELOG.md +324 -0
- package/README.md +223 -0
- package/dist/backends/aci-standby-pool/index.d.ts +22 -4
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
- package/dist/backends/aci-standby-pool/index.js +31 -7
- package/dist/backends/aci-standby-pool/index.js.map +1 -1
- package/dist/backends/docker/index.d.ts +408 -26
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +1173 -168
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/firecracker/transport.d.ts +156 -1
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +223 -29
- package/dist/backends/firecracker/transport.js.map +1 -1
- package/dist/backends/http-worker-client.d.ts +64 -2
- package/dist/backends/http-worker-client.d.ts.map +1 -1
- package/dist/backends/http-worker-client.js +78 -7
- package/dist/backends/http-worker-client.js.map +1 -1
- package/dist/backends/kubernetes/egress-policy.d.ts +193 -102
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/egress-policy.js +321 -146
- package/dist/backends/kubernetes/egress-policy.js.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts +6 -6
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.js +21 -53
- package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -1
- package/dist/backends/kubernetes/transport.d.ts +7 -0
- package/dist/backends/kubernetes/transport.d.ts.map +1 -1
- package/dist/backends/kubernetes/transport.js.map +1 -1
- package/dist/egress/proxy.d.ts +47 -2
- package/dist/egress/proxy.d.ts.map +1 -1
- package/dist/egress/proxy.js +31 -7
- package/dist/egress/proxy.js.map +1 -1
- package/dist/index.d.ts +130 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/backends/aci-standby-pool/index.ts +37 -7
- package/src/backends/docker/index.ts +1475 -196
- package/src/backends/firecracker/transport.ts +387 -36
- package/src/backends/http-worker-client.ts +89 -5
- package/src/backends/kubernetes/egress-policy.ts +455 -187
- package/src/backends/kubernetes/per-sandbox-policy.ts +21 -66
- package/src/backends/kubernetes/transport.ts +7 -0
- package/src/egress/proxy.ts +65 -8
- package/src/index.ts +162 -5
|
@@ -21,6 +21,68 @@ type WorkerEvent =
|
|
|
21
21
|
}
|
|
22
22
|
| { readonly type: 'error'; readonly error: string }
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* What every call to a worker's control API carries, if the worker has a
|
|
26
|
+
* credential at all.
|
|
27
|
+
*
|
|
28
|
+
* The worker (`packages/sandbox/worker/server.js`) requires
|
|
29
|
+
* `Authorization: Bearer <token>` on every route but `/healthz`, where the
|
|
30
|
+
* token is the per-instance `NAMZU_SANDBOX_TOKEN` it was started with. An
|
|
31
|
+
* absent token here sends no header, which is exactly right for a worker
|
|
32
|
+
* that has none: a loopback-bound dev worker, or a warm-pool worker whose
|
|
33
|
+
* profile authenticates nothing.
|
|
34
|
+
*
|
|
35
|
+
* A worker the host did NOT create is the case this cannot solve by
|
|
36
|
+
* itself. The token is minted by whoever starts the container and travels
|
|
37
|
+
* in its environment, so a warm pool has to be provisioned with the same
|
|
38
|
+
* token before a host can claim it — there is no channel back. Constructing
|
|
39
|
+
* this client with no token against such a worker fails at the first call
|
|
40
|
+
* with a `401`, not silently.
|
|
41
|
+
*/
|
|
42
|
+
export function workerAuthorization(token: string | undefined): Record<string, string> {
|
|
43
|
+
return token ? { authorization: `Bearer ${token}` } : {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The `401` a worker answers with, and what to do about it.
|
|
48
|
+
*
|
|
49
|
+
* Hung on every path that can be a caller's FIRST request to a worker —
|
|
50
|
+
* `reserve`, `execute`, and the container backend's direct `read-file` /
|
|
51
|
+
* `write-file` — because the bare status is the one failure whose cause is
|
|
52
|
+
* never visible from the sandbox the caller thinks it is talking to: the
|
|
53
|
+
* container is up, the port is open, and every command fails.
|
|
54
|
+
*/
|
|
55
|
+
export const WORKER_UNAUTHORIZED_HINT =
|
|
56
|
+
'The worker requires the per-instance token it was started with, as `Authorization: Bearer <token>`. If this host created the worker, the token it minted and the token the client sends have diverged. If it did not — a warm pool, a shared profile, a container someone else started — that worker must be provisioned with the token by whoever builds it: the worker has no channel back to hand one over.'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The same failure, for the one backend that can never fix it.
|
|
60
|
+
*
|
|
61
|
+
* The standby pool has no way to present a token. The claim API admits
|
|
62
|
+
* exactly one property override, and it is not `env`: it is a config map,
|
|
63
|
+
* and a config map reaches the container as a FILE MOUNT under
|
|
64
|
+
* `/mnt/configmap/<containername>/<key>`, not as an environment variable.
|
|
65
|
+
* This worker reads its credential from `process.env` once at startup, so
|
|
66
|
+
* a value delivered that way is not read at all — and Microsoft's own
|
|
67
|
+
* guidance is that config map values are not validated by the runtime and
|
|
68
|
+
* that a value affecting application security belongs in an environment
|
|
69
|
+
* variable instead. The channel exists; this credential is declined for
|
|
70
|
+
* it, at both ends. See `docs/sdk/container-sandbox-worker.md` for the
|
|
71
|
+
* answer, the reason, and the change that would close the gap.
|
|
72
|
+
*
|
|
73
|
+
* A token on the shared profile would make the worker boot and then refuse
|
|
74
|
+
* every call the backend makes — an unrecoverable loop that looks like a
|
|
75
|
+
* broken worker. So what works there is the address first and the
|
|
76
|
+
* worker-side escape second, and the hint says which of the two situations
|
|
77
|
+
* the caller is in.
|
|
78
|
+
*
|
|
79
|
+
* `HttpWorkerClient` uses {@link WORKER_UNAUTHORIZED_HINT} rather than this
|
|
80
|
+
* one: the client is shared by two backends, and a 401 it sees could be
|
|
81
|
+
* either situation.
|
|
82
|
+
*/
|
|
83
|
+
export const STANDBY_POOL_UNAUTHORIZED_HINT =
|
|
84
|
+
'The worker requires a per-instance token, and this backend cannot present one: the claim API admits a config map and nothing else, and a config map arrives as a file mount under /mnt/configmap, not as an environment variable this worker reads. Putting a token on the shared container group profile does NOT fix this — the worker boots, the backend sends no header, and every call 401s. What works today is the address and then the worker-side escape: claim the group with `subnetId` so it sits on a private network, and set `NAMZU_SANDBOX_ALLOW_UNAUTHENTICATED=1` on that group profile, which is the only configuration in which a pooled worker starts and this backend can talk to it. Any other workload should run on a backend that can carry a credential. See `docs/sdk/container-sandbox-worker.md`.'
|
|
85
|
+
|
|
24
86
|
function parseWorkerEvent(line: string): WorkerEvent {
|
|
25
87
|
let parsed: unknown
|
|
26
88
|
try {
|
|
@@ -57,6 +119,7 @@ function parseWorkerEvent(line: string): WorkerEvent {
|
|
|
57
119
|
|
|
58
120
|
async function readExecution(
|
|
59
121
|
baseUrl: string,
|
|
122
|
+
token: string | undefined,
|
|
60
123
|
executionId: string | undefined,
|
|
61
124
|
command: string,
|
|
62
125
|
argv: string[] | undefined,
|
|
@@ -67,7 +130,7 @@ async function readExecution(
|
|
|
67
130
|
try {
|
|
68
131
|
response = await fetch(`${baseUrl}/execute`, {
|
|
69
132
|
method: 'POST',
|
|
70
|
-
headers: { 'content-type': 'application/json' },
|
|
133
|
+
headers: { 'content-type': 'application/json', ...workerAuthorization(token) },
|
|
71
134
|
signal: transportSignal,
|
|
72
135
|
body: JSON.stringify({
|
|
73
136
|
...(executionId ? { executionId } : {}),
|
|
@@ -94,6 +157,12 @@ async function readExecution(
|
|
|
94
157
|
'The worker was reachable when the sandbox started, so it has most likely exited, been killed, or become unreachable since. Check the container logs and runtime exit state.',
|
|
95
158
|
)
|
|
96
159
|
}
|
|
160
|
+
if (response.status === 401) {
|
|
161
|
+
throw withHint(
|
|
162
|
+
new Error(`execute failed: HTTP 401 ${await response.text()}`),
|
|
163
|
+
WORKER_UNAUTHORIZED_HINT,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
97
166
|
if (!response.ok || !response.body) {
|
|
98
167
|
throw new Error(`execute failed: HTTP ${response.status} ${await response.text()}`)
|
|
99
168
|
}
|
|
@@ -163,16 +232,24 @@ async function readExecution(
|
|
|
163
232
|
/**
|
|
164
233
|
* A per-sandbox HTTP worker client. Every command must reserve an identity
|
|
165
234
|
* through the exact worker protocol before it can be admitted.
|
|
235
|
+
*
|
|
236
|
+
* `token` is the per-instance credential the worker was started with, and
|
|
237
|
+
* it rides on every request this client makes. It is optional because a
|
|
238
|
+
* worker that was never given one — a loopback dev worker — requires none;
|
|
239
|
+
* say what happens when it is missing from the wrong side rather than
|
|
240
|
+
* sending nothing quietly: the worker answers `401` and the failure names
|
|
241
|
+
* the reason.
|
|
166
242
|
*/
|
|
167
243
|
export class HttpWorkerClient {
|
|
168
244
|
private readonly controller: RemoteExecutionController
|
|
169
245
|
|
|
170
|
-
constructor(baseUrl: string) {
|
|
246
|
+
constructor(baseUrl: string, token?: string) {
|
|
171
247
|
const adapter: RemoteExecutionAdapter = {
|
|
172
248
|
label: 'HTTP worker',
|
|
173
249
|
reserve: async (signal) => {
|
|
174
250
|
const response = await fetch(`${baseUrl}/executions/reserve`, {
|
|
175
251
|
method: 'POST',
|
|
252
|
+
headers: workerAuthorization(token),
|
|
176
253
|
signal,
|
|
177
254
|
})
|
|
178
255
|
if (response.status === 404) {
|
|
@@ -180,6 +257,12 @@ export class HttpWorkerClient {
|
|
|
180
257
|
'The sandbox worker does not implement the required execution protocol. Rebuild the worker image or standby-pool profile from the same Namzu release before admitting commands.',
|
|
181
258
|
)
|
|
182
259
|
}
|
|
260
|
+
if (response.status === 401) {
|
|
261
|
+
throw withHint(
|
|
262
|
+
new Error(`execution reservation failed: HTTP 401 ${await response.text()}`),
|
|
263
|
+
WORKER_UNAUTHORIZED_HINT,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
183
266
|
if (!response.ok) {
|
|
184
267
|
throw new Error(
|
|
185
268
|
`execution reservation failed: HTTP ${response.status} ${await response.text()}`,
|
|
@@ -190,7 +273,7 @@ export class HttpWorkerClient {
|
|
|
190
273
|
cancel: async (executionId, signal) => {
|
|
191
274
|
const response = await fetch(`${baseUrl}/cancel`, {
|
|
192
275
|
method: 'POST',
|
|
193
|
-
headers: { 'content-type': 'application/json' },
|
|
276
|
+
headers: { 'content-type': 'application/json', ...workerAuthorization(token) },
|
|
194
277
|
body: JSON.stringify({ executionId }),
|
|
195
278
|
signal,
|
|
196
279
|
})
|
|
@@ -200,7 +283,7 @@ export class HttpWorkerClient {
|
|
|
200
283
|
return await response.json()
|
|
201
284
|
},
|
|
202
285
|
execute: async (executionId, command, argv, opts, signal) =>
|
|
203
|
-
await readExecution(baseUrl, executionId, command, argv, opts, signal),
|
|
286
|
+
await readExecution(baseUrl, token, executionId, command, argv, opts, signal),
|
|
204
287
|
}
|
|
205
288
|
this.controller = new RemoteExecutionController(adapter)
|
|
206
289
|
}
|
|
@@ -220,6 +303,7 @@ export async function execViaHttpWorker(
|
|
|
220
303
|
command: string,
|
|
221
304
|
argv: string[] | undefined,
|
|
222
305
|
opts: SandboxExecOptions | undefined,
|
|
306
|
+
token?: string,
|
|
223
307
|
): Promise<SandboxExecResult> {
|
|
224
|
-
return await new HttpWorkerClient(baseUrl).exec(command, argv, opts)
|
|
308
|
+
return await new HttpWorkerClient(baseUrl, token).exec(command, argv, opts)
|
|
225
309
|
}
|