@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
@@ -76,7 +76,9 @@
76
76
  * the kernel, not by the workload's cooperation.
77
77
  */
78
78
  import type { EgressPolicy } from '../../index.js';
79
+ import { type SelectorMatch, type UnreadPolicySource } from './ingress-policy.js';
79
80
  import { type KubernetesClient } from './k8s-client.js';
81
+ import type { KubernetesOwnerReference } from './objects.js';
80
82
  /**
81
83
  * `'core'` (the default) is plain Kubernetes `NetworkPolicy` — every cluster
82
84
  * has it, and it cannot express a hostname. `'cilium'` opts into emitting a
@@ -84,6 +86,151 @@ import { type KubernetesClient } from './k8s-client.js';
84
86
  * the target cluster actually runs Cilium as its CNI.
85
87
  */
86
88
  export type KubernetesEgressEngine = 'core' | 'cilium';
89
+ /**
90
+ * The two egress kinds that exist only here, because only a `NetworkPolicy`
91
+ * can express them and the shared {@link EgressPolicy} union describes what
92
+ * every tier can carry.
93
+ *
94
+ * - `'no-network'` is the one the shared union has no word for: NOTHING
95
+ * leaves the pod, the cluster's own resolver included. `'deny-all'` is not
96
+ * that and never was — it emits {@link CLUSTER_DNS_EGRESS_RULE}, and a
97
+ * cluster resolver forwards outside names upstream, so a `'deny-all'`
98
+ * sandbox keeps a channel out through DNS. `'deny-all'` is deliberately
99
+ * NOT tightened into this: its emitted manifest is byte-identical to
100
+ * every release before this one, because verification of the named object
101
+ * is an exact match and changing the translation would fail every
102
+ * `create()` on every deployment that already applied a policy until an
103
+ * operator re-applied it. A workload under `'no-network'` resolves
104
+ * nothing at all — that is the point, and the agent needs no resolver
105
+ * because the host dials in.
106
+ * - `'public-internet'` is `'allow-all'` minus everything that is not the
107
+ * public internet: the private ranges, the carrier-grade NAT range, the
108
+ * link-local range that carries cloud instance metadata, loopback, the
109
+ * platform endpoint some clouds answer on, and IPv6's equivalents. It
110
+ * exists because `'allow-all'` reaches the node, the API server, the
111
+ * service network and every other sandbox pod, and nothing between the
112
+ * two said "out, but not sideways".
113
+ *
114
+ * `exceptCidrs` adds to the excluded list; it never removes from it. A CIDR
115
+ * that is not one this check can parse is refused at construction rather
116
+ * than emitted into a manifest the API server would reject on apply.
117
+ */
118
+ export type KubernetesOnlyEgressPolicy = {
119
+ readonly kind: 'no-network';
120
+ } | {
121
+ readonly kind: 'public-internet';
122
+ readonly exceptCidrs?: readonly string[];
123
+ };
124
+ /**
125
+ * What {@link KubernetesEgressConfig.policy} accepts: the shared
126
+ * {@link EgressPolicy} union plus the two {@link KubernetesOnlyEgressPolicy}
127
+ * kinds. The shared union itself is untouched — a kind no other tier can
128
+ * enforce does not belong in the type every tier reads.
129
+ */
130
+ export type KubernetesEgressPolicy = EgressPolicy | KubernetesOnlyEgressPolicy;
131
+ /**
132
+ * How thoroughly the applied boundary is checked before a sandbox is handed
133
+ * back.
134
+ *
135
+ * - `'union'` (the default) reads the NAMED object exactly as before AND
136
+ * enumerates every `NetworkPolicy` — and, under `engine: 'cilium'`, every
137
+ * `CiliumNetworkPolicy` — in the namespace, refusing when any policy that
138
+ * selects the pod allows egress the configured translation does not. That
139
+ * is not belt-and-braces: the API server UNIONS every policy selecting a
140
+ * pod, so a second policy widens egress however exactly the named one
141
+ * matches, and a `SandboxTemplate`'s own `networkPolicy` block becomes
142
+ * exactly such a policy.
143
+ * - `'named-object-only'` is the documented opt-out, and restores the
144
+ * previous behaviour exactly: one GET of the named object, memoized for
145
+ * the backend's lifetime, and no enumeration. For a deployment whose
146
+ * other policies a namespaced Role cannot read, or which accepts the
147
+ * union it has. It mirrors `ingress: 'unverified'` — a claim a deployment
148
+ * makes on purpose rather than a default it inherits.
149
+ */
150
+ export type KubernetesEgressVerification = 'union' | 'named-object-only';
151
+ /**
152
+ * How {@link KubernetesCiliumEgressNarrowing.dnsNames} narrows the kube-dns
153
+ * L7 rule. `true` uses every default below; an object customises them.
154
+ *
155
+ * The exact-name list a narrowed policy emits is, for each allowed host,
156
+ * the bare name plus the host under `<namespace>.svc.<clusterDomain>`,
157
+ * `svc.<clusterDomain>` and `<clusterDomain>` — because Cilium's `matchName`
158
+ * is an EXACT name and does not match across a `.` the way `matchPattern`
159
+ * does, and a search-list query for `github.com.svc.cluster.local` is a
160
+ * DIFFERENT name than `github.com`. `searchSuffixes` adds more: kubelet
161
+ * appends the node's own search domains, which this backend cannot see, so a
162
+ * deployment whose nodes carry extra search domains lists them here or a
163
+ * search-list query for one of them is refused by the DNS proxy — and, per
164
+ * Cilium's own docs, some images (musl/Alpine) stop trying the search list
165
+ * entirely the first time that happens, breaking the bare name lookup too.
166
+ */
167
+ export interface KubernetesCiliumDnsNarrowing {
168
+ /** Defaults to the egress target's own namespace (where the sandbox pods run). */
169
+ readonly namespace?: string;
170
+ /** Defaults to `'cluster.local'`. */
171
+ readonly clusterDomain?: string;
172
+ /** Appended after the three built-in suffixes, not replacing them. */
173
+ readonly searchSuffixes?: readonly string[];
174
+ }
175
+ /**
176
+ * Opt-in narrowing of a `static`/`resolver` hostname allowlist under
177
+ * `engine: 'cilium'` — see the module doc and `#490`. Every field here is
178
+ * OFF unless set, and setting none of them leaves the translation
179
+ * byte-for-byte what it always emitted: that is the compatibility guarantee
180
+ * a deployment with an already-applied policy relies on.
181
+ *
182
+ * Setting any field switches the translation from one shared `toFQDNs` rule
183
+ * naming every host to one `toFQDNs` rule PER HOST, so ports and server
184
+ * names can differ host by host. Refused synchronously (alongside
185
+ * {@link assertEgressPolicyIsEnforceable}'s existing refusals) unless
186
+ * `engine` is `'cilium'` and `policy.kind` is `'static'` or `'resolver'`.
187
+ */
188
+ export interface KubernetesCiliumEgressNarrowing {
189
+ /**
190
+ * TCP ports allowed to every host that has no entry in `hostPorts`, e.g.
191
+ * `[443]`. Leaving both this and `hostPorts` unset — with `dnsNames` and
192
+ * `tlsServerNames` also unset — means no port narrowing: a host's
193
+ * `toFQDNs` rule carries no `toPorts` at all, exactly as the
194
+ * unnarrowed translation emits today (every port reachable).
195
+ */
196
+ readonly ports?: readonly number[];
197
+ /**
198
+ * Per-host TCP port overrides, keyed by the host exactly as it appears in
199
+ * `allowedHosts` or a `resolver`'s result. A host with no entry here
200
+ * falls back to `ports`.
201
+ */
202
+ readonly hostPorts?: Readonly<Record<string, readonly number[]>>;
203
+ /**
204
+ * Narrow the kube-dns L7 rule from `rules.dns: [{ matchPattern: '*' }]`
205
+ * to an exact `matchName` per allowed host, and per host-plus-suffix. See
206
+ * {@link KubernetesCiliumDnsNarrowing}.
207
+ */
208
+ readonly dnsNames?: boolean | KubernetesCiliumDnsNarrowing;
209
+ /**
210
+ * Add `serverNames: [<host>]` to each host's TLS ports (SNI enforcement,
211
+ * which needs Cilium's L7 proxy — see the module doc). A host with no
212
+ * port configured in `ports`/`hostPorts` is limited to `tlsPorts` rather
213
+ * than left with no port restriction at all, because a `serverNames`
214
+ * rule needs a port to attach to.
215
+ *
216
+ * REFUSED for a `.domain` allowlist entry, with
217
+ * {@link KubernetesNetworkPolicyHostError} and nothing written: a server
218
+ * name is one exact SNI value a handshake presents, while the entry means
219
+ * a name PLUS its subdomains, and no single value means both — see
220
+ * {@link assertHostsFitNarrowing}. The refusal is the entry's, not this
221
+ * option's, so it fires wherever `tlsServerNames` can be set:
222
+ * `config.egress.ciliumNarrowing` on the config-level allowlist and
223
+ * `config.egress.perSandbox.narrowing` on the per-sandbox one.
224
+ */
225
+ readonly tlsServerNames?: boolean;
226
+ /**
227
+ * Which of a host's configured ports are TLS ports, for
228
+ * `tlsServerNames`: those get `serverNames` on their own `toPorts`
229
+ * entry, the rest (if any) get a separate entry with none. Defaults to
230
+ * `[443]`. Meaningless unless `tlsServerNames` is set.
231
+ */
232
+ readonly tlsPorts?: readonly number[];
233
+ }
87
234
  /**
88
235
  * The config-level egress hook on {@link KubernetesBackendConfig}.
89
236
  *
@@ -96,7 +243,7 @@ export type KubernetesEgressEngine = 'core' | 'cilium';
96
243
  * the one egress policy the whole backend enforces.
97
244
  */
