alasio 7.0.0 → 9.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 (161) hide show
  1. package/dist/cli/src/api-server.js +18 -0
  2. package/dist/cli/src/api-server.js.map +1 -0
  3. package/dist/cli/src/branches.js +123 -46
  4. package/dist/cli/src/branches.js.map +1 -1
  5. package/dist/cli/src/cluster/docker.js +25 -10
  6. package/dist/cli/src/cluster/docker.js.map +1 -1
  7. package/dist/cli/src/cluster/host.js +33 -7
  8. package/dist/cli/src/cluster/host.js.map +1 -1
  9. package/dist/cli/src/cluster/k3s.js +51 -12
  10. package/dist/cli/src/cluster/k3s.js.map +1 -1
  11. package/dist/cli/src/cluster/node.js +115 -33
  12. package/dist/cli/src/cluster/node.js.map +1 -1
  13. package/dist/cli/src/cluster/stargz.js +55 -0
  14. package/dist/cli/src/cluster/stargz.js.map +1 -0
  15. package/dist/cli/src/commands/branch.js +5 -4
  16. package/dist/cli/src/commands/branch.js.map +1 -1
  17. package/dist/cli/src/commands/common.js +9 -3
  18. package/dist/cli/src/commands/common.js.map +1 -1
  19. package/dist/cli/src/commands/folder.js +14 -0
  20. package/dist/cli/src/commands/folder.js.map +1 -0
  21. package/dist/cli/src/commands/init.js +54 -4
  22. package/dist/cli/src/commands/init.js.map +1 -1
  23. package/dist/cli/src/commands/lake.js +40 -6
  24. package/dist/cli/src/commands/lake.js.map +1 -1
  25. package/dist/cli/src/commands/restore-filer-store.js +40 -8
  26. package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
  27. package/dist/cli/src/commands/status.js +216 -20
  28. package/dist/cli/src/commands/status.js.map +1 -1
  29. package/dist/cli/src/commands/uninstall.js +8 -9
  30. package/dist/cli/src/commands/uninstall.js.map +1 -1
  31. package/dist/cli/src/commands/up.js +11 -3
  32. package/dist/cli/src/commands/up.js.map +1 -1
  33. package/dist/cli/src/commands/upgrade.js +1 -1
  34. package/dist/cli/src/commands/upgrade.js.map +1 -1
  35. package/dist/cli/src/commands.js +2 -1
  36. package/dist/cli/src/commands.js.map +1 -1
  37. package/dist/cli/src/config.js +24 -9
  38. package/dist/cli/src/config.js.map +1 -1
  39. package/dist/cli/src/cpu-baseline.js +8 -4
  40. package/dist/cli/src/cpu-baseline.js.map +1 -1
  41. package/dist/cli/src/folders.js +67 -0
  42. package/dist/cli/src/folders.js.map +1 -0
  43. package/dist/cli/src/gateway-calls.js +58 -0
  44. package/dist/cli/src/gateway-calls.js.map +1 -0
  45. package/dist/cli/src/install.js +119 -17
  46. package/dist/cli/src/install.js.map +1 -1
  47. package/dist/cli/src/kept-releases.js +27 -20
  48. package/dist/cli/src/kept-releases.js.map +1 -1
  49. package/dist/cli/src/kube/api.js +11 -5
  50. package/dist/cli/src/kube/api.js.map +1 -1
  51. package/dist/cli/src/kube/apply.js +16 -73
  52. package/dist/cli/src/kube/apply.js.map +1 -1
  53. package/dist/cli/src/kube/rollout.js +84 -26
  54. package/dist/cli/src/kube/rollout.js.map +1 -1
  55. package/dist/cli/src/lake.js +124 -0
  56. package/dist/cli/src/lake.js.map +1 -0
  57. package/dist/cli/src/manifests/admission.js +381 -223
  58. package/dist/cli/src/manifests/admission.js.map +1 -1
  59. package/dist/cli/src/manifests/alasio.js +79 -362
  60. package/dist/cli/src/manifests/alasio.js.map +1 -1
  61. package/dist/cli/src/manifests/branch.js +10 -26
  62. package/dist/cli/src/manifests/branch.js.map +1 -1
  63. package/dist/cli/src/manifests/collector.js +250 -135
  64. package/dist/cli/src/manifests/collector.js.map +1 -1
  65. package/dist/cli/src/manifests/common.js +32 -2
  66. package/dist/cli/src/manifests/common.js.map +1 -1
  67. package/dist/cli/src/manifests/config.js +256 -92
  68. package/dist/cli/src/manifests/config.js.map +1 -1
  69. package/dist/cli/src/manifests/disruption.js +10 -0
  70. package/dist/cli/src/manifests/disruption.js.map +1 -1
  71. package/dist/cli/src/manifests/folders.js +128 -0
  72. package/dist/cli/src/manifests/folders.js.map +1 -0
  73. package/dist/cli/src/manifests/grafana.js +4 -2
  74. package/dist/cli/src/manifests/grafana.js.map +1 -1
  75. package/dist/cli/src/manifests/index.js +15 -8
  76. package/dist/cli/src/manifests/index.js.map +1 -1
  77. package/dist/cli/src/manifests/keda.js +2 -1
  78. package/dist/cli/src/manifests/keda.js.map +1 -1
  79. package/dist/cli/src/manifests/lake.js +350 -73
  80. package/dist/cli/src/manifests/lake.js.map +1 -1
  81. package/dist/cli/src/manifests/log.js +316 -0
  82. package/dist/cli/src/manifests/log.js.map +1 -0
  83. package/dist/cli/src/manifests/neon.js +128 -49
  84. package/dist/cli/src/manifests/neon.js.map +1 -1
  85. package/dist/cli/src/manifests/network-policies.js +263 -100
  86. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  87. package/dist/cli/src/manifests/node-agent.js +24 -8
  88. package/dist/cli/src/manifests/node-agent.js.map +1 -1
  89. package/dist/cli/src/manifests/object-store.js +4 -2
  90. package/dist/cli/src/manifests/object-store.js.map +1 -1
  91. package/dist/cli/src/manifests/sandbox-gateway.js +663 -0
  92. package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
  93. package/dist/cli/src/manifests/scalers.js +16 -5
  94. package/dist/cli/src/manifests/scalers.js.map +1 -1
  95. package/dist/cli/src/manifests/sizing.js +16 -8
  96. package/dist/cli/src/manifests/sizing.js.map +1 -1
  97. package/dist/cli/src/manifests/valkey.js +11 -5
  98. package/dist/cli/src/manifests/valkey.js.map +1 -1
  99. package/dist/cli/src/manifests/vpa.js +3 -1
  100. package/dist/cli/src/manifests/vpa.js.map +1 -1
  101. package/dist/cli/src/manifests/webhook.js +102 -0
  102. package/dist/cli/src/manifests/webhook.js.map +1 -0
  103. package/dist/cli/src/manifests/workspace-storage.js +174 -89
  104. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  105. package/dist/cli/src/neon-storage.js +155 -0
  106. package/dist/cli/src/neon-storage.js.map +1 -0
  107. package/dist/cli/src/release.js +9 -6
  108. package/dist/cli/src/release.js.map +1 -1
  109. package/dist/cli/src/secrets.js +45 -4
  110. package/dist/cli/src/secrets.js.map +1 -1
  111. package/dist/cli/src/target.js +1 -1
  112. package/dist/cli/src/target.js.map +1 -1
  113. package/dist/cli/src/telegram.js +56 -10
  114. package/dist/cli/src/telegram.js.map +1 -1
  115. package/dist/cli/src/webhook.js +182 -0
  116. package/dist/cli/src/webhook.js.map +1 -0
  117. package/dist/cluster/node/pins.json +12 -2
  118. package/dist/neon/control/branches.js +126 -21
  119. package/dist/neon/control/branches.js.map +1 -1
  120. package/dist/neon/control/endpoints.js +9 -0
  121. package/dist/neon/control/endpoints.js.map +1 -1
  122. package/dist/neon/control/jwt.js +16 -3
  123. package/dist/neon/control/jwt.js.map +1 -1
  124. package/dist/neon/control/secrets.js +51 -7
  125. package/dist/neon/control/secrets.js.map +1 -1
  126. package/dist/src/binder/names.js +33 -0
  127. package/dist/src/binder/names.js.map +1 -0
  128. package/dist/src/binder/tokens.js +58 -0
  129. package/dist/src/binder/tokens.js.map +1 -0
  130. package/dist/src/branch/names.js +12 -2
  131. package/dist/src/branch/names.js.map +1 -1
  132. package/dist/src/events/names.js +7 -0
  133. package/dist/src/events/names.js.map +1 -0
  134. package/dist/src/gateway/names.js +201 -0
  135. package/dist/src/gateway/names.js.map +1 -0
  136. package/dist/src/harness/names.js +49 -0
  137. package/dist/src/harness/names.js.map +1 -0
  138. package/dist/src/kube/sandbox-shape.js +19 -69
  139. package/dist/src/kube/sandbox-shape.js.map +1 -1
  140. package/dist/src/node-agent/names.js +118 -2
  141. package/dist/src/node-agent/names.js.map +1 -1
  142. package/dist/src/sandbox/names.js +12 -20
  143. package/dist/src/sandbox/names.js.map +1 -1
  144. package/dist/src/sandbox/otlp-resource.js +173 -0
  145. package/dist/src/sandbox/otlp-resource.js.map +1 -0
  146. package/dist/src/sandbox/telemetry.js +43 -0
  147. package/dist/src/sandbox/telemetry.js.map +1 -0
  148. package/dist/src/telegram/deliveries.js +40 -0
  149. package/dist/src/telegram/deliveries.js.map +1 -0
  150. package/dist/src/telemetry/config.js +131 -0
  151. package/dist/src/telemetry/config.js.map +1 -0
  152. package/dist/src/telemetry/forward.js +154 -0
  153. package/dist/src/telemetry/forward.js.map +1 -0
  154. package/package.json +1 -1
  155. package/dist/cli/src/manifests/agent-sandbox.js +0 -92
  156. package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
  157. package/dist/cli/src/manifests/juicefs-csi.js +0 -319
  158. package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
  159. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
  160. package/dist/src/kube/config.js +0 -143
  161. package/dist/src/kube/config.js.map +0 -1
