alasio 8.0.0 → 10.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 (183) 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/bound-harnesses.js +98 -0
  4. package/dist/cli/src/bound-harnesses.js.map +1 -0
  5. package/dist/cli/src/branches.js +79 -22
  6. package/dist/cli/src/branches.js.map +1 -1
  7. package/dist/cli/src/cluster/docker.js +40 -15
  8. package/dist/cli/src/cluster/docker.js.map +1 -1
  9. package/dist/cli/src/cluster/host.js +38 -8
  10. package/dist/cli/src/cluster/host.js.map +1 -1
  11. package/dist/cli/src/cluster/k3s.js +105 -14
  12. package/dist/cli/src/cluster/k3s.js.map +1 -1
  13. package/dist/cli/src/cluster/node.js +142 -33
  14. package/dist/cli/src/cluster/node.js.map +1 -1
  15. package/dist/cli/src/cluster/stargz.js +92 -0
  16. package/dist/cli/src/cluster/stargz.js.map +1 -0
  17. package/dist/cli/src/commands/branch.js +5 -4
  18. package/dist/cli/src/commands/branch.js.map +1 -1
  19. package/dist/cli/src/commands/common.js +11 -5
  20. package/dist/cli/src/commands/common.js.map +1 -1
  21. package/dist/cli/src/commands/folder.js +14 -0
  22. package/dist/cli/src/commands/folder.js.map +1 -0
  23. package/dist/cli/src/commands/lake.js +1 -1
  24. package/dist/cli/src/commands/lake.js.map +1 -1
  25. package/dist/cli/src/commands/login-relay.js +91 -0
  26. package/dist/cli/src/commands/login-relay.js.map +1 -0
  27. package/dist/cli/src/commands/login.js +11 -10
  28. package/dist/cli/src/commands/login.js.map +1 -1
  29. package/dist/cli/src/commands/logs.js +89 -6
  30. package/dist/cli/src/commands/logs.js.map +1 -1
  31. package/dist/cli/src/commands/restart.js +10 -4
  32. package/dist/cli/src/commands/restart.js.map +1 -1
  33. package/dist/cli/src/commands/restore-filer-store.js +27 -10
  34. package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
  35. package/dist/cli/src/commands/status.js +119 -20
  36. package/dist/cli/src/commands/status.js.map +1 -1
  37. package/dist/cli/src/commands/uninstall.js +8 -9
  38. package/dist/cli/src/commands/uninstall.js.map +1 -1
  39. package/dist/cli/src/commands/up.js +20 -9
  40. package/dist/cli/src/commands/up.js.map +1 -1
  41. package/dist/cli/src/commands/upgrade.js +22 -6
  42. package/dist/cli/src/commands/upgrade.js.map +1 -1
  43. package/dist/cli/src/commands.js +2 -1
  44. package/dist/cli/src/commands.js.map +1 -1
  45. package/dist/cli/src/config.js +4 -1
  46. package/dist/cli/src/config.js.map +1 -1
  47. package/dist/cli/src/cpu-baseline.js +8 -4
  48. package/dist/cli/src/cpu-baseline.js.map +1 -1
  49. package/dist/cli/src/folders.js +63 -0
  50. package/dist/cli/src/folders.js.map +1 -0
  51. package/dist/cli/src/gateway-calls.js +64 -0
  52. package/dist/cli/src/gateway-calls.js.map +1 -0
  53. package/dist/cli/src/install.js +45 -31
  54. package/dist/cli/src/install.js.map +1 -1
  55. package/dist/cli/src/kept-releases.js +27 -20
  56. package/dist/cli/src/kept-releases.js.map +1 -1
  57. package/dist/cli/src/kube/api.js +61 -15
  58. package/dist/cli/src/kube/api.js.map +1 -1
  59. package/dist/cli/src/kube/apply.js +122 -81
  60. package/dist/cli/src/kube/apply.js.map +1 -1
  61. package/dist/cli/src/kube/rollout.js +43 -21
  62. package/dist/cli/src/kube/rollout.js.map +1 -1
  63. package/dist/cli/src/main.js +2 -1
  64. package/dist/cli/src/main.js.map +1 -1
  65. package/dist/cli/src/manifests/admission.js +558 -222
  66. package/dist/cli/src/manifests/admission.js.map +1 -1
  67. package/dist/cli/src/manifests/alasio.js +180 -428
  68. package/dist/cli/src/manifests/alasio.js.map +1 -1
  69. package/dist/cli/src/manifests/branch.js +16 -27
  70. package/dist/cli/src/manifests/branch.js.map +1 -1
  71. package/dist/cli/src/manifests/codex-login.js +101 -0
  72. package/dist/cli/src/manifests/codex-login.js.map +1 -0
  73. package/dist/cli/src/manifests/collector.js +83 -62
  74. package/dist/cli/src/manifests/collector.js.map +1 -1
  75. package/dist/cli/src/manifests/common.js +82 -25
  76. package/dist/cli/src/manifests/common.js.map +1 -1
  77. package/dist/cli/src/manifests/config.js +137 -105
  78. package/dist/cli/src/manifests/config.js.map +1 -1
  79. package/dist/cli/src/manifests/disruption.js +18 -3
  80. package/dist/cli/src/manifests/disruption.js.map +1 -1
  81. package/dist/cli/src/manifests/folders.js +131 -0
  82. package/dist/cli/src/manifests/folders.js.map +1 -0
  83. package/dist/cli/src/manifests/grafana.js +4 -2
  84. package/dist/cli/src/manifests/grafana.js.map +1 -1
  85. package/dist/cli/src/manifests/harness-pools.js +236 -0
  86. package/dist/cli/src/manifests/harness-pools.js.map +1 -0
  87. package/dist/cli/src/manifests/image-holder.js +61 -0
  88. package/dist/cli/src/manifests/image-holder.js.map +1 -0
  89. package/dist/cli/src/manifests/index.js +14 -7
  90. package/dist/cli/src/manifests/index.js.map +1 -1
  91. package/dist/cli/src/manifests/keda.js +4 -2
  92. package/dist/cli/src/manifests/keda.js.map +1 -1
  93. package/dist/cli/src/manifests/lake.js +18 -20
  94. package/dist/cli/src/manifests/lake.js.map +1 -1
  95. package/dist/cli/src/manifests/log.js +84 -19
  96. package/dist/cli/src/manifests/log.js.map +1 -1
  97. package/dist/cli/src/manifests/neon.js +60 -80
  98. package/dist/cli/src/manifests/neon.js.map +1 -1
  99. package/dist/cli/src/manifests/network-policies.js +232 -94
  100. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  101. package/dist/cli/src/manifests/node-agent.js +42 -15
  102. package/dist/cli/src/manifests/node-agent.js.map +1 -1
  103. package/dist/cli/src/manifests/object-store.js +99 -25
  104. package/dist/cli/src/manifests/object-store.js.map +1 -1
  105. package/dist/cli/src/manifests/sandbox-gateway.js +761 -0
  106. package/dist/cli/src/manifests/sandbox-gateway.js.map +1 -0
  107. package/dist/cli/src/manifests/scalers.js +17 -5
  108. package/dist/cli/src/manifests/scalers.js.map +1 -1
  109. package/dist/cli/src/manifests/sizing.js +17 -9
  110. package/dist/cli/src/manifests/sizing.js.map +1 -1
  111. package/dist/cli/src/manifests/valkey.js +30 -10
  112. package/dist/cli/src/manifests/valkey.js.map +1 -1
  113. package/dist/cli/src/manifests/volume-lock.js +43 -0
  114. package/dist/cli/src/manifests/volume-lock.js.map +1 -0
  115. package/dist/cli/src/manifests/vpa.js +15 -2
  116. package/dist/cli/src/manifests/vpa.js.map +1 -1
  117. package/dist/cli/src/manifests/workspace-storage.js +182 -94
  118. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  119. package/dist/cli/src/neon-storage.js +54 -16
  120. package/dist/cli/src/neon-storage.js.map +1 -1
  121. package/dist/cli/src/release.js +9 -9
  122. package/dist/cli/src/release.js.map +1 -1
  123. package/dist/cli/src/setup-pod.js +44 -0
  124. package/dist/cli/src/setup-pod.js.map +1 -0
  125. package/dist/cli/src/target.js +2 -2
  126. package/dist/cli/src/target.js.map +1 -1
  127. package/dist/cli/src/telegram.js +13 -4
  128. package/dist/cli/src/telegram.js.map +1 -1
  129. package/dist/cli/src/wakes.js +68 -0
  130. package/dist/cli/src/wakes.js.map +1 -0
  131. package/dist/cli/src/webhook.js +5 -2
  132. package/dist/cli/src/webhook.js.map +1 -1
  133. package/dist/cluster/node/pins.json +11 -5
  134. package/dist/neon/control/branches.js +13 -6
  135. package/dist/neon/control/branches.js.map +1 -1
  136. package/dist/neon/control/endpoints.js +81 -10
  137. package/dist/neon/control/endpoints.js.map +1 -1
  138. package/dist/neon/control/secrets.js +24 -8
  139. package/dist/neon/control/secrets.js.map +1 -1
  140. package/dist/src/binder/bindings.js +195 -0
  141. package/dist/src/binder/bindings.js.map +1 -0
  142. package/dist/src/binder/names.js +63 -0
  143. package/dist/src/binder/names.js.map +1 -0
  144. package/dist/src/binder/tokens.js +71 -0
  145. package/dist/src/binder/tokens.js.map +1 -0
  146. package/dist/src/branch/names.js +20 -6
  147. package/dist/src/branch/names.js.map +1 -1
  148. package/dist/src/events/names.js +28 -0
  149. package/dist/src/events/names.js.map +1 -0
  150. package/dist/src/gateway/names.js +240 -0
  151. package/dist/src/gateway/names.js.map +1 -0
  152. package/dist/src/harness/names.js +67 -0
  153. package/dist/src/harness/names.js.map +1 -0
  154. package/dist/src/kube/client.js +212 -0
  155. package/dist/src/kube/client.js.map +1 -0
  156. package/dist/src/kube/sandbox-shape.js +14 -68
  157. package/dist/src/kube/sandbox-shape.js.map +1 -1
  158. package/dist/src/kube/started-map.js +12 -0
  159. package/dist/src/kube/started-map.js.map +1 -0
  160. package/dist/src/node-agent/names.js +62 -5
  161. package/dist/src/node-agent/names.js.map +1 -1
  162. package/dist/src/sandbox/names.js +12 -20
  163. package/dist/src/sandbox/names.js.map +1 -1
  164. package/dist/src/sandbox/otlp-resource.js +173 -0
  165. package/dist/src/sandbox/otlp-resource.js.map +1 -0
  166. package/dist/src/sandbox/telemetry.js +43 -0
  167. package/dist/src/sandbox/telemetry.js.map +1 -0
  168. package/dist/src/telegram/bot-commands.js +17 -0
  169. package/dist/src/telegram/bot-commands.js.map +1 -0
  170. package/dist/src/telegram/stop.js +12 -0
  171. package/dist/src/telegram/stop.js.map +1 -0
  172. package/dist/src/telemetry/config.js +131 -0
  173. package/dist/src/telemetry/config.js.map +1 -0
  174. package/dist/src/telemetry/forward.js +154 -0
  175. package/dist/src/telemetry/forward.js.map +1 -0
  176. package/package.json +1 -1
  177. package/dist/cli/src/manifests/agent-sandbox.js +0 -92
  178. package/dist/cli/src/manifests/agent-sandbox.js.map +0 -1
  179. package/dist/cli/src/manifests/juicefs-csi.js +0 -320
  180. package/dist/cli/src/manifests/juicefs-csi.js.map +0 -1
  181. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +0 -6295
  182. package/dist/src/kube/config.js +0 -145
  183. package/dist/src/kube/config.js.map +0 -1
