@namzu/sandbox 26.0.0 → 27.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 (83) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +20 -7
  3. package/dist/backends/firecracker/protocol.d.ts +46 -0
  4. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  5. package/dist/backends/firecracker/protocol.js +31 -0
  6. package/dist/backends/firecracker/protocol.js.map +1 -1
  7. package/dist/backends/firecracker/transport.d.ts +15 -1
  8. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  9. package/dist/backends/firecracker/transport.js +16 -0
  10. package/dist/backends/firecracker/transport.js.map +1 -1
  11. package/dist/backends/kubernetes/allocation.d.ts +115 -0
  12. package/dist/backends/kubernetes/allocation.d.ts.map +1 -0
  13. package/dist/backends/kubernetes/allocation.js +130 -0
  14. package/dist/backends/kubernetes/allocation.js.map +1 -0
  15. package/dist/backends/kubernetes/egress-policy.d.ts +111 -35
  16. package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
  17. package/dist/backends/kubernetes/egress-policy.js +81 -41
  18. package/dist/backends/kubernetes/egress-policy.js.map +1 -1
  19. package/dist/backends/kubernetes/identity.d.ts +129 -0
  20. package/dist/backends/kubernetes/identity.d.ts.map +1 -1
  21. package/dist/backends/kubernetes/identity.js +150 -0
  22. package/dist/backends/kubernetes/identity.js.map +1 -1
  23. package/dist/backends/kubernetes/index.d.ts +23 -1
  24. package/dist/backends/kubernetes/index.d.ts.map +1 -1
  25. package/dist/backends/kubernetes/index.js +22 -3
  26. package/dist/backends/kubernetes/index.js.map +1 -1
  27. package/dist/backends/kubernetes/objects.d.ts +76 -0
  28. package/dist/backends/kubernetes/objects.d.ts.map +1 -1
  29. package/dist/backends/kubernetes/objects.js +61 -0
  30. package/dist/backends/kubernetes/objects.js.map +1 -1
  31. package/dist/backends/kubernetes/per-sandbox-fence.d.ts +194 -0
  32. package/dist/backends/kubernetes/per-sandbox-fence.d.ts.map +1 -0
  33. package/dist/backends/kubernetes/per-sandbox-fence.js +522 -0
  34. package/dist/backends/kubernetes/per-sandbox-fence.js.map +1 -0
  35. package/dist/backends/kubernetes/per-sandbox-policy.d.ts +45 -3
  36. package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -1
  37. package/dist/backends/kubernetes/per-sandbox-policy.js +98 -8
  38. package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -1
  39. package/dist/backends/kubernetes/sandbox.d.ts +4 -9
  40. package/dist/backends/kubernetes/sandbox.d.ts.map +1 -1
  41. package/dist/backends/kubernetes/sandbox.js +4 -9
  42. package/dist/backends/kubernetes/sandbox.js.map +1 -1
  43. package/dist/backends/kubernetes/transport.d.ts +108 -1
  44. package/dist/backends/kubernetes/transport.d.ts.map +1 -1
  45. package/dist/backends/kubernetes/transport.js +150 -6
  46. package/dist/backends/kubernetes/transport.js.map +1 -1
  47. package/dist/backends/kubernetes/workspace-allocate.d.ts +206 -0
  48. package/dist/backends/kubernetes/workspace-allocate.d.ts.map +1 -0
  49. package/dist/backends/kubernetes/workspace-allocate.js +332 -0
  50. package/dist/backends/kubernetes/workspace-allocate.js.map +1 -0
  51. package/dist/backends/kubernetes/workspace-observe.d.ts +187 -0
  52. package/dist/backends/kubernetes/workspace-observe.d.ts.map +1 -0
  53. package/dist/backends/kubernetes/workspace-observe.js +343 -0
  54. package/dist/backends/kubernetes/workspace-observe.js.map +1 -0
  55. package/dist/backends/kubernetes/workspace-per-sandbox.d.ts +94 -0
  56. package/dist/backends/kubernetes/workspace-per-sandbox.d.ts.map +1 -0
  57. package/dist/backends/kubernetes/workspace-per-sandbox.js +119 -0
  58. package/dist/backends/kubernetes/workspace-per-sandbox.js.map +1 -0
  59. package/dist/backends/kubernetes/workspace.d.ts +139 -5
  60. package/dist/backends/kubernetes/workspace.d.ts.map +1 -1
  61. package/dist/backends/kubernetes/workspace.js +434 -36
  62. package/dist/backends/kubernetes/workspace.js.map +1 -1
  63. package/dist/index.d.ts +83 -7
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +100 -6
  66. package/dist/index.js.map +1 -1
  67. package/package.json +3 -3
  68. package/src/backends/firecracker/protocol.ts +47 -0
  69. package/src/backends/firecracker/transport.ts +31 -0
  70. package/src/backends/kubernetes/allocation.ts +199 -0
  71. package/src/backends/kubernetes/egress-policy.ts +165 -42
  72. package/src/backends/kubernetes/identity.ts +210 -0
  73. package/src/backends/kubernetes/index.ts +61 -2
  74. package/src/backends/kubernetes/objects.ts +98 -0
  75. package/src/backends/kubernetes/per-sandbox-fence.ts +800 -0
  76. package/src/backends/kubernetes/per-sandbox-policy.ts +152 -17
  77. package/src/backends/kubernetes/sandbox.ts +4 -9
  78. package/src/backends/kubernetes/transport.ts +203 -4
  79. package/src/backends/kubernetes/workspace-allocate.ts +607 -0
  80. package/src/backends/kubernetes/workspace-observe.ts +590 -0
  81. package/src/backends/kubernetes/workspace-per-sandbox.ts +179 -0
  82. package/src/backends/kubernetes/workspace.ts +655 -34
  83. package/src/index.ts +193 -3
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Per-sandbox egress on a workspace: the pieces `createKubernetesWorkspace`,
3
+ * `allocateKubernetesWorkspace` and a workspace handle share.
4
+ *
5
+ * ## The shape, and where it differs from a task sandbox
6
+ *
7
+ * - **Label.** The per-sandbox selector label rides on the workspace's
8
+ * directly created `Sandbox` pod template, composed by
9
+ * `composeAdditionalPodLabels` beside the egress profile. Its VALUE is the
10
+ * Sandbox's own name, `namzu-ws-<workspaceId>`: deterministic, known before
11
+ * the POST, and stable across a suspend and a resume, because a resume
12
+ * patches `operatingMode` and the pod template (labels included) is carried
13
+ * by the object, not by the pod.
14
+ * - **Owner.** The `Sandbox` itself. A policy names it in `ownerReferences`
15
+ * with its `metadata.uid`, so deleting the workspace (`destroy({ deleteDisk:
16
+ * true })`, `deleteKubernetesWorkspace`) lets the cluster's garbage
17
+ * collector remove every policy it owns. The default `destroy()` only
18
+ * suspends, keeps the object, and so keeps the policy.
19
+ * - **Name.** `namzu-sbx-<sandbox uid>`, the same rule as a task sandbox. The
20
+ * workspace NAME is deterministic and its uid is not, so a workspace deleted
21
+ * and created again under the same name selects by the same label value
22
+ * and owns a policy of a different name.
23
+ *
24
+ * ## Suspend and resume
25
+ *
26
+ * The behaviour is one rule, applied on every path that is about to run the
27
+ * workspace's pod: a resume, and an open of an existing workspace.
28
+ *
29
+ * 1. The admission fence is re-verified from scratch (never from a memo). A
30
+ * fence that is missing or would not enforce REFUSES the resume, before
31
+ * the patch: the workspace stays suspended, which is the only state that
32
+ * is safe without the fence, and nothing is written.
33
+ * 2. Every per-sandbox policy in the namespace that selects this workspace
34
+ * (name prefix `namzu-sbx-`, selector label value equal to the Sandbox's
35
+ * name) and that this Sandbox does not own (no `ownerReferences` entry of
36
+ * kind `Sandbox` with this name and the uid the cluster reports NOW) is
37
+ * deleted. Such a policy belongs to a workspace that was deleted and
38
+ * created again under the same name and has not been garbage-collected
39
+ * yet; left alone it would grant the new workspace what the old one's
40
+ * consent covered.
41
+ * 3. A policy this Sandbox owns is KEPT, unchanged. A suspend is not a
42
+ * revocation: the grant was admitted by the fence when it was written and
43
+ * the resumed pod carries the same label, so the policy applies to it
44
+ * again. A host that wants the grant gone calls `setNetworkPolicy({
45
+ * allowedHosts: [] })` (which deletes it) before it suspends.
46
+ *
47
+ * A freshly created workspace gets step 2 after the create reply, with the uid
48
+ * the reply carried.
49
+ *
50
+ * ## What is NOT proven here
51
+ *
52
+ * That any of it is enforced. Enforcement is the CNI's data plane: it needs a
53
+ * cluster running Cilium, the same as the task path.
54
+ */
55
+
56
+ import type { KubernetesEgressConfig, KubernetesPerSandboxEgressConfig } from './egress-policy.js'
57
+ import { perSandboxEgressLabelKey } from './egress-policy.js'
58
+ import { KubernetesAlreadyGoneError, type KubernetesClient } from './k8s-client.js'
59
+ import { ciliumNetworkPolicyCollectionPath, ciliumNetworkPolicyPath } from './objects.js'
60
+ import { PER_SANDBOX_POLICY_NAME_PREFIX, type PerSandboxPolicyFence } from './per-sandbox-policy.js'
61
+
62
+ /** The profile variant a workspace with per-sandbox egress configured carries. */
63
+ export const KUBERNETES_PER_SANDBOX_PROFILE_VARIANT = 'core/no-network+per-sandbox'
64
+
65
+ /**
66
+ * The extra pod-label map a workspace named `sandboxName` contributes to
67
+ * `composeAdditionalPodLabels`, or `undefined` when `perSandbox` is not
68
+ * configured, which is what keeps that workspace's request log unchanged.
69
+ */
70
+ export function perSandboxWorkspaceLabels(
71
+ egress: KubernetesEgressConfig | undefined,
72
+ sandboxName: string,
73
+ ): Readonly<Record<string, string>> | undefined {
74
+ const key = perSandboxEgressLabelKey(egress)
75
+ return key === undefined ? undefined : { [key]: sandboxName }
76
+ }
77
+
78
+ /** `labels` without the per-sandbox key `extra` carries; `labels` itself when there is none. */
79
+ export function withoutPerSandboxLabel(
80
+ labels: Readonly<Record<string, string>>,
81
+ extra: Readonly<Record<string, string>> | undefined,
82
+ ): Readonly<Record<string, string>> {
83
+ if (extra === undefined) return labels
84
+ return Object.fromEntries(Object.entries(labels).filter(([key]) => !(key in extra)))
85
+ }
86
+
87
+ /** What a list of CiliumNetworkPolicies answers with. */
88
+ interface CiliumPolicyList {
89
+ readonly items?: readonly {
90
+ readonly metadata?: {
91
+ readonly name?: string
92
+ readonly ownerReferences?: readonly {
93
+ readonly kind?: string
94
+ readonly name?: string
95
+ readonly uid?: string
96
+ }[]
97
+ }
98
+ readonly spec?: {
99
+ readonly endpointSelector?: { readonly matchLabels?: Readonly<Record<string, unknown>> }
100
+ }
101
+ }[]
102
+ }
103
+
104
+ /**
105
+ * The names of the per-sandbox policies this workspace's label selects and
106
+ * its Sandbox does not own. Read only.
107
+ */
108
+ export async function findForeignPerSandboxPolicies(
109
+ client: KubernetesClient,
110
+ namespace: string,
111
+ labelKey: string,
112
+ sandboxName: string,
113
+ ownerUid: string,
114
+ signal?: AbortSignal,
115
+ ): Promise<readonly string[]> {
116
+ const list = await client.request<CiliumPolicyList>(
117
+ 'GET',
118
+ ciliumNetworkPolicyCollectionPath(namespace),
119
+ undefined,
120
+ signal,
121
+ )
122
+ const foreign: string[] = []
123
+ for (const item of list?.items ?? []) {
124
+ const name = item?.metadata?.name
125
+ if (typeof name !== 'string' || !name.startsWith(PER_SANDBOX_POLICY_NAME_PREFIX)) continue
126
+ if (item?.spec?.endpointSelector?.matchLabels?.[labelKey] !== sandboxName) continue
127
+ const owned = (item.metadata?.ownerReferences ?? []).some(
128
+ (ref) => ref?.kind === 'Sandbox' && ref.name === sandboxName && ref.uid === ownerUid,
129
+ )
130
+ if (!owned) foreign.push(name)
131
+ }
132
+ return foreign
133
+ }
134
+
135
+ /** What {@link reverifyPerSandboxEgress} needs. */
136
+ export interface PerSandboxReverification {
137
+ readonly client: KubernetesClient
138
+ readonly namespace: string
139
+ readonly sandboxName: string
140
+ /** The uid the cluster reports for the Sandbox NOW. */
141
+ readonly ownerUid: string
142
+ readonly egress: KubernetesEgressConfig & {
143
+ readonly perSandbox: KubernetesPerSandboxEgressConfig
144
+ }
145
+ /** A fence whose memo has not been used yet. */
146
+ readonly fence: PerSandboxPolicyFence
147
+ readonly signal?: AbortSignal
148
+ }
149
+
150
+ /**
151
+ * Steps 1 and 2 of the resume rule above: prove the fence, then delete the
152
+ * policies that select this workspace and that its Sandbox does not own.
153
+ * Returns the names it deleted. Throws the fence's own refusal, having
154
+ * written nothing, when the fence does not verify.
155
+ */
156
+ export async function reverifyPerSandboxEgress(
157
+ options: PerSandboxReverification,
158
+ ): Promise<readonly string[]> {
159
+ const { client, namespace, sandboxName, ownerUid, egress, fence, signal } = options
160
+ await fence.assertApplied(signal)
161
+ const labelKey = perSandboxEgressLabelKey(egress)
162
+ if (labelKey === undefined) return []
163
+ const foreign = await findForeignPerSandboxPolicies(
164
+ client,
165
+ namespace,
166
+ labelKey,
167
+ sandboxName,
168
+ ownerUid,
169
+ signal,
170
+ )
171
+ for (const name of foreign) {
172
+ try {
173
+ await client.request('DELETE', ciliumNetworkPolicyPath(namespace, name), undefined, signal)
174
+ } catch (err) {
175
+ if (!(err instanceof KubernetesAlreadyGoneError)) throw err
176
+ }
177
+ }
178
+ return foreign
179
+ }