@@ -1,263 +1,37 @@
1
- /**
2
- * alasio itself: its Deployment, its telemetry receiver's Service, the
3
- * namespaces its workspaces run in, its identity in them, and the templates it makes
4
- * its workspaces' Sandboxes from (src/kube/config.ts); main's, or a branch environment's
5
- * in its own namespaces (./branch.ts), and main's Service of the forks it makes for them.
6
- */
7
- import { isDeepStrictEqual } from "node:util";
8
- import { Result } from "effect";
9
- import { BRANCH_FORK_PORT, branchSessionsNamespace } from "../../../src/branch/names.js";
10
- import { decodeKubeTemplates } from "../../../src/kube/config.js";
11
- import { CPU_FEATURES_ANNOTATION } from "../../../src/kube/sandbox-shape.js";
12
- import { gvisorRuntimeClass, NODE_AGENT_PORT, NODE_AGENT_TOKEN_VARIABLE } from "../../../src/node-agent/names.js";
1
+ import { BRANCH_FORK_PORT, BRANCH_PARENT_LABEL, branchSessionsNamespace, MAIN_ENVIRONMENT_NAME } from "../../../src/branch/names.js";
2
+ import { ALASIO_GATEWAY_CALLER_KEY_VARIABLE, ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, ALASIO_GATEWAY_FOLDERS_VARIABLE, ALASIO_GATEWAY_URL_VARIABLE } from "../../../src/gateway/names.js";
13
3
  import { gvisorReleases, NODE_PINS } from "../cluster/node.js";
14
4
  import { imageReference } from "../images.js";
