@namzu/sandbox 14.0.0 → 15.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 (99) hide show
  1. package/CHANGELOG.md +838 -0
  2. package/README.md +310 -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.map +1 -1
  7. package/dist/backends/docker/index.js +19 -1
  8. package/dist/backends/docker/index.js.map +1 -1
  9. package/dist/backends/firecracker/index.d.ts.map +1 -1
  10. package/dist/backends/firecracker/index.js +12 -2
  11. package/dist/backends/firecracker/index.js.map +1 -1
  12. package/dist/backends/firecracker/protocol.d.ts +459 -8
  13. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  14. package/dist/backends/firecracker/protocol.js +136 -0
  15. package/dist/backends/firecracker/protocol.js.map +1 -1
  16. package/dist/backends/firecracker/transport.d.ts +539 -6
  17. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  18. package/dist/backends/firecracker/transport.js +1171 -24
  19. package/dist/backends/firecracker/transport.js.map +1 -1
  20. package/dist/backends/kubernetes/egress-policy.d.ts +1088 -11
  21. package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
  22. package/dist/backends/kubernetes/egress-policy.js +2173 -29
  23. package/dist/backends/kubernetes/egress-policy.js.map +1 -1
  24. package/dist/backends/kubernetes/identity.d.ts +193 -0
  25. package/dist/backends/kubernetes/identity.d.ts.map +1 -0
  26. package/dist/backends/kubernetes/identity.js +147 -0
  27. package/dist/backends/kubernetes/identity.js.map +1 -0
  28. package/dist/backends/kubernetes/index.d.ts +678 -33
  29. package/dist/backends/kubernetes/index.d.ts.map +1 -1
  30. package/dist/backends/kubernetes/index.js +1180 -95
  31. package/dist/backends/kubernetes/index.js.map +1 -1
  32. package/dist/backends/kubernetes/ingress-policy.d.ts +375 -0
  33. package/dist/backends/kubernetes/ingress-policy.d.ts.map +1 -0
  34. package/dist/backends/kubernetes/ingress-policy.js +1050 -0
  35. package/dist/backends/kubernetes/ingress-policy.js.map +1 -0
  36. package/dist/backends/kubernetes/k8s-client.d.ts +213 -4
  37. package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -1
  38. package/dist/backends/kubernetes/k8s-client.js +359 -52
  39. package/dist/backends/kubernetes/k8s-client.js.map +1 -1
  40. package/dist/backends/kubernetes/lease.d.ts +40 -14
  41. package/dist/backends/kubernetes/lease.d.ts.map +1 -1
  42. package/dist/backends/kubernetes/lease.js +68 -18
  43. package/dist/backends/kubernetes/lease.js.map +1 -1
  44. package/dist/backends/kubernetes/objects.d.ts +423 -3
  45. package/dist/backends/kubernetes/objects.d.ts.map +1 -1
  46. package/dist/backends/kubernetes/objects.js +364 -2
  47. package/dist/backends/kubernetes/objects.js.map +1 -1
  48. package/dist/backends/kubernetes/per-sandbox-policy.d.ts +219 -0
  49. package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -0
  50. package/dist/backends/kubernetes/per-sandbox-policy.js +407 -0
  51. package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -0
  52. package/dist/backends/kubernetes/rbac.d.ts +153 -0
  53. package/dist/backends/kubernetes/rbac.d.ts.map +1 -0
  54. package/dist/backends/kubernetes/rbac.js +177 -0
  55. package/dist/backends/kubernetes/rbac.js.map +1 -0
  56. package/dist/backends/kubernetes/sandbox.d.ts +81 -14
  57. package/dist/backends/kubernetes/sandbox.d.ts.map +1 -1
  58. package/dist/backends/kubernetes/sandbox.js +149 -15
  59. package/dist/backends/kubernetes/sandbox.js.map +1 -1
  60. package/dist/backends/kubernetes/transport.d.ts +935 -9
  61. package/dist/backends/kubernetes/transport.d.ts.map +1 -1
  62. package/dist/backends/kubernetes/transport.js +1958 -62
  63. package/dist/backends/kubernetes/transport.js.map +1 -1
  64. package/dist/backends/kubernetes/workspace.d.ts +1149 -18
  65. package/dist/backends/kubernetes/workspace.d.ts.map +1 -1
  66. package/dist/backends/kubernetes/workspace.js +2825 -186
  67. package/dist/backends/kubernetes/workspace.js.map +1 -1
  68. package/dist/backends/remote-execution-controller.d.ts +14 -0
  69. package/dist/backends/remote-execution-controller.d.ts.map +1 -1
  70. package/dist/backends/remote-execution-controller.js.map +1 -1
  71. package/dist/index.d.ts +231 -13
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +247 -5
  74. package/dist/index.js.map +1 -1
  75. package/dist/testing/sandbox-conformance.d.ts +39 -5
  76. package/dist/testing/sandbox-conformance.d.ts.map +1 -1
  77. package/dist/testing/sandbox-conformance.js +436 -5
  78. package/dist/testing/sandbox-conformance.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/backends/aci-standby-pool/index.ts +16 -1
  81. package/src/backends/docker/index.ts +22 -1
  82. package/src/backends/firecracker/index.ts +14 -2
  83. package/src/backends/firecracker/protocol.ts +514 -6
  84. package/src/backends/firecracker/transport.ts +1492 -40
  85. package/src/backends/kubernetes/egress-policy.ts +3064 -53
  86. package/src/backends/kubernetes/identity.ts +261 -0
  87. package/src/backends/kubernetes/index.ts +1785 -127
  88. package/src/backends/kubernetes/ingress-policy.ts +1344 -0
  89. package/src/backends/kubernetes/k8s-client.ts +444 -54
  90. package/src/backends/kubernetes/lease.ts +75 -19
  91. package/src/backends/kubernetes/objects.ts +626 -6
  92. package/src/backends/kubernetes/per-sandbox-policy.ts +542 -0
  93. package/src/backends/kubernetes/rbac.ts +192 -0
  94. package/src/backends/kubernetes/sandbox.ts +218 -20
  95. package/src/backends/kubernetes/transport.ts +2733 -124
  96. package/src/backends/kubernetes/workspace.ts +4476 -222
  97. package/src/backends/remote-execution-controller.ts +14 -0
  98. package/src/index.ts +595 -14
  99. package/src/testing/sandbox-conformance.ts +540 -5
