@namzu/sandbox 14.0.0 → 16.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 (100) hide show
  1. package/CHANGELOG.md +924 -0
  2. package/README.md +369 -14
  3. package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
  4. package/dist/backends/aci-standby-pool/index.js +13 -1
  5. package/dist/backends/aci-standby-pool/index.js.map +1 -1
  6. package/dist/backends/docker/index.d.ts +169 -6
  7. package/dist/backends/docker/index.d.ts.map +1 -1
  8. package/dist/backends/docker/index.js +499 -85
  9. package/dist/backends/docker/index.js.map +1 -1
  10. package/dist/backends/firecracker/index.d.ts.map +1 -1
  11. package/dist/backends/firecracker/index.js +12 -2
  12. package/dist/backends/firecracker/index.js.map +1 -1
  13. package/dist/backends/firecracker/protocol.d.ts +459 -8
  14. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  15. package/dist/backends/firecracker/protocol.js +136 -0
  16. package/dist/backends/firecracker/protocol.js.map +1 -1
  17. package/dist/backends/firecracker/transport.d.ts +539 -6
  18. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  19. package/dist/backends/firecracker/transport.js +1171 -24
  20. package/dist/backends/firecracker/transport.js.map +1 -1
  21. package/dist/backends/kubernetes/egress-policy.d.ts +1181 -13
  22. package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
  23. package/dist/backends/kubernetes/egress-policy.js +2350 -31
  24. package/dist/backends/kubernetes/egress-policy.js.map +1 -1
  25. package/dist/backends/kubernetes/identity.d.ts +193 -0
  26. package/dist/backends/kubernetes/identity.d.ts.map +1 -0
  27. package/dist/backends/kubernetes/identity.js +147 -0
  28. package/dist/backends/kubernetes/identity.js.map +1 -0
  29. package/dist/backends/kubernetes/index.d.ts +678 -33
  30. package/dist/backends/kubernetes/index.d.ts.map +1 -1
  31. package/dist/backends/kubernetes/index.js +1180 -95
  32. package/dist/backends/kubernetes/index.js.map +1 -1
  33. package/dist/backends/kubernetes/ingress-policy.d.ts +375 -0
  34. package/dist/backends/kubernetes/ingress-policy.d.ts.map +1 -0
  35. package/dist/backends/kubernetes/ingress-policy.js +1050 -0
  36. package/dist/backends/kubernetes/ingress-policy.js.map +1 -0
  37. package/dist/backends/kubernetes/k8s-client.d.ts +213 -4
  38. package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -1
  39. package/dist/backends/kubernetes/k8s-client.js +359 -52
  40. package/dist/backends/kubernetes/k8s-client.js.map +1 -1
  41. package/dist/backends/kubernetes/lease.d.ts +40 -14
  42. package/dist/backends/kubernetes/lease.d.ts.map +1 -1
  43. package/dist/backends/kubernetes/lease.js +68 -18
  44. package/dist/backends/kubernetes/lease.js.map +1 -1
  45. package/dist/backends/kubernetes/objects.d.ts +423 -3
  46. package/dist/backends/kubernetes/objects.d.ts.map +1 -1
  47. package/dist/backends/kubernetes/objects.js +364 -2
  48. package/dist/backends/kubernetes/objects.js.map +1 -1
  49. package/dist/backends/kubernetes/per-sandbox-policy.d.ts +219 -0
  50. package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -0
  51. package/dist/backends/kubernetes/per-sandbox-policy.js +375 -0
  52. package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -0
  53. package/dist/backends/kubernetes/rbac.d.ts +153 -0
  54. package/dist/backends/kubernetes/rbac.d.ts.map +1 -0
  55. package/dist/backends/kubernetes/rbac.js +177 -0
  56. package/dist/backends/kubernetes/rbac.js.map +1 -0
  57. package/dist/backends/kubernetes/sandbox.d.ts +81 -14
  58. package/dist/backends/kubernetes/sandbox.d.ts.map +1 -1
  59. package/dist/backends/kubernetes/sandbox.js +149 -15
  60. package/dist/backends/kubernetes/sandbox.js.map +1 -1
  61. package/dist/backends/kubernetes/transport.d.ts +935 -9
  62. package/dist/backends/kubernetes/transport.d.ts.map +1 -1
  63. package/dist/backends/kubernetes/transport.js +1958 -62
  64. package/dist/backends/kubernetes/transport.js.map +1 -1
  65. package/dist/backends/kubernetes/workspace.d.ts +1149 -18
  66. package/dist/backends/kubernetes/workspace.d.ts.map +1 -1
  67. package/dist/backends/kubernetes/workspace.js +2825 -186
  68. package/dist/backends/kubernetes/workspace.js.map +1 -1
  69. package/dist/backends/remote-execution-controller.d.ts +14 -0
  70. package/dist/backends/remote-execution-controller.d.ts.map +1 -1
  71. package/dist/backends/remote-execution-controller.js.map +1 -1
  72. package/dist/index.d.ts +294 -18
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +280 -10
  75. package/dist/index.js.map +1 -1
  76. package/dist/testing/sandbox-conformance.d.ts +39 -5
  77. package/dist/testing/sandbox-conformance.d.ts.map +1 -1
  78. package/dist/testing/sandbox-conformance.js +436 -5
  79. package/dist/testing/sandbox-conformance.js.map +1 -1
  80. package/package.json +3 -3
  81. package/src/backends/aci-standby-pool/index.ts +16 -1
  82. package/src/backends/docker/index.ts +617 -100
  83. package/src/backends/firecracker/index.ts +14 -2
  84. package/src/backends/firecracker/protocol.ts +514 -6
  85. package/src/backends/firecracker/transport.ts +1492 -40
  86. package/src/backends/kubernetes/egress-policy.ts +3334 -55
  87. package/src/backends/kubernetes/identity.ts +261 -0
  88. package/src/backends/kubernetes/index.ts +1785 -127
  89. package/src/backends/kubernetes/ingress-policy.ts +1344 -0
  90. package/src/backends/kubernetes/k8s-client.ts +444 -54
  91. package/src/backends/kubernetes/lease.ts +75 -19
  92. package/src/backends/kubernetes/objects.ts +626 -6
  93. package/src/backends/kubernetes/per-sandbox-policy.ts +497 -0
  94. package/src/backends/kubernetes/rbac.ts +192 -0
  95. package/src/backends/kubernetes/sandbox.ts +218 -20
  96. package/src/backends/kubernetes/transport.ts +2733 -124
  97. package/src/backends/kubernetes/workspace.ts +4476 -222
  98. package/src/backends/remote-execution-controller.ts +14 -0
  99. package/src/index.ts +668 -19
  100. package/src/testing/sandbox-conformance.ts +540 -5