15
- import { alasioAccount, busyLabelPolicy, cloneJobPolicy, resumedConditionPolicy, sandboxPolicy, sandboxPolicyName, secretPolicy } from "./admission.js";
16
- import { BRANCH_FORK_SECRET, BRANCH_LABEL, componentName, databaseSecret, envOf, given, goJson, helperResources, hostVolumeMounts, hostVolumes, imagePullSecrets, labels, MAIN_ENVIRONMENT, NAMESPACE, nodeAgentRuns, ONE, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, roleVerifiers, selectorLabels, sha256, } from "./common.js";
5
+ import { busyLabelPolicy } from "./admission.js";
6
+ import { foldersRun } from "./folders.js";
7
+ import { carriedPools, GATEWAY, GATEWAY_PORT, sandboxServiceRuns } from "./sandbox-gateway.js";
8
+ import { alasioGroup, environmentLogSecret, environmentName, lakeWakeTopic, LOG_BROKERS, telegramTopic } from "./log.js";
9
+ import { BRANCH_FORK_SECRET, BRANCH_LABEL, componentName, databaseSecret, envOf, given, hostVolumeMounts, hostVolumes, imagePullSecrets, labels, lakeRuns, MAIN_ENVIRONMENT, NAMESPACE, ONE, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, roleVerifiers, selectorLabels, SESSIONS_CALLER_SECRET, } from "./common.js";
17
10
  import { durationSeconds } from "./config.js";
18
11
  import { busyBudget } from "./disruption.js";
19
12
  import { priority } from "./priorities.js";
20
13
  import { verticalPodAutoscaler } from "./sizing.js";
21
- import { cloneJob } from "./workspace-storage.js";
22
- /** The port every workspace's bayma serves MCP on. */
23
- const BAYMA_PORT = 7290;
24
14
  /** The namespace of the sessions of the alasio of `environment`: the configured one, or a branch environment's own. */
25
15
  export function sessionsNamespace(config, { branch }) {
26
16
  return branch ? branchSessionsNamespace(branch) : config.sessions.namespace;
27
17
  }