@@ -16,36 +16,48 @@
16
16
  *
17
17
  * One `setTimeout` chained per tick, never `setInterval`: a renewal PATCH
18
18
  * that takes longer than the interval must not queue a second one behind
19
- * it. The interval is HALF the TTL, so a single failed tick still leaves a
20
- * whole half-TTL of headroom for the next one to succeed, and it is
21
- * jittered ±10% so a hundred handles acquired in the same second do not
22
- * PATCH the API server in the same millisecond forever after.
19
+ * it. On SUCCESS the interval is HALF the TTL, jittered ±10% so a hundred
20
+ * handles acquired in the same second do not PATCH the API server in the
21
+ * same millisecond forever after.
23
22
  *
24
23
  * The timer is `unref`'d: a host process that has finished its work should
25
24
  * exit, not linger because a sandbox handle is still counting. A handle
26
25
  * nobody destroyed then expires on the cluster's clock exactly as an
27
26
  * abandoned one does, which is the behaviour the TTL exists for.
28
27
  *
28
+ * ## A failed tick does not wait for the next half-TTL
29
+ *
30
+ * Waiting a full half-TTL before retrying a FAILED renewal means one blip at
31
+ * exactly the wrong moment is a coin flip against the object's own
32
+ * `shutdownTime`: the retry and the expiry are both roughly a TTL after the
33
+ * last success, so a single failure can lose that race. A failed tick
34
+ * instead retries on capped exponential backoff — starting at one second,
35
+ * doubling, capped at whichever is smaller of thirty seconds or a
36
+ * twentieth of the TTL — so an outage around a scheduled renewal gets many
37
+ * attempts inside the window that actually matters, not one. Every success
38
+ * resets the backoff and returns the loop to the normal half-TTL cadence.
39
+ *
29
40
  * ## Every tick is bounded
30
41
  *
31
- * A renewal that FAILS is survivable — it is reported and retried with half
32
- * a TTL of headroom. A renewal that HANGS is not: the next tick is scheduled
42
+ * A renewal that FAILS is survivable — it is reported and retried on a
43
+ * short backoff. A renewal that HANGS is not: the next tick is scheduled
33
44
  * only after the current one settles, so a PATCH that never answers parks
34
45
  * the loop forever, reports nothing, and lets the lease expire in silence —
35
46
  * precisely the defect this file exists to close, moved onto the failure
36
47
  * path. An API server that accepts a connection and then never responds is
37
48
  * an ordinary cluster event, so each PATCH runs under its own deadline: it
38
49
  * aborts the request through the signal the client already takes, and an
39
- * expiry is then just another reported failure that retries on the next
40
- * tick.
50
+ * expiry is then just another reported failure that retries on backoff.
41
51
  *
42
52
  * ## What each outcome means
43
53
  *
44
- * - Success → the object's expiry moves a full TTL into the future.
54
+ * - Success → the object's expiry moves a full TTL into the future, the
55
+ * backoff resets, and the next tick is a half-TTL away again.
45
56
  * - Any error, a tick that ran out of time included → reported to
46
- * `onRenewalError` and RETRIED on the next tick. A transient API blip
47
- * must not tear down a working sandbox, and there is still half a TTL of
48
- * headroom.
57
+ * `onRenewalError` and RETRIED on a backoff far shorter than the
58
+ * half-TTL interval. A transient API blip must not tear down a working
59
+ * sandbox, and the loop keeps trying rather than spend the object's
60
+ * remaining headroom waiting.
49
61
  * - Already gone (404/410) → the object this handle owns no longer exists.
50
62
  * Nothing will bring it back, so the loop stops and the handle is marked
51
63
  * gone; every later call fails with a named error instead of dialing an
@@ -59,6 +71,21 @@ import { KubernetesAlreadyGoneError } from './k8s-client.js';
59
71
  * derived quarter-interval would otherwise be 7.5 minutes of silence.
60
72
  */
61
73
  const MAX_RENEWAL_TIMEOUT_MS = 30_000;