@@ -0,0 +1,261 @@
1
+ /**
2
+ * What a Kubernetes workspace handle is bound to, and what it says when the
3
+ * thing behind the name changes underneath it.
4
+ *
5
+ * Its own module for one structural reason: `workspace.ts` owns the handle
6
+ * and `transport.ts` owns the rebind that follows a replaced pod, the
7
+ * workspace imports the transport, and the refusal the transport has to
8
+ * carry back out belongs to neither of them alone. Putting these here is
9
+ * what lets the rebind routine rethrow a workspace-level verdict without
10
+ * `transport.ts` importing the file that imports it.
11
+ *
12
+ * Nothing in here reaches `@namzu/sdk`'s `Sandbox`. A handle's identity is a
13
+ * Kubernetes fact — a Sandbox uid, a PVC uid, a pod uid — and a tier with no
14
+ * such objects has nothing to answer.
15
+ */
16
+
17
+ import { RemoteCancellationUnknownError } from '../remote-execution-controller.js'
18
+
19
+ /**
20
+ * The four objects a workspace handle is holding at once.
21
+ *
22
+ * Read as a set rather than one at a time, because the QUESTION a host has is
23
+ * never about one of them: "is the work I did still there" is answered by the
24
+ * disk (`sandboxUid` and `volumeClaimUids`), and "are the processes I started
25
+ * still there" by the guest (`podUid` and `guestBootId`). The two can move
26
+ * independently and each moving means something different.
27
+ *
28
+ * - `sandboxUid` — the Sandbox object itself. A workspace deleted and
29
+ * recreated under the same id gets a new one, and an empty disk with it.
30
+ * Undefined only if the API server answered the handle's reads without a
31
+ * `metadata.uid`, which no real one does.
32
+ * - `volumeClaimUids` — one entry per `volumeClaimTemplates` entry NAME,
33
+ * holding that PVC's uid. Empty when the Role does not grant `get` on
34
+ * `persistentvolumeclaims`: the disk identity is reported when it can be
35
+ * read and the workspace works either way, rather than every create
36
+ * failing on a verb an existing deployment's Role does not have.
37
+ * - `podUid` — the live pod, and the agent's bind token. `undefined` exactly
38
+ * when the handle is bound to no pod: a suspended or destroyed workspace,
39
+ * and the window a `suspend()` opens between giving its pod back and a
40
+ * `resume()` binding the next one. It is NOT blanked for the length of a
41
+ * transition — a resume that has bound its pod and is probing it names
42
+ * that pod, because that is the pod every answer in that window is about.
43
+ * - `guestBootId` — the agent PROCESS inside that pod. `undefined` wherever
44
+ * `podUid` is, against a guest too old to report one (which is why nothing
45
+ * may require it), and until the pod it names has answered once. A changed
46
+ * value with an unchanged `podUid` is a container the kubelet restarted in
47
+ * place: same pod, same token, and every process the caller started gone.
48
+ */
49
+ export interface KubernetesWorkspaceIdentity {
50
+ readonly sandboxUid: string | undefined
51
+ readonly volumeClaimUids: Readonly<Record<string, string>>
52
+ readonly podUid: string | undefined
53
+ readonly guestBootId: string | undefined
54
+ }
55
+
56
+ /**
57
+ * Why the guest a handle is talking to is not the guest it was talking to.
58
+ *
59
+ * - `pod-replaced` — a different pod. The controller replaced it (a resume
60
+ * the handle did not make, an eviction, a node drain), and the handle has
61
+ * rebound to it with a new bind token.
62
+ * - `container-restarted` — the same pod, a different agent process. The
63
+ * kubelet restarted the container in place, so nothing about the address
64
+ * or the token changed and only the boot id says the guest did.
65
+ */
66
+ export type KubernetesGuestRestartReason = 'pod-replaced' | 'container-restarted'
67
+
68
+ /**
69
+ * One guest restart, delivered to every
70
+ * {@link KubernetesWorkspace.onGuestRestart} listener.
71
+ *
72
+ * `previous` and `current` are whole identities rather than the one field
73
+ * that moved: a listener deciding what of its own state to throw away needs
74
+ * to know what did NOT move as much as what did.
75
+ *
76
+ * Both halves name a pod, ALWAYS, and that is a promise about the payload
77
+ * rather than about the handle: they are built from the uids the routine
78
+ * announcing the move is holding, never read back off a handle that may be
79
+ * in the middle of a transition. The event a host is most likely to act on
80
+ * is the one raised while a `resume()` is still in flight — somebody else's
81
+ * replacement, found by the privilege probe — and an identity assembled from
82
+ * the handle's state would announce a pod replacement there while naming
83
+ * neither pod.
84
+ *
85
+ * On `pod-replaced`, `current.guestBootId` is `undefined` — the replacement
86
+ * has not answered yet, and naming a process nobody has heard from would be a
87
+ * guess. `current.podUid` is the field that moved, and the process that
88
+ * answers from that pod is in `workspace.identity` once the call that
89
+ * rebound has returned. On `container-restarted` the pod did not move and
90
+ * both boot ids are present.
91
+ */
92
+ export interface KubernetesGuestRestart {
93
+ readonly reason: KubernetesGuestRestartReason
94
+ readonly previous: KubernetesWorkspaceIdentity
95
+ readonly current: KubernetesWorkspaceIdentity
96
+ }
97
+
98
+ /**
99
+ * Thrown instead of rebinding when the object behind the workspace's name is
100
+ * not the object this handle was opened on.
101
+ *
102
+ * The distinction this class exists for is the whole reason a rebind is
103
+ * allowed at all. Following a REPLACED POD is safe: the Sandbox is the same
104
+ * object, so the disk behind it is the same disk and the work the caller did
105
+ * is still there. Following a replaced SANDBOX is not: the name is
106
+ * deterministic, so a workspace deleted and recreated stands under it with an
107
+ * empty disk, and a handle that silently carried on would write a caller's
108
+ * next file into a workspace its records say holds a month of work.
109
+ *
110
+ * So this is terminal for the handle: nothing retries it, nothing rebinds
111
+ * after it, and the way forward is to open a new handle and decide what the
112
+ * new disk is worth.
113
+ */
114
+ export class KubernetesWorkspaceReplacedError extends Error {
115
+ override readonly name = 'KubernetesWorkspaceReplacedError'
116
+
117
+ constructor(
118
+ readonly workspaceId: string,
119
+ readonly sandboxName: string,
120
+ /** The Sandbox uid this handle was opened on. */
121
+ readonly expectedSandboxUid: string | undefined,
122
+ /**
123
+ * The uid standing under that name now — `undefined` when there is no
124
+ * Sandbox there at all any more.
125
+ */
126
+ readonly actualSandboxUid: string | undefined,
127
+ options?: ErrorOptions,
128
+ ) {
129
+ super(
130
+ actualSandboxUid === undefined
131
+ ? `kubernetes: workspace ${workspaceId} (Sandbox ${sandboxName}, uid ${String(
132
+ expectedSandboxUid,
133
+ )}) no longer exists — the guest refused this handle's bind token and a re-read found no Sandbox of that name. Somebody deleted it, and a DELETE cascades to the disk. This handle is finished: it will not rebind, because there is nothing to rebind TO, and the failed call's outcome in the pod that is gone is unknown. Open a new workspace with the same id to get a fresh object with an empty disk.`
134
+ : `kubernetes: workspace ${workspaceId} (Sandbox ${sandboxName}) is a DIFFERENT object than the one this handle was opened on — it holds uid ${actualSandboxUid} where this handle bound uid ${String(
135
+ expectedSandboxUid,
136
+ )}. The name is deterministic, so somebody deleted the workspace and created it again under it; the disk behind the name is a new, empty disk and none of this handle's work is on it. This handle deliberately did NOT rebind to it — a call that silently succeeded against it would write into a workspace whose records say it holds the old one's work. Open a new handle and decide what the new disk is worth.`,
137
+ options,
138
+ )
139
+ }
140
+ }
141
+
142
+ /**
143
+ * What a bounded look at the guest found after a cancellation could not be
144
+ * confirmed — the identity half of the diagnosis, beside the `healthz` half.
145
+ *
146
+ * - `same-guest` — the pod this handle is bound to is still the live pod and
147
+ * the agent process is the one it has been talking to. A command of
148
+ * unknown state may genuinely still be running in it.
149
+ * - `pod-replaced` — a live pod stands under the name with a DIFFERENT uid.
150
+ * The pod that ran the command is gone, and everything in its pid
151
+ * namespace went with it.
152
+ * - `pod-gone` — no live pod under the name at all.
153
+ * - `container-restarted` — the same pod, a different agent process: the
154
+ * kubelet restarted the container, so the command's process tree is gone
155
+ * even though the address and the token still work.
156
+ * - `unknown` — the look itself could not be completed (the API read failed,
157
+ * or its own short deadline expired). Nothing may be concluded from it.
158
+ */
159
+ export type KubernetesGuestEvidence =
160
+ | 'same-guest'
161
+ | 'pod-replaced'
162
+ | 'pod-gone'
163
+ | 'container-restarted'
164
+ | 'unknown'
165
+
166
+ /**
167
+ * A cancellation that could not be confirmed, raised against a guest that is
168
+ * demonstrably no longer there.
169
+ *
170
+ * It extends {@link RemoteCancellationUnknownError} rather than replacing it,
171
+ * and that is the point: every host already catching the base class goes on
172
+ * catching this, the `retirement` observation still rides on it, and the rule
173
+ * it states is unchanged — **the command's outcome is unknown and it must not
174
+ * be retried automatically**. What this adds is the EVIDENCE, which the base
175
+ * class cannot carry: which guest the command was started on, which guest is
176
+ * there now, and how they differ.
177
+ *
178
+ * It changes nothing about the cluster. No patch is sent on this path — a
179
+ * `Suspended` patch cannot stop a command whose pod is already gone, and
180
+ * would only take the replacement away from every other holder — so a
181
+ * `Running` workspace stays `Running` and `suspended` stays `false`, and the
182
+ * next call rebinds to the live pod on its own.
183
+ *
184
+ * `previous` is the guest the COMMAND was running in — the pod its
185
+ * reservation was accepted by and the agent process inside it — and not
186
+ * whatever the handle is bound to by the time the diagnosis runs. The two
187
+ * differ exactly when it matters: the failing call's own `cancel-execution`
188
+ * refusal already told the handle about a restarted container, and another
189
+ * call may already have rebound the handle to the replacement pod. `current`
190
+ * is what stands under the workspace name now, and it names an agent process
191
+ * only when that process belongs to the pod it names — a pod nobody has heard
192
+ * from reports `guestBootId: undefined` rather than borrowing another pod's.
193
+ *
194
+ * It is never how a FOREIGN SUSPEND is reported, even though a suspended
195
+ * workspace has no pod either and produces the same evidence. That case
196
+ * leaves as `KubernetesWorkspaceSuspendedError` instead: the handle adopts
197
+ * the suspension, so `suspended` reads `true` and `resume()` brings a pod
198
+ * back — a recovery this error has no way to offer.
199
+ */
200
+ export class KubernetesWorkspaceGuestGoneError extends RemoteCancellationUnknownError {
201
+ override readonly name = 'KubernetesWorkspaceGuestGoneError'
202
+
203
+ constructor(
204
+ readonly workspaceId: string,
205
+ readonly sandboxName: string,
206
+ /** See {@link KubernetesGuestEvidence} — never `same-guest` here. */
207
+ readonly evidence: KubernetesGuestEvidence,
208
+ readonly previous: KubernetesWorkspaceIdentity,
209
+ readonly current: KubernetesWorkspaceIdentity,
210
+ options?: ErrorOptions,
211
+ ) {
212
+ super(
213
+ `kubernetes: a command on workspace ${workspaceId} (Sandbox ${sandboxName}) could not have its cancellation confirmed, and the guest it was running in is gone: ${describeEvidence(
214
+ evidence,
215
+ )} (${describeMove(previous, current)}). The command's outcome is UNKNOWN — nothing on this side ever saw it end — so it must not be retried automatically; whatever it had already written to the disk is on the disk. Nothing was patched from here: no Suspended patch was sent, the workspace still holds its disk, and the next call binds to whatever pod stands under the name. Every process the command's guest was running, this command included, is gone with it.`,
216
+ options,
217
+ )
218
+ }
219
+ }
220
+
221
+ /**
222
+ * What moved between the guest the command RESERVED on and the guest standing
223
+ * there now, said without ever printing a value neither identity holds.
224
+ *
225
+ * Both halves of each identity come from the same pod by construction, so
226
+ * this only has to render them: an absent `podUid` is no pod at all, and an
227
+ * absent `guestBootId` on `current` is a pod nothing has answered from yet —
228
+ * never a process this handle happens to know about in some other pod.
229
+ *
230
+ * "No pod" is spelled out on BOTH sides rather than stringified. Every
231
+ * caller here guards on a bound pod before it builds a diagnosis, so an
232
+ * absent `previous.podUid` should not arrive — and a sentence reading "pod
233
+ * undefined, unchanged" is the kind of thing an operator quotes back, so it
234
+ * is worded rather than left to `String`.
235
+ */
236
+ function describeMove(
237
+ previous: KubernetesWorkspaceIdentity,
238
+ current: KubernetesWorkspaceIdentity,
239
+ ): string {
240
+ const was = previous.podUid ?? 'no pod this handle could name'
241
+ const pod =
242
+ previous.podUid === current.podUid
243
+ ? `pod ${was}, unchanged`
244
+ : `pod ${was} → ${current.podUid ?? 'no pod under the name'}`
245
+ if (previous.guestBootId === undefined && current.guestBootId === undefined) {
246
+ return `${pod}; this guest reports no boot id, so its agent process cannot be named`
247
+ }
248
+ if (previous.guestBootId === current.guestBootId)
249
+ return `${pod}, agent ${String(current.guestBootId)}, unchanged`
250
+ return `${pod}, agent ${previous.guestBootId ?? 'unknown'} → ${current.guestBootId ?? 'nothing has answered from there yet'}`
251
+ }
252
+
253
+ function describeEvidence(evidence: KubernetesGuestEvidence): string {
254
+ if (evidence === 'pod-replaced') return 'a different pod stands under the workspace name'
255
+ if (evidence === 'pod-gone') return 'no live pod stands under the workspace name'
256
+ if (evidence === 'container-restarted') {
257
+ return 'the same pod is running a different agent process, so its container was restarted in place'
258
+ }
259
+ if (evidence === 'unknown') return 'the guest could not be identified at all'
260
+ return 'the guest is unchanged'
261
+ }