28
- /** The namespaces whose Sandboxes alasio drives: the sessions', and the host profile's. */
29
- function workspaceNamespaces(config, environment) {
30
- return [...(config.sessions.enabled ? [sessionsNamespace(config, environment)] : []), ...(config.host.enabled ? [config.host.namespace] : [])];
31
- }
32
- /**
33
- * The template of session filesystems' Sandboxes: an empty, isolated workspace under
34
- * the sessions' runtime, restricted, on a volume of workspace storage's class when it is
35
- * on. alasio adds what is per Sandbox: its name and labels, bayma's token, and its
36
- * network mode. A branch environment's are in its own namespace, and neither clones a
37
- * session (its parent does, src/branch/fork.ts) nor removes JuiceFS's mount pods, which
38
- * are main's to drive.
39
- */
40
- function sessionsProfile(config, environment, { cpuBaseline, gvisorReleases }) {
41
- const { sessions, workspaceStorage } = config;
42
- const main = environment.branch === null;
43
- // Snapshotted once every node's CPU features are known, its sandboxes held to those all have.
44
- const snapshotted = main && nodeAgentRuns(config) && cpuBaseline !== null;
45
- const agent = imageReference(config.images.agent);
46
- const storageClassName = sessions.storage.storageClassName || (workspaceStorage.enabled ? workspaceStorage.storageClassName : "");
47
- // On workspace storage, the volume's mounts follow its mount pod's, as upstream's
48
- // template has them (gVisor does not, and alasio restarts a session whose mount is
49
- // gone), and its files are given the pod's group only when its root is not the group's,
50
- // as walking a JuiceFS volume is slow.
51
- const propagation = workspaceStorage.enabled ? { mountPropagation: "HostToContainer" } : {};
52
- const ownership = workspaceStorage.enabled ? { fsGroupChangePolicy: "OnRootMismatch" } : {};
53
- return {
54
- namespace: sessionsNamespace(config, environment),
55
- port: BAYMA_PORT,
56
- workspaceDir: "/workspace",
57
- egressGate: sessions.egressGate,
58
- fullModeNameservers: [...sessions.fullModeNameservers],
59
- ...(main && mountPodsRemoved(config) ? { mountPodNamespace: workspaceStorage.csi.namespace } : {}),
60
- podTemplate: {
61
- metadata: {
62
- labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "session" },
63
- // Under gVisor, /tmp is a tmpfs of the sandbox's own, by gVisor's mount hints, rather
64
- // than the emptyDir's directory on the node: a snapshot of the sandbox holds it, as it
65
- // holds the files that bayma's runtimes keep open there.
66
- annotations: {
67
- "dev.gvisor.spec.mount.tmp.share": "container",
68
- "dev.gvisor.spec.mount.tmp.type": "tmpfs",
69
- "dev.gvisor.spec.mount.tmp.options": "rw",
70
- ...(snapshotted ? { [CPU_FEATURES_ANNOTATION]: cpuBaseline.join(",") } : {}),
71
- },
72
- },
73
- spec: {
74
- ...(sessions.runtimeClassName ? { runtimeClassName: sessions.runtimeClassName } : {}),
75
- ...priority("turn"),
76
- // A pod that ends is ended: alasio brings the session up in a new one, and nothing
77
- // starts bayma again in a sandbox that a snapshot has just ended.
78
- restartPolicy: "Never",
79
- securityContext: { ...restrictedPod(1000, 1000), ...ownership },
80
- ...imagePullSecrets(config),
81
- ...given("nodeSelector", { ...sessions.nodeSelector }),
82
- ...given("tolerations", [...sessions.tolerations]),
83
- initContainers: [
84
- // The workspace and home on the session's volume, and bayma's toolbelt, which the
85
- // image installs, linked into the home.
86
- {
87
- name: "prepare",
88
- image: agent,
89
- imagePullPolicy: config.images.pullPolicy,
90
- command: [
91
- "/bin/sh",
92
- "-c",
93
- "mkdir -p /data/workspace /data/home/.local/share/bayma && ln -sfn /opt/bayma-data/bayma/toolbelt /data/home/.local/share/bayma/toolbelt",
94
- ],
95
- securityContext: restrictedContainer(),
96
- ...helperResources(),
97
- volumeMounts: [{ name: "data", mountPath: "/data" }],
98
- },
99
- ],
100
- containers: [{
101
- name: "bayma",
102
- image: agent,
103
- imagePullPolicy: config.images.pullPolicy,
104
- command: ["/usr/bin/tini", "-s", "--", "node", "/opt/bayma/bayma.js"],
105
- args: ["mcp-http", "--host", "0.0.0.0", "--port", String(BAYMA_PORT), "--state-dir", "/home/agent/.bayma"],
106
- workingDir: "/workspace",
107
- env: [
108
- { name: "HOME", value: "/home/agent" },
109
- { name: "USER", value: "agent" },
110
- { name: "BAYMA_PAYLOAD_DIR", value: "/opt/bayma/payload" },
111
- ],
112
- ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
113
- securityContext: restrictedContainer(),
114
- resources: sessions.resources,
115
- volumeMounts: [
116
- { name: "data", mountPath: "/workspace", subPath: "workspace", ...propagation },
117
- { name: "data", mountPath: "/home/agent", subPath: "home", ...propagation },
118
- { name: "tmp", mountPath: "/tmp" },
119
- ],
120
- }],
121
- volumes: [{ name: "tmp", emptyDir: { sizeLimit: "2Gi" } }],
122
- },
123
- },
124
- volumeClaimTemplates: [{
125
- metadata: { name: "data" },
126
- spec: {
127
- accessModes: ["ReadWriteOnce"],
128
- ...(storageClassName ? { storageClassName } : {}),
129
- resources: { requests: { storage: sessions.storage.size } },
130
- },
131
- }],
132
- ...(main && sessionsForked(config) ? { clone: { claimTemplate: "data", job: cloneJob(config) } } : {}),
133
- ...(snapshotted ? { snapshots: sessionSnapshots(config, gvisorReleases) } : {}),
134
- };
135
- }
136
- /**
137
- * How session sandboxes are snapshotted as they are released (src/sandbox/snapshots.ts),
138
- * where the node agents run: once sessions.snapshots.releaseAfter has passed without an
139
- * exec or a turn, under the gVisor `releases` alasio keeps (../kept-releases.ts), the
140
- * current one first, each restored with a RuntimeClass of its own, by the agents in
141
- * alasio's namespace.
142
- */
143
- function sessionSnapshots(config, releases) {
144
- return {
145
- // A duration, as the configuration checks it.
146
- releaseAfterSeconds: durationSeconds(config.sessions.snapshots.releaseAfter) ?? 0,
147
- releases: releases.map((release) => ({ release, runtimeClassName: gvisorRuntimeClass(release) })),
148
- agents: {
149
- namespace: NAMESPACE,
150
- selector: Object.entries(selectorLabels("node-agent")).map(([label, value]) => `${label}=${value}`).join(","),
151
- port: NODE_AGENT_PORT,
152
- },
153
- };
154
- }
155
- /** Whether alasio forks sessions: where their volumes are workspace storage's, which JuiceFS clones. */
156
- export function sessionsForked({ sessions, workspaceStorage }) {
157
- return workspaceStorage.enabled && [workspaceStorage.storageClassName, ""].includes(sessions.storage.storageClassName);
18
+ /** How long a session's bayma holds a stop alasio did not make (sessions.snapshots.stopWait). */
19
+ export function stopWaitSeconds(config) {
20
+ // A duration, as the configuration checks it.
21
+ return durationSeconds(config.sessions.snapshots.stopWait) ?? 0;
158
22
  }
159
- /** The template of folder workspaces' bayma: bayma itself, as the operator, with the machine's mounts. */
160
- function hostProfile(config) {
161
- const { host } = config;
162
- return {
163
- namespace: host.namespace,
164
- port: BAYMA_PORT,
165
- stateRoot: host.stateRoot,
166
- podTemplate: {
167
- metadata: { labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "folder-bayma" } },
168
- spec: {
169
- // May restart alasio, by rolling out its Deployment, as an agent restarts it.
170
- serviceAccountName: componentName("host-agent"),
171
- ...priority("turn"),
172
- securityContext: {
173
- runAsUser: host.uid,
174
- runAsGroup: host.gid,
175
- // The group the bayma token's Secret volume is given, which the operator's user
176
- // reads it as; the node's own paths the mounts give are left as they are.
177
- fsGroup: host.gid,
178
- ...given("supplementalGroups", [...host.supplementalGroups]),
179
- },
180
- ...imagePullSecrets(config),
181
- containers: [{
182
- name: "bayma",
183
- image: imageReference(host.baymaImage),
184
- imagePullPolicy: "IfNotPresent",
185
- args: ["mcp-http", "--host", "0.0.0.0", "--port", String(BAYMA_PORT), "--default-durability", "checkpointed"],
186
- workingDir: host.home,
187
- env: [{ name: "HOME", value: host.home }, ...envOf(host.env)],
188
- ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
189
- securityContext: {
190
- // What bayma needs to snapshot its REPL sessions' processes as it stops and
191
- // restore them whole as it starts again.
192
- capabilities: { add: ["CHECKPOINT_RESTORE", "SYS_PTRACE"] },
193
- seccompProfile: { type: "Unconfined" },
194
- appArmorProfile: { type: "Unconfined" },
195
- },
196
- resources: host.resources,
197
- ...given("volumeMounts", hostVolumeMounts(config)),
198
- }],
199
- ...given("volumes", hostVolumes(config)),
200
- },
201
- },
202
- };
203
- }
204
- /** What a cluster keeps where up has found nothing of it yet: no baseline, and the releases pinned. */
205
- export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS) };
23
+ /** What a cluster keeps where up has found nothing of it yet: no baseline, the releases pinned, no bot, no API server known, and none of the mount's Services. */
24
+ export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS), botChecksum: null, apiServer: null, mountServices: {} };
206
25
  /** Nothing carried forward: a first installation's, and a branch environment's. */