74
+ /**
75
+ * The floor of the retry backoff after a failed renewal: one second. Far
76
+ * short of the half-TTL interval, on purpose — a failure needs another
77
+ * chance long before the object's `shutdownTime` is at risk, not after
78
+ * waiting as long as a successful tick would have.
79
+ */
80
+ const RETRY_BACKOFF_FLOOR_MS = 1_000;
81
+ /**
82
+ * The ceiling of the retry backoff, whichever is smaller: thirty seconds, or
83
+ * a twentieth of the TTL. The TTL fraction keeps a short-TTL sandbox (tests,
84
+ * mainly) from retrying so slowly that the backoff alone could still lose
85
+ * the race against expiry; thirty seconds keeps an hour-plus TTL from
86
+ * retrying needlessly often once the ceiling is reached.
87
+ */
88
+ const MAX_RETRY_BACKOFF_MS = 30_000;
62
89
  /** ±10%: enough to spread a synchronised fleet, far too little to matter
63
90
  * against a half-TTL of headroom. */
64
91
  const JITTER_FRACTION = 0.1;
@@ -78,13 +105,20 @@ export class KubernetesLeaseRenewal {
78
105
  started = false;
79
106
  baseIntervalMs;
80
107
  patchTimeoutMs;
108
+ retryBackoffCapMs;
81
109
  random;
110
+ /**
111
+ * Non-gone failures since the last success (or since the loop started).
112
+ * Reset to 0 by every success; drives how far the next retry backs off.
113
+ */
114
+ consecutiveFailures = 0;
82
115
  constructor(options) {
83
116
  this.options = options;
84
117
  this.baseIntervalMs = options.intervalMs ?? Math.max(1, (options.ttlSeconds * 1_000) / 2);
85
118
  this.patchTimeoutMs =
86
119
  options.patchTimeoutMs ??
87
120
  Math.max(1, Math.min(MAX_RENEWAL_TIMEOUT_MS, Math.round(this.baseIntervalMs / 4)));
121
+ this.retryBackoffCapMs = Math.max(1, Math.min(MAX_RETRY_BACKOFF_MS, (options.ttlSeconds * 1_000) / 20));
88
122
  this.random = options.random ?? Math.random;
89
123
  }
90
124
  /**
@@ -101,7 +135,7 @@ export class KubernetesLeaseRenewal {
101
135
  if (this.stopped || this.timer !== undefined)
102
136
  return;
103
137
  this.started = true;
104
- this.schedule();
138
+ this.scheduleNext(this.baseIntervalMs);
105
139
  }
106
140
  stop() {
107
141
  this.stopped = true;
@@ -110,16 +144,27 @@ export class KubernetesLeaseRenewal {
110
144
  this.timer = undefined;
111
145
  }
112
146
  }
113
- schedule() {
147
+ scheduleNext(baseMs) {
114
148
  if (this.stopped)
115
149
  return;
116
150
  const timer = setTimeout(() => {
117
151
  void this.tick();
118
- }, jitteredInterval(this.baseIntervalMs, this.random));
152
+ }, jitteredInterval(baseMs, this.random));
119
153
  // A pending renewal must never be the reason a host process stays up.
120
154
  timer.unref?.();
121
155
  this.timer = timer;
122
156
  }
157
+ /**
158
+ * The delay before the NEXT retry after a non-gone failure: capped
159
+ * exponential backoff from {@link RETRY_BACKOFF_FLOOR_MS}, doubling on
160
+ * every consecutive failure, ceilinged at {@link retryBackoffCapMs}.
161
+ * Called only once `consecutiveFailures` has already been incremented for
162
+ * the failure that just happened, so the first retry uses the floor.
163
+ */
164
+ retryDelayMs() {
165
+ const doubled = RETRY_BACKOFF_FLOOR_MS * 2 ** (this.consecutiveFailures - 1);
166
+ return Math.min(this.retryBackoffCapMs, doubled);
167
+ }
123
168
  /** Exposed for tests: one renewal attempt plus its scheduling decision. */
124
169
  async tick() {
125
170
  this.timer = undefined;
@@ -141,11 +186,16 @@ export class KubernetesLeaseRenewal {
141
186
  return;
142
187
  }
143
188
  // Everything else is transient until proven otherwise: report it
144
- // and try again on the next tick, which is still half a TTL
145
- // before anything expires.
189
+ // and retry on a backoff far shorter than the half-TTL interval —
190
+ // a coin-flip race against the object's own expiry is exactly what
191
+ // this file exists to avoid.
192
+ this.consecutiveFailures += 1;
146
193
  this.options.onRenewalError?.(error);
194
+ this.scheduleNext(this.retryDelayMs());
195
+ return;
147
196
  }
148
- this.schedule();
197
+ this.consecutiveFailures = 0;
198
+ this.scheduleNext(this.baseIntervalMs);
149
199
  }
150
200
  }
151
201
  //# sourceMappingURL=lease.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lease.js","sourceRoot":"","sources":["../../../src/backends/kubernetes/lease.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lease.js","sourceRoot":"","sources":["../../../src/backends/kubernetes/lease.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,25 @@ export interface KubernetesObjectMeta {
33
33
  readonly name?: string;
34
34
  readonly namespace?: string;
35
35
  readonly uid?: string;