98
245
  export interface KubernetesEgressConfig {
99
- readonly policy: EgressPolicy;
246
+ readonly policy: KubernetesEgressPolicy;
100
247
  /**
101
248
  * Name of the `NetworkPolicy` (or `CiliumNetworkPolicy`, under the
102
249
  * `'cilium'` engine) an operator applied. Defaults to
@@ -106,7 +253,442 @@ export interface KubernetesEgressConfig {
106
253
  readonly networkPolicyName?: string;
107
254
  /** Default `'core'`. See the type doc. */
108
255
  readonly engine?: KubernetesEgressEngine;
256
+ /** Default `'union'`. See {@link KubernetesEgressVerification}. */
257
+ readonly verify?: KubernetesEgressVerification;
258
+ /**
259
+ * Opt-in port/DNS-name/TLS-server-name narrowing for a `static`/
260
+ * `resolver` allowlist under `engine: 'cilium'`. Unset (the default)
261
+ * emits exactly what every release before this one did. See
262
+ * {@link KubernetesCiliumEgressNarrowing}.
263
+ */
264
+ readonly ciliumNarrowing?: KubernetesCiliumEgressNarrowing;
265
+ /**
266
+ * Which egress PROFILE the sandboxes this backend produces run under —
267
+ * a DNS-1123 label value such as `none` or `internet`.
268
+ *
269
+ * Unset (the default) is the single-profile world this backend has
270
+ * always had: one policy per backend, selected by the template label
271
+ * alone, and every emitted body, selector and policy name byte-identical
272
+ * to the release before profiles existed.
273
+ *
274
+ * SET, it becomes a pod LABEL — {@link profileLabelKey} is its key —
275
+ * which travels three places at once: onto the `SandboxClaim`'s
276
+ * `additionalPodMetadata.labels`, onto a directly created Sandbox's pod
277
+ * template, and into the translated policy's own selector. That is what
278
+ * lets ONE warm pool serve several network modes: a claim carrying a
279
+ * profile label adopts a warm replica and the controller patches the
280
+ * label onto the running pod, with no cold start and no second pool —
281
+ * measured on agent-sandbox v1.0.2 (two profiles out of one two-replica
282
+ * pool, every adopt under 70 ms, each bound pod a replica that already
283
+ * existed). A label is the only claim-time metadata that is warm-safe:
284
+ * `env` and `volumeClaimTemplates` force a cold start, which is why
285
+ * neither appears on the claim body.
286
+ *
287
+ * OPERATOR PREREQUISITE, and it is not optional: the controller refuses
288
+ * a claim whose label key sits outside its `allowed-label-domains`
289
+ * allowlist (the `agent-sandbox-config` ConfigMap in the controller's
290
+ * namespace; default `sandbox.users.io`), so the DEFAULT key below is
291
+ * refused by a stock controller until an operator adds
292
+ * `sandbox.namzu.ai` to that key — or sets {@link profileLabelKey} to
293
+ * something already allowed. That refusal is fast and carries the
294
+ * controller's own reason and message: it is the acquire's ordinary
295
+ * `claim-rejected` failure, with a
296
+ * {@link KubernetesPodLabelsRejectedError} as its cause naming the labels
297
+ * that were sent and the key that moves them.
298
+ */
299
+ readonly profile?: string;
300
+ /**
301
+ * Label key {@link profile} is written under. Defaults to
302
+ * {@link DEFAULT_EGRESS_PROFILE_LABEL_KEY}.
303
+ *
304
+ * Worth setting to `sandbox.users.io/egress-profile` on a cluster whose
305
+ * controller still carries the stock `allowed-label-domains` — that
306
+ * domain is upstream's own default and needs no ConfigMap edit at all.
307
+ */
308
+ readonly profileLabelKey?: string;
309
+ /**
310
+ * Opt in to PER-SANDBOX egress: one `CiliumNetworkPolicy` per live
311
+ * sandbox, written by this host when a caller calls
312
+ * `Sandbox.setNetworkPolicy`, owned by the object the acquire created so
313
+ * the cluster garbage-collects it.
314
+ *
315
+ * Unset (the default) is every release before this one: `setNetworkPolicy`
316
+ * is ABSENT from the handle, exactly as the SDK's omit-or-throw contract
317
+ * asks of a backend that cannot honour an optional method, and this
318
+ * backend issues no policy write of any kind. Presence depends only on
319
+ * this field — never on a runtime probe — so a host can decide what it
320
+ * has from its own config rather than from a call that might fail.
321
+ *
322
+ * TWO OPERATOR PREREQUISITES, and neither is a nicety: the admission
323
+ * policy named below (with its binding) has to exist, and the host's
324
+ * ServiceAccount needs the write verbs the default Role deliberately does
325
+ * not grant. See {@link KubernetesPerSandboxEgressConfig}.
326
+ */
327
+ readonly perSandbox?: KubernetesPerSandboxEgressConfig;
328
+ }
329
+ /**
330
+ * Per-sandbox egress: what it takes to let a host narrow ONE live sandbox's
331
+ * egress without touching any other sandbox's.
332
+ *
333
+ * The mechanism is a `CiliumNetworkPolicy` per sandbox, named
334
+ * `namzu-sbx-<uid of the object this backend created>`, selecting that one
335
+ * sandbox's pod through a per-sandbox label, and carrying an
336
+ * `ownerReferences` entry naming that same object — so `destroy()` deletes
337
+ * the claim (or the Sandbox) and the cluster's garbage collector removes the
338
+ * policy, with this backend issuing no deletion of its own.
339
+ *
340
+ * It is NOT the docker backend's mechanism. That one is an egress PROXY with
341
+ * no relationship to any policy object; the only two things worth taking from
342
+ * it are its refusal discipline and `SandboxNetworkPolicy.allowedHosts`'s
343
+ * `.domain`-prefix wildcard semantics.
344
+ *
345
+ * A per-sandbox list ADDS to {@link KubernetesEgressConfig.policy}, because
346
+ * the cluster unions every policy that selects a pod. That makes it the whole
347
+ * boundary under a `'no-network'` or `'deny-all'` baseline — the deployment
348
+ * this is for — and a no-op addition under `'allow-all'`, where
349
+ * `setNetworkPolicy([])` also does not deny everything the way
350
+ * `SandboxNetworkPolicy` describes. Neither combination is refused; pair this
351
+ * with a denying `policy.kind` when `setNetworkPolicy` is meant to BE the
352
+ * boundary rather than to widen one.
353
+ */
354
+ export interface KubernetesPerSandboxEgressConfig {
355
+ /**
356
+ * Must be `'cilium'`. `'core'` is accepted by the TYPE and refused
357
+ * SYNCHRONOUSLY during host wiring, the same way
358
+ * {@link assertEgressPolicyIsEnforceable} refuses a hostname allowlist
359
+ * with no FQDN-capable engine: core `NetworkPolicy` has no hostname
360
+ * concept at all, so there is nothing for an `allowedHosts` list to
361
+ * become. It is spelled out in the type rather than fixed to the one
362
+ * legal value so the refusal can name what was configured.
363
+ */
364
+ readonly engine: KubernetesEgressEngine;
365
+ /**
366
+ * Name of the operator-applied `ValidatingAdmissionPolicy` that bounds
367
+ * what this host may write. Checked — with its binding — BEFORE the first
368
+ * write, and the write is refused with nothing sent if either object is
369
+ * missing.
370
+ *
371
+ * The fence is the whole reason this capability can be granted at all:
372
+ * the RBAC it needs is `create`/`patch`/`delete` on the namespace's
373
+ * `ciliumnetworkpolicies`, which without a fence would let a compromised
374
+ * host widen or delete the operator's own baseline policy. The shipped
375
+ * example is `k8s/manifests/validatingadmissionpolicy-cilium.yaml`.
376
+ */
377
+ readonly admissionPolicyName: string;
378
+ /**
379
+ * Name of the `ValidatingAdmissionPolicyBinding` that ATTACHES the policy
380
+ * above. Defaults to `${admissionPolicyName}-binding`, which is what the
381
+ * shipped manifest names it.
382
+ *
383
+ * Checked separately because a `ValidatingAdmissionPolicy` with no
384
+ * binding validates nothing at all — it is inert, and an inert fence
385
+ * reads exactly like an enforced one from the object alone.
386
+ */
387
+ readonly admissionPolicyBindingName?: string;
388
+ /**
389
+ * Label key the per-sandbox selector is written under. Defaults to
390
+ * {@link DEFAULT_PER_SANDBOX_EGRESS_LABEL_KEY}.
391
+ *
392
+ * Its VALUE is the name of the object this backend created for the
393
+ * sandbox, so it is unique per acquire and known before the create POST —
394
+ * which is what lets it travel as claim-time pod metadata through
395
+ * {@link composeAdditionalPodLabels}, the ONE composer, rather than as a
396
+ * patch to a running pod this backend has no verb for.
397
+ *
398
+ * Same operator prerequisite as {@link KubernetesEgressConfig.profile}:
399
+ * the key's domain has to be in the controller's `allowed-label-domains`
400
+ * allowlist, or the claim is refused with the controller's own reason.
401
+ */
402
+ readonly labelKey?: string;
403
+ /**
404
+ * Port, DNS-name and TLS-server-name narrowing applied to the policies
405
+ * `setNetworkPolicy` writes — the same shape as
406
+ * {@link KubernetesEgressConfig.ciliumNarrowing}, and deliberately its own
407
+ * field rather than a reuse of it: that one narrows the ONE config-level
408
+ * policy, which is only a hostname allowlist at all when
409
+ * `policy.kind` is `'static'` or `'resolver'`, while these narrow the
410
+ * per-sandbox policies of a deployment whose baseline is usually
411
+ * `'deny-all'` or `'no-network'`.
412
+ *
413
+ * Unset (the default) emits one `toFQDNs` rule per host with no
414
+ * `toPorts` at all — every port on an allowed host reachable, which is
415
+ * what `SandboxNetworkPolicy` itself says (it names hosts, not ports).
416
+ *
417
+ * `hostPorts` is keyed by the `allowedHosts` entry AS WRITTEN, leading
418
+ * dot included: a `.example.com` entry needs the key `.example.com`, even
419
+ * though the rule it produces says `example.com` plus `*.example.com`.
420
+ *
421
+ * `tlsServerNames` and a `.domain` entry are REFUSED together, with
422
+ * {@link KubernetesNetworkPolicyHostError} and nothing written. A TLS
423
+ * server name is one exact SNI value; the expanded entry is a name and a
424
+ * pattern, and no single SNI value means both — `example.com` would deny
425
+ * every subdomain the policy claims to allow. List the exact hosts, or
426
+ * leave `tlsServerNames` off for a domain list.
427
+ */
428
+ readonly narrowing?: KubernetesCiliumEgressNarrowing;
429
+ }
430
+ /**
431
+ * Default {@link KubernetesEgressConfig.profileLabelKey} — this backend's
432
+ * own label domain, matching `SANDBOX_TEMPLATE_LABEL_KEY`'s prefix so
433
+ * every label a namzu host puts on a sandbox pod reads as one family.
434
+ *
435
+ * It is deliberately NOT upstream's `sandbox.users.io`: a key in someone
436
+ * else's domain is a key someone else may define differently. The cost is
437
+ * the ConfigMap edit named on {@link KubernetesEgressConfig.profile}, and
438
+ * the controller's refusal spells that edit out itself.
439
+ */
440
+ export declare const DEFAULT_EGRESS_PROFILE_LABEL_KEY = "sandbox.namzu.ai/egress-profile";
441
+ /** One resolved profile label: the key it is written under and its value. */
442
+ export interface EgressProfileLabel {
443
+ readonly key: string;
444
+ readonly value: string;
445
+ }
446
+ /**
447
+ * Named refusal for an egress PROFILE this backend can read but not use.
448
+ * Sibling of {@link KubernetesEgressPolicyConfigError} rather than a reuse of
449
+ * it: that one names a field under `config.egress.policy` and this one names
450
+ * a field beside it, and an operator reading either should not have to work
451
+ * out which level of the config the path belongs to.
452
+ *
453
+ * Thrown SYNCHRONOUSLY from `buildKubernetesBackend` and
454
+ * `createKubernetesWorkspace`, so a misconfigured profile surfaces during
455
+ * host wiring rather than on the first `create()`.
456
+ */
457
+ export declare class KubernetesEgressProfileConfigError extends Error {
458
+ readonly field: 'profile' | 'profileLabelKey';
459
+ readonly value: string;
460
+ readonly name = "KubernetesEgressProfileConfigError";
461
+ constructor(field: 'profile' | 'profileLabelKey', value: string, reason: string);
462
+ }
463
+ /**
464
+ * The profile label this config asks for, or nothing at all — the ONE place
465
+ * the key/value pair is derived, so the claim body, the Sandbox pod
466
+ * template, the policy selector and the policy name cannot disagree about
467
+ * what the profile is.
468
+ *
469
+ * Validates as it resolves: the value has to be a DNS-1123 label and the key
470
+ * a legal label key, both refused with {@link KubernetesEgressProfileConfigError}.
471
+ */
472
+ export declare function egressProfileLabel(egress: KubernetesEgressConfig | undefined): EgressProfileLabel | undefined;
473
+ /**
474
+ * Validate the profile without needing its value — the wiring-time hook, so
475
+ * `buildKubernetesBackend` and `createKubernetesWorkspace` refuse a bad
476
+ * profile the same moment they refuse an unenforceable policy.
477
+ */
478
+ export declare function assertEgressProfileIsUsable(egress: KubernetesEgressConfig | undefined): void;
479
+ /**
480
+ * Default {@link KubernetesPerSandboxEgressConfig.labelKey} — the key the
481
+ * per-sandbox policy's `endpointSelector` matches on.
482
+ *
483
+ * In this backend's own label domain, for the same reason the profile key is
484
+ * (see {@link DEFAULT_EGRESS_PROFILE_LABEL_KEY}), and carrying the same
485
+ * operator prerequisite: the controller's `allowed-label-domains` allowlist
486
+ * has to admit `sandbox.namzu.ai`, or every claim carrying it is refused with
487
+ * {@link KubernetesPodLabelsRejectedError} as the cause. A deployment that
488
+ * would rather not edit that ConfigMap sets this to a key under
489
+ * `sandbox.users.io`, which is upstream's own default domain.
490
+ */
491
+ export declare const DEFAULT_PER_SANDBOX_EGRESS_LABEL_KEY = "sandbox.namzu.ai/per-sandbox-egress";
492
+ /**
493
+ * Named refusal for a `config.egress.perSandbox` this backend can read but
494
+ * not honour. Thrown SYNCHRONOUSLY from `buildKubernetesBackend`, beside the
495
+ * policy and profile refusals, so a host that has mis-declared the capability
496
+ * learns it during wiring rather than from the first `setNetworkPolicy` call
497
+ * — which may be an hour into a run, after work that cannot be redone.
498
+ *
499
+ * Its own class rather than a reuse of {@link KubernetesEgressProfileConfigError}
500
+ * for the reason that one is not a reuse of
501
+ * {@link KubernetesEgressPolicyConfigError}: an operator reading a refusal
502
+ * should not have to work out which level of `config.egress` the named field
503
+ * belongs to.
504
+ */
505
+ export declare class KubernetesPerSandboxEgressConfigError extends Error {
506
+ readonly field: 'engine' | 'admissionPolicyName' | 'admissionPolicyBindingName' | 'labelKey';
507
+ readonly value: string;
508
+ readonly name = "KubernetesPerSandboxEgressConfigError";
509
+ constructor(field: 'engine' | 'admissionPolicyName' | 'admissionPolicyBindingName' | 'labelKey', value: string, reason: string);
510
+ }
511
+ /**
512
+ * The per-sandbox selector label KEY this config asks for, or nothing at all
513
+ * when `perSandbox` is unset — the one place the default is applied, so the
514
+ * pod label, the policy selector and the admission policy's own expectation
515
+ * cannot disagree.
516
+ *
517
+ * Validates as it resolves, exactly as {@link egressProfileLabel} does.
518
+ */
519
+ export declare function perSandboxEgressLabelKey(egress: KubernetesEgressConfig | undefined): string | undefined;
520
+ /**
521
+ * Validate `config.egress.perSandbox` in full — the wiring-time hook, so a
522
+ * mis-declared capability is refused the same moment an unenforceable policy
523
+ * or an unusable profile is.
524
+ *
525
+ * `engine: 'core'` is the refusal the SDK's contract cares about: core
526
+ * `NetworkPolicy` cannot express a hostname at all, so a host that configured
527
+ * it would be told "policy applied" about an object that could never carry
528
+ * the allowlist. It is refused here rather than from the method, which means
529
+ * a `'core'` deployment never gets a handle carrying the method in the first
530
+ * place.
531
+ */
532
+ export declare function assertPerSandboxEgressIsUsable(egress: KubernetesEgressConfig | undefined): void;
533
+ /**
534
+ * Named refusal for `config.egress.perSandbox` reaching an entry point that
535
+ * can never carry the capability it configures: `createKubernetesWorkspace`.
536
+ *
537
+ * `perSandbox` exists to make `Sandbox.setNetworkPolicy` PRESENT on a TASK
538
+ * handle, where the acquire creates the object each policy is named after and
539
+ * owned by and stamps the per-sandbox pod label its selector matches, and
540
+ * where the RBAC and the admission fence are the ones that bound the write.
541
+ * A workspace's create path does none of that — it composes no per-sandbox
542
+ * pod label and tracks no owner uid for one — so on that path the option
543
+ * would be accepted and mean nothing at all: exactly the "declared and
544
+ * silently unused" configuration this module refuses everywhere else. The
545
+ * alternative, omitting the method and saying nothing, is how a host comes to
546
+ * believe it narrowed a workspace's egress.
547
+ *
548
+ * NOT a variant of {@link KubernetesPerSandboxEgressConfigError}, which is
549
+ * about a `perSandbox` value this backend cannot honour ANYWHERE (an engine
550
+ * with no hostname concept, an unnamed fence). This one is about a `perSandbox`
551
+ * value it honours perfectly well on the other entry point, so a caller
552
+ * catching the sibling for a typo'd value does not also catch a correct
553
+ * configuration aimed at the wrong path.
554
+ */
555
+ export declare class KubernetesWorkspacePerSandboxEgressConfigError extends Error {
556
+ readonly name = "KubernetesWorkspacePerSandboxEgressConfigError";
557
+ constructor();
558
+ }
559
+ /**
560
+ * Refuse `config.egress.perSandbox` on the entry point that cannot carry it —
561
+ * `createKubernetesWorkspace`. See
562
+ * {@link KubernetesWorkspacePerSandboxEgressConfigError}.
563
+ *
564
+ * Deliberately not {@link assertPerSandboxEgressIsUsable}, which validates the
565
+ * same config for the path that DOES honour it: a workspace has nothing to
566
+ * validate the option for, whatever its `engine` or `admissionPolicyName` say,
567
+ * because it serves no `setNetworkPolicy` at all. Calling that validator here
568
+ * instead would be worse than saying nothing — it would report a
569
+ * `perSandbox` this deployment can use as if it were in use.
570
+ */
571
+ export declare function assertWorkspaceCarriesNoPerSandboxEgress(egress: KubernetesEgressConfig | undefined): void;
572
+ /**
573
+ * The ONE composer for a sandbox pod's `additionalPodMetadata.labels`.
574
+ *
575
+ * Every label this backend asks the controller to put on a POD is built
576
+ * here: the egress profile today, and whatever a later capability
577
+ * contributes through `extra` (a per-sandbox policy selector, for one). Two
578
+ * independent constructions would be two answers to "what labels is this pod
579
+ * selected by", and the policy selector is built from the same resolution —
580
+ * so a second builder would be a pod bound under a policy nobody checked.
581
+ *
582
+ * Deliberately NOT where `KubernetesBackendInternalConfig.claimLabels`
583
+ * goes. Those are a host's own bookkeeping on the CLAIM object's
584
+ * `metadata.labels`; putting them on the pod would change what selectors
585
+ * match a running sandbox, which is a different question on a different
586
+ * object.
587
+ *
588
+ * Returns an empty object when nothing applies, which every caller reads as
589
+ * "emit nothing at all" — that is what keeps an unprofiled body byte-identical.
590
+ *
591
+ * A key in `extra` that is ALSO the profile's is refused rather than merged
592
+ * either way round. Whichever won, the loser would be a label the translated
593
+ * policy's selector still expects: the profile's selector is built from this
594
+ * same resolution, so a pod carrying the other value is selected by no
595
+ * per-profile policy while `create()` reported the boundary verified. It is
596
+ * the same failure {@link egressProfileLabel} refuses the template key for,
597
+ * reached from the other direction.
598
+ */
599
+ export declare function composeAdditionalPodLabels(egress: KubernetesEgressConfig | undefined, extra?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
600
+ /**
601
+ * Why a claim the controller refused with `InvalidMetadata` was refused, in
602
+ * this backend's own terms — in practice a pod label whose domain is not in
603
+ * the controller's `allowed-label-domains` allowlist, which today means the
604
+ * egress profile's.
605
+ *
606
+ * NOT what an acquire throws. A refused claim comes out of `create()` as
607
+ * `KubernetesAcquireError { reason: 'claim-rejected' }` whether or not a
608
+ * profile is configured — one condition, one taxonomy, one `catch` — and this
609
+ * rides as that error's `cause`. Two classes for one controller condition
610
+ * would make a host's error handling correct or incorrect depending on
611
+ * whether `config.egress.profile` happened to be set.
612
+ *
613
+ * What it adds to the acquire error is what the controller cannot know: the
614
+ * map this backend actually sent, and the `config.egress.profileLabelKey`
615
+ * that moves the offending key to an allowed domain. It carries the whole map
616
+ * rather than the profile alone, and `profile` is optional, because the map is
617
+ * {@link composeAdditionalPodLabels}'s — the profile is the only thing in it
618
+ * today, and a later capability adding a second key would otherwise get an
619
+ * explanation that named a label it did not send.
620
+ *
621
+ * It carries the controller's OWN `reason` and `message` rather than a
622
+ * translation of them: the message agent-sandbox v1.0.2 writes names the
623
+ * offending key, the domain, the ConfigMap key to edit and its default, and
624
+ * no paraphrase of it would be as useful. Measured verbatim against a kind
625
+ * cluster running that controller:
626
+ *
627
+ * > invalid additionalPodMetadata: failed to validate label
628
+ * > "sandbox.namzu.ai/egress-profile": label domain "sandbox.namzu.ai" is
629
+ * > not in the allowlist (configure the allowed-label-domains key of the
630
+ * > agent-sandbox-config ConfigMap in the controller namespace; default:
631
+ * > sandbox.users.io)
632
+ *
633
+ * The refusal is raised as soon as that condition is read rather than after
634
+ * the readiness budget, because `InvalidMetadata` is one of
635
+ * `TERMINAL_CLAIM_REASONS` — nothing about it becomes true by waiting — and
636
+ * the claim is deleted on the way out, so a misconfigured profile costs one
637
+ * round trip rather than a minute of polling.
638
+ */
639
+ export declare class KubernetesPodLabelsRejectedError extends Error {
640
+ /** Every label this backend put on the claim's `additionalPodMetadata`. */
641
+ readonly requestedPodLabels: Readonly<Record<string, string>>;
642
+ readonly claimName: string;
643
+ readonly namespace: string;
644
+ /** The controller's own condition `reason`, e.g. `InvalidMetadata`. */
645
+ readonly controllerReason: string;
646
+ /** The controller's own condition `message`, verbatim. */
647
+ readonly controllerMessage: string;
648
+ /** The egress profile among those labels, when one is configured. */
649
+ readonly profile?: EgressProfileLabel | undefined;
650
+ readonly name = "KubernetesPodLabelsRejectedError";
651
+ constructor(
652
+ /** Every label this backend put on the claim's `additionalPodMetadata`. */
653
+ requestedPodLabels: Readonly<Record<string, string>>, claimName: string, namespace: string,
654
+ /** The controller's own condition `reason`, e.g. `InvalidMetadata`. */
655
+ controllerReason: string,
656
+ /** The controller's own condition `message`, verbatim. */
657
+ controllerMessage: string,
658
+ /** The egress profile among those labels, when one is configured. */
659
+ profile?: EgressProfileLabel | undefined);
660
+ }
661
+ /**
662
+ * Named refusal for a bound pod that never carried a label this backend asked
663
+ * the controller to put on it — the egress profile's, today the only one
664
+ * {@link composeAdditionalPodLabels} produces, which is why the class is named
665
+ * for the LABEL rather than for the profile.
666
+ *
667
+ * This is the one failure this capability must not have quietly. An
668
+ * unlabelled pod handed back is a sandbox running under the DEFAULT policy
669
+ * while the host believes it is on a narrower profile — the translated
670
+ * policy's selector includes the profile label, so a pod without it is
671
+ * selected by neither this profile's policy nor, necessarily, anything else.
672
+ * Refusing is correct and waiting is correct; proceeding is not, so the
673
+ * acquire releases what it claimed and raises this instead.
674
+ *
675
+ * `missingLabel` is the pair that never arrived rather than "the profile", so
676
+ * the refusal stays true for whatever a later capability contributes to that
677
+ * same map: the wait in `readAddressedPod` already covers every entry of it,
678
+ * and this refusal covers exactly the same set.
679
+ */
680
+ export declare class KubernetesPodLabelNotObservedError extends Error {
681
+ /** The label this backend requested and never saw on the bound pod. */
682
+ readonly missingLabel: EgressProfileLabel;
683
+ readonly subject: string;
684
+ readonly observedLabels: Readonly<Record<string, string>>;
685
+ readonly name = "KubernetesPodLabelNotObservedError";
686
+ constructor(
687
+ /** The label this backend requested and never saw on the bound pod. */
688
+ missingLabel: EgressProfileLabel, subject: string, observedLabels: Readonly<Record<string, string>>);
109
689
  }
690
+ /** `true` unless the deployment asked for the single-object check by name. */
691
+ export declare function egressUnionVerificationEnabled(egress: KubernetesEgressConfig | undefined): boolean;
110
692
  /** Where a translated policy is targeted, and what its `podSelector` names. */
111
693
  export interface EgressPolicyTarget {
112
694
  readonly namespace: string;
@@ -118,7 +700,26 @@ export interface EgressPolicyTarget {
118
700
  * carries it.
119
701
  */
120
702
  readonly sandboxTemplateName: string;
703
+ /**
704
+ * The egress PROFILE label this policy also selects, when
705
+ * {@link KubernetesEgressConfig.profile} is set. Absent, the selector is
706
+ * the template label alone and every translation is what it always was.
707
+ *
708
+ * It is part of the SELECTOR rather than a second policy because that is
709
+ * what makes one warm pool serve several modes: pods out of one template
710
+ * carry one template label and differ only by this one, so
711
+ * `${template}-none-egress` selects exactly the `none` pods and
712
+ * `${template}-internet-egress` exactly the `internet` ones.
713
+ */
714
+ readonly profile?: EgressProfileLabel;
121
715
  }
716
+ /**
717
+ * What a translated policy's `podSelector`/`endpointSelector` matches: the
718
+ * template label, plus the profile label when one is configured. One
719
+ * function so the two manifest builders below and every reader of a
720
+ * translation agree on the selector down to the key order.
721
+ */
722
+ export declare function egressPolicySelectorLabels(target: EgressPolicyTarget): Readonly<Record<string, string>>;
122
723
  /**
123
724
  * A translated policy: which kind of Kubernetes object it is, where it
124
725
  * lives, and the manifest an operator applies verbatim. `kind` is the
@@ -131,12 +732,31 @@ export interface EgressPolicyTarget {
131
732
  */
132
733
  export interface KubernetesTranslatedEgressPolicy {
133
734
  readonly kind: 'NetworkPolicy' | 'CiliumNetworkPolicy';
735
+ /**
736
+ * The CONFIGURED kind this came from, carried so a refusal can name what
737
+ * the deployment asked for (`no-network`, `public-internet`, …) rather
738
+ * than only the resource it produced.
739
+ */
740
+ readonly policyKind: KubernetesEgressPolicy['kind'];
134
741
  readonly namespace: string;
135
742
  readonly name: string;
136
743
  readonly manifest: Readonly<Record<string, unknown>>;
137
744
  }
138
- /** `${sandboxTemplateName}-egress`, the name {@link KubernetesEgressConfig.networkPolicyName} defaults to. */
139
- export declare function defaultEgressPolicyName(sandboxTemplateName: string): string;
745
+ /**
746
+ * `${sandboxTemplateName}-egress`, the name
747
+ * {@link KubernetesEgressConfig.networkPolicyName} defaults to — or
748
+ * `${sandboxTemplateName}-${profile}-egress` when a profile is configured,
749
+ * because one template under two profiles needs two policy objects and a
750
+ * single default name would have the second silently verify against the
751
+ * first's manifest.
752
+ *
753
+ * The profile is bounded at 63 characters on its own, but the CONCATENATION
754
+ * is what has to be a legal object name, and only this function knows both
755
+ * halves. Refused here, during host wiring, rather than as an API-server
756
+ * rejection on the first policy GET of the first `create()`: `networkPolicyName`
757
+ * is the way out and it is a config field, so this is a config error.
758
+ */
759
+ export declare function defaultEgressPolicyName(sandboxTemplateName: string, profile?: string): string;
140
760
  /**
141
761
  * Named refusal for a hostname allowlist with no FQDN-capable engine
142
762
  * declared. Thrown SYNCHRONOUSLY from `buildKubernetesBackend` — see
@@ -149,15 +769,236 @@ export declare class KubernetesUnenforceableEgressPolicyError extends Error {
149
769
  readonly name = "KubernetesUnenforceableEgressPolicyError";
150
770
  constructor(policyKind: 'static' | 'resolver');
151
771
  }
772
+ /**
773
+ * Named refusal for a config value this backend can read but not use — today
774
+ * only a `public-internet` `exceptCidrs` entry that is not a CIDR. Thrown
775
+ * SYNCHRONOUSLY from `buildKubernetesBackend`, beside
776
+ * {@link KubernetesUnenforceableEgressPolicyError}, so a typo surfaces
777
+ * during host wiring rather than as a policy the API server rejects when an
778
+ * operator applies it.
779
+ */
780
+ export declare class KubernetesEgressPolicyConfigError extends Error {
781
+ readonly field: string;
782
+ readonly name = "KubernetesEgressPolicyConfigError";
783
+ constructor(field: string, reason: string);
784
+ }
785
+ /**
786
+ * Named refusal for `config.egress.ciliumNarrowing` set on a policy/engine
787
+ * combination it does not apply to. Thrown SYNCHRONOUSLY from
788
+ * {@link assertEgressPolicyIsEnforceable}, beside
789
+ * {@link KubernetesUnenforceableEgressPolicyError}, so a narrowing option
790
+ * that would silently do nothing is refused at host-wiring time rather than
791
+ * accepted and ignored.
792
+ */
793
+ export declare class KubernetesEgressNarrowingUnsupportedError extends Error {
794
+ readonly policyKind: KubernetesEgressPolicy['kind'];
795
+ readonly engine: KubernetesEgressEngine;
796
+ readonly name = "KubernetesEgressNarrowingUnsupportedError";
797
+ constructor(policyKind: KubernetesEgressPolicy['kind'], engine: KubernetesEgressEngine);
798
+ }
799
+ /**
800
+ * Raised for an `allowedHosts` entry this backend will not translate.
801
+ *
802
+ * Named for the ENTRY rather than for a config field, because entries arrive
803
+ * from two directions: a host's `Sandbox.setNetworkPolicy` call, and the
804
+ * `allowedHosts` of a `static`/`resolver` `config.egress.policy`.
805
+ * `SandboxNetworkPolicy` names HOSTS: `api.example.com` for one host,
806
+ * `.example.com` for a domain and its subdomains. A glob, a URL, a port
807
+ * suffix or an address is refused rather than emitted, because a
808
+ * `CiliumNetworkPolicy` carrying one is an object the API server rejects on
809
+ * apply — or, worse, accepts as a name that resolves to nothing, which reads
810
+ * from the outside exactly like a policy that is working.
811
+ *
812
+ * `stateHostnameGrammar` is the one part of the message a caller can turn
813
+ * off, because NOT every refusal can make that claim. It is false for the
814
+ * config-level refusal of a leading-dot entry under `tlsServerNames`, whose
815
+ * body has just said that the entry reaches the object as a literal
816
+ * `matchName` admitting nothing with the option on or off: closing that
817
+ * message with "'.example.com' for that domain and its subdomains" would
818
+ * re-assert, as implemented, the very grammar whose absence is why the entry
819
+ * is refused. Every other refusal keeps the sentence, the expanding branch of
820
+ * the same one included.
821
+ */
822
+ export declare class KubernetesNetworkPolicyHostError extends Error {
823
+ readonly host: string;
824
+ readonly name = "KubernetesNetworkPolicyHostError";
825
+ constructor(host: string, reason: string, options?: {
826
+ readonly stateHostnameGrammar?: boolean;
827
+ });
828
+ }
829
+ /**
830
+ * Every entry in `narrowing.ports`, `narrowing.hostPorts` and
831
+ * `narrowing.tlsPorts` is a port the API server will actually accept, none of
832
+ * those three is an explicitly empty array, and every DNS suffix
833
+ * `narrowing.dnsNames` names is a non-empty string — checked here,
834
+ * synchronously, for the same reason {@link assertEgressPolicyIsEnforceable}
835
+ * checks `exceptCidrs`: a value the API server rejects on apply would leave a
836
+ * policy that never verifies.
837
+ */
838
+ /**
839
+ * Refuse a narrowing the API server would reject on apply, naming the field
840
+ * that has to change.
841
+ *
842
+ * `fieldPath` is how the refusal spells the option's location, because the
843
+ * same shape is configurable in two places now: `ciliumNarrowing` narrows the
844
+ * CONFIG-level translated policy, and `perSandbox.narrowing` narrows the
845
+ * per-sandbox policies a host writes through `setNetworkPolicy`. One
846
+ * validator, because they are one shape and two would disagree the first time
847
+ * either grew a field.
848
+ */
849
+ export declare function assertCiliumNarrowingIsUsable(narrowing: KubernetesCiliumEgressNarrowing, fieldPath?: string): void;
152
850
  /**
153
851
  * Synchronous, no-I/O precondition: can `policy.kind` be enforced under
154
- * `engine` at all. Deliberately decided from the KIND alone — a `resolver`
155
- * policy's `resolve()` is never invoked here, both because calling it just to
156
- * prove a refusal would be wasted work (and possibly a side effect the host
157
- * did not expect yet) and because this has to stay callable synchronously
158
- * from `buildKubernetesBackend`, which contacts nothing.
852
+ * `engine` at all, and — if `narrowing` is set — does it apply to this
853
+ * policy/engine combination. Deliberately decided from the KIND alone — a
854
+ * `resolver` policy's `resolve()` is never invoked here, both because calling
855
+ * it just to prove a refusal would be wasted work (and possibly a side effect
856
+ * the host did not expect yet) and because this has to stay callable
857
+ * synchronously from `buildKubernetesBackend`, which contacts nothing.
858
+ */
859
+ export declare function assertEgressPolicyIsEnforceable(policy: KubernetesEgressPolicy, engine: KubernetesEgressEngine, narrowing?: KubernetesCiliumEgressNarrowing): void;
860
+ /**
861
+ * Which translation a host refusal is being spelled for, as ONE value.
862
+ *
863
+ * The refusal has two halves that have to agree — the sentence, which follows
864
+ * from whether the translation expands a leading-dot entry, and the field
865
+ * path, which follows from which field the caller set — and both are facts
866
+ * about the SAME thing: the translation the caller is on. Passed as two
867
+ * arguments they are decided from two inputs, and an expanding caller was
868
+ * told to repair `config.egress.ciliumNarrowing` with the remedy that belongs
869
+ * to `config.egress.perSandbox.narrowing` — a message sending a reader to a
870
+ * field the entry never came from. Neither half means anything without the
871
+ * other, so they travel together.
872
+ */
873
+ export interface HostsFitNarrowingContext {
874
+ /**
875
+ * The field the refusal sends the reader to, spelled as the caller set it
876
+ * — the full path, as every other refusal in this module spells one.
877
+ */
878
+ readonly fieldPath: string;
879
+ /**
880
+ * Whether this translation turns a leading-dot entry into a `matchName`
881
+ * plus a `matchPattern` — {@link ciliumFqdnEntries}. `true` for the
882
+ * per-sandbox translation, `false` for the config-level one, whose emitted
883
+ * bytes are pinned. Decides the refusal's sentence and its closing grammar
884
+ * as well as the bytes, and all three are the same fact about one entry.
885
+ */
886
+ readonly expandsDottedEntries: boolean;
887
+ }
888
+ /**
889
+ * The refusal context of the PER-SANDBOX translation — the one that expands,
890
+ * and so the one `expandDomains: true` builds.
891
+ *
892
+ * Shared deliberately: the per-sandbox writer refuses a host by this context
893
+ * before it reads the fence, and {@link buildCiliumEgressManifest} refuses the
894
+ * same host by the same context on the way through the translation, so the two
895
+ * cannot name different fields for one entry however the earlier check is
896
+ * reached or skipped.
897
+ */
898
+ export declare const PER_SANDBOX_NARROWING_REFUSAL: HostsFitNarrowingContext;
899
+ /**
900
+ * The refusal context of the config-level translation — `config.egress.policy`
901
+ * and its `config.egress.ciliumNarrowing`, which expands nothing.
902
+ */
903
+ export declare const CONFIG_LEVEL_NARROWING_REFUSAL: HostsFitNarrowingContext;
904
+ /**
905
+ * Refuse the one allowlist entry a narrowing option cannot express.
906
+ *
907
+ * `tlsServerNames` puts the entry on the rule as a TLS server name — one
908
+ * exact SNI value a handshake presents — and a `.domain` entry is a set of
909
+ * names no single SNI value means. Either way the emitted object would be
910
+ * admitted by the shipped fence, read back deep-equal to what was sent, and
911
+ * deny what the caller asked to allow — the failure every other refusal in
912
+ * this module exists to prevent. Refused rather than translated another way,
913
+ * because both other ways are guesses: dropping the subdomains silently
914
+ * narrows what the caller asked for, and a wildcard SNI value is not
915
+ * something the Cilium versions these manifests are written against are
916
+ * known here to match — an SNI that matches nothing denies just as
917
+ * completely, and more quietly.
918
+ *
919
+ * The entry is refused for a DIFFERENT reason on each of the two paths, and
920
+ * the message says which one the caller is on, because the same sentence
921
+ * cannot be true of both:
922
+ *
923
+ * - `expandDomains` — the per-sandbox writer's translation, which turns
924
+ * `.domain` into `matchName: domain` PLUS `matchPattern: '*.domain'`. No
925
+ * single SNI value means that pair: `domain` alone denies every subdomain
926
+ * the pattern admits, and the entry as written is not a name any handshake
927
+ * presents at all.
928
+ * - unexpanded — the config-level translation, which expands nothing. There
929
+ * the entry reaches the object as the literal `matchName: '.domain'`,
930
+ * which no DNS answer carries, so it already admits nothing; `serverNames`
931
+ * on top of it is a second, independent denial. Telling this caller to
932
+ * "leave tlsServerNames off for a domain list" would name a repair that is
933
+ * not one — the config-level `.domain` entry denies the domain and every
934
+ * subdomain with or without the option (a pre-existing defect of this
935
+ * translation, deferred to its own change) — so it is not offered here.
936
+ *
937
+ * `context` is WHICH translation the refusal is being spelled for, as one
938
+ * value — see {@link HostsFitNarrowingContext}. A reader has to be sent to
939
+ * the field they actually set, and the sentence they are sent by has to be
940
+ * true of the translation they are on, and both follow from that one fact:
941
+ * passed separately, an expanding caller gets the per-sandbox remedy attached
942
+ * to the config-level field name, a message whose only repair is a field the
943
+ * entry never came from. Called from BOTH — the translation itself, so every
944
+ * caller is covered, and the per-sandbox writer, which refuses earlier still,
945
+ * before it reads the fence.
159
946
  */
160
- export declare function assertEgressPolicyIsEnforceable(policy: EgressPolicy, engine: KubernetesEgressEngine): void;
947
+ export declare function assertHostsFitNarrowing(allowedHosts: readonly string[], narrowing: KubernetesCiliumEgressNarrowing | undefined, context: HostsFitNarrowingContext): void;
948
+ /**
949
+ * One allowlist entry, expanded to the `toFQDNs` entries it means.
950
+ *
951
+ * `SandboxNetworkPolicy.allowedHosts`'s own grammar, which the SDK states and
952
+ * the docker backend implements: `api.example.com` is that host, and
953
+ * `.example.com` is the domain AND its subdomains. Cilium's `matchName` is an
954
+ * exact name and does not match across a `.`, so the domain form needs the
955
+ * name plus a `matchPattern` — `*.example.com` alone would admit
956
+ * `a.example.com` and not `example.com` itself.
957
+ *
958
+ * Used by the PER-SANDBOX policy only. The config-level translation
959
+ * (`config.egress.policy`) deliberately does not expand anything: what it
960
+ * emits for a given config is pinned byte-for-byte, because verification of
961
+ * the named object is an exact match and a changed translation fails every
962
+ * `create()` on every deployment that already applied a policy.
963
+ */
964
+ export declare function ciliumFqdnEntries(entry: string): readonly Readonly<Record<string, string>>[];
965
+ /**
966
+ * Everything a `CiliumNetworkPolicy` for a hostname allowlist is built from.
967
+ *
968
+ * ONE builder, two callers: the config-level translation below, whose
969
+ * selector is the template (and profile) label and whose emitted bytes are
970
+ * pinned, and the per-sandbox policy in `per-sandbox-policy.ts`, whose
971
+ * selector is one per-sandbox label and which additionally carries an
972
+ * `ownerReferences` entry so the cluster garbage-collects it. A second
973
+ * builder would be a second answer to what a namzu egress policy looks like,
974
+ * and the read-back comparator would then be verifying one of them against
975
+ * the other's shape.
976
+ */
977
+ export interface CiliumEgressManifestOptions {
978
+ readonly namespace: string;
979
+ readonly name: string;
980
+ /** What `spec.endpointSelector.matchLabels` carries, verbatim. */
981
+ readonly selectorLabels: Readonly<Record<string, string>>;
982
+ readonly allowedHosts: readonly string[];
983
+ /** The CONFIGURED kind this came from, for a refusal's wording. */
984
+ readonly policyKind: KubernetesEgressPolicy['kind'];
985
+ readonly narrowing?: KubernetesCiliumEgressNarrowing;
986
+ /** `metadata.ownerReferences`. Absent ⇒ the metadata is what it always was. */
987
+ readonly ownerReferences?: readonly KubernetesOwnerReference[];
988
+ /**
989
+ * Expand a leading-dot entry into `matchName` plus `matchPattern` — see
990
+ * {@link ciliumFqdnEntries}. Off by default, because the config-level
991
+ * translation's emitted bytes are pinned.
992
+ *
993
+ * This option IS the translation, not a formatting flag on it: it selects
994
+ * the emitted bytes, the sentence a refusal carries and the field that
995
+ * refusal names, all from one value — see
996
+ * {@link HostsFitNarrowingContext}. There is no way to ask for one without
997
+ * the others, and none should be added.
998
+ */
999
+ readonly expandDomains?: boolean;
1000
+ }
1001
+ export declare function buildCiliumEgressManifest(options: CiliumEgressManifestOptions): KubernetesTranslatedEgressPolicy;
161
1002
  /**
162
1003
  * The pure translation: an {@link EgressPolicy} plus the declared
163
1004
  * {@link KubernetesEgressEngine} in, the concrete manifest this backend can
@@ -175,7 +1016,7 @@ export declare function assertEgressPolicyIsEnforceable(policy: EgressPolicy, en
175
1016
  * hosts against, and re-resolving on every `create()` would only produce a
176
1017
  * value nothing downstream re-applies to the cluster.
177
1018
  */
178
- export declare function translateEgressPolicy(policy: EgressPolicy, engine: KubernetesEgressEngine, target: EgressPolicyTarget): Promise<KubernetesTranslatedEgressPolicy>;
1019
+ export declare function translateEgressPolicy(policy: KubernetesEgressPolicy, engine: KubernetesEgressEngine, target: EgressPolicyTarget, ciliumNarrowing?: KubernetesCiliumEgressNarrowing): Promise<KubernetesTranslatedEgressPolicy>;
179
1020
  /**
180
1021
  * Raised by {@link verifyEgressPolicyApplied} when the named object does not
181
1022
  * exist at all. This backend never CREATES the policy itself — like the
@@ -211,9 +1052,245 @@ export declare class KubernetesEgressPolicyMismatchError extends Error {
211
1052
  * - `NetworkPolicy` additionally declares `policyTypes` including
212
1053
  * `'Egress'` — a `NetworkPolicy` with an `egress` array but no `'Egress'`
213
1054
  * in `policyTypes` enforces nothing on egress at all;
214
- * - the `egress` rule array matches the translation exactly.
1055
+ * - the `egress` rule array matches the translation exactly;
1056
+ * - and, ONLY when the translation carries `metadata.ownerReferences` (the
1057
+ * per-sandbox policies of `per-sandbox-policy.ts`, never the operator's
1058
+ * own object), that the live object still carries each of them — an owner
1059
+ * reference dropped between the write and the read is a policy the
1060
+ * cluster will never collect with the sandbox it belongs to, which is the
1061
+ * leak the reference exists to prevent, and it is invisible in `spec`.
215
1062
  *
216
1063
  * Never mutates and never creates — a 404/410 is refused, not repaired.
217
1064
  */
218
1065
  export declare function verifyEgressPolicyApplied(client: KubernetesClient, translated: KubernetesTranslatedEgressPolicy, signal?: AbortSignal): Promise<void>;
1066
+ /** A CIDR as a masked base address and a prefix length. */
1067
+ export interface ParsedCidr {
1068
+ readonly version: 4 | 6;
1069
+ readonly base: bigint;
1070
+ readonly bits: number;
1071
+ }
1072
+ /**
1073
+ * A CIDR, or `undefined` when it is not one this check can read. Host bits
1074
+ * are masked off rather than rejected: `10.0.0.1/8` and `10.0.0.0/8` name the
1075
+ * same block, and an operator who wrote the first meant the second.
1076
+ */
1077
+ export declare function parseCidr(text: unknown): ParsedCidr | undefined;
1078
+ /** One port, or a range of them, on one protocol. `start` absent is every port. */
1079
+ interface PortRange {
1080
+ /** `undefined` is EVERY protocol — how Cilium spells `ANY`. Core defaults to TCP. */
1081
+ readonly protocol?: string;
1082
+ readonly start?: number;
1083
+ readonly end?: number;
1084
+ }
1085
+ /** A destination, as either side of the comparison names it. */
1086
+ type PolicyPeer = {
1087
+ readonly kind: 'everything';
1088
+ } | {
1089
+ readonly kind: 'cidr';
1090
+ readonly cidr: ParsedCidr;
1091
+ readonly except: readonly ParsedCidr[];
1092
+ readonly text: string;
1093
+ } | {
1094
+ readonly kind: 'selector';
1095
+ readonly namespaceSelector?: unknown;
1096
+ readonly podSelector?: unknown;
1097
+ readonly text: string;
1098
+ };
1099
+ interface AllowedDestination {
1100
+ readonly peer: PolicyPeer;
1101
+ readonly ports: 'all' | readonly PortRange[];
1102
+ }
1103
+ /** One `toFQDNs` entry the translation allows, with the ports it allows it on. */
1104
+ interface AllowedFqdn {
1105
+ readonly host: string;
1106
+ readonly ports: 'all' | readonly PortRange[];
1107
+ }
1108
+ /**
1109
+ * Everything the configured translation lets out, in the one shape the union
1110
+ * check compares against.
1111
+ *
1112
+ * Built from the manifest {@link translateEgressPolicy} just produced for
1113
+ * every core kind, so there is no second statement anywhere of what
1114
+ * `deny-all` or `public-internet` permit — the emitted object IS the
1115
+ * statement, and a test asserts each translation is within its own
1116
+ * allowance. The Cilium kinds are the one exception and say so: a
1117
+ * `CiliumNetworkPolicy`'s DNS-visibility rule is read into the core-shaped
1118
+ * destination it is equivalent to (UDP/TCP 53 to the resolver's pods), so
1119
+ * that a core `NetworkPolicy` allowing exactly cluster DNS is not reported as
1120
+ * widening a hostname allowlist that already allows it.
1121
+ */
1122
+ export interface EgressAllowance {
1123
+ readonly destinations: readonly AllowedDestination[];
1124
+ /**
1125
+ * `toFQDNs` names a `static`/`resolver` translation allows, each with the
1126
+ * ports it allows that name on (`'all'` for the unnarrowed shape, which
1127
+ * puts no `toPorts` on its `toFQDNs` rule at all). Empty for every core
1128
+ * kind. A name can appear more than once — one entry per `toFQDNs` rule
1129
+ * naming it — and it is allowed on a port if ANY entry covers that port,
1130
+ * the same "any matching destination" rule {@link destinationIsAllowed}
1131
+ * applies to CIDR and selector peers.
1132
+ */
1133
+ readonly fqdns: readonly AllowedFqdn[];
1134
+ /**
1135
+ * The exact DNS names a `static`/`resolver` translation's kube-dns rule
1136
+ * restricts LOOKUPS to when `ciliumNarrowing.dnsNames` is set — the
1137
+ * `matchName` list {@link narrowedDnsVisibilityRule} builds. `'all'` for
1138
+ * every translation that does not narrow DNS: every core kind (which
1139
+ * cannot express an L7 DNS restriction at all) and an unnarrowed
1140
+ * `static`/`resolver` (`rules.dns: [{ matchPattern: '*' }]`).
1141
+ *
1142
+ * `destinations` alone cannot carry this: reachability to the resolver's
1143
+ * peer and port is the same whether or not DNS is narrowed, so a peer/port
1144
+ * check reports a plain kube-dns rule as `within` a narrowed translation
1145
+ * exactly as it would an unnarrowed one. {@link reachesResolverAtDnsPort}
1146
+ * is the check that actually reads this field, in both
1147
+ * {@link coreEgressRuleVerdict} and {@link ciliumEgressRuleVerdict}.
1148
+ */
1149
+ readonly dnsNarrowedTo: 'all' | readonly string[];
1150
+ readonly permitsEverything: boolean;
1151
+ readonly permitsNothing: boolean;
1152
+ /** The configured kind, for the refusal message. */
1153
+ readonly policyKind: KubernetesEgressPolicy['kind'];
1154
+ }
1155
+ /**
1156
+ * The allowance a translated policy expresses. See {@link EgressAllowance}.
1157
+ */
1158
+ export declare function egressAllowance(translated: KubernetesTranslatedEgressPolicy): EgressAllowance;
1159
+ /** One egress rule, judged against the translation. */
1160
+ export interface EgressRuleVerdict {
1161
+ readonly beyond: boolean | 'unknown';
1162
+ readonly detail?: string;
1163
+ }
1164
+ /** The pod the union check is about. */
1165
+ export interface EgressVerificationTarget {
1166
+ readonly namespace: string;
1167
+ /**
1168
+ * The pod's REAL labels — for a directly created Sandbox the labels the
1169
+ * create body stamps (known before the POST, so a refusal leaves no
1170
+ * Sandbox and no PVC behind), for a claimed one the bound pod's own
1171
+ * `metadata.labels`. The same value the ingress check is given, for the
1172
+ * same reason: a name proves an object exists, a label is what a selector
1173
+ * actually matches.
1174
+ */
1175
+ readonly podLabels: Readonly<Record<string, string>>;
1176
+ readonly engine: KubernetesEgressEngine;
1177
+ /** How the refusal names the thing being created, e.g. `Sandbox namzu-ws-demo`. */
1178
+ readonly subject: string;
1179
+ }
1180
+ /** What one examined policy turned out to be. One line of the refusal. */
1181
+ export type EgressPolicyVerdict =
1182
+ /** Selects the pod and lets out nothing the translation does not. */
1183
+ 'within'
1184
+ /** Selects the pod and allows egress the translation does not. */
1185
+ | 'widens-egress'
1186
+ /** Its selector does not match the pod's labels. */
1187
+ | 'does-not-select'
1188
+ /** Selects the pod but does not enforce egress, so its egress block is inert. */
1189
+ | 'not-egress-scoped'
1190
+ /** Contains something this check cannot decide. */
1191
+ | 'not-evaluable';
1192
+ /** One policy, as the refusal reports it. */
1193
+ export interface ExaminedEgressPolicy {
1194
+ readonly kind: 'NetworkPolicy' | 'CiliumNetworkPolicy';
1195
+ readonly name: string;
1196
+ readonly verdict: EgressPolicyVerdict;
1197
+ /** Why, for every verdict that is not a plain match or non-match. */
1198
+ readonly detail?: string;
1199
+ }
1200
+ /**
1201
+ * Which of the three refusals this is — three different operator actions, so
1202
+ * they are carried apart rather than folded into one message:
1203
+ *
1204
+ * - `policy-widens-egress` — narrow or delete the policy that lets more out
1205
+ * than `config.egress` says.
1206
+ * - `no-enforcing-policy` — nothing puts this pod in egress default-deny, so
1207
+ * the translation is not the boundary; apply a policy that selects these
1208
+ * labels.
1209
+ * - `not-evaluable` — this check cannot decide; grant the missing verb, fix
1210
+ * the unreadable policy, or declare `egress.verify: 'named-object-only'`.
1211
+ */
1212
+ export type EgressPolicyRefusal = 'policy-widens-egress' | 'no-enforcing-policy' | 'not-evaluable';
1213
+ /**
1214
+ * One policy, reduced to the three questions the union rule asks: does it
1215
+ * select this pod, does it put it in egress default-deny, and does anything
1216
+ * it allows fall outside the configured translation.
1217
+ */
1218
+ export interface EgressPolicyDocument {
1219
+ readonly kind: 'NetworkPolicy' | 'CiliumNetworkPolicy';
1220
+ readonly name: string;
1221
+ readonly selects: SelectorMatch;
1222
+ /**
1223
+ * Does this policy put the pod into egress DEFAULT-DENY — the only thing
1224
+ * that makes the translation a boundary at all. A core policy whose
1225
+ * `policyTypes` leaves Egress out does not (and the API server ignores its
1226
+ * `egress` block outright), and neither does a Cilium rule carrying
1227
+ * `enableDefaultDeny.egress: false`.
1228
+ */
1229
+ readonly enforcesEgress: boolean;
1230
+ readonly rules: readonly EgressRuleVerdict[];
1231
+ /** Set when the OBJECT could not be read. It decides alone. */
1232
+ readonly unreadable?: string;
1233
+ }
1234
+ /** Every core `NetworkPolicy` in the list, reduced to {@link EgressPolicyDocument}. */
1235
+ export declare function readCoreEgressPolicies(items: readonly unknown[], target: EgressVerificationTarget, allowance: EgressAllowance): EgressPolicyDocument[];
1236
+ /** Every `CiliumNetworkPolicy` in the list, reduced the same way. */
1237
+ export declare function readCiliumEgressPolicies(items: readonly unknown[], target: EgressVerificationTarget, allowance: EgressAllowance): EgressPolicyDocument[];
1238
+ export interface EgressUnionDecision {
1239
+ readonly examined: readonly ExaminedEgressPolicy[];
1240
+ /** Absent when every selecting policy stays inside the translation. */
1241
+ readonly refusal?: {
1242
+ readonly kind: EgressPolicyRefusal;
1243
+ readonly summary: string;
1244
+ };
1245
+ }
1246
+ /**
1247
+ * The union rule, applied. Pure — no I/O, no client, no clock — so every
1248
+ * shape that has to be refused can be asserted one per test.
1249
+ *
1250
+ * Kubernetes UNIONS every policy selecting a pod: traffic leaves if ANY
1251
+ * selecting policy allows it. So one policy allowing more than the
1252
+ * translation is the finding however many narrower ones sit beside it, and a
1253
+ * pod no policy default-denies has no egress boundary at all whatever the
1254
+ * named object says.
1255
+ */
1256
+ export declare function decideEgressUnion(documents: readonly EgressPolicyDocument[], allowance: EgressAllowance): EgressUnionDecision;
1257
+ /**
1258
+ * The named refusal. Distinct from {@link KubernetesEgressPolicyMismatchError}
1259
+ * — which is about the ONE named object drifting from the translation — and
1260
+ * from `KubernetesIngressPolicyError`, which is about the agent port being
1261
+ * reachable. An operator debugging a release that ships all three tells them
1262
+ * apart by class and by the first clause of the message.
1263
+ *
1264
+ * It carries the pod's labels and EVERY policy examined, with a verdict each,
1265
+ * because that list is the operator's whole debugging session: "why does my
1266
+ * policy not count?" is answered by the line saying it did not select these
1267
+ * labels.
1268
+ */
1269
+ export declare class KubernetesEgressPolicyUnionError extends Error {
1270
+ readonly refusal: EgressPolicyRefusal;
1271
+ readonly subject: string;
1272
+ readonly podLabels: Readonly<Record<string, string>>;
1273
+ readonly policyKind: KubernetesEgressPolicy['kind'];
1274
+ readonly examined: readonly ExaminedEgressPolicy[];
1275
+ /** Empty on every decision made from policies that WERE read. */
1276
+ readonly unread: readonly UnreadPolicySource[];
1277
+ readonly name = "KubernetesEgressPolicyUnionError";
1278
+ constructor(refusal: EgressPolicyRefusal, subject: string, podLabels: Readonly<Record<string, string>>, policyKind: KubernetesEgressPolicy['kind'], examined: readonly ExaminedEgressPolicy[], summary: string,
1279
+ /** Empty on every decision made from policies that WERE read. */
1280
+ unread?: readonly UnreadPolicySource[]);
1281
+ }
1282
+ /**
1283
+ * Verify-not-trust, widened from one object to the union: list the
1284
+ * namespace's policies, evaluate every one that selects this pod against the
1285
+ * configured translation, and refuse unless nothing lets out more than
1286
+ * `config.egress` says.
1287
+ *
1288
+ * Runs beside the ingress check on every create path — before the POST for a
1289
+ * directly created Sandbox, where the labels are known and a refusal leaves
1290
+ * nothing behind, and after the bind for a claimed one, where the pool's own
1291
+ * template decides the labels and a refusal releases the claim through the
1292
+ * acquire path's cleanup.
1293
+ */
1294
+ export declare function verifyEgressPolicyUnion(client: KubernetesClient, translated: KubernetesTranslatedEgressPolicy, target: EgressVerificationTarget, signal?: AbortSignal): Promise<void>;
1295
+ export {};
219
1296
  //# sourceMappingURL=egress-policy.d.ts.map