@@ -1,311 +1,72 @@
1
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.
2
+ * alasio itself: its Deployment, the namespaces its workspaces run in, its identity in
3
+ * them, and the template it makes its folder workspaces' Sandboxes from (src/kube/
4
+ * config.ts); main's, or a branch environment's in its own namespaces (./branch.ts), and
5
+ * main's Service of the forks it asks the sandbox gateway for them.
6
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 { BAYMA_STOP_SECONDS, CPU_FEATURES_ANNOTATION, STOP_WAIT_ARG } from "../../../src/kube/sandbox-shape.js";
12
- import { gvisorRuntimeClass, NODE_AGENT_PORT, NODE_AGENT_TOKEN_VARIABLE } from "../../../src/node-agent/names.js";
7
+ import { posix } from "node:path";
8
+ import { BRANCH_PARENT_LABEL, branchSessionsNamespace, forksGroup, forksTopic, MAIN_ENVIRONMENT_NAME } from "../../../src/branch/names.js";
9
+ import { eventsGroup, eventsTopic } from "../../../src/events/names.js";
10
+ import { ALASIO_GATEWAY_CALLER_KEY_VARIABLE, ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, ALASIO_GATEWAY_FOLDERS_VARIABLE, ALASIO_GATEWAY_URL_VARIABLE } from "../../../src/gateway/names.js";
11
+ import { STARTED_CONFIG_MAP } from "../../../src/kube/started-map.js";
13
12
  import { gvisorReleases, NODE_PINS } from "../cluster/node.js";
14
13
  import { imageReference } from "../images.js";
15
- import { alasioAccount, busyLabelPolicy, cloneJobPolicy, resumedConditionPolicy, sandboxPolicy, sandboxPolicyName, secretPolicy } from "./admission.js";
16
- import { alasioGroup, environmentLogSecret, environmentName, lakeWakeTopic, LOG_BROKERS, telegramTopic } from "./log.js";
17
- import { BRANCH_FORK_SECRET, BRANCH_LABEL, componentName, databaseSecret, envOf, given, goJson, helperResources, hostVolumeMounts, hostVolumes, imagePullSecrets, labels, lakeRuns, MAIN_ENVIRONMENT, NAMESPACE, nodeAgentRuns, ONE, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, roleVerifiers, selectorLabels, sha256, } from "./common.js";
14
+ import { busyLabelPolicy } from "./admission.js";
15
+ import { codexLoginObjects, codexLoginUrl } from "./codex-login.js";
16
+ import { foldersRun } from "./folders.js";
17
+ import { carriedPools, GATEWAY, GATEWAY_PORT } from "./sandbox-gateway.js";
18
+ import { alasioGroup, environmentLogSecret, environmentName, lagTrigger, lakeWakeTopic, LOG_BROKERS, logAuthentication, telegramTopic } from "./log.js";
19
+ import { scaledObject } from "./scalers.js";
20
+ import { STOP_GRACE_SECONDS } from "../../../src/telegram/stop.js";
21
+ import { BRANCH_FORK_SECRET, BRANCH_LABEL, componentName, DATABASE_SECRET, envOf, given, imagePullSecrets, labels, lakeRuns, MAIN_ENVIRONMENT, NAMESPACE, otelEnv, RELEASE, replicasOf, restrictedContainer, restrictedPod, roleVerifiers, selectorLabels, SESSIONS_CALLER_SECRET, } from "./common.js";
18
22
  import { durationSeconds } from "./config.js";
19
- import { busyBudget } from "./disruption.js";
23
+ import { busyBudget, replicaBudget, spread } from "./disruption.js";
20
24
  import { priority } from "./priorities.js";
21
25
  import { verticalPodAutoscaler } from "./sizing.js";
22
- import { cloneJob } from "./workspace-storage.js";
23
- /** The port every workspace's bayma serves MCP on. */
24
- const BAYMA_PORT = 7290;
25
26
  /** The namespace of the sessions of the alasio of `environment`: the configured one, or a branch environment's own. */
26
27
  export function sessionsNamespace(config, { branch }) {
27
28
  return branch ? branchSessionsNamespace(branch) : config.sessions.namespace;
28
29
  }