36
+ /**
37
+ * RFC 3339, written by the API server on admission and never by a client.
38
+ * Read only to report how old a workspace is — see
39
+ * `workspace.ts`'s `listKubernetesWorkspaces`.
40
+ */
41
+ readonly creationTimestamp?: string;
42
+ /**
43
+ * The object's version as this read saw it, written by the API server on
44
+ * every write and never by a client.
45
+ *
46
+ * It is here for exactly one use and no other: the fallback `test` clause
47
+ * of a holder-epoch patch aimed at an object that does not carry the
48
+ * annotation yet, and the `preconditions.resourceVersion` of a fenced
49
+ * DELETE — both INSIDE the one read-write pair that read it. It is never
50
+ * stored on a handle, never carried across calls and never streamed, so
51
+ * the "no watch, no informers, no resourceVersion tracking" invariant
52
+ * `k8s-client.ts` states still holds. See {@link buildHolderEpochPatch}.
53
+ */
54
+ readonly resourceVersion?: string;
36
55
  /**
37
56
  * Set the moment a DELETE is accepted, long before the object goes away.
38
57
  * A pod that carries one is on its way out and must never be bound to —
@@ -86,12 +105,28 @@ export interface SandboxClaimLifecycle {
86
105
  * absent from this type: setting either forces the claim to cold-start rather
87
106
  * than adopt a warm pool sandbox, which is the one thing the warm path exists
88
107
  * to avoid. A field that cannot be named cannot be set by accident.
108
+ *
109
+ * `additionalPodMetadata` is the exception, and the reason the rule above is
110
+ * about COLD STARTS rather than about claim-time metadata in general: labels
111
+ * are merged into an adopted warm sandbox without one. Measured on
112
+ * agent-sandbox v1.0.2 — two claims out of one two-replica pool, each
113
+ * carrying a different label value, both binding a replica that already
114
+ * existed, and the controller patching the label onto the running pod and
115
+ * into the Sandbox's own podTemplate. See `egress-policy.ts`'s profile
116
+ * support.
89
117
  */
90
118
  export interface SandboxClaimResourceSpec {
91
119
  readonly warmPoolRef: {
92
120
  readonly name: string;
93
121
  };
94
122
  readonly lifecycle?: SandboxClaimLifecycle;
123
+ /**
124
+ * Labels (and annotations, which this backend never sets) the controller
125
+ * merges onto the pod it binds. Warm-safe — see the type comment.
126
+ */
127
+ readonly additionalPodMetadata?: {
128
+ readonly labels?: Readonly<Record<string, string>>;
129
+ };
95
130
  }
