@namzu/sandbox 13.0.0 → 14.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +309 -0
  2. package/README.md +151 -0
  3. package/dist/backends/firecracker/protocol.d.ts +22 -0
  4. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  5. package/dist/backends/firecracker/protocol.js.map +1 -1
  6. package/dist/backends/firecracker/transport.d.ts +104 -9
  7. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  8. package/dist/backends/firecracker/transport.js +139 -13
  9. package/dist/backends/firecracker/transport.js.map +1 -1
  10. package/dist/backends/kubernetes/egress-policy.d.ts +219 -0
  11. package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -0
  12. package/dist/backends/kubernetes/egress-policy.js +314 -0
  13. package/dist/backends/kubernetes/egress-policy.js.map +1 -0
  14. package/dist/backends/kubernetes/index.d.ts +374 -0
  15. package/dist/backends/kubernetes/index.d.ts.map +1 -0
  16. package/dist/backends/kubernetes/index.js +671 -0
  17. package/dist/backends/kubernetes/index.js.map +1 -0
  18. package/dist/backends/kubernetes/k8s-client.d.ts +125 -0
  19. package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -0
  20. package/dist/backends/kubernetes/k8s-client.js +246 -0
  21. package/dist/backends/kubernetes/k8s-client.js.map +1 -0
  22. package/dist/backends/kubernetes/lease.d.ts +119 -0
  23. package/dist/backends/kubernetes/lease.d.ts.map +1 -0
  24. package/dist/backends/kubernetes/lease.js +151 -0
  25. package/dist/backends/kubernetes/lease.js.map +1 -0
  26. package/dist/backends/kubernetes/objects.d.ts +282 -0
  27. package/dist/backends/kubernetes/objects.d.ts.map +1 -0
  28. package/dist/backends/kubernetes/objects.js +156 -0
  29. package/dist/backends/kubernetes/objects.js.map +1 -0
  30. package/dist/backends/kubernetes/privilege-probe.d.ts +136 -0
  31. package/dist/backends/kubernetes/privilege-probe.d.ts.map +1 -0
  32. package/dist/backends/kubernetes/privilege-probe.js +185 -0
  33. package/dist/backends/kubernetes/privilege-probe.js.map +1 -0
  34. package/dist/backends/kubernetes/sandbox.d.ts +123 -0
  35. package/dist/backends/kubernetes/sandbox.d.ts.map +1 -0
  36. package/dist/backends/kubernetes/sandbox.js +299 -0
  37. package/dist/backends/kubernetes/sandbox.js.map +1 -0
  38. package/dist/backends/kubernetes/transport.d.ts +122 -0
  39. package/dist/backends/kubernetes/transport.d.ts.map +1 -0
  40. package/dist/backends/kubernetes/transport.js +197 -0
  41. package/dist/backends/kubernetes/transport.js.map +1 -0
  42. package/dist/backends/kubernetes/workspace.d.ts +381 -0
  43. package/dist/backends/kubernetes/workspace.d.ts.map +1 -0
  44. package/dist/backends/kubernetes/workspace.js +1064 -0
  45. package/dist/backends/kubernetes/workspace.js.map +1 -0
  46. package/dist/index.d.ts +132 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +102 -34
  49. package/dist/index.js.map +1 -1
  50. package/dist/testing/sandbox-conformance.d.ts +193 -0
  51. package/dist/testing/sandbox-conformance.d.ts.map +1 -0
  52. package/dist/testing/sandbox-conformance.js +465 -0
  53. package/dist/testing/sandbox-conformance.js.map +1 -0
  54. package/package.json +5 -4
  55. package/src/backends/firecracker/protocol.ts +27 -0
  56. package/src/backends/firecracker/transport.ts +199 -28
  57. package/src/backends/kubernetes/egress-policy.ts +437 -0
  58. package/src/backends/kubernetes/index.ts +1012 -0
  59. package/src/backends/kubernetes/k8s-client.ts +352 -0
  60. package/src/backends/kubernetes/lease.ts +198 -0
  61. package/src/backends/kubernetes/objects.ts +363 -0
  62. package/src/backends/kubernetes/privilege-probe.ts +261 -0
  63. package/src/backends/kubernetes/sandbox.ts +395 -0
  64. package/src/backends/kubernetes/transport.ts +286 -0
  65. package/src/backends/kubernetes/workspace.ts +1386 -0
  66. package/src/index.ts +257 -35
  67. package/src/testing/sandbox-conformance.ts +667 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Acquire-time privilege probe: prove the guest process really was