207
- export const NOTHING_CARRIED = { previousSessions: null, ...NOTHING_KEPT };
208
- /** The ConfigMap of the templates, by its name. */
209
- export const SANDBOX_TEMPLATES = componentName("sandbox-templates");
26
+ export const NOTHING_CARRIED = { previousPools: { none: null, full: null }, ...NOTHING_KEPT };
210
27
  /**
211
28
  * What the installation `config` describes, with what the cluster keeps, `kept`, carries
212
- * forward from `live`, the templates of the installation it replaces (its ConfigMap's
213
- * `templates.json`, if it has one): the sessions' profile it ran, where their template
214
- * changes, or else the one it carried. A profile is carried for one change of the
215
- * template, and no more. Pure, for tests.
216
- *
217
- * What it carries is pinned whole into the Sandbox policy (./admission.ts) as a template
218
- * alasio may make a Sandbox from, read back from the cluster rather than rendered from the
219
- * config: the ConfigMap is the anchor of that trust. It is one as long as only the
220
- * operator, by `alasio up`, writes it: alasio's ServiceAccount may write no ConfigMap
221
- * (./alasio.ts identity). Were alasio given that, it could write any pod template into
222
- * the ConfigMap, and the next `up` would let it make Sandboxes of it.
29
+ * forward from `liveGateway`, the sandbox gateway's configuration of the installation it
30
+ * replaces (its ConfigMap's `config.json`, if it has one): its pools' templates before the
31
+ * current ones (./sandbox-gateway.ts carriedPools). Pure, for tests.
223
32
  */
224
- export function carriedFrom(config, live, kept) {
225
- const nothing = { ...NOTHING_CARRIED, ...kept };
226
- if (!config.sessions.enabled || live === undefined)
227
- return nothing;
228
- // Carried as the ConfigMap held them, which alasio read as they are: the profile a snapshot recorded the hash of.
229
- if (Result.isFailure(decodeKubeTemplates(SANDBOX_TEMPLATES, live)))
230
- return nothing;
231
- const held = JSON.parse(live);
232
- if (!held.sessions)
233
- return nothing;
234
- // The current profile as the ConfigMap holds it, to compare with what it held.
235
- const current = JSON.parse(goJson(sessionsProfile(config, MAIN_ENVIRONMENT, kept), ""));
236
- return { previousSessions: isDeepStrictEqual(held.sessions.podTemplate, current.podTemplate) ? (held.previousSessions ?? null) : held.sessions, ...kept };
237
- }
238
- /**
239
- * The CPU baseline the installation whose templates are `live` (as carriedFrom takes
240
- * them) holds sessions to: what its sessions' template sets, if anything.
241
- */
242
- export function recordedCpuBaseline(live) {
243
- if (live === undefined || Result.isFailure(decodeKubeTemplates(SANDBOX_TEMPLATES, live)))
244
- return null;
245
- const held = JSON.parse(live);
246
- const recorded = held.sessions?.podTemplate.metadata?.annotations?.[CPU_FEATURES_ANNOTATION];
247
- return recorded ? recorded.split(",") : null;
248
- }
249
- /** The ConfigMap of the templates, and their checksum, which alasio's pod carries so it is replaced when they change. */
250
- function sandboxTemplates(config, environment, carried) {
251
- const name = SANDBOX_TEMPLATES;
252
- const templates = goJson({
253
- ...(config.sessions.enabled ? { sessions: sessionsProfile(config, environment, carried) } : {}),
254
- ...(config.sessions.enabled && carried.previousSessions ? { previousSessions: carried.previousSessions } : {}),
255
- ...(config.host.enabled ? { host: hostProfile(config) } : {}),
256
- }, " ");
257
- return {
258
- configMap: { apiVersion: "v1", kind: "ConfigMap", metadata: { name, namespace: environment.namespace, labels: labels("alasio") }, data: { "templates.json": templates } },
259
- checksum: sha256(templates),
260
- };
33
+ export function carriedFrom(config, kept, liveGateway) {
34
+ return { ...kept, previousPools: carriedPools(config, liveGateway, kept) };
261
35
  }
262
36
  /** Where alasio's Secrets of branch environments are mounted: main's key, or a branch's token. */
263
37
  const BRANCH_SECRETS = "/run/alasio/branch";
@@ -283,9 +57,9 @@ function branchSecretVolume(config, { branch }) {
283
57
  return [{ name: "branch", secret: { secretName: BRANCH_FORK_SECRET, items: [{ key: "token", path: "token" }] } }];
284
58
  return forksForBranches(config) ? [{ name: "branch", secret: { secretName: componentName("branches"), items: [{ key: "fork-key", path: "fork-key" }], optional: true } }] : [];
285
59
  }
286
- /** Whether main forks its sessions for branch environments: where it has Neon to branch, and forks sessions. */
60
+ /** Whether main forks its sessions for branch environments: where it has Neon to branch, and the sandbox service, whose node agents clone them. */
287
61
  export function forksForBranches(config) {
288
- return config.neon.enabled && sessionsForked(config);
62
+ return config.neon.enabled && sandboxServiceRuns(config);
289
63
  }
290
64
  /**
291
65
  * The roles main's alasio makes for what reads its Neon beside it (common.ts
@@ -299,7 +73,6 @@ function readerRoles(config, { branch }) {
299
73
  function alasioEnv(config, home, operatorHome, environment) {
300
74
  const { alasio, host } = config;
301
75
  return [
302
- { name: "ALASIO_KUBE_TEMPLATES", value: "/etc/alasio/templates.json" },
303
76
  { name: "ALASIO_STATE_DIR", value: "/var/lib/alasio/state" },
304
77
  { name: "HOME", value: home },
305
78
  // A home of alasio's own does not outlast its pod; the operator's keeps their login.
@@ -311,35 +84,51 @@ function alasioEnv(config, home, operatorHome, environment) {
311
84
  { name: "TELEGRAM_ALLOWED_USER_IDS", valueFrom: { secretKeyRef: { name: alasio.telegram.existingSecret, key: "allowedUserIds" } } },
312
85
  { name: "ALASIO_DATABASE_URL_FILE", value: "/run/alasio/database/url" },
313
86
  { name: "ALASIO_LAKE_VERIFIER_FILE", value: "/run/alasio/database/lake-verifier" },
87
+ { name: "ALASIO_HARNESS_VERIFIER_FILE", value: "/run/alasio/database/harness-verifier" },
314
88
  ...readerRoles(config, environment).map(({ key, variable }) => ({ name: variable, value: `/run/alasio/database/${key}` })),
315
89
  { name: "ALASIO_LAKE_ENABLED", value: config.lake.enabled && config.neon.enabled ? "1" : "0" },
316
- { name: "ALASIO_TELEMETRY_RECEIVER_SERVICE", value: `${componentName("telemetry")}.${environment.namespace}.svc` },
317
- { name: "ALASIO_TELEMETRY_RECEIVER_PORT", value: String(config.telemetry.receiverPort) },
90
+ // Its Telegram updates: its environment's topic, read in its group as its principal.
91
+ { name: "ALASIO_LOG_BROKERS", value: LOG_BROKERS },
92
+ { name: "ALASIO_LOG_USERNAME", valueFrom: { secretKeyRef: { name: environmentLogSecret(environment), key: "username" } } },
93
+ { name: "ALASIO_LOG_PASSWORD_FILE", value: "/run/alasio/log/password" },
94
+ { name: "ALASIO_TELEGRAM_TOPIC", value: telegramTopic(environmentName(environment)) },
95
+ { name: "ALASIO_TELEGRAM_GROUP", value: alasioGroup(environmentName(environment)) },
96
+ // Its environment's wake topic, which it tells the lake's loader of each transcript it writes on, as that principal.
97
+ ...(lakeRuns(config) ? [{ name: "ALASIO_LAKE_WAKE_TOPIC", value: lakeWakeTopic(environmentName(environment)) }] : []),
318
98
  ...(alasio.defaultHarness ? [{ name: "ALASIO_DEFAULT_HARNESS", value: alasio.defaultHarness }] : []),
319
99
  ...(host.enabled && host.workspaceRoot ? [{ name: "ALASIO_WORKSPACE_ROOT", value: host.workspaceRoot }] : []),
320
100
  ...(alasio.claude.existingSecret
321
101
  ? [{ name: "CLAUDE_CODE_OAUTH_TOKEN", valueFrom: { secretKeyRef: { name: alasio.claude.existingSecret, key: alasio.claude.key } } }]
322
102
  : []),
323
- // What it presents to the node agents, which snapshot its sessions.
324
- ...(environment.branch === null && nodeAgentRuns(config)
325
- ? [{ name: NODE_AGENT_TOKEN_VARIABLE, valueFrom: { secretKeyRef: { name: componentName("node-agent"), key: "token" } } }]
103
+ // Its environment's sessions, through the sandbox gateway, by its environment's own key of the gateway's.
104
+ ...(sandboxServiceRuns(config)
105
+ ? [
106
+ { name: ALASIO_GATEWAY_URL_VARIABLE, value: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}` },
107
+ { name: ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, value: environmentName(environment) },
108
+ { name: ALASIO_GATEWAY_CALLER_KEY_VARIABLE, valueFrom: { secretKeyRef: { name: SESSIONS_CALLER_SECRET, key: "caller-key" } } },
109
+ ]
326
110
  : []),
111
+ // Main's folder workspaces, the gateway's folder instances, where the host profile renders them.
112
+ ...(environment.branch === null && foldersRun(config) ? [{ name: ALASIO_GATEWAY_FOLDERS_VARIABLE, value: "1" }] : []),
327
113
  ...branchEnv(config, environment),
328
114
  ...otelEnv(config, environment),
329
115
  ...envOf(alasio.env),
330
116
  ];
331
117
  }
332
118
  /**
333
- * alasio's Deployment: one replica, as Telegram's updates have one poller, so an update
334
- * stops the old pod before the new one starts. It keeps no volume: its state is in Neon,
119
+ * alasio's Deployment: one replica, the one member of its group on its topic's one
120
+ * partition, which its turns run in, so an update stops the old pod before the new one
121
+ * starts. It keeps no volume: its state is in Neon,
335
122
  * and what it writes to disk (received files written for the agent, its home unless it
336
123
  * is the operator's) is written again as needed, on emptyDirs. Under the
337
124
  * host profile it runs as the operator, in their home, with the machine's mounts. A
338
125
  * branch environment's runs in its namespace on the branch's database, told it is the
339
126
  * branch, with its token to ask main to fork the sessions it inherited; main's is given
340
- * the key branch environments' tokens are signed with, where it forks sessions.
127
+ * the key branch environments' tokens are signed with, where it forks sessions. Its pod
128
+ * carries, where up read it, the checksum of its bot's Secret, so it is replaced when it
129
+ * changes.
341
130
  */
342
- function deployment(config, templatesChecksum, environment) {
131
+ function deployment(config, botChecksum, environment) {
343
132
  const { alasio, host } = config;
344
133
  const operatorHome = host.enabled && host.alasioHome;
345
134
  const home = operatorHome ? host.home : "/var/lib/alasio/home";
@@ -357,7 +146,10 @@ function deployment(config, templatesChecksum, environment) {
357
146
  strategy: { type: "Recreate" },
358
147
  selector: { matchLabels: selectorLabels("alasio") },
359
148
  template: {
360
- metadata: { labels: labels("alasio"), annotations: { "checksum/sandbox-templates": templatesChecksum } },
149
+ metadata: {
150
+ labels: labels("alasio"),
151
+ ...(botChecksum === null ? {} : { annotations: { "checksum/telegram": botChecksum } }),
152
+ },
361
153
  spec: {
362
154
  serviceAccountName: RELEASE,
363
155
  ...priority("turn"),
@@ -372,17 +164,14 @@ function deployment(config, templatesChecksum, environment) {
372
164
  workingDir: home,
373
165
  env: alasioEnv(config, home, operatorHome, environment),
374
166
  ...given("envFrom", [...alasio.envFrom]),
375
- ports: [
376
- { name: "telemetry", containerPort: config.telemetry.receiverPort },
377
- ...(environment.branch === null && forksForBranches(config) ? [{ name: "branch-forks", containerPort: BRANCH_FORK_PORT }] : []),
378
- ],
167
+ ...(environment.branch === null && forksForBranches(config) ? { ports: [{ name: "branch-forks", containerPort: BRANCH_FORK_PORT }] } : {}),
379
168
  securityContext: restrictedContainer(),
380
169
  resources: alasio.resources,
381
170
  volumeMounts: [
382
171
  { name: "state", mountPath: "/var/lib/alasio/state" },
383
172
  ...(operatorHome ? [] : [{ name: "alasio-home", mountPath: home }]),
384
- { name: "templates", mountPath: "/etc/alasio", readOnly: true },
385
173
  { name: "database", mountPath: "/run/alasio/database", readOnly: true },
174
+ { name: "log", mountPath: "/run/alasio/log", readOnly: true },
386
175
  ...branchSecret.map(({ name }) => ({ name, mountPath: BRANCH_SECRETS, readOnly: true })),
387
176
  ...(host.enabled ? hostVolumeMounts(config) : []),
388
177
  ],
@@ -390,14 +179,14 @@ function deployment(config, templatesChecksum, environment) {
390
179
  volumes: [
391
180
  { name: "state", emptyDir: {} },
392
181
  ...(operatorHome ? [] : [{ name: "alasio-home", emptyDir: {} }]),
393
- { name: "templates", configMap: { name: componentName("sandbox-templates") } },
394
182
  {
395
183
  name: "database",
396
184
  secret: {
397
185
  secretName: databaseSecret(config),
398
- items: ["url", "lake-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
186
+ items: ["url", "lake-verifier", "harness-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
399
187
  },
400
188
  },
189
+ { name: "log", secret: { secretName: environmentLogSecret(environment), items: [{ key: "password", path: "password" }] } },
401
190
  ...branchSecret,
402
191
  ...(host.enabled ? hostVolumes(config) : []),
403
192
  ],
@@ -409,15 +198,6 @@ function deployment(config, templatesChecksum, environment) {
409
198
  },
410
199
  };
411
200
  }
412
- /** Where session sandboxes export their telemetry: alasio's receiver. */
413
- function telemetryService(config, environment) {
414
- return {
415
- apiVersion: "v1",
416
- kind: "Service",
417
- metadata: { name: componentName("telemetry"), namespace: environment.namespace, labels: labels("alasio") },
418
- spec: { selector: selectorLabels("alasio"), ports: [{ name: "otlp-http", port: config.telemetry.receiverPort, targetPort: "telemetry" }] },
419
- };
420
- }
421
201
  /** Where branch environments ask main to fork the sessions they inherited (src/branch/fork.ts). */
422
202
  function branchForksService() {
423
203
  return {
@@ -430,8 +210,13 @@ function branchForksService() {
430
210
  /**
431
211
  * The namespaces workspaces run in, which alasio makes unless told not to. Sessions'
432
212
  * enforces Pod Security "restricted": nothing privileged, no host paths, no added
433
- * capabilities. The host profile's is "privileged", as its pods mount the machine. A
434
- * branch environment always has its own, beside its own namespace.
213
+ * capabilities; but where the sandbox service runs, whose pools' pods mount their own
214
+ * directories on their nodes, a host path no level but "privileged" allows, it enforces that
215
+ * and warns and audits of "restricted", the sandbox pods' policy holding every pod of it to the
216
+ * pools' templates instead (./admission.ts sandboxPodPolicy). The host profile's is
217
+ * "privileged", as its pods mount the machine. A branch environment always has its own,
218
+ * beside its own namespace, which records the environment it was branched from, main's
219
+ * (BRANCH_PARENT_LABEL), as the sandbox gateway forks main's sessions into it for that alone.
435
220
  */
436
221
  function namespaces(config, environment) {
437
222
  const { sessions, host } = config;
@@ -448,9 +233,12 @@ function namespaces(config, environment) {
448
233
  labels: {
449
234
  ...labels("session"),
450
235
  ...branched,
451
- "pod-security.kubernetes.io/enforce": "restricted",
452
- "pod-security.kubernetes.io/enforce-version": "latest",
236
+ ...(branch ? { [BRANCH_PARENT_LABEL]: MAIN_ENVIRONMENT_NAME } : {}),
237
+ ...(sandboxServiceRuns(config)
238
+ ? { "pod-security.kubernetes.io/enforce": "privileged", "pod-security.kubernetes.io/audit": "restricted", "pod-security.kubernetes.io/audit-version": "latest" }
239
+ : { "pod-security.kubernetes.io/enforce": "restricted", "pod-security.kubernetes.io/enforce-version": "latest" }),
453
240
  "pod-security.kubernetes.io/warn": "restricted",
241
+ "pod-security.kubernetes.io/warn-version": "latest",
454
242
  },
455
243
  },
456
244
  }]
@@ -465,80 +253,22 @@ function namespaces(config, environment) {
465
253
  ];
466
254
  }
467
255
  /**
468
- * Whether alasio removes the JuiceFS mount pods of a session whose mount is lost as it
469
- * restarts it (src/sandbox/index.ts): where it installs JuiceFS's driver, whose mount pods
470
- * it then knows to be in the driver's namespace.
471
- */
472
- function mountPodsRemoved({ workspaceStorage }) {
473
- return workspaceStorage.enabled && workspaceStorage.csi.enabled;
474
- }
475
- /**
476
- * alasio's identity: it drives its workspaces' Sandboxes, their token Secrets, and reads
477
- * files from sessions through exec, in the namespaces it owns, labelling their pods busy
478
- * while turns run in them, as it does its own in its namespace (src/kube/busy.ts); where
479
- * it snapshots sessions, sets a restored session's readiness; where it forks
480
- * sessions, reads sessions' volume claims and runs the Jobs that clone them in its own;
481
- * where the node agents run, watches pods in its own, theirs among them; and, where it
482
- * installs JuiceFS's driver, lists and deletes pods in the driver's namespace, its mount
483
- * pods; and nothing else.
256
+ * alasio's identity: it labels its own pod busy while turns run in it (src/kube/busy.ts), and
257
+ * nothing else. Its sessions and its folder workspaces are the sandbox gateway's, which it
258
+ * reaches by its key alone.
484
259
  */
485
260
  function identity(config, environment) {
486
- const main = environment.branch === null;
487
- const binding = (metadata) => ({
261
+ const inOwnNamespace = { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") };
262
+ const own = { apiVersion: "rbac.authorization.k8s.io/v1", kind: "Role", metadata: inOwnNamespace, rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["list", "patch"] }] };
263
+ const binding = {
488
264
  apiVersion: "rbac.authorization.k8s.io/v1",
489
265
  kind: "RoleBinding",
490
- metadata,
266
+ metadata: inOwnNamespace,
491
267
  subjects: [{ kind: "ServiceAccount", name: RELEASE, namespace: environment.namespace }],
492
268
  roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: RELEASE },
493
- });
494
- const inDriverNamespace = { name: RELEASE, namespace: config.workspaceStorage.csi.namespace, labels: labels("alasio") };
495
- const mountPods = {
496
- apiVersion: "rbac.authorization.k8s.io/v1",
497
- kind: "Role",
498
- metadata: inDriverNamespace,
499
- rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["list", "delete"] }],
500
- };
501
- const inOwnNamespace = { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") };
502
- const own = {
503
- apiVersion: "rbac.authorization.k8s.io/v1",
504
- kind: "Role",
505
- metadata: inOwnNamespace,
506
- rules: [
507
- // Its own pod's busy label; and each node's agent, found by its pod on the node.
508
- { apiGroups: [""], resources: ["pods"], verbs: main && nodeAgentRuns(config) ? ["list", "watch", "patch"] : ["list", "patch"] },
509
- ...(main && sessionsForked(config) ? [{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["create", "get", "delete"] }] : []),
510
- ],
511
269
  };
512
270
  const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") } };
513
- return [
514
- serviceAccount,
515
- ...workspaceNamespaces(config, environment).flatMap((namespace) => {
516
- const metadata = { name: RELEASE, namespace, labels: labels("alasio") };
517
- const forked = main && sessionsForked(config) && namespace === config.sessions.namespace;
518
- const snapshotted = main && nodeAgentRuns(config) && namespace === config.sessions.namespace;
519
- const role = {
520
- apiVersion: "rbac.authorization.k8s.io/v1",
521
- kind: "Role",
522
- metadata,
523
- rules: [
524
- { apiGroups: ["agents.x-k8s.io"], resources: ["sandboxes"], verbs: ["get", "list", "watch", "create", "patch", "delete"] },
525
- { apiGroups: [""], resources: ["secrets"], verbs: ["get", "create"] },
526
- { apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "patch"] },
527
- { apiGroups: [""], resources: ["pods/exec"], verbs: ["create", "get"] },
528
- ...(forked ? [{ apiGroups: [""], resources: ["persistentvolumeclaims"], verbs: ["get"] }] : []),
529
- // A restored session's readiness, once its sandbox is let run, and the network policies that must hold it first (src/sandbox/snapshots.ts).
530
- ...(snapshotted
531
- ? [{ apiGroups: [""], resources: ["pods/status"], verbs: ["patch"] }, { apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["list"] }]
532
- : []),
533
- ],
534
- };
535
- return [role, binding(metadata)];
536
- }),
537
- own,
538
- binding(inOwnNamespace),
539
- ...(main && mountPodsRemoved(config) ? [mountPods, binding(inDriverNamespace)] : []),
540
- ...(config.host.enabled ? hostAgentIdentity(config) : []),
541
- ];
271
+ return [serviceAccount, own, binding, ...(environment.branch === null && foldersRun(config) ? hostAgentIdentity(config) : [])];
542
272
  }
543
273
  /**
544
274
  * Folder workspaces' bayma: the operator's agents, which may restart alasio as they
@@ -573,31 +303,18 @@ function hostAgentIdentity(config) {
573
303
  }
574
304
  /** The objects of the alasio of `environment`, main's unless a branch environment's, with what main's carries forward. */
575
305
  export function alasioObjects(config, environment = MAIN_ENVIRONMENT, carried = NOTHING_CARRIED) {
576
- const templates = sandboxTemplates(config, environment, carried);
577
- const alasio = deployment(config, templates.checksum, environment);
306
+ const alasio = deployment(config, carried.botChecksum, environment);
578
307
  return [
579
308
  ...namespaces(config, environment),
580
309
  ...identity(config, environment),
581
- templates.configMap,
582
310
  alasio,
583
311
  // It runs turns, in its harnesses, which a recreation would end.
584
312
  verticalPodAutoscaler(alasio, "Recommended"),
585
- // A drain waits for the turns running in its pod and its workspaces' to end.
586
- ...[environment.namespace, ...workspaceNamespaces(config, environment)].map(busyBudget),
587
- telemetryService(config, environment),
313
+ // A drain waits for the turns running in its pod to end.
314
+ busyBudget(environment.namespace),
588
315
  ...(environment.branch === null && forksForBranches(config) ? [branchForksService()] : []),
589
- // What alasio may do beyond what RBAC can say; the cluster's, but for its Sandboxes', made with main's.
590
- ...(environment.branch === null ? [...busyLabelPolicy(), ...secretPolicy()] : []),
591
- // Where sessions are snapshotted, what alasio may set of a restored session's pod's status.
592
- ...(environment.branch === null && config.sessions.enabled && nodeAgentRuns(config)
593
- ? resumedConditionPolicy(componentName("resumed-condition"), alasioAccount(environment.namespace), config.sessions.namespace)
594
- : []),
595
- ...(environment.branch === null && sessionsForked(config) ? cloneJobPolicy(cloneJob(config)) : []),
596
- ...sandboxPolicy(sandboxPolicyName(environment.namespace), alasioAccount(environment.namespace), {
597
- sessions: config.sessions.enabled ? sessionsProfile(config, environment, carried) : null,
598
- previousSessions: config.sessions.enabled ? carried.previousSessions : null,
599
- host: config.host.enabled ? hostProfile(config) : null,
600
- }),
316
+ // What alasio may do beyond what RBAC can say: the cluster's, made with main's.
317
+ ...(environment.branch === null ? busyLabelPolicy() : []),
601
318
  ];
602
319
  }
603
320
  //# sourceMappingURL=alasio.js.map