96
131
  /**
97
132
  * `SandboxClaim.status`. `sandbox` is the whole reason the claim path reads
@@ -113,6 +148,33 @@ export interface SandboxClaimResource {
113
148
  readonly spec?: SandboxClaimResourceSpec;
114
149
  readonly status?: SandboxClaimResourceStatus;
115
150
  }
151
+ /**
152
+ * A `GET` of the claims COLLECTION. Read by `readKubernetesTaskCapacity` (a
153
+ * count) and `releaseKubernetesTaskSandboxes` (the names to `DELETE`) — both
154
+ * in `index.ts`. Same "no watch, no `continue`" shape as
155
+ * {@link SandboxListResource}, for the same reason: this backend does no
156
+ * watch at all.
157
+ */
158
+ export interface SandboxClaimListResource {
159
+ readonly items?: readonly SandboxClaimResource[];
160
+ }
161
+ /**
162
+ * `SandboxWarmPool.spec`/`.status`, read by `readKubernetesTaskCapacity`
163
+ * alone — the first place in this backend that reads a `SandboxWarmPool`
164
+ * rather than only naming one in a claim's `warmPoolRef`. Partial in the
165
+ * same way every other shape here is: `replicas` and `readyReplicas` are the
166
+ * two fields a capacity read needs, off the exact same object
167
+ * `k8s/scripts/acquire-p50.mjs` already polls by hand.
168
+ */
169
+ export interface SandboxWarmPoolResource {
170
+ readonly metadata?: KubernetesObjectMeta;
171
+ readonly spec?: {
172
+ readonly replicas?: number;
173
+ };
174
+ readonly status?: {
175
+ readonly readyReplicas?: number;
176
+ };
177
+ }
116
178
  /**
117
179
  * `podTemplate` on a Sandbox or a SandboxTemplate. `spec` is a core `PodSpec`,
118
180
  * carried opaquely: this backend copies one from a template into a Sandbox and
@@ -178,6 +240,16 @@ export interface SandboxResource {
178
240
  readonly spec?: SandboxResourceSpec;
179
241
  readonly status?: SandboxResourceStatus;
180
242
  }
243
+ /**
244
+ * A `GET` of the sandboxes COLLECTION. `items` is the only field anything
245
+ * here reads: this backend does no watch, so `metadata.resourceVersion` and
246
+ * `continue` have nothing to feed — the namespace a deployment gives its
247
+ * sandboxes holds tens of objects, not the thousands that would make a page
248
+ * boundary a real answer rather than a truncated one.
249
+ */
250
+ export interface SandboxListResource {
251
+ readonly items?: readonly SandboxResource[];
252
+ }
181
253
  export interface SandboxTemplateResource {
182
254
  readonly metadata?: KubernetesObjectMeta;
183
255
  readonly spec?: {
@@ -187,16 +259,59 @@ export interface SandboxTemplateResource {
187
259
  };
188
260
  }
189
261
  /**
190
- * `metadata.uid` is the per-instance agent bind token; the other two fields
191
- * exist only to answer "is this the pod that uid belongs to, or the one being
192
- * deleted?" — see {@link isPodLive}.
262
+ * `metadata.uid` is the per-instance agent bind token; `deletionTimestamp`
263
+ * and `phase` exist only to answer "is this the pod that uid belongs to, or
264
+ * the one being deleted?" — see {@link isPodLive}.
265
+ *
266
+ * `podIP` is read by the `pod-ip` address mode only, and deliberately from
267
+ * the SAME object the uid comes from: an address taken from one pod and a
268
+ * token taken from another is the mismatch that reports as a flat
269
+ * `unauthorized` with nothing pointing at the pod that was replaced in
270
+ * between. Both spellings are carried because a dual-stack cluster fills
271
+ * `podIPs` and single-stack clusters have always filled `podIP`; the API
272
+ * server sets `podIP` to the first entry of `podIPs` on every cluster that
273
+ * sets either, so {@link readPodIP} prefers it and falls back.
193
274
  */
194
275
  export interface PodResource {
195
276
  readonly metadata?: KubernetesObjectMeta;
196
277
  readonly status?: {
197
278
  readonly phase?: string;
279
+ readonly podIP?: string;
280
+ readonly podIPs?: readonly {
281
+ readonly ip?: string;
282
+ }[];
283
+ /**
284
+ * `status.conditions` — read on ONE path only, and never on a healthy
285
+ * one: after an acquire has already run out of readiness budget,
286
+ * `PodScheduled=False` with reason `Unschedulable` is what separates
287
+ * "the cluster has no room" from "the sandbox is just slow". Nothing
288
+ * waits on a pod condition: `Ready` here is the kubelet's view of the
289
+ * container, and readiness on this backend is the Sandbox's own
290
+ * `Ready`, which is what {@link isConditionTrue} is called with
291
+ * everywhere else. See `index.ts`'s `diagnoseUnreadyPod`.
292
+ */
293
+ readonly conditions?: readonly KubernetesCondition[];
294
+ /**
295
+ * `status.containerStatuses` — read on the same one path, for the same
296
+ * one question. A container stuck in `waiting` with an image-pull
297
+ * reason is a permanent failure wearing the clothes of a slow start,
298
+ * and it is the only one of those this backend can name from the API.
299
+ */
300
+ readonly containerStatuses?: readonly PodContainerStatus[];
198
301
  };
199
302
  }
303
+ /** The single `status.containerStatuses` field the diagnosis above reads. */
304
+ export interface PodContainerStatus {
305
+ readonly name?: string;
306
+ readonly state?: {
307
+ readonly waiting?: {
308
+ readonly reason?: string;
309
+ readonly message?: string;
310
+ };
311
+ };
312
+ }
313
+ /** The pod's own address, whichever of the two fields this cluster fills. */
314
+ export declare function readPodIP(pod: PodResource | undefined): string | undefined;
200
315
  export interface PodListResource {
201
316
  readonly items?: readonly PodResource[];
202
317
  }
@@ -258,6 +373,260 @@ export declare function isPodStopped(pod: PodResource | undefined): boolean;
258
373
  * `docs/sdk/kubernetes-sandbox.md`'s egress section.
259
374
  */
260
375
  export declare const SANDBOX_TEMPLATE_LABEL_KEY = "sandbox.namzu.ai/template";
376
+ /**
377
+ * Backend-owned annotation naming when a Sandbox's `spec.operatingMode` was
378
+ * last changed BY THIS BACKEND, RFC 3339.
379
+ *
380
+ * It exists because nothing already on the object answers the question, and
381
+ * an inventory that never wakes a workspace is the reason to ask it: a
382
+ * retention pass deleting the workspaces nobody has resumed for a month reads
383
+ * this and `metadata.creationTimestamp` and nothing else.
384
+ *
385
+ * The obvious candidate is the controller's own `Suspended` condition and its
386
+ * `lastTransitionTime`, and upstream's `sandbox_types.go` rules it out in the
387
+ * same breath it documents it: "the controller does not currently remove this
388
+ * condition when the Sandbox is resumed", so after a resume the condition is
389
+ * still True and its timestamp still names the suspend that preceded it. The
390
+ * `Ready` condition's timestamp is no better — it moves for every pod that
391
+ * comes and goes, a crash-restart included, and a workspace whose pod
392
+ * restarted has not changed operating mode at all.
393
+ *
394
+ * So the two patches that DO change the mode stamp the moment they were sent,
395
+ * and the value is exactly that: the host's clock at the moment it asked, not
396
+ * the cluster's at the moment it applied. It is an inventory column, never a
397
+ * lock or an ordering, and nothing in this backend reads it back to make a
398
+ * decision. A Sandbox whose mode has never been changed since it was created
399
+ * carries no annotation at all, and is reported without one rather than with
400
+ * a guess.
401
+ *
402
+ * Same prefix as {@link SANDBOX_TEMPLATE_LABEL_KEY}, for the same reason.
403
+ */
404
+ export declare const OPERATING_MODE_CHANGED_AT_ANNOTATION_KEY = "sandbox.namzu.ai/operating-mode-changed-at";
405
+ /**
406
+ * Backend-owned annotation carrying the HOLDER EPOCH: a decimal integer the
407
+ * host raises whenever authority over this workspace moves to another
408
+ * process.
409
+ *
410
+ * A workspace id is a name, not a lock, and a host that drives one workspace
411
+ * from more than one process has to decide which of them may suspend, resume
412
+ * or delete it. Checking its own epoch and then calling `suspend()` does not
413
+ * close the race, because the write that follows is a separate request and
414
+ * the API server accepts it. So the epoch is stored HERE, on the object every
415
+ * lifecycle write targets, and every such write carries it as a condition in
416
+ * the same request — see {@link buildHolderEpochPatch}.
417
+ *
418
+ * The rule: a write carrying epoch `e` applies when the stored epoch is `<=
419
+ * e`, and sets the stored epoch to `e` in the same request. A stored epoch
420
+ * greater than `e` refuses it. An object with NO annotation reads as 0, so
421
+ * every workspace created before this existed accepts its first
422
+ * epoch-carrying write.
423
+ *
424
+ * It is the caller's number, never this backend's: nothing here invents,
425
+ * increments or persists an epoch of its own, and a call that passes none
426
+ * sends exactly the requests it always sent.
427
+ *
428
+ * Same prefix as {@link SANDBOX_TEMPLATE_LABEL_KEY} and
429
+ * {@link OPERATING_MODE_CHANGED_AT_ANNOTATION_KEY}, for the same reason.
430
+ */
431
+ export declare const HOLDER_EPOCH_ANNOTATION_KEY = "sandbox.namzu.ai/holder-epoch";
432
+ /**
433
+ * Backend-owned annotation carrying a hash of the pod template a Sandbox was
434
+ * last built with.
435
+ *
436
+ * A Sandbox's `spec.podTemplate` is a COPY of the SandboxTemplate's, taken
437
+ * once, and the controller rebuilds every replacement pod from that copy
438
+ * rather than from the template — so a workspace kept for weeks runs the pod
439
+ * spec it was created with, and an edit to the template (a new image tag, a
440
+ * memory limit, a grace period, an env entry) reaches only workspaces created
441
+ * after it. Nothing on the object answers "is this copy still the template's
442
+ * current one": the two are separate objects with separate
443
+ * `resourceVersion`s, and comparing the templates field by field on every
444
+ * open would be a second, weaker copy of the overlay rules.
445
+ *
446
+ * So the value is a hash of exactly what was written: the template's
447
+ * `podTemplate` AFTER this backend's own overlays (the template label and the
448
+ * configured `runtimeClassName`), which is the object the Sandbox carries.
449
+ * Hashing before the overlays would report drift on every workspace whose
450
+ * RuntimeClass this backend chose.
451
+ *
452
+ * Written by the workspace paths only — the create POST and the refresh patch
453
+ * — and read back as `templateRevision` on a handle. A task sandbox is
454
+ * ephemeral and has nothing to drift from, so its create body is unchanged.
455
+ * A workspace created before this existed carries no annotation and reports
456
+ * `templateRevision: undefined`, which reads honestly as "unknown", never as
457
+ * "current".
458
+ *
459
+ * Same prefix as {@link SANDBOX_TEMPLATE_LABEL_KEY}, for the same reason.
460
+ */
461
+ export declare const POD_TEMPLATE_HASH_ANNOTATION_KEY = "sandbox.namzu.ai/pod-template-hash";
462
+ /**
463
+ * `sha256:<hex>` over the pod template, for
464
+ * {@link POD_TEMPLATE_HASH_ANNOTATION_KEY}.
465
+ *
466
+ * It is an identity, not a checksum of anything security-relevant: two hosts
467
+ * running the same release against the same template must compute the same
468
+ * string, and a template edit must change it. Nothing here compares it
469
+ * against a value an untrusted party chose.
470
+ */
471
+ export declare function podTemplateHash(podTemplate: SandboxPodTemplate): string;
472
+ /**
473
+ * One RFC 6902 operation, in the only three shapes this backend sends.
474
+ *
475
+ * `test` is the condition, `add` is every mutation. `add` rather than
476
+ * `replace` throughout: RFC 6902 §4.1 says that on a JSON object member `add`
477
+ * creates the member when it is missing and replaces its value when it is
478
+ * present, while §4.3's `replace` fails outright on a missing one — and
479
+ * `spec.operatingMode` is absent on a Sandbox that has never been suspended,
480
+ * as is the epoch annotation on every workspace created before this release.
481
+ * A `replace` would turn both of those ordinary cases into a rejected patch.
482
+ *
483
+ * So where a design or an issue says the wire carries `replace /spec/…`, this
484
+ * is that write: on a member that is already there the two operations are the
485
+ * same write, and on one that is not, only this one lands.
486
+ */
487
+ export interface JsonPatchOperation {
488
+ readonly op: 'test' | 'add';
489
+ readonly path: string;
490
+ readonly value: unknown;
491
+ }
492
+ /**
493
+ * One JSON Pointer reference token (RFC 6901 §3): `~` becomes `~0` and `/`
494
+ * becomes `~1`, in that order — the reverse order would turn a literal `~1`
495
+ * into a slash.
496
+ *
497
+ * An annotation key always contains a `/` (`sandbox.namzu.ai/holder-epoch`),
498
+ * so the pointer to one is unusable without this.
499
+ */
500
+ export declare function escapeJsonPointerSegment(token: string): string;
501
+ /** Pointer to one annotation on an object's own metadata. */
502
+ export declare function annotationPointer(key: string): string;
503
+ /** What one read of an object saw about its holder epoch. */
504
+ export interface HolderEpochReading {
505
+ /**
506
+ * The stored epoch: the annotation parsed, or `0` when there is none.
507
+ *
508
+ * `undefined` means the annotation is PRESENT and is not a decimal
509
+ * integer, which no version of this backend writes. It is reported as
510
+ * unreadable rather than as 0 on purpose: reading a value this code does
511
+ * not understand as "nobody holds this workspace" would let a write
512
+ * overwrite a fence somebody else established, which is the one thing the
513
+ * annotation exists to prevent.
514
+ */
515
+ readonly epoch?: number;
516
+ /**
517
+ * The annotation exactly as stored, and the value the `test` clause
518
+ * carries. Absent when the object has no such annotation.
519
+ */
520
+ readonly annotation?: string;
521
+ /** `metadata.annotations` existed at all — decides which `add` is sent. */
522
+ readonly hasAnnotations: boolean;
523
+ /** `metadata.resourceVersion`, for the fallback `test` and a fenced DELETE. */
524
+ readonly resourceVersion?: string;
525
+ }
526
+ /** Read {@link HOLDER_EPOCH_ANNOTATION_KEY} off an object's metadata. */
527
+ export declare function readHolderEpoch(meta: KubernetesObjectMeta | undefined): HolderEpochReading;
528
+ /**
529
+ * Read {@link POD_TEMPLATE_HASH_ANNOTATION_KEY} off an object's metadata.
530
+ *
531
+ * Absent — an object created before this existed, or one an older release
532
+ * refreshed — is `undefined`, which reads as "unknown" everywhere it is
533
+ * consumed. It is deliberately never compared as an empty string: a
534
+ * revision nobody recorded is not a revision that differs.
535
+ */
536
+ export declare function readPodTemplateHash(meta: KubernetesObjectMeta | undefined): string | undefined;
537
+ /** A stored epoch of `epoch` or lower lets a write carrying `epoch` through. */
538
+ export declare function holderEpochAllows(reading: HolderEpochReading, epoch: number): boolean;
539
+ /** What {@link buildHolderEpochPatch} is asked to write, and under what condition. */
540
+ export interface HolderEpochPatchInput {
541
+ /** The metadata the GET returned. The condition is built from THIS read. */
542
+ readonly reading: HolderEpochReading;
543
+ /**
544
+ * The epoch the write carries, and the one it stores.
545
+ *
546
+ * ABSENT is the unfenced conditional write: no epoch clause is tested and
547
+ * no epoch annotation is written, and {@link tests} then carries the whole
548
+ * condition — which is why an epoch-less call with no `tests` is refused
549
+ * below rather than sent unconditionally. A workspace refresh is the one
550
+ * caller: its condition is `spec.operatingMode`, and a host that has not
551
+ * opted into the fence must not acquire one by asking for a new pod
552
+ * template.
553
+ */
554
+ readonly epoch?: number;
555
+ /**
556
+ * Further `test` clauses composed into the SAME body.
557
+ *
558
+ * This is the seam a second condition uses instead of a second request: a
559
+ * write that also has to assert, say, `spec.operatingMode` passes its
560
+ * clause here and the object is still written under one atomic patch. Two
561
+ * call sites each sending their own conditional patch would be two writes
562
+ * and two chances to lose a race between them.
563
+ */
564
+ readonly tests?: readonly JsonPatchOperation[];
565
+ /** Written to `spec.operatingMode`; omitted leaves the mode alone. */
566
+ readonly operatingMode?: 'Running' | 'Suspended';
567
+ /**
568
+ * RFC 3339 stamp for {@link OPERATING_MODE_CHANGED_AT_ANNOTATION_KEY}.
569
+ * Passed only by a write that actually CHANGES the mode — the annotation
570
+ * says when the mode last changed, and a write that merely restamps the
571
+ * epoch has not changed it.
572
+ */
573
+ readonly operatingModeChangedAt?: string;
574
+ /**
575
+ * Further annotations written in the SAME body, merged with the epoch's
576
+ * and the mode stamp's rather than sent after them.
577
+ *
578
+ * {@link POD_TEMPLATE_HASH_ANNOTATION_KEY} is the only caller: the hash
579
+ * has to land with the pod template it describes, or a patch that applied
580
+ * half of the pair would leave the object claiming a revision it is not
581
+ * running.
582
+ */
583
+ readonly annotations?: Readonly<Record<string, string>>;
584
+ /**
585
+ * Written to `spec.podTemplate`, replacing it WHOLE — which is the reason
586
+ * this is a JSON Patch at all. A merge patch recurses into maps, so a
587
+ * `nodeSelector` entry the template dropped would survive in the object
588
+ * and the pod would keep a constraint nobody can see in the template any
589
+ * more.
590
+ */
591
+ readonly podTemplate?: SandboxPodTemplate;
592
+ }
593
+ /**
594
+ * The one conditional-write builder this backend has, and the only place a
595
+ * JSON Patch body is composed.
596
+ *
597
+ * Every clause is decided from ONE read, and the whole thing goes up as one
598
+ * request: the condition and the mutation are in the same body, so there is
599
+ * no window between checking and writing for another holder to fit into.
600
+ *
601
+ * Three shapes, decided by what that read saw:
602
+ *
603
+ * - the object carries the epoch annotation ⇒ `test` it by its exact stored
604
+ * string, then `add` the new value over it;
605
+ * - the object carries annotations but not this one ⇒ `test`
606
+ * `/metadata/resourceVersion` instead, then `add` the member;
607
+ * - the object carries no `metadata.annotations` at all ⇒ the same
608
+ * `resourceVersion` test, then `add` the map whole, because there is no
609
+ * member to add one to.
610
+ *
611
+ * The `resourceVersion` fallback is the migration case and nothing more: it
612
+ * fires once, on a workspace created before this release, and from the first
613
+ * epoch write onwards the annotation is what is tested. That matters because
614
+ * a controller status write moves `resourceVersion` without touching the
615
+ * annotation, and under the annotation test those are simply not conditions
616
+ * this write is interested in.
617
+ *
618
+ * A call carrying NO epoch skips all three: it tests only what
619
+ * {@link HolderEpochPatchInput.tests} carries and writes no epoch annotation,
620
+ * which is how a workspace refresh conditions itself on `spec.operatingMode`
621
+ * without fencing a host that never asked for a fence.
622
+ *
623
+ * Every `test` precedes every mutation, which RFC 6902 requires of a
624
+ * condition: operations apply in order, so a `test` written after an `add`
625
+ * would be testing this patch's own work. Mutations go up in a fixed order —
626
+ * annotations, then `spec.podTemplate`, then `spec.operatingMode` — so the
627
+ * body a given input produces is one body and a test can assert it.
628
+ */
629
+ export declare function buildHolderEpochPatch(input: HolderEpochPatchInput): readonly JsonPatchOperation[];
261
630
  /** `{ [SANDBOX_TEMPLATE_LABEL_KEY]: sandboxTemplateName }`, as a matchLabels-ready object. */
262
631
  export declare function sandboxTemplateLabel(sandboxTemplateName: string): Readonly<Record<string, string>>;
263
632
  /** Core `NetworkPolicy` — a stock resource, no CRD. */
@@ -272,11 +641,62 @@ export declare const CILIUM_NETWORK_POLICY_API_GROUP = "cilium.io";
272
641
  export declare const CILIUM_NETWORK_POLICY_API_VERSION = "v2";
273
642
  export declare function claimCollectionPath(namespace: string): string;
274
643
  export declare function claimPath(namespace: string, name: string): string;
644
+ /** The claims collection, narrowed to a `labelSelector` — same shape as {@link podListPath}. */
645
+ export declare function claimListPath(namespace: string, labelSelector: string): string;
646
+ export declare function warmPoolPath(namespace: string, name: string): string;
275
647
  export declare function sandboxCollectionPath(namespace: string): string;
276
648
  export declare function sandboxPath(namespace: string, name: string): string;
277
649
  export declare function sandboxTemplatePath(namespace: string, name: string): string;
650
+ /**
651
+ * The PVC the controller creates for one `volumeClaimTemplates` entry:
652
+ * `<entry name>-<sandbox name>`, in the Sandbox's own namespace.
653
+ *
654
+ * Written out here rather than derived at each call site because it is a
655
+ * NAME the controller owns, not one this backend chooses — a release that
656
+ * changes it breaks every read of it at once, and the one place to notice
657
+ * that is a function whose whole body is the convention.
658
+ */
659
+ export declare function persistentVolumeClaimPath(namespace: string, sandboxName: string, claimTemplateName: string): string;
278
660
  export declare function podPath(namespace: string, name: string): string;
661
+ /** The whole pods collection, unfiltered. Read by `readKubernetesTaskCapacity` alone. */
662
+ export declare function podCollectionPath(namespace: string): string;
279
663
  export declare function podListPath(namespace: string, labelSelector: string): string;
664
+ export declare function networkPolicyCollectionPath(namespace: string): string;
280
665
  export declare function networkPolicyPath(namespace: string, name: string): string;
666
+ export declare function ciliumNetworkPolicyCollectionPath(namespace: string): string;
281
667
  export declare function ciliumNetworkPolicyPath(namespace: string, name: string): string;
668
+ /**
669
+ * Core Kubernetes' own admission-policy resources — `ValidatingAdmissionPolicy`
670
+ * and its binding, both CLUSTER-scoped and both stock since v1.30, so a
671
+ * cluster that serves this group needs nothing installed.
672
+ *
673
+ * Read by exactly one caller: the per-sandbox policy fence
674
+ * (`per-sandbox-policy.ts`), which refuses to write a `CiliumNetworkPolicy`
675
+ * at all unless an operator has applied the policy that bounds what this
676
+ * host may write. Never written by this backend — the fence is the operator's
677
+ * object, reviewed by whoever has cluster-admin, and a host that could create
678
+ * its own fence would not have one.
679
+ */
680
+ export declare const ADMISSION_REGISTRATION_API_GROUP = "admissionregistration.k8s.io";
681
+ export declare const ADMISSION_REGISTRATION_API_VERSION = "v1";
682
+ export declare function validatingAdmissionPolicyPath(name: string): string;
683
+ export declare function validatingAdmissionPolicyBindingPath(name: string): string;
684
+ /**
685
+ * An `ownerReferences` entry, as this backend writes it.
686
+ *
687
+ * `controller` and `blockOwnerDeletion` are deliberately ABSENT rather than
688
+ * written as `false`. `blockOwnerDeletion: true` would make the API server's
689
+ * `OwnerReferencesPermissionEnforcement` admission plugin demand `update` on
690
+ * the OWNER's `finalizers` subresource — a verb no Role in this repo grants
691
+ * and no host needs — so a field whose only legal value here is `false` is
692
+ * one the body is better off not carrying at all. Garbage collection does not
693
+ * need either field: a dependent whose owners are all gone is deleted, owning
694
+ * controller or not.
695
+ */
696
+ export interface KubernetesOwnerReference {
697
+ readonly apiVersion: string;
698
+ readonly kind: string;
699
+ readonly name: string;
700
+ readonly uid: string;
701
+ }
282
702
  //# sourceMappingURL=objects.d.ts.map