3
+ * deprivileged, rather than assume the entrypoint did its job.
4
+ *
5
+ * The image's entrypoint mounts the workspace as root and then
6
+ * `exec setpriv --reuid --regid --clear-groups --inh-caps=-all
7
+ * --bounding-set=-all --no-new-privs -- tini -- node agent.cjs` (`tini` is
8
+ * the container's pid 1 so it can reap an orphan `agent.cjs` itself never
9
+ * spawned — see `k8s/entrypoint.sh` and the Dockerfile). Nothing in
10
+ * `agent.cjs` knows about any of that, and nothing on the host can see it
11
+ * either — an image built from an older entrypoint, a `RuntimeClass` change,
12
+ * a hand-edited `SandboxTemplate` all produce a sandbox that works perfectly
13
+ * and is not deprivileged. So the backend asks the guest, once, before it
14
+ * hands a caller a handle.
15
+ *
16
+ * ## Why `execute`, and why not `read-file`
17
+ *
18
+ * The guest's `read-file` resolves every path against `READ_ROOTS`
19
+ * (`WORKSPACE_ROOT` only), so it cannot reach `/proc` at all, and widening
20
+ * `READ_ROOTS` to make the probe work would hand every caller of `readFile`
21
+ * a window into the guest's process tree for the sake of one diagnostic.
22
+ * `handleExecute` jails only `cwd`, so a command whose ARGUMENT is an
23
+ * absolute path outside the workspace runs fine. The probe therefore spends
24
+ * one `exec` and touches no jail.
25
+ *
26
+ * ## Why all four masks
27
+ *
28
+ * Checking `CapEff` alone is a true-looking answer: an ordinary unprivileged
29
+ * process shows `CapEff: 0000000000000000` whether or not its bounding set
30
+ * was ever dropped, so a container running as uid 0 with the full bounding
31
+ * set still passes. `CapBnd` is the one that says a capability can never be
32
+ * regained; `CapInh` and `CapPrm` close the two ways one could be carried
33
+ * across an exec. `NoNewPrivs: 1` is what makes a setuid binary inside the
34
+ * guest unable to raise any of it back.
35
+ *
36
+ * ## Why every failure is a refusal
37
+ *
38
+ * A probe that could not run, one that never answered at all, output that
39
+ * could not be parsed and a process that is genuinely privileged are all
40
+ * reasons NOT to hand back a handle, and they are separated only in the error
41
+ * TEXT — a distroless image with no `cat` on `PATH` must be diagnosable as
42
+ * exactly that rather than read as a hardening failure. There is no
43
+ * configuration that turns this off.
44
+ *
45
+ * ## The clock is the caller's, and it lives in `index.ts`
46
+ *
47
+ * Nothing here has a timeout: the probe is one `exec`, and the budget it may
48
+ * spend belongs to the `create()` that ordered it. `admitProbedSandbox` runs
49
+ * it under an `OperationDeadline` and turns an expiry into
50
+ * {@link privilegeProbeTimedOut}, so a guest that accepts the connection and
51
+ * then goes quiet is refused on the caller's clock rather than on the
52
+ * execution controller's five-minute generic default.
53
+ */
54
+ import type { SandboxExecResult } from '@namzu/sdk';
55
+ /**
56
+ * The probe command. `cat` rather than an absolute `/bin/cat` so a guest
57
+ * that keeps its coreutils somewhere else still answers, and rather than a
58
+ * shell so there is no quoting to get wrong. A guest without it fails with
59
+ * a spawn error the refusal repeats verbatim.
60
+ */
61
+ export declare const PRIVILEGE_PROBE_COMMAND = "cat";
62
+ /** `/proc/self/status` — of the process the guest agent spawns, which
63
+ * inherits exactly the agent's own credentials and capability masks. */
64
+ export declare const PRIVILEGE_PROBE_ARGS: readonly string[];
65
+ /** Why a probe refused. The text says the same thing in words. */
66
+ export type PrivilegeProbeFailure =
67
+ /** The `exec` failed, exited non-zero, or never answered at all. */
68
+ 'probe-failed'
69
+ /** It ran, but its output is not a readable `/proc/<pid>/status`. */
70
+ | 'unreadable-output'
71
+ /** It ran, it parsed, and the process has capabilities it should not. */
72
+ | 'privileged';
73
+ /**
74
+ * Raised by {@link runPrivilegeProbe} and {@link parseProcStatus}. The
75
+ * `reason` is the machine-readable form of the distinction the message
76
+ * draws in prose: `'privileged'` means the guest is under-hardened, and the
77
+ * other two mean the backend could not tell.
78
+ */
79
+ export declare class KubernetesPrivilegeProbeError extends Error {
80
+ readonly reason: PrivilegeProbeFailure;
81
+ readonly name = "KubernetesPrivilegeProbeError";
82
+ constructor(reason: PrivilegeProbeFailure, message: string, options?: ErrorOptions);
83
+ }
84
+ /**
85
+ * The five fields the probe reads, parsed. The four capability masks are
86
+ * `bigint` because a capability mask is 64 bits wide and `Number` stops
87
+ * being exact at 53 — `000001ffffffffff` is only 41 bits today, but a mask
88
+ * that silently rounds is precisely the bug this whole module exists to
89
+ * catch.
90
+ */
91
+ export interface ProcStatusPrivileges {
92
+ readonly capInh: bigint;
93
+ readonly capPrm: bigint;
94
+ readonly capEff: bigint;
95
+ readonly capBnd: bigint;
96
+ /** `prctl(PR_GET_NO_NEW_PRIVS)`, 0 or 1 as the kernel prints it. */
97
+ readonly noNewPrivs: number;
98
+ }
99
+ /**
100
+ * Parse `/proc/<pid>/status` into the five fields that decide admission.
101
+ *
102
+ * Pure: no transport, no clock, no I/O. Everything it cannot read is a
103
+ * throw, never a default — a zero substituted for a missing mask is the one
104
+ * mistake that would make this function report hardening that is not there.
105
+ */
106
+ export declare function parseProcStatus(text: string): ProcStatusPrivileges;
107
+ /**
108
+ * Admit only an all-zero capability set with `no_new_privs` set. Every
109
+ * non-zero mask is named in the refusal, because "one of them is set" sends
110
+ * the reader back to the guest to find out which.
111
+ */
112
+ export declare function assertDeprivileged(privileges: ProcStatusPrivileges, sandboxName: string): void;
113
+ /**
114
+ * Run the probe over an already-built sandbox's `exec` and admit or refuse.
115
+ *
116
+ * `run` is the sandbox's own `exec`, not the raw transport, so the probe
117
+ * traverses exactly the path every later call will: reserve, admit, stream,
118
+ * confirm. A probe that cannot get through this is a sandbox a caller
119
+ * cannot use either.
120
+ */
121
+ export declare function runPrivilegeProbe(run: (command: string, args: string[]) => Promise<SandboxExecResult>, sandboxName: string): Promise<ProcStatusPrivileges>;
122
+ /**
123
+ * The refusal for a probe that never answered.
124
+ *
125
+ * A guest that accepts the TCP connection and then goes quiet — an agent
126
+ * process out of memory, an event loop blocked by the workload, a container
127
+ * alive with a listener that has stopped reading — cannot be told apart from
128
+ * a healthy one by the wire alone, so the caller's acquire budget is the only
129
+ * thing that ends the wait. That expiry is a `'probe-failed'` like any other
130
+ * way the probe could not run, but it gets its own words: "the deadline
131
+ * expired" on its own says nothing about WHICH half of the acquire stopped
132
+ * answering, and a reader who sees `cat` blamed for a hang goes looking for a
133
+ * missing binary that is not missing.
134
+ */
135
+ export declare function privilegeProbeTimedOut(sandboxName: string, timeoutMs: number, cause: unknown): KubernetesPrivilegeProbeError;
136
+ //# sourceMappingURL=privilege-probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privilege-probe.d.ts","sourceRoot":"","sources":["../../../src/backends/kubernetes/privilege-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAQ,CAAA;AAE5C;wEACwE;AACxE,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAA0B,CAAA;AAE5E,kEAAkE;AAClE,MAAM,MAAM,qBAAqB;AAChC,oEAAoE;AAClE,cAAc;AAChB,qEAAqE;GACnE,mBAAmB;AACrB,yEAAyE;GACvE,YAAY,CAAA;AAEf;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IAItD,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAHvC,SAAkB,IAAI,mCAAkC;gBAG9C,MAAM,EAAE,qBAAqB,EACtC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAIvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B;AAuBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,CA8BlE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAe9F;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,EACpE,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA6B/B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACrC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,OAAO,GACZ,6BAA6B,CAQ/B"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Acquire-time privilege probe: prove the guest process really was
3
+ * deprivileged, rather than assume the entrypoint did its job.
4
+ *
5
+ * The image's entrypoint mounts the workspace as root and then
6
+ * `exec setpriv --reuid --regid --clear-groups --inh-caps=-all
7
+ * --bounding-set=-all --no-new-privs -- tini -- node agent.cjs` (`tini` is
8
+ * the container's pid 1 so it can reap an orphan `agent.cjs` itself never
9
+ * spawned — see `k8s/entrypoint.sh` and the Dockerfile). Nothing in
10
+ * `agent.cjs` knows about any of that, and nothing on the host can see it
11
+ * either — an image built from an older entrypoint, a `RuntimeClass` change,
12
+ * a hand-edited `SandboxTemplate` all produce a sandbox that works perfectly
13
+ * and is not deprivileged. So the backend asks the guest, once, before it
14
+ * hands a caller a handle.
15
+ *
16
+ * ## Why `execute`, and why not `read-file`
17
+ *
18
+ * The guest's `read-file` resolves every path against `READ_ROOTS`
19
+ * (`WORKSPACE_ROOT` only), so it cannot reach `/proc` at all, and widening
20
+ * `READ_ROOTS` to make the probe work would hand every caller of `readFile`
21
+ * a window into the guest's process tree for the sake of one diagnostic.
22
+ * `handleExecute` jails only `cwd`, so a command whose ARGUMENT is an
23
+ * absolute path outside the workspace runs fine. The probe therefore spends
24
+ * one `exec` and touches no jail.
25
+ *
26
+ * ## Why all four masks
27
+ *
28
+ * Checking `CapEff` alone is a true-looking answer: an ordinary unprivileged
29
+ * process shows `CapEff: 0000000000000000` whether or not its bounding set
30
+ * was ever dropped, so a container running as uid 0 with the full bounding
31
+ * set still passes. `CapBnd` is the one that says a capability can never be
32
+ * regained; `CapInh` and `CapPrm` close the two ways one could be carried
33
+ * across an exec. `NoNewPrivs: 1` is what makes a setuid binary inside the
34
+ * guest unable to raise any of it back.
35
+ *
36
+ * ## Why every failure is a refusal
37
+ *
38
+ * A probe that could not run, one that never answered at all, output that
39
+ * could not be parsed and a process that is genuinely privileged are all
40
+ * reasons NOT to hand back a handle, and they are separated only in the error
41
+ * TEXT — a distroless image with no `cat` on `PATH` must be diagnosable as
42
+ * exactly that rather than read as a hardening failure. There is no
43
+ * configuration that turns this off.
44
+ *
45
+ * ## The clock is the caller's, and it lives in `index.ts`
46
+ *
47
+ * Nothing here has a timeout: the probe is one `exec`, and the budget it may
48
+ * spend belongs to the `create()` that ordered it. `admitProbedSandbox` runs
49
+ * it under an `OperationDeadline` and turns an expiry into
50
+ * {@link privilegeProbeTimedOut}, so a guest that accepts the connection and
51
+ * then goes quiet is refused on the caller's clock rather than on the
52
+ * execution controller's five-minute generic default.
53
+ */
54
+ /**
55
+ * The probe command. `cat` rather than an absolute `/bin/cat` so a guest
56
+ * that keeps its coreutils somewhere else still answers, and rather than a
57
+ * shell so there is no quoting to get wrong. A guest without it fails with
58
+ * a spawn error the refusal repeats verbatim.
59
+ */
60
+ export const PRIVILEGE_PROBE_COMMAND = 'cat';
61
+ /** `/proc/self/status` — of the process the guest agent spawns, which
62
+ * inherits exactly the agent's own credentials and capability masks. */
63
+ export const PRIVILEGE_PROBE_ARGS = ['/proc/self/status'];
64
+ /**
65
+ * Raised by {@link runPrivilegeProbe} and {@link parseProcStatus}. The
66
+ * `reason` is the machine-readable form of the distinction the message
67
+ * draws in prose: `'privileged'` means the guest is under-hardened, and the
68
+ * other two mean the backend could not tell.
69
+ */
70
+ export class KubernetesPrivilegeProbeError extends Error {
71
+ reason;
72
+ name = 'KubernetesPrivilegeProbeError';
73
+ constructor(reason, message, options) {
74
+ super(message, options);
75
+ this.reason = reason;
76
+ }
77
+ }
78
+ const CAPABILITY_FIELDS = ['CapInh', 'CapPrm', 'CapEff', 'CapBnd'];
79
+ const NO_NEW_PRIVS_FIELD = 'NoNewPrivs';
80
+ /** Clip a value before it goes into an error message. */
81
+ function clip(value) {
82
+ return value.length > 40 ? `${value.slice(0, 40)}…` : value;
83
+ }
84
+ function readField(text, field) {
85
+ for (const rawLine of text.split(/\r?\n/)) {
86
+ const colon = rawLine.indexOf(':');
87
+ if (colon < 0)
88
+ continue;
89
+ if (rawLine.slice(0, colon).trim() !== field)
90
+ continue;
91
+ return rawLine.slice(colon + 1).trim();
92
+ }
93
+ throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but its output carries no ${field} line, so this sandbox's privileges could not be read. The probe is \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` — a guest whose /proc is not mounted, or whose kernel does not publish ${field}, cannot be admitted, because an unreadable answer is not a safe one.`);
94
+ }
95
+ /**
96
+ * Parse `/proc/<pid>/status` into the five fields that decide admission.
97
+ *
98
+ * Pure: no transport, no clock, no I/O. Everything it cannot read is a
99
+ * throw, never a default — a zero substituted for a missing mask is the one
100
+ * mistake that would make this function report hardening that is not there.
101
+ */
102
+ export function parseProcStatus(text) {
103
+ const masks = CAPABILITY_FIELDS.map((field) => {
104
+ const raw = readField(text, field);
105
+ // The kernel prints a bare, fixed-width hex mask with no `0x`. A
106
+ // `0x` prefix, a sign, whitespace inside, or anything non-hex means
107
+ // this is not the file this parser thinks it is.
108
+ if (!/^[0-9a-fA-F]+$/.test(raw)) {
109
+ throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but ${field} is ${JSON.stringify(clip(raw))}, which is not the bare hexadecimal capability mask /proc/<pid>/status publishes, so this sandbox's privileges could not be read.`);
110
+ }
111
+ return BigInt(`0x${raw}`);
112
+ });
113
+ const noNewPrivsRaw = readField(text, NO_NEW_PRIVS_FIELD);
114
+ if (!/^\d+$/.test(noNewPrivsRaw)) {
115
+ throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but ${NO_NEW_PRIVS_FIELD} is ${JSON.stringify(clip(noNewPrivsRaw))}, which is not the integer /proc/<pid>/status publishes, so this sandbox's privileges could not be read.`);
116
+ }
117
+ return {
118
+ capInh: masks[0],
119
+ capPrm: masks[1],
120
+ capEff: masks[2],
121
+ capBnd: masks[3],
122
+ noNewPrivs: Number(noNewPrivsRaw),
123
+ };
124
+ }
125
+ /**
126
+ * Admit only an all-zero capability set with `no_new_privs` set. Every
127
+ * non-zero mask is named in the refusal, because "one of them is set" sends
128
+ * the reader back to the guest to find out which.
129
+ */
130
+ export function assertDeprivileged(privileges, sandboxName) {
131
+ const offenders = [];
132
+ if (privileges.capInh !== 0n)
133
+ offenders.push(`CapInh=${privileges.capInh.toString(16)}`);
134
+ if (privileges.capPrm !== 0n)
135
+ offenders.push(`CapPrm=${privileges.capPrm.toString(16)}`);
136
+ if (privileges.capEff !== 0n)
137
+ offenders.push(`CapEff=${privileges.capEff.toString(16)}`);
138
+ if (privileges.capBnd !== 0n)
139
+ offenders.push(`CapBnd=${privileges.capBnd.toString(16)}`);
140
+ if (privileges.noNewPrivs !== 1)
141
+ offenders.push(`NoNewPrivs=${privileges.noNewPrivs}`);
142
+ if (offenders.length === 0)
143
+ return;
144
+ throw new KubernetesPrivilegeProbeError('privileged', `kubernetes sandbox ${sandboxName} is PRIVILEGED and was refused: ${offenders.join(', ')} (every capability mask must be 0 and NoNewPrivs must be 1). The probe ran and was read successfully — this is the guest's real state, not a diagnostic failure. The image's entrypoint is expected to end with \`exec setpriv --reuid --regid --clear-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- tini -- node agent.cjs\`; a sandbox that reaches this message is running with capabilities the workload could use.`);
145
+ }
146
+ /**
147
+ * Run the probe over an already-built sandbox's `exec` and admit or refuse.
148
+ *
149
+ * `run` is the sandbox's own `exec`, not the raw transport, so the probe
150
+ * traverses exactly the path every later call will: reserve, admit, stream,
151
+ * confirm. A probe that cannot get through this is a sandbox a caller
152
+ * cannot use either.
153
+ */
154
+ export async function runPrivilegeProbe(run, sandboxName) {
155
+ let result;
156
+ try {
157
+ result = await run(PRIVILEGE_PROBE_COMMAND, [...PRIVILEGE_PROBE_ARGS]);
158
+ }
159
+ catch (error) {
160
+ throw new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: ${error instanceof Error ? error.message : String(error)}. The probe is \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\`; an image without it on PATH cannot be admitted, because a sandbox whose privileges cannot be checked is refused rather than trusted.`, { cause: error });
161
+ }
162
+ if (result.exitCode !== 0) {
163
+ throw new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` exited ${result.exitCode}${result.stderr.trim() ? ` (${clip(result.stderr.trim())})` : ''}. This is a diagnostic failure, not a privilege failure — the sandbox is refused because its state is unknown.`);
164
+ }
165
+ const privileges = parseProcStatus(result.stdout);
166
+ assertDeprivileged(privileges, sandboxName);
167
+ return privileges;
168
+ }
169
+ /**
170
+ * The refusal for a probe that never answered.
171
+ *
172
+ * A guest that accepts the TCP connection and then goes quiet — an agent
173
+ * process out of memory, an event loop blocked by the workload, a container
174
+ * alive with a listener that has stopped reading — cannot be told apart from
175
+ * a healthy one by the wire alone, so the caller's acquire budget is the only
176
+ * thing that ends the wait. That expiry is a `'probe-failed'` like any other
177
+ * way the probe could not run, but it gets its own words: "the deadline
178
+ * expired" on its own says nothing about WHICH half of the acquire stopped
179
+ * answering, and a reader who sees `cat` blamed for a hang goes looking for a
180
+ * missing binary that is not missing.
181
+ */
182
+ export function privilegeProbeTimedOut(sandboxName, timeoutMs, cause) {
183
+ return new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: the guest accepted the connection and did not answer \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` within ${timeoutMs} ms, so the probe was abandoned. This is a diagnostic failure, not a privilege failure — the sandbox is refused because its state is unknown. A wedged agent (out of memory, an event loop blocked by the workload) looks exactly like this from the host; check the pod's logs before raising the acquire budget.`, { cause });
184
+ }
185
+ //# sourceMappingURL=privilege-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privilege-probe.js","sourceRoot":"","sources":["../../../src/backends/kubernetes/privilege-probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The {@link Sandbox} a kubernetes acquire hands back: the SDK contract,
3
+ * served over the guest agent's TCP transport, with the lease that keeps the
4
+ * cluster from deleting the pod out from under a long run.
5
+ *
6
+ * Split out of `index.ts` because that file is about the CONTROL plane —
7
+ * claim, poll, address, release — and this one is about the DATA plane, and
8
+ * the two are read for different reasons.
9
+ *
10
+ * ## What it implements, and what it deliberately does not
11
+ *
12
+ * Implemented: `exec` (through the shared {@link RemoteExecutionController},
13
+ * so an `AbortSignal` terminates the guest process rather than abandoning
14
+ * the wait), `writeFile`, `readFile`, `listFiles`, `openTerminal`,
15
+ * `openTcpConnection`, `destroy`.
16
+ *
17
+ * Absent on purpose, because the SDK's contract says a backend that cannot
18
+ * honour an optional method must omit it rather than accept and ignore:
19
+ *
20
+ * - `setNetworkPolicy` — egress here is a `NetworkPolicy` attached to the
21
+ * pool's `SandboxTemplate`. There is no per-running-pod knob to turn, and
22
+ * a policy accepted and not applied is worse than one never offered: the
23
+ * caller stops looking.
24
+ * - `spawnDetached` — the guest agent has no op that starts a process and
25
+ * returns it running. A host asking for background jobs must be told no.
26
+ * - `walkFiles` — not in this batch. A host that requires bounded search
27
+ * refuses an absent method, which is the honest answer today.
28
+ *
29
+ * ## Terminals are owned
30
+ *
31
+ * `openTerminal` is only a compliant implementation if `destroy()` kills and
32
+ * awaits every terminal it returned, so open terminals are tracked and
33
+ * reaped before the object is released — the same thing the Firecracker
34
+ * backend does, for the same contract.
35
+ *
36
+ * ## Two terminal states, one `SandboxStatus`
37
+ *
38
+ * `SandboxStatus` has exactly four members and this change does not widen
39
+ * the SDK's union, so both ways a sandbox ends report `'destroyed'`. They
40
+ * are told apart by the error a later call throws:
41
+ * {@link KubernetesSandboxDestroyedError} (this host released it) and
42
+ * {@link KubernetesSandboxGoneError} (the cluster deleted it — the lease
43
+ * renewal found the object already gone).
44
+ */
45
+ import type { Sandbox } from '@namzu/sdk';
46
+ import type { KubernetesAgentTransport } from './transport.js';
47
+ /** Thrown by any operation on a sandbox this host already destroyed. */
48
+ export declare class KubernetesSandboxDestroyedError extends Error {
49
+ readonly operation: string;
50
+ readonly sandboxName: string;
51
+ readonly name = "KubernetesSandboxDestroyedError";
52
+ constructor(operation: string, sandboxName: string);
53
+ }
54
+ /**
55
+ * Thrown by any operation on a sandbox the CLUSTER removed while this
56
+ * handle still held it — the lease renewal PATCH came back 404/410. Distinct
57
+ * from {@link KubernetesSandboxDestroyedError} because nothing this host did
58
+ * caused it: the object expired, an operator deleted it, or the controller
59
+ * reaped it, and the actionable advice is different.
60
+ */
61
+ export declare class KubernetesSandboxGoneError extends Error {
62
+ readonly operation: string;
63
+ readonly sandboxName: string;
64
+ readonly name = "KubernetesSandboxGoneError";
65
+ constructor(operation: string, sandboxName: string);
66
+ }
67
+ interface KubernetesSandboxBaseOptions {
68
+ /** The cluster's own name for the bound sandbox — also the sandbox id. */
69
+ readonly name: string;
70
+ readonly rootDir: string;
71
+ readonly transport: KubernetesAgentTransport;
72
+ /** DELETE the object this backend created. Already-gone counts as done. */
73
+ readonly release: (signal?: AbortSignal) => Promise<void>;
74
+ }
75
+ /**
76
+ * The lease half of the options: a way to move the expiry, and the expiry it
77
+ * is moving. Required TOGETHER, because `renew` without `ttlSeconds` is a
78
+ * renewal loop with nothing to stamp — it would re-stamp `now + 0`, an
79
+ * expiry already in the past, and hand the object straight to the
80
+ * controller's reaper while reporting every tick a success. A pair is the
81
+ * only shape that cannot be half-configured.
82
+ */
83
+ interface KubernetesSandboxLeaseOptions {
84
+ /** PATCH the object's `shutdownTime` forward. See `lease.ts`. */
85
+ readonly renew: (shutdownTime: string, signal?: AbortSignal) => Promise<void>;
86
+ /** The TTL acquire stamped; each renewal re-stamps exactly this much. */
87
+ readonly ttlSeconds: number;
88
+ readonly onRenewalError?: (error: unknown) => void;
89
+ /** Test seam: the renewal loop's base interval. Default: half the TTL. */
90
+ readonly leaseIntervalMs?: number;
91
+ }
92
+ /**
93
+ * The other arm: an object that carries no expiry, so this handle runs no
94
+ * renewal loop at all — the persistent workspace (`workspace.ts`), which is
95
+ * explicitly managed and must outlive a host that stopped renewing. A no-op
96
+ * `renew` would be the wrong way to say that: it would leave a timer ticking
97
+ * forever to do nothing. The lease fields are typed `undefined` rather than
98
+ * omitted so that passing one of them here is a type error and not an
99
+ * excess-property check a spread would slip past.
100
+ */
101
+ interface KubernetesSandboxUnleasedOptions {
102
+ readonly renew?: undefined;
103
+ readonly ttlSeconds?: undefined;
104
+ readonly onRenewalError?: undefined;
105
+ readonly leaseIntervalMs?: undefined;
106
+ }
107
+ export type KubernetesSandboxOptions = KubernetesSandboxBaseOptions & (KubernetesSandboxLeaseOptions | KubernetesSandboxUnleasedOptions);
108
+ /**
109
+ * What this backend hands back: the SDK contract, with the two optional
110
+ * members it DOES implement narrowed to present, so a caller that composes
111
+ * one — `workspace.ts` wraps this handle — does not have to re-check for a
112
+ * method this file always defines.
113
+ */
114
+ export type KubernetesSandboxHandle = Sandbox & Required<Pick<Sandbox, 'openTerminal' | 'openTcpConnection'>>;
115
+ /**
116
+ * Build the handle. It does NOT run the acquire-time privilege probe — that
117
+ * is `create()`'s job in `index.ts`, so that a refusal can destroy this
118
+ * object before any caller has a reference to it, and so this function stays
119
+ * usable by the workspace path that runs its own probe.
120
+ */
121
+ export declare function buildKubernetesSandbox(options: KubernetesSandboxOptions): KubernetesSandboxHandle;
122
+ export {};
123
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/backends/kubernetes/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAEX,OAAO,EAWP,MAAM,YAAY,CAAA;AAQnB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAW9D,wEAAwE;AACxE,qBAAa,+BAAgC,SAAQ,KAAK;IAIxD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM;IAJ7B,SAAkB,IAAI,qCAAoC;gBAGhD,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM;CAM7B;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IAInD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM;IAJ7B,SAAkB,IAAI,gCAA+B;gBAG3C,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM;CAM7B;AAED,UAAU,4BAA4B;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAA;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;GAOG;AACH,UAAU,6BAA6B;IACtC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7E,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;GAQG;AACH,UAAU,gCAAgC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAA;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,CAAA;CACpC;AAED,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAClE,CAAC,6BAA6B,GAAG,gCAAgC,CAAC,CAAA;AASnE;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAC5C,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,mBAAmB,CAAC,CAAC,CAAA;AAE9D;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,wBAAwB,GAAG,uBAAuB,CAuNjG"}