29
- /** The namespaces whose Sandboxes alasio drives: the sessions', and the host profile's. */
30
- function workspaceNamespaces(config, environment) {
31
- return [...(config.sessions.enabled ? [sessionsNamespace(config, environment)] : []), ...(config.host.enabled ? [config.host.namespace] : [])];
32
- }
33
- /**
34
- * The template of session filesystems' Sandboxes: an empty, isolated workspace under
35
- * the sessions' runtime, restricted, on a volume of workspace storage's class when it is
36
- * on. alasio adds what is per Sandbox: its name and labels, bayma's token, and its
37
- * network mode. A branch environment's are in its own namespace, and neither clones a
38
- * session (its parent does, src/branch/fork.ts) nor removes JuiceFS's mount pods, which
39
- * are main's to drive.
40
- */
41
- function sessionsProfile(config, environment, { cpuBaseline, gvisorReleases }) {
42
- const { sessions, workspaceStorage } = config;
43
- const main = environment.branch === null;
44
- // Snapshotted once every node's CPU features are known, its sandboxes held to those all have.
45
- const snapshotted = main && nodeAgentRuns(config) && cpuBaseline !== null;
46
- const agent = imageReference(config.images.agent);
47
- const storageClassName = sessions.storage.storageClassName || (workspaceStorage.enabled ? workspaceStorage.storageClassName : "");
48
- // On workspace storage, the volume's mounts follow its mount pod's, as upstream's
49
- // template has them (gVisor does not, and alasio restarts a session whose mount is
50
- // gone), and its files are given the pod's group only when its root is not the group's,
51
- // as walking a JuiceFS volume is slow.
52
- const propagation = workspaceStorage.enabled ? { mountPropagation: "HostToContainer" } : {};
53
- const ownership = workspaceStorage.enabled ? { fsGroupChangePolicy: "OnRootMismatch" } : {};
54
- return {
55
- namespace: sessionsNamespace(config, environment),
56
- port: BAYMA_PORT,
57
- workspaceDir: "/workspace",
58
- egressGate: sessions.egressGate,
59
- fullModeNameservers: [...sessions.fullModeNameservers],
60
- ...(main && mountPodsRemoved(config) ? { mountPodNamespace: workspaceStorage.csi.namespace } : {}),
61
- podTemplate: {
62
- metadata: {
63
- labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "session" },
64
- // Under gVisor, /tmp is a tmpfs of the sandbox's own, by gVisor's mount hints, rather
65
- // than the emptyDir's directory on the node: a snapshot of the sandbox holds it, as it
66
- // holds the files that bayma's runtimes keep open there.
67
- annotations: {
68
- "dev.gvisor.spec.mount.tmp.share": "container",
69
- "dev.gvisor.spec.mount.tmp.type": "tmpfs",
70
- "dev.gvisor.spec.mount.tmp.options": "rw",
71
- ...(snapshotted ? { [CPU_FEATURES_ANNOTATION]: cpuBaseline.join(",") } : {}),
72
- },
73
- },
74
- spec: {
75
- ...(sessions.runtimeClassName ? { runtimeClassName: sessions.runtimeClassName } : {}),
76
- ...priority("turn"),
77
- // A pod that ends is ended: alasio brings the session up in a new one, and nothing
78
- // starts bayma again in a sandbox that a snapshot has just ended.
79
- restartPolicy: "Never",
80
- // Ended by other than alasio, bayma holds its stop for alasio's snapshot, then stops.
81
- ...(snapshotted ? { terminationGracePeriodSeconds: stopWaitSeconds(config) + BAYMA_STOP_SECONDS } : {}),
82
- securityContext: { ...restrictedPod(1000, 1000), ...ownership },
83
- ...imagePullSecrets(config),
84
- ...given("nodeSelector", { ...sessions.nodeSelector }),
85
- ...given("tolerations", [...sessions.tolerations]),
86
- initContainers: [
87
- // The workspace and home on the session's volume, and bayma's toolbelt, which the
88
- // image installs, linked into the home.
89
- {
90
- name: "prepare",
91
- image: agent,
92
- imagePullPolicy: config.images.pullPolicy,
93
- command: [
94
- "/bin/sh",
95
- "-c",
96
- "mkdir -p /data/workspace /data/home/.local/share/bayma && ln -sfn /opt/bayma-data/bayma/toolbelt /data/home/.local/share/bayma/toolbelt",
97
- ],
98
- securityContext: restrictedContainer(),
99
- ...helperResources(),
100
- volumeMounts: [{ name: "data", mountPath: "/data" }],
101
- },
102
- ],
103
- containers: [{
104
- name: "bayma",
105
- image: agent,
106
- imagePullPolicy: config.images.pullPolicy,
107
- command: ["/usr/bin/tini", "-s", "--", "node", "/opt/bayma/bayma.js"],
108
- args: [
109
- "mcp-http",
110
- "--host",
111
- "0.0.0.0",
112
- "--port",
113
- String(BAYMA_PORT),
114
- "--state-dir",
115
- "/home/agent/.bayma",
116
- ...(snapshotted ? [STOP_WAIT_ARG, String(stopWaitSeconds(config) * 1000)] : []),
117
- ],
118
- workingDir: "/workspace",
119
- env: [
120
- { name: "HOME", value: "/home/agent" },
121
- { name: "USER", value: "agent" },
122
- { name: "BAYMA_PAYLOAD_DIR", value: "/opt/bayma/payload" },
123
- ],
124
- ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
125
- securityContext: restrictedContainer(),
126
- resources: sessions.resources,
127
- volumeMounts: [
128
- { name: "data", mountPath: "/workspace", subPath: "workspace", ...propagation },
129
- { name: "data", mountPath: "/home/agent", subPath: "home", ...propagation },
130
- { name: "tmp", mountPath: "/tmp" },
131
- ],
132
- }],
133
- volumes: [{ name: "tmp", emptyDir: { sizeLimit: "2Gi" } }],
134
- },
135
- },
136
- volumeClaimTemplates: [{
137
- metadata: { name: "data" },
138
- spec: {
139
- accessModes: ["ReadWriteOnce"],
140
- ...(storageClassName ? { storageClassName } : {}),
141
- resources: { requests: { storage: sessions.storage.size } },
142
- },
143
- }],
144
- ...(main && sessionsForked(config) ? { clone: { claimTemplate: "data", job: cloneJob(config) } } : {}),
145
- ...(snapshotted ? { snapshots: sessionSnapshots(config, gvisorReleases) } : {}),
146
- };
147
- }
148
30
  /** How long a session's bayma holds a stop alasio did not make (sessions.snapshots.stopWait). */
149
- function stopWaitSeconds(config) {
31
+ export function stopWaitSeconds(config) {
150
32
  // A duration, as the configuration checks it.
151
33
  return durationSeconds(config.sessions.snapshots.stopWait) ?? 0;
152
34
  }
153
- /**
154
- * How session sandboxes are snapshotted as they are released (src/sandbox/snapshots.ts),
155
- * where the node agents run: once sessions.snapshots.releaseAfter has passed without an
156
- * exec or a turn, live every sessions.snapshots.liveEvery while execs run, and as their
157
- * pods are ended by other than alasio, within sessions.snapshots.stopWait, under the
158
- * gVisor `releases` alasio keeps (../kept-releases.ts), the
159
- * current one first, each restored with a RuntimeClass of its own, by the agents in
160
- * alasio's namespace.
161
- */
162
- function sessionSnapshots(config, releases) {
163
- return {
164
- // A duration, as the configuration checks it.
165
- releaseAfterSeconds: durationSeconds(config.sessions.snapshots.releaseAfter) ?? 0,
166
- liveEverySeconds: durationSeconds(config.sessions.snapshots.liveEvery) ?? 0,
167
- releases: releases.map((release) => ({ release, runtimeClassName: gvisorRuntimeClass(release) })),
168
- agents: {
169
- namespace: NAMESPACE,
170
- selector: Object.entries(selectorLabels("node-agent")).map(([label, value]) => `${label}=${value}`).join(","),
171
- port: NODE_AGENT_PORT,
172
- },
173
- };
174
- }
175
- /** Whether alasio forks sessions: where their volumes are workspace storage's, which JuiceFS clones. */
176
- export function sessionsForked({ sessions, workspaceStorage }) {
177
- return workspaceStorage.enabled && [workspaceStorage.storageClassName, ""].includes(sessions.storage.storageClassName);
178
- }
179
- /** The template of folder workspaces' bayma: bayma itself, as the operator, with the machine's mounts. */
180
- function hostProfile(config) {
181
- const { host } = config;
182
- return {
183
- namespace: host.namespace,
184
- port: BAYMA_PORT,
185
- stateRoot: host.stateRoot,
186
- podTemplate: {
187
- metadata: { labels: { "app.kubernetes.io/part-of": "alasio", "app.kubernetes.io/instance": RELEASE, "app.kubernetes.io/component": "folder-bayma" } },
188
- spec: {
189
- // May restart alasio, by rolling out its Deployment, as an agent restarts it.
190
- serviceAccountName: componentName("host-agent"),
191
- ...priority("turn"),
192
- securityContext: {
193
- runAsUser: host.uid,
194
- runAsGroup: host.gid,
195
- // The group the bayma token's Secret volume is given, which the operator's user
196
- // reads it as; the node's own paths the mounts give are left as they are.
197
- fsGroup: host.gid,
198
- ...given("supplementalGroups", [...host.supplementalGroups]),
199
- },
200
- ...imagePullSecrets(config),
201
- containers: [{
202
- name: "bayma",
203
- image: imageReference(host.baymaImage),
204
- imagePullPolicy: "IfNotPresent",
205
- args: ["mcp-http", "--host", "0.0.0.0", "--port", String(BAYMA_PORT), "--default-durability", "checkpointed"],
206
- workingDir: host.home,
207
- env: [{ name: "HOME", value: host.home }, ...envOf(host.env)],
208
- ports: [{ name: "mcp", containerPort: BAYMA_PORT }],
209
- securityContext: {
210
- // What bayma needs to snapshot its REPL sessions' processes as it stops and
211
- // restore them whole as it starts again.
212
- capabilities: { add: ["CHECKPOINT_RESTORE", "SYS_PTRACE"] },
213
- seccompProfile: { type: "Unconfined" },
214
- appArmorProfile: { type: "Unconfined" },
215
- },
216
- resources: host.resources,
217
- ...given("volumeMounts", hostVolumeMounts(config)),
218
- }],
219
- ...given("volumes", hostVolumes(config)),
220
- },
221
- },
222
- };
223
- }
224
- /** What a cluster keeps where up has found nothing of it yet: no baseline, the releases pinned, and no bot. */
225
- export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS), botChecksum: null };
35
+ /** 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. */
36
+ export const NOTHING_KEPT = { cpuBaseline: null, gvisorReleases: gvisorReleases(NODE_PINS), botChecksum: null, apiServer: null, mountServices: {} };
226
37
  /** Nothing carried forward: a first installation's, and a branch environment's. */
227
- export const NOTHING_CARRIED = { previousSessions: null, ...NOTHING_KEPT };
228
- /** The ConfigMap of the templates, by its name. */
229
- export const SANDBOX_TEMPLATES = componentName("sandbox-templates");
38
+ export const NOTHING_CARRIED = { previousPools: { none: null, full: null }, ...NOTHING_KEPT };
230
39
  /**
231
40
  * What the installation `config` describes, with what the cluster keeps, `kept`, carries
232
- * forward from `live`, the templates of the installation it replaces (its ConfigMap's
233
- * `templates.json`, if it has one): the sessions' profile it ran, where their template
234
- * changes, or else the one it carried. A profile is carried for one change of the
235
- * template, and no more. Pure, for tests.
236
- *
237
- * What it carries is pinned whole into the Sandbox policy (./admission.ts) as a template
238
- * alasio may make a Sandbox from, read back from the cluster rather than rendered from the
239
- * config: the ConfigMap is the anchor of that trust. It is one as long as only the
240
- * operator, by `alasio up`, writes it: alasio's ServiceAccount may write no ConfigMap
241
- * (./alasio.ts identity). Were alasio given that, it could write any pod template into
242
- * the ConfigMap, and the next `up` would let it make Sandboxes of it.
243
- */
244
- export function carriedFrom(config, live, kept) {
245
- const nothing = { ...NOTHING_CARRIED, ...kept };
246
- if (!config.sessions.enabled || live === undefined)
247
- return nothing;
248
- // Carried as the ConfigMap held them, which alasio read as they are: the profile a snapshot recorded the hash of.
249
- if (Result.isFailure(decodeKubeTemplates(SANDBOX_TEMPLATES, live)))
250
- return nothing;
251
- const held = JSON.parse(live);
252
- if (!held.sessions)
253
- return nothing;
254
- // The current profile as the ConfigMap holds it, to compare with what it held.
255
- const current = JSON.parse(goJson(sessionsProfile(config, MAIN_ENVIRONMENT, kept), ""));
256
- return { previousSessions: isDeepStrictEqual(held.sessions.podTemplate, current.podTemplate) ? (held.previousSessions ?? null) : held.sessions, ...kept };
257
- }
258
- /**
259
- * The CPU baseline the installation whose templates are `live` (as carriedFrom takes
260
- * them) holds sessions to: what its sessions' template sets, if anything.
41
+ * forward from `liveGateway`, the sandbox gateway's configuration of the installation it
42
+ * replaces (its ConfigMap's `config.json`, if it has one): its pools' templates before the
43
+ * current ones (./sandbox-gateway.ts carriedPools). Pure, for tests.
261
44
  */
262
- export function recordedCpuBaseline(live) {
263
- if (live === undefined || Result.isFailure(decodeKubeTemplates(SANDBOX_TEMPLATES, live)))
264
- return null;
265
- const held = JSON.parse(live);
266
- const recorded = held.sessions?.podTemplate.metadata?.annotations?.[CPU_FEATURES_ANNOTATION];
267
- return recorded ? recorded.split(",") : null;
268
- }
269
- /** The ConfigMap of the templates, and their checksum, which alasio's pod carries so it is replaced when they change. */
270
- function sandboxTemplates(config, environment, carried) {
271
- const name = SANDBOX_TEMPLATES;
272
- const templates = goJson({
273
- ...(config.sessions.enabled ? { sessions: sessionsProfile(config, environment, carried) } : {}),
274
- ...(config.sessions.enabled && carried.previousSessions ? { previousSessions: carried.previousSessions } : {}),
275
- ...(config.host.enabled ? { host: hostProfile(config) } : {}),
276
- }, " ");
277
- return {
278
- configMap: { apiVersion: "v1", kind: "ConfigMap", metadata: { name, namespace: environment.namespace, labels: labels("alasio") }, data: { "templates.json": templates } },
279
- checksum: sha256(templates),
280
- };
45
+ export function carriedFrom(config, kept, liveGateway) {
46
+ return { ...kept, previousPools: carriedPools(config, liveGateway, kept) };
281
47
  }
282
48
  /** Where alasio's Secrets of branch environments are mounted: main's key, or a branch's token. */
283
49
  const BRANCH_SECRETS = "/run/alasio/branch";
284
50
  /**
285
51
  * What alasio is told of branch environments: main, where it forks sessions, where the
286
52
  * key their tokens are signed with is, from the stack's Secret, which may be made after
287
- * it starts; a branch, which it is, and where its parent forks the sessions it inherited
288
- * for it, with its token (src/branch/fork.ts).
53
+ * it starts; a branch, which it is, and its token to ask its parent to fork the sessions it
54
+ * inherited with, on the log (src/branch/fork.ts).
289
55
  */
290
- function branchEnv(config, { branch }) {
56
+ function branchEnv({ branch }) {
291
57
  if (branch) {
292
58
  return [
293
59
  { name: "ALASIO_BRANCH", value: branch },
294
- { name: "ALASIO_PARENT_FORKS_URL", value: `http://${componentName("branches")}.${NAMESPACE}.svc:${BRANCH_FORK_PORT}` },
295
60
  { name: "ALASIO_BRANCH_FORK_TOKEN_FILE", value: `${BRANCH_SECRETS}/token` },
296
61
  ];
297
62
  }
298
- return forksForBranches(config) ? [{ name: "ALASIO_BRANCH_FORK_KEY_FILE", value: `${BRANCH_SECRETS}/fork-key` }] : [];
63
+ return [{ name: "ALASIO_BRANCH_FORK_KEY_FILE", value: `${BRANCH_SECRETS}/fork-key` }];
299
64
  }
300
65
  /** The volume of branch environments' Secret: main's key, kept optional, or a branch's token. */
301
- function branchSecretVolume(config, { branch }) {
66
+ function branchSecretVolume({ branch }) {
302
67
  if (branch)
303
68
  return [{ name: "branch", secret: { secretName: BRANCH_FORK_SECRET, items: [{ key: "token", path: "token" }] } }];
304
- return forksForBranches(config) ? [{ name: "branch", secret: { secretName: componentName("branches"), items: [{ key: "fork-key", path: "fork-key" }], optional: true } }] : [];
305
- }
306
- /** Whether main forks its sessions for branch environments: where it has Neon to branch, and forks sessions. */
307
- export function forksForBranches(config) {
308
- return config.neon.enabled && sessionsForked(config);
69
+ return [{ name: "branch", secret: { secretName: componentName("branches"), items: [{ key: "fork-key", path: "fork-key" }], optional: true } }];
309
70
  }
310
71
  /**
311
72
  * The roles main's alasio makes for what reads its Neon beside it (common.ts
@@ -319,20 +80,20 @@ function readerRoles(config, { branch }) {
319
80
  function alasioEnv(config, home, operatorHome, environment) {
320
81
  const { alasio, host } = config;
321
82
  return [
322
- { name: "ALASIO_KUBE_TEMPLATES", value: "/etc/alasio/templates.json" },
323
83
  { name: "ALASIO_STATE_DIR", value: "/var/lib/alasio/state" },
324
84
  { name: "HOME", value: home },
325
85
  // A home of alasio's own does not outlast its pod; the operator's keeps their login.
326
86
  ...(operatorHome ? [] : [{ name: "ALASIO_KEEP_CODEX_LOGIN", value: "1" }]),
327
- { name: "ALASIO_DEPLOYMENT", value: RELEASE },
328
87
  { name: "ALASIO_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
329
88
  { name: "ALASIO_POD", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
330
89
  { name: "TELEGRAM_BOT_TOKEN", valueFrom: { secretKeyRef: { name: alasio.telegram.existingSecret, key: "token" } } },
331
90
  { name: "TELEGRAM_ALLOWED_USER_IDS", valueFrom: { secretKeyRef: { name: alasio.telegram.existingSecret, key: "allowedUserIds" } } },
332
91
  { name: "ALASIO_DATABASE_URL_FILE", value: "/run/alasio/database/url" },
333
92
  { name: "ALASIO_LAKE_VERIFIER_FILE", value: "/run/alasio/database/lake-verifier" },
93
+ { name: "ALASIO_HARNESS_VERIFIER_FILE", value: "/run/alasio/database/harness-verifier" },
94
+ { name: "ALASIO_CODEX_LOGIN_VERIFIER_FILE", value: "/run/alasio/database/codex-login-verifier" },
334
95
  ...readerRoles(config, environment).map(({ key, variable }) => ({ name: variable, value: `/run/alasio/database/${key}` })),
335
- { name: "ALASIO_LAKE_ENABLED", value: config.lake.enabled && config.neon.enabled ? "1" : "0" },
96
+ { name: "ALASIO_LAKE_ENABLED", value: config.lake.enabled ? "1" : "0" },
336
97
  // Its Telegram updates: its environment's topic, read in its group as its principal.
337
98
  { name: "ALASIO_LOG_BROKERS", value: LOG_BROKERS },
338
99
  { name: "ALASIO_LOG_USERNAME", valueFrom: { secretKeyRef: { name: environmentLogSecret(environment), key: "username" } } },
@@ -341,36 +102,84 @@ function alasioEnv(config, home, operatorHome, environment) {
341
102
  { name: "ALASIO_TELEGRAM_GROUP", value: alasioGroup(environmentName(environment)) },
342
103
  // Its environment's wake topic, which it tells the lake's loader of each transcript it writes on, as that principal.
343
104
  ...(lakeRuns(config) ? [{ name: "ALASIO_LAKE_WAKE_TOPIC", value: lakeWakeTopic(environmentName(environment)) }] : []),
344
- { name: "ALASIO_TELEMETRY_RECEIVER_SERVICE", value: `${componentName("telemetry")}.${environment.namespace}.svc` },
345
- { name: "ALASIO_TELEMETRY_RECEIVER_PORT", value: String(config.telemetry.receiverPort) },
346
105
  ...(alasio.defaultHarness ? [{ name: "ALASIO_DEFAULT_HARNESS", value: alasio.defaultHarness }] : []),
347
106
  ...(host.enabled && host.workspaceRoot ? [{ name: "ALASIO_WORKSPACE_ROOT", value: host.workspaceRoot }] : []),
348
107
  ...(alasio.claude.existingSecret
349
108
  ? [{ name: "CLAUDE_CODE_OAUTH_TOKEN", valueFrom: { secretKeyRef: { name: alasio.claude.existingSecret, key: alasio.claude.key } } }]
350
109
  : []),
351
- // What it presents to the node agents, which snapshot its sessions.
352
- ...(environment.branch === null && nodeAgentRuns(config)
353
- ? [{ name: NODE_AGENT_TOKEN_VARIABLE, valueFrom: { secretKeyRef: { name: componentName("node-agent"), key: "token" } } }]
354
- : []),
355
- ...branchEnv(config, environment),
110
+ // Its environment's sessions, through the sandbox gateway, by its environment's own key of the gateway's.
111
+ { name: ALASIO_GATEWAY_URL_VARIABLE, value: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}` },
112
+ { name: ALASIO_GATEWAY_ENVIRONMENT_VARIABLE, value: environmentName(environment) },
113
+ { name: ALASIO_GATEWAY_CALLER_KEY_VARIABLE, valueFrom: { secretKeyRef: { name: SESSIONS_CALLER_SECRET, key: "caller-key" } } },
114
+ // Main's folder workspaces, the gateway's folder instances, where the host profile renders them.
115
+ ...(environment.branch === null && foldersRun(config) ? [{ name: ALASIO_GATEWAY_FOLDERS_VARIABLE, value: "1" }] : []),
116
+ // Where its conversations' harnesses' pools' pods reach its environment's relay of its Codex login (./codex-login.ts).
117
+ { name: "ALASIO_CODEX_LOGIN_URL", value: codexLoginUrl(environment) },
118
+ ...branchEnv(environment),
356
119
  ...otelEnv(config, environment),
357
120
  ...envOf(alasio.env),
358
121
  ];
359
122
  }
360
123
  /**
361
- * alasio's Deployment: one replica, the one member of its group on its topic's one
362
- * partition, which its turns run in, so an update stops the old pod before the new one
363
- * starts. It keeps no volume: its state is in Neon,
124
+ * What of the machine alasio's pod mounts under the host profile: what alasio itself reads and
125
+ * writes there, now that no harness runs in its pod, each where the host profile's mounts put it
126
+ * (its folder conversations' harnesses run in pools of their own, with every mount):
127
+ * - the root its folder workspaces are under (ALASIO_WORKSPACE_ROOT, else its home), which it
128
+ * lists and makes folders in, reads a reply's media from, and starts Claude Code in to list
129
+ * the models its login can use (../../../src/harness/claude/models.ts);
130
+ * - where its home is the operator's and outside that root, the operator's Claude and Codex
131
+ * homes in it, `~/.claude` and `~/.codex`: Claude Code's login and settings it lists models
132
+ * with, and the Codex login its app-server keeps fresh and lists models with, and the
133
+ * configuration it gives folder conversations' Codex (../../../src/codex/configuration.ts);
134
+ * the rest of that home its own, an emptyDir.
135
+ * Nothing else: no socket, binary, or other path of the machine any mount gives. A path no mount
136
+ * gives is mounted from nowhere, as it was not given the machine's.
137
+ */
138
+ export function hostPaths(config, home) {
139
+ const { host } = config;
140
+ if (!host.enabled)
141
+ return { mounts: [], volumes: [], homeIsGiven: false };
142
+ const under = (path, of) => path === of || path.startsWith(`${of.replace(/\/$/u, "")}/`);
143
+ /** Where on the machine the mount that gives `path` has it, and whether it gives it read-only; null where none does. */
144
+ const given = (path) => {
145
+ const [mount] = host.mounts.filter(({ mountPath }) => under(path, mountPath)).sort((a, b) => b.mountPath.length - a.mountPath.length);
146
+ return mount ? { hostPath: posix.join(mount.hostPath, posix.relative(mount.mountPath, path)), readOnly: mount.readOnly } : null;
147
+ };
148
+ const root = host.workspaceRoot || home;
149
+ const operatorHome = host.alasioHome;
150
+ const paths = [
151
+ { name: "workspace-root", path: root, type: "Directory" },
152
+ ...(operatorHome && !under(home, root)
153
+ ? [{ name: "claude-home", path: posix.join(home, ".claude"), type: "DirectoryOrCreate" }, { name: "codex-home", path: posix.join(home, ".codex"), type: "DirectoryOrCreate" }]
154
+ : []),
155
+ ];
156
+ const mounts = [];
157
+ const volumes = [];
158
+ for (const { name, path, type } of paths) {
159
+ const at = given(path);
160
+ if (!at)
161
+ continue;
162
+ mounts.push({ name, mountPath: path, ...(at.readOnly ? { readOnly: true } : {}) });
163
+ volumes.push({ name, hostPath: { path: at.hostPath, type } });
164
+ }
165
+ return { mounts, volumes, homeIsGiven: operatorHome && under(home, root) && given(root) !== null };
166
+ }
167
+ /**
168
+ * alasio's Deployment: of alasio.scaling's replicas, which KEDA moves between its min and max
169
+ * where they are apart (alasioScaling). At one at most, an update stops the old pod before the
170
+ * new one starts; beyond, the new one starts beside the old, which drains as it stops, within
171
+ * STOP_GRACE_SECONDS (src/telegram/stop.ts, src/telegram/drain.ts). It keeps no volume: its state is in Neon,
364
172
  * and what it writes to disk (received files written for the agent, its home unless it
365
173
  * is the operator's) is written again as needed, on emptyDirs. Under the
366
- * host profile it runs as the operator, in their home, with the machine's mounts. A
174
+ * host profile it runs as the operator, in their home, with what of the machine it reads
175
+ * itself (hostPaths), no more of the machine's mounts. A
367
176
  * branch environment's runs in its namespace on the branch's database, told it is the
368
177
  * branch, with its token to ask main to fork the sessions it inherited; main's is given
369
178
  * the key branch environments' tokens are signed with, where it forks sessions. Its pod
370
- * carries the checksums of its templates and, where up read it, of its bot's Secret, so
371
- * it is replaced when either changes.
179
+ * carries, where up read it, the checksum of its bot's Secret, so it is replaced when it
180
+ * changes.
372
181
  */
373
- function deployment(config, templatesChecksum, botChecksum, environment) {
182
+ function deployment(config, botChecksum, environment) {
374
183
  const { alasio, host } = config;
375
184
  const operatorHome = host.enabled && host.alasioHome;
376
185
  const home = operatorHome ? host.home : "/var/lib/alasio/home";
@@ -378,25 +187,28 @@ function deployment(config, templatesChecksum, botChecksum, environment) {
378
187
  const gid = host.enabled ? host.gid : alasio.runAsGroup;
379
188
  const groups = [...new Set([...alasio.supplementalGroups, ...(host.enabled ? host.supplementalGroups : [])])];
380
189
  const image = imageReference(config.images.alasio);
381
- const branchSecret = branchSecretVolume(config, environment);
190
+ const branchSecret = branchSecretVolume(environment);
191
+ const machine = hostPaths(config, home);
192
+ // Its home its own, an emptyDir, but where the machine's root it is given holds it.
193
+ const ownHome = !machine.homeIsGiven;
382
194
  return {
383
195
  apiVersion: "apps/v1",
384
196
  kind: "Deployment",
385
197
  metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") },
386
198
  spec: {
387
- ...replicasOf(ONE),
388
- strategy: { type: "Recreate" },
199
+ ...replicasOf(alasio.scaling),
200
+ strategy: alasio.scaling.max > 1 ? { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } } : { type: "Recreate" },
389
201
  selector: { matchLabels: selectorLabels("alasio") },
390
202
  template: {
391
203
  metadata: {
392
204
  labels: labels("alasio"),
393
- annotations: { "checksum/sandbox-templates": templatesChecksum, ...(botChecksum === null ? {} : { "checksum/telegram": botChecksum }) },
205
+ ...(botChecksum === null ? {} : { annotations: { "checksum/telegram": botChecksum } }),
394
206
  },
395
207
  spec: {
396
208
  serviceAccountName: RELEASE,
397
209
  ...priority("turn"),
398
- // alasio finishes its turns' bookkeeping and records what it interrupts as it stops.
399
- terminationGracePeriodSeconds: 60,
210
+ // alasio finishes its turns' bookkeeping and records what it interrupts as it stops, and delivers what it owes.
211
+ terminationGracePeriodSeconds: STOP_GRACE_SECONDS,
400
212
  securityContext: { ...restrictedPod(uid, gid), ...given("supplementalGroups", groups) },
401
213
  ...imagePullSecrets(config),
402
214
  containers: [{
@@ -406,68 +218,51 @@ function deployment(config, templatesChecksum, botChecksum, environment) {
406
218
  workingDir: home,
407
219
  env: alasioEnv(config, home, operatorHome, environment),
408
220
  ...given("envFrom", [...alasio.envFrom]),
409
- ports: [
410
- { name: "telemetry", containerPort: config.telemetry.receiverPort },
411
- ...(environment.branch === null && forksForBranches(config) ? [{ name: "branch-forks", containerPort: BRANCH_FORK_PORT }] : []),
412
- ],
413
221
  securityContext: restrictedContainer(),
414
222
  resources: alasio.resources,
415
223
  volumeMounts: [
416
224
  { name: "state", mountPath: "/var/lib/alasio/state" },
417
- ...(operatorHome ? [] : [{ name: "alasio-home", mountPath: home }]),
418
- { name: "templates", mountPath: "/etc/alasio", readOnly: true },
225
+ ...(ownHome ? [{ name: "alasio-home", mountPath: home }] : []),
419
226
  { name: "database", mountPath: "/run/alasio/database", readOnly: true },
420
227
  { name: "log", mountPath: "/run/alasio/log", readOnly: true },
421
228
  ...branchSecret.map(({ name }) => ({ name, mountPath: BRANCH_SECRETS, readOnly: true })),
422
- ...(host.enabled ? hostVolumeMounts(config) : []),
229
+ ...machine.mounts,
423
230
  ],
424
231
  }],
425
232
  volumes: [
426
233
  { name: "state", emptyDir: {} },
427
- ...(operatorHome ? [] : [{ name: "alasio-home", emptyDir: {} }]),
428
- { name: "templates", configMap: { name: componentName("sandbox-templates") } },
234
+ ...(ownHome ? [{ name: "alasio-home", emptyDir: {} }] : []),
429
235
  {
430
236
  name: "database",
431
237
  secret: {
432
- secretName: databaseSecret(config),
433
- items: ["url", "lake-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
238
+ secretName: DATABASE_SECRET,
239
+ items: ["url", "lake-verifier", "harness-verifier", "codex-login-verifier", ...readerRoles(config, environment).map(({ key }) => key)].map((key) => ({ key, path: key })),
434
240
  },
435
241
  },
436
242
  { name: "log", secret: { secretName: environmentLogSecret(environment), items: [{ key: "password", path: "password" }] } },
437
243
  ...branchSecret,
438
- ...(host.enabled ? hostVolumes(config) : []),
244
+ ...machine.volumes,
439
245
  ],
440
246
  ...given("nodeSelector", { ...alasio.nodeSelector }),
441
247
  ...given("tolerations", [...alasio.tolerations]),
442
248
  ...given("affinity", alasio.affinity),
249
+ // Its replicas, where it may run more than one, on as many nodes as there are.
250
+ ...spread("alasio", alasio.scaling),
443
251
  },
444
252
  },
445
253
  },
446
254
  };
447
255
  }
448
- /** Where session sandboxes export their telemetry: alasio's receiver. */
449
- function telemetryService(config, environment) {
450
- return {
451
- apiVersion: "v1",
452
- kind: "Service",
453
- metadata: { name: componentName("telemetry"), namespace: environment.namespace, labels: labels("alasio") },
454
- spec: { selector: selectorLabels("alasio"), ports: [{ name: "otlp-http", port: config.telemetry.receiverPort, targetPort: "telemetry" }] },
455
- };
456
- }
457
- /** Where branch environments ask main to fork the sessions they inherited (src/branch/fork.ts). */
458
- function branchForksService() {
459
- return {
460
- apiVersion: "v1",
461
- kind: "Service",
462
- metadata: { name: componentName("branches"), namespace: NAMESPACE, labels: labels("alasio") },
463
- spec: { selector: selectorLabels("alasio"), ports: [{ name: "branch-forks", port: BRANCH_FORK_PORT, targetPort: "branch-forks" }] },
464
- };
465
- }
466
256
  /**
467
257
  * The namespaces workspaces run in, which alasio makes unless told not to. Sessions'
468
258
  * enforces Pod Security "restricted": nothing privileged, no host paths, no added
469
- * capabilities. The host profile's is "privileged", as its pods mount the machine. A
470
- * branch environment always has its own, beside its own namespace.
259
+ * capabilities; but where the sandbox service runs, whose pools' pods mount their own
260
+ * directories on their nodes, a host path no level but "privileged" allows, it enforces that
261
+ * and warns and audits of "restricted", the sandbox pods' policy holding every pod of it to the
262
+ * pools' templates instead (./admission.ts sandboxPodPolicy). The host profile's is
263
+ * "privileged", as its pods mount the machine. A branch environment always has its own,
264
+ * beside its own namespace, which records the environment it was branched from, main's
265
+ * (BRANCH_PARENT_LABEL), as the sandbox gateway forks main's sessions into it for that alone.
471
266
  */
472
267
  function namespaces(config, environment) {
473
268
  const { sessions, host } = config;
@@ -475,7 +270,7 @@ function namespaces(config, environment) {
475
270
  const branched = branch ? { [BRANCH_LABEL]: branch } : {};
476
271
  return [
477
272
  ...(branch ? [{ apiVersion: "v1", kind: "Namespace", metadata: { name: environment.namespace, labels: { ...labels("alasio"), ...branched } } }] : []),
478
- ...(sessions.enabled && (sessions.createNamespace || branch)
273
+ ...(sessions.createNamespace || branch
479
274
  ? [{
480
275
  apiVersion: "v1",
481
276
  kind: "Namespace",
@@ -484,9 +279,12 @@ function namespaces(config, environment) {
484
279
  labels: {
485
280
  ...labels("session"),
486
281
  ...branched,
487
- "pod-security.kubernetes.io/enforce": "restricted",
488
- "pod-security.kubernetes.io/enforce-version": "latest",
282
+ ...(branch ? { [BRANCH_PARENT_LABEL]: MAIN_ENVIRONMENT_NAME } : {}),
283
+ "pod-security.kubernetes.io/enforce": "privileged",
284
+ "pod-security.kubernetes.io/audit": "restricted",
285
+ "pod-security.kubernetes.io/audit-version": "latest",
489
286
  "pod-security.kubernetes.io/warn": "restricted",
287
+ "pod-security.kubernetes.io/warn-version": "latest",
490
288
  },
491
289
  },
492
290
  }]
@@ -501,97 +299,34 @@ function namespaces(config, environment) {
501
299
  ];
502
300
  }
503
301
  /**
504
- * Whether alasio removes the JuiceFS mount pods of a session whose mount is lost as it
505
- * restarts it (src/sandbox/index.ts): where it installs JuiceFS's driver, whose mount pods
506
- * it then knows to be in the driver's namespace.
507
- */
508
- function mountPodsRemoved({ workspaceStorage }) {
509
- return workspaceStorage.enabled && workspaceStorage.csi.enabled;
510
- }
511
- /**
512
- * alasio's identity: it drives its workspaces' Sandboxes, their token Secrets, and reads
513
- * files from sessions through exec, in the namespaces it owns, labelling their pods busy
514
- * while turns run in them, as it does its own in its namespace (src/kube/busy.ts); where
515
- * it snapshots sessions, sets a restored session's readiness; where it forks
516
- * sessions, reads sessions' volume claims and runs the Jobs that clone them in its own;
517
- * where the node agents run, watches pods in its own, theirs among them, and reads the
518
- * cluster's nodes, for a restore to go to one that takes its pod; and, where it installs
519
- * JuiceFS's driver, lists and deletes pods in the driver's namespace, its mount pods; and
520
- * nothing else.
302
+ * alasio's identity: it labels its own pod busy while turns run in it (src/kube/busy.ts),
303
+ * reads it to stop as it is deleted (src/kube/own-pod.ts), and keeps the time of its last
304
+ * start in a ConfigMap of its own, made here empty, which it may read and patch alone
305
+ * (src/kube/started.ts); nothing else. Its sessions and its folder workspaces are the sandbox
306
+ * gateway's, which it reaches by its key alone.
521
307
  */
522
308
  function identity(config, environment) {
523
- const main = environment.branch === null;
524
- const binding = (metadata) => ({
525
- apiVersion: "rbac.authorization.k8s.io/v1",
526
- kind: "RoleBinding",
527
- metadata,
528
- subjects: [{ kind: "ServiceAccount", name: RELEASE, namespace: environment.namespace }],
529
- roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: RELEASE },
530
- });
531
- const inDriverNamespace = { name: RELEASE, namespace: config.workspaceStorage.csi.namespace, labels: labels("alasio") };
532
- const mountPods = {
533
- apiVersion: "rbac.authorization.k8s.io/v1",
534
- kind: "Role",
535
- metadata: inDriverNamespace,
536
- rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["list", "delete"] }],
537
- };
538
309
  const inOwnNamespace = { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") };
539
310
  const own = {
540
311
  apiVersion: "rbac.authorization.k8s.io/v1",
541
312
  kind: "Role",
542
313
  metadata: inOwnNamespace,
543
314
  rules: [
544
- // Its own pod's busy label; and each node's agent, found by its pod on the node.
545
- { apiGroups: [""], resources: ["pods"], verbs: main && nodeAgentRuns(config) ? ["list", "watch", "patch"] : ["list", "patch"] },
546
- ...(main && sessionsForked(config) ? [{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["create", "get", "delete"] }] : []),
315
+ { apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "patch"] },
316
+ { apiGroups: [""], resources: ["configmaps"], resourceNames: [STARTED_CONFIG_MAP], verbs: ["get", "patch"] },
547
317
  ],
548
318
  };
549
- const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") } };
550
- // The nodes, read alone, for a restore to go to one that takes the session's pod (src/sandbox/snapshots.ts).
551
- const nodes = { name: componentName("nodes"), labels: labels("alasio") };
552
- const nodesRole = {
553
- apiVersion: "rbac.authorization.k8s.io/v1",
554
- kind: "ClusterRole",
555
- metadata: nodes,
556
- rules: [{ apiGroups: [""], resources: ["nodes"], verbs: ["get"] }],
557
- };
558
- const nodesBinding = {
319
+ // Of no data, which alasio's starts write and `alasio up` leaves to them.
320
+ const started = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: STARTED_CONFIG_MAP, namespace: environment.namespace, labels: labels("alasio") } };
321
+ const binding = {
559
322
  apiVersion: "rbac.authorization.k8s.io/v1",
560
- kind: "ClusterRoleBinding",
561
- metadata: nodes,
323
+ kind: "RoleBinding",
324
+ metadata: inOwnNamespace,
562
325
  subjects: [{ kind: "ServiceAccount", name: RELEASE, namespace: environment.namespace }],
563
- roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: nodes.name },
326
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: RELEASE },
564
327
  };
565
- return [
566
- serviceAccount,
567
- ...workspaceNamespaces(config, environment).flatMap((namespace) => {
568
- const metadata = { name: RELEASE, namespace, labels: labels("alasio") };
569
- const forked = main && sessionsForked(config) && namespace === config.sessions.namespace;
570
- const snapshotted = main && nodeAgentRuns(config) && namespace === config.sessions.namespace;
571
- const role = {
572
- apiVersion: "rbac.authorization.k8s.io/v1",
573
- kind: "Role",
574
- metadata,
575
- rules: [
576
- { apiGroups: ["agents.x-k8s.io"], resources: ["sandboxes"], verbs: ["get", "list", "watch", "create", "patch", "delete"] },
577
- { apiGroups: [""], resources: ["secrets"], verbs: ["get", "create"] },
578
- { apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "patch"] },
579
- { apiGroups: [""], resources: ["pods/exec"], verbs: ["create", "get"] },
580
- ...(forked ? [{ apiGroups: [""], resources: ["persistentvolumeclaims"], verbs: ["get"] }] : []),
581
- // A restored session's readiness, once its sandbox is let run, and the network policies that must hold it first (src/sandbox/snapshots.ts).
582
- ...(snapshotted
583
- ? [{ apiGroups: [""], resources: ["pods/status"], verbs: ["patch"] }, { apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["list"] }]
584
- : []),
585
- ],
586
- };
587
- return [role, binding(metadata)];
588
- }),
589
- own,
590
- binding(inOwnNamespace),
591
- ...(main && mountPodsRemoved(config) ? [mountPods, binding(inDriverNamespace)] : []),
592
- ...(main && nodeAgentRuns(config) ? [nodesRole, nodesBinding] : []),
593
- ...(config.host.enabled ? hostAgentIdentity(config) : []),
594
- ];
328
+ const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: RELEASE, namespace: environment.namespace, labels: labels("alasio") } };
329
+ return [serviceAccount, own, binding, started, ...(environment.branch === null && foldersRun(config) ? hostAgentIdentity(config) : [])];
595
330
  }
596
331
  /**
597
332
  * Folder workspaces' bayma: the operator's agents, which may restart alasio as they
@@ -624,33 +359,50 @@ function hostAgentIdentity(config) {
624
359
  };
625
360
  return [serviceAccount, role, binding];
626
361
  }
362
+ /** How many updates waiting on its environment's Telegram topic ask for one more alasio: a few chats' bursts. */
363
+ const UPDATES_PER_ALASIO = 20;
364
+ /** What a bound harness asks of alasio's scale: so much that any number of them ask for one alasio, to hear and deliver what they do, and no more. */
365
+ const BOUND_TARGET = 1_000_000;
366
+ /**
367
+ * KEDA's scaling of the environment's alasio (deployment), where its scale's min and max are
368
+ * apart: up from none at the first update, event or fork request waiting on its topics, or
369
+ * harness bound to one of its conversations, and up by one for each UPDATES_PER_ALASIO updates
370
+ * waiting; back to its min once none of that has been seen for its cooldown. Its groups' lags
371
+ * read as the log's scaler principal; a group that has read none of a topic counts its records
372
+ * as it reads them, from its first or from the next written (src/events/consume.ts).
373
+ */
374
+ export function alasioScaling(config, environment, alasio) {
375
+ const name = environmentName(environment);
376
+ const authentication = RELEASE;
377
+ const triggers = [
378
+ lagTrigger(alasioGroup(name), telegramTopic(name), UPDATES_PER_ALASIO, authentication),
379
+ lagTrigger(eventsGroup(name), eventsTopic(name), 1, authentication, environment.branch === null ? "latest" : "earliest"),
380
+ ...(environment.branch === null ? [lagTrigger(forksGroup(MAIN_ENVIRONMENT_NAME), forksTopic(MAIN_ENVIRONMENT_NAME), 1, authentication)] : []),
381
+ // Its harnesses bound, of every pool in its namespace, as the gateway counts them.
382
+ {
383
+ type: "metrics-api",
384
+ metadata: { url: `http://${GATEWAY}.${NAMESPACE}.svc.cluster.local:${GATEWAY_PORT}/counts/${environment.namespace}`, valueLocation: "bound", targetValue: String(BOUND_TARGET), activationTargetValue: "0" },
385
+ },
386
+ ];
387
+ const scaledObjects = scaledObject(alasio, config.alasio.scaling, triggers, { pollingSeconds: config.alasio.pollingSeconds, cooldownSeconds: config.alasio.cooldownSeconds });
388
+ return scaledObjects.length > 0 ? [logAuthentication(authentication, environment.namespace, "alasio"), ...scaledObjects] : [];
389
+ }
627
390
  /** The objects of the alasio of `environment`, main's unless a branch environment's, with what main's carries forward. */
628
391
  export function alasioObjects(config, environment = MAIN_ENVIRONMENT, carried = NOTHING_CARRIED) {
629
- const templates = sandboxTemplates(config, environment, carried);
630
- const alasio = deployment(config, templates.checksum, carried.botChecksum, environment);
392
+ const alasio = deployment(config, carried.botChecksum, environment);
631
393
  return [
632
394
  ...namespaces(config, environment),
633
395
  ...identity(config, environment),
634
- templates.configMap,
635
396
  alasio,
397
+ ...alasioScaling(config, environment, alasio),
636
398
  // It runs turns, in its harnesses, which a recreation would end.
637
- verticalPodAutoscaler(alasio, "Recommended"),
638
- // A drain waits for the turns running in its pod and its workspaces' to end.
639
- ...[environment.namespace, ...workspaceNamespaces(config, environment)].map(busyBudget),
640
- telemetryService(config, environment),
641
- ...(environment.branch === null && forksForBranches(config) ? [branchForksService()] : []),
642
- // What alasio may do beyond what RBAC can say; the cluster's, but for its Sandboxes', made with main's.
643
- ...(environment.branch === null ? [...busyLabelPolicy(), ...secretPolicy()] : []),
644
- // Where sessions are snapshotted, what alasio may set of a restored session's pod's status.
645
- ...(environment.branch === null && config.sessions.enabled && nodeAgentRuns(config)
646
- ? resumedConditionPolicy(componentName("resumed-condition"), alasioAccount(environment.namespace), config.sessions.namespace)
647
- : []),
648
- ...(environment.branch === null && sessionsForked(config) ? cloneJobPolicy(cloneJob(config)) : []),
649
- ...sandboxPolicy(sandboxPolicyName(environment.namespace), alasioAccount(environment.namespace), {
650
- sessions: config.sessions.enabled ? sessionsProfile(config, environment, carried) : null,
651
- previousSessions: config.sessions.enabled ? carried.previousSessions : null,
652
- host: config.host.enabled ? hostProfile(config) : null,
653
- }),
399
+ verticalPodAutoscaler(alasio, "AtStart"),
400
+ // A drain waits for the turns running in its pod to end, and evicts one of its pods not busy at a time.
401
+ busyBudget(environment.namespace),
402
+ ...replicaBudget("alasio", environment.namespace, config.alasio.scaling, { unlessBusy: true }),
403
+ ...codexLoginObjects(config, environment),
404
+ // What alasio may do beyond what RBAC can say: the cluster's, made with main's.
405
+ ...(environment.branch === null ? busyLabelPolicy() : []),
654
406
  ];
655
407
  }
656
408
  //# sourceMappingURL=alasio.js.map