@moondeck/station 0.0.1 → 1.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 (326) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/bin/uai-host.mjs +130 -0
  4. package/db/migrations/0000_host_tasks.sql +12 -0
  5. package/db/migrations/0001_host_ui.sql +11 -0
  6. package/db/migrations/0002_host_github_tokens.sql +8 -0
  7. package/db/migrations/0003_host_ssh_keys.sql +8 -0
  8. package/db/migrations/0004_host_owner_name.sql +1 -0
  9. package/db/migrations/0005_host_env_vars.sql +5 -0
  10. package/db/migrations/0006_host_project_env.sql +9 -0
  11. package/db/migrations/0007_host_github_token_kind.sql +7 -0
  12. package/db/migrations/0008_host_mcp_connections.sql +18 -0
  13. package/db/migrations/0009_host_agent_sessions.sql +15 -0
  14. package/db/migrations/0010_host_engine_accounts.sql +13 -0
  15. package/db/migrations/0011_codex_thread_id.sql +5 -0
  16. package/db/migrations/0012_agent_session_attach_key.sql +4 -0
  17. package/db/migrations/0013_github_credential_generations.sql +11 -0
  18. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  19. package/db/migrations/0015_host_settings.sql +9 -0
  20. package/db/migrations/0016_task_environment.sql +2 -0
  21. package/db/migrations/0017_engine_account_scope.sql +7 -0
  22. package/db/migrations/0018_engine_account_position.sql +3 -0
  23. package/db/migrations/0019_engine_account_store_ref.sql +3 -0
  24. package/db/migrations/0020_user_credential_store_ref.sql +6 -0
  25. package/db/migrations/0021_project_env_store_ref.sql +3 -0
  26. package/db/migrations/0022_task_project_ids.sql +5 -0
  27. package/db/migrations/0023_project_env_org.sql +4 -0
  28. package/db/migrations/0024_task_env_mode.sql +9 -0
  29. package/db/migrations/0025_project_env_authority.sql +147 -0
  30. package/db/migrations/0026_project_env_replica.sql +150 -0
  31. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  32. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  33. package/db/migrations/0029_task_shared_files_mode.sql +5 -0
  34. package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
  35. package/db/migrations/meta/_journal.json +223 -0
  36. package/db/schema.ts +850 -0
  37. package/images/standard/Dockerfile +347 -0
  38. package/images/standard/README.md +167 -0
  39. package/images/standard/container/code-server-settings.json +63 -0
  40. package/images/standard/container/corepack-version +1 -0
  41. package/images/standard/container/uai-init +704 -0
  42. package/images/standard/container/uai-materialize-runtimes +1527 -0
  43. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  44. package/images/standard/tool-versions +2 -0
  45. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  46. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  47. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  48. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  49. package/lib/agent-cli.ts +1507 -0
  50. package/lib/agent-errors.ts +22 -0
  51. package/lib/agent.ts +672 -0
  52. package/lib/agents/attach-compatibility.ts +51 -0
  53. package/lib/agents/claude.ts +918 -0
  54. package/lib/agents/codex-token-usage.fixture.json +130 -0
  55. package/lib/agents/codex.ts +1392 -0
  56. package/lib/agents/cursor-period-usage.ts +253 -0
  57. package/lib/agents/cursor.ts +562 -0
  58. package/lib/agents/dispatch.ts +78 -0
  59. package/lib/agents/durable-proc.ts +334 -0
  60. package/lib/agents/factory.ts +57 -0
  61. package/lib/agents/grok.ts +381 -0
  62. package/lib/agents/kimi.ts +329 -0
  63. package/lib/agents/machine-durable.ts +604 -0
  64. package/lib/agents/mock.ts +133 -0
  65. package/lib/agents/mode.ts +64 -0
  66. package/lib/agents/opencode.ts +508 -0
  67. package/lib/agents/output-source.ts +3 -0
  68. package/lib/agents/proc.ts +221 -0
  69. package/lib/agents/rate-limit.ts +337 -0
  70. package/lib/agents/registry.ts +182 -0
  71. package/lib/agents/session-state.ts +197 -0
  72. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  73. package/lib/agents/transport.ts +741 -0
  74. package/lib/agents/types.ts +254 -0
  75. package/lib/agents/uai.ts +106 -0
  76. package/lib/agents/usage.ts +44 -0
  77. package/lib/apple-one-off.ts +277 -0
  78. package/lib/apple-runtime-recycle.ts +237 -0
  79. package/lib/apple-uninstall-teardown.ts +224 -0
  80. package/lib/assignment-evidence.ts +94 -0
  81. package/lib/attachments.ts +48 -0
  82. package/lib/browser-testing.ts +1537 -0
  83. package/lib/capture-flow.ts +332 -0
  84. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  85. package/lib/cloud-credential-host.ts +512 -0
  86. package/lib/cloud-credential-leases.ts +2587 -0
  87. package/lib/cloud-credential-runtime.ts +1357 -0
  88. package/lib/cloud-state.ts +56 -0
  89. package/lib/codex-auth.ts +456 -0
  90. package/lib/command-db.ts +310 -0
  91. package/lib/compose-network-recovery.ts +283 -0
  92. package/lib/connection-secret-redaction.ts +25 -0
  93. package/lib/container-runtime.ts +1453 -0
  94. package/lib/credential-cleanup-inventory.ts +490 -0
  95. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  96. package/lib/credential-consumers/engines.ts +797 -0
  97. package/lib/credential-consumers/files.ts +934 -0
  98. package/lib/credential-consumers/github.ts +702 -0
  99. package/lib/credential-consumers/mcp.ts +389 -0
  100. package/lib/credential-consumers/task-files.ts +127 -0
  101. package/lib/credential-consumers/task-secrets.ts +10 -0
  102. package/lib/credential-store-pickup.ts +149 -0
  103. package/lib/credential-store-scope.ts +112 -0
  104. package/lib/credential-store.ts +932 -0
  105. package/lib/credentials-activation-authority.ts +709 -0
  106. package/lib/credentials-purge.ts +1006 -0
  107. package/lib/custom-connection-capture.ts +395 -0
  108. package/lib/custom-connection-launch.ts +384 -0
  109. package/lib/custom-connection-runtime.ts +141 -0
  110. package/lib/db.ts +80 -0
  111. package/lib/disk-pressure.ts +42 -0
  112. package/lib/docker-exec.ts +156 -0
  113. package/lib/engine-accounts.ts +1970 -0
  114. package/lib/engine-connect.ts +321 -0
  115. package/lib/engine-credential-env.ts +52 -0
  116. package/lib/engine-login.ts +2933 -0
  117. package/lib/engines.ts +1342 -0
  118. package/lib/enrollment-state.ts +251 -0
  119. package/lib/env-file.ts +246 -0
  120. package/lib/env-schema.ts +891 -0
  121. package/lib/env.ts +195 -0
  122. package/lib/fs-aliases.ts +49 -0
  123. package/lib/git-clone-credentials.ts +334 -0
  124. package/lib/git-diff.ts +530 -0
  125. package/lib/git-identity.ts +715 -0
  126. package/lib/github-git-auth.ts +207 -0
  127. package/lib/github-origin.ts +28 -0
  128. package/lib/github-tokens.ts +2439 -0
  129. package/lib/host-cloud-url.ts +62 -0
  130. package/lib/host-config.ts +279 -0
  131. package/lib/host-env.ts +424 -0
  132. package/lib/host-logs.ts +965 -0
  133. package/lib/internal-connections.ts +5 -0
  134. package/lib/keyed-promise-tail.ts +23 -0
  135. package/lib/launch-mode.ts +25 -0
  136. package/lib/lease.ts +239 -0
  137. package/lib/legacy-host.ts +769 -0
  138. package/lib/legacy-runtime-retirement.ts +424 -0
  139. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  140. package/lib/machine-exec.ts +238 -0
  141. package/lib/machine-gateway-tunnel.ts +344 -0
  142. package/lib/machine-keys.ts +65 -0
  143. package/lib/machine-project-env-loader.ts +152 -0
  144. package/lib/machine-provider-aws.ts +923 -0
  145. package/lib/machine-provider-local.ts +338 -0
  146. package/lib/machine-provider.ts +90 -0
  147. package/lib/managed-activation-watcher.ts +72 -0
  148. package/lib/managed-install-owner-watcher.ts +55 -0
  149. package/lib/managed-operation-drain.ts +57 -0
  150. package/lib/managed-runtime.ts +4296 -0
  151. package/lib/managed-update-scheduler.ts +125 -0
  152. package/lib/mcp-capture.ts +865 -0
  153. package/lib/mcp-config-lock.ts +2 -0
  154. package/lib/mcp-connections.ts +959 -0
  155. package/lib/mcp-gateway.ts +2594 -0
  156. package/lib/obs.ts +512 -0
  157. package/lib/openrouter-budget.ts +247 -0
  158. package/lib/orchestrator.ts +11409 -0
  159. package/lib/person-capture.ts +327 -0
  160. package/lib/preview-ports.ts +85 -0
  161. package/lib/preview-sidecar.ts +331 -0
  162. package/lib/project-env-authority.ts +1319 -0
  163. package/lib/project-env-feature.ts +74 -0
  164. package/lib/project-env-legacy-tags.ts +130 -0
  165. package/lib/project-env-loader.ts +242 -0
  166. package/lib/project-env-replica.ts +2094 -0
  167. package/lib/project-env-schema-capture.ts +342 -0
  168. package/lib/project-env-staging.ts +148 -0
  169. package/lib/project-env-task-policy.ts +176 -0
  170. package/lib/project-env-task-replica.ts +1752 -0
  171. package/lib/release-feed.ts +61 -0
  172. package/lib/release-manifest.ts +710 -0
  173. package/lib/release-trust.ts +46 -0
  174. package/lib/repo-clone.ts +38 -0
  175. package/lib/routing/shadow.ts +261 -0
  176. package/lib/runner-boot.ts +10 -0
  177. package/lib/runtime-activation-tail.ts +279 -0
  178. package/lib/runtime-archive.ts +1216 -0
  179. package/lib/runtime-authority.ts +166 -0
  180. package/lib/runtime-guard.ts +36 -0
  181. package/lib/runtime-provider-state.ts +169 -0
  182. package/lib/runtime-reconnect.ts +86 -0
  183. package/lib/runtime-state.ts +1033 -0
  184. package/lib/safe-http.ts +364 -0
  185. package/lib/secrets-proxy/address.ts +95 -0
  186. package/lib/secrets-proxy/audit.ts +30 -0
  187. package/lib/secrets-proxy/ca-store.ts +200 -0
  188. package/lib/secrets-proxy/ca.ts +470 -0
  189. package/lib/secrets-proxy/container-steps.ts +138 -0
  190. package/lib/secrets-proxy/der.ts +273 -0
  191. package/lib/secrets-proxy/engine.ts +1633 -0
  192. package/lib/secrets-proxy/host.ts +93 -0
  193. package/lib/secrets-proxy/hostname.ts +57 -0
  194. package/lib/secrets-proxy/placeholders.ts +196 -0
  195. package/lib/secrets-proxy/policy.ts +150 -0
  196. package/lib/secrets-proxy/readiness.ts +148 -0
  197. package/lib/secrets-proxy/runtime.ts +582 -0
  198. package/lib/secrets-proxy/scrub.ts +559 -0
  199. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  200. package/lib/secrets-proxy/task-scripts.ts +262 -0
  201. package/lib/secrets-proxy/task-trust.ts +605 -0
  202. package/lib/secrets-proxy/trust-watch.ts +80 -0
  203. package/lib/secrets.ts +71 -0
  204. package/lib/shared-files-s3.ts +976 -0
  205. package/lib/shared-files.ts +156 -0
  206. package/lib/sidecar-assign.ts +49 -0
  207. package/lib/sidecar-guard.ts +58 -0
  208. package/lib/sidecar-resume.ts +178 -0
  209. package/lib/sidecar.ts +69 -0
  210. package/lib/signin-box-credentials.ts +465 -0
  211. package/lib/signin-box.ts +114 -0
  212. package/lib/skill-index-example.fixture.ts +56 -0
  213. package/lib/skill-layout.ts +417 -0
  214. package/lib/skills.ts +489 -0
  215. package/lib/ssh.ts +401 -0
  216. package/lib/standard-image-cleanup.ts +117 -0
  217. package/lib/standard-image-pin.ts +207 -0
  218. package/lib/standard-image.ts +1697 -0
  219. package/lib/stopped-task-status-queue.ts +120 -0
  220. package/lib/task-container-cli.ts +269 -0
  221. package/lib/task-credential-cleanup.ts +752 -0
  222. package/lib/task-diff-sandbox.ts +464 -0
  223. package/lib/task-diff.ts +91 -0
  224. package/lib/task-env-forwarder.ts +228 -0
  225. package/lib/task-env-source.ts +249 -0
  226. package/lib/task-environment/anchor.ts +31 -0
  227. package/lib/task-environment/apple-container.ts +860 -0
  228. package/lib/task-environment/docker.ts +1344 -0
  229. package/lib/task-environment/index.ts +466 -0
  230. package/lib/task-environment/legacy-adoption.ts +467 -0
  231. package/lib/task-environment/machine-environment.ts +61 -0
  232. package/lib/task-environment/machine-task-up.ts +1227 -0
  233. package/lib/task-environment/machine.ts +974 -0
  234. package/lib/task-environment/registry.ts +59 -0
  235. package/lib/task-environment/self-identity.ts +20 -0
  236. package/lib/task-environment/self.ts +566 -0
  237. package/lib/task-environment/types.ts +642 -0
  238. package/lib/task-environment/workspace-files.ts +685 -0
  239. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  240. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  241. package/lib/task-files-machine-retirement.ts +116 -0
  242. package/lib/task-files-readiness.ts +127 -0
  243. package/lib/task-gateway-port.ts +22 -0
  244. package/lib/task-gateway-protocol.ts +190 -0
  245. package/lib/task-gateway-recovery.ts +47 -0
  246. package/lib/task-gateway-state.ts +512 -0
  247. package/lib/task-identity.ts +19 -0
  248. package/lib/task-inventory.ts +682 -0
  249. package/lib/task-machine-credentials-activation.ts +95 -0
  250. package/lib/task-machine-swap-safety.ts +729 -0
  251. package/lib/task-person-credentials.ts +76 -0
  252. package/lib/task-project-ops-host.ts +147 -0
  253. package/lib/task-project-ops.ts +1062 -0
  254. package/lib/task-secret-staging.ts +16 -0
  255. package/lib/task-status.ts +46 -0
  256. package/lib/task-token.ts +46 -0
  257. package/lib/token-broker-adopt.ts +344 -0
  258. package/lib/token-broker-lambda.ts +105 -0
  259. package/lib/token-broker-local.ts +392 -0
  260. package/lib/token-broker-revision.ts +310 -0
  261. package/lib/token-broker-ssm.ts +525 -0
  262. package/lib/token-broker.ts +218 -0
  263. package/lib/token-lanes/openai-oauth.ts +123 -0
  264. package/lib/token-lanes/registry.ts +45 -0
  265. package/lib/token-lanes/types.ts +163 -0
  266. package/lib/token-lanes/xai-oauth.ts +165 -0
  267. package/lib/transcript.ts +128 -0
  268. package/lib/tunnel-registry.ts +220 -0
  269. package/lib/tunnel-runtime.ts +235 -0
  270. package/lib/ulid.ts +16 -0
  271. package/lib/user-bus-env.ts +116 -0
  272. package/lib/user-credentials-store.ts +435 -0
  273. package/lib/vnode-pressure.ts +142 -0
  274. package/package.json +115 -8
  275. package/runner/runner.mjs +442 -0
  276. package/scripts/agent/_common.sh +633 -0
  277. package/scripts/agent/env-hook.sh +626 -0
  278. package/scripts/agent/moondeck-cli.sh +5 -0
  279. package/scripts/agent/task-down.sh +291 -0
  280. package/scripts/agent/task-status.sh +70 -0
  281. package/scripts/agent/task-up.sh +3761 -0
  282. package/scripts/agent/uai-env-exec.sh +26 -0
  283. package/scripts/install/darwin.ts +1048 -0
  284. package/scripts/install/linux.ts +958 -0
  285. package/scripts/install/types.ts +78 -0
  286. package/scripts/install/util.ts +254 -0
  287. package/scripts/install/win.ts +143 -0
  288. package/src/activation-respawn.ts +155 -0
  289. package/src/apple-tunnel-route.ts +104 -0
  290. package/src/capture-wire.ts +102 -0
  291. package/src/cli.ts +2583 -0
  292. package/src/cloud-credentials.ts +2144 -0
  293. package/src/cloud-origins.ts +23 -0
  294. package/src/credential-location.ts +248 -0
  295. package/src/credential-wire-validation.ts +25 -0
  296. package/src/custom-connections.ts +237 -0
  297. package/src/engine-account-paths.ts +11 -0
  298. package/src/engine-models.ts +39 -0
  299. package/src/engine-usage.ts +104 -0
  300. package/src/event-outbox.ts +185 -0
  301. package/src/index.ts +2899 -0
  302. package/src/load-env.ts +97 -0
  303. package/src/machine-tunnel-route.ts +156 -0
  304. package/src/main.ts +6349 -0
  305. package/src/never-proxied-domains.ts +31 -0
  306. package/src/outbound-frame.ts +31 -0
  307. package/src/paths.ts +81 -0
  308. package/src/project-env-latest.ts +130 -0
  309. package/src/project-env-policy.ts +396 -0
  310. package/src/project-env-wording.ts +53 -0
  311. package/src/protocol.ts +5545 -0
  312. package/src/runtime-bootstrap.ts +165 -0
  313. package/src/station-identity.ts +96 -0
  314. package/src/task-env-names.ts +28 -0
  315. package/src/task-secrets.ts +75 -0
  316. package/src/tunnel-dial.ts +102 -0
  317. package/src/ui/server.ts +852 -0
  318. package/src/ui/types.ts +173 -0
  319. package/src/workspace-path-policy.ts +41 -0
  320. package/ui/app.js +1102 -0
  321. package/ui/index.html +90 -0
  322. package/ui/style.css +685 -0
  323. package/ui/uai-favicon.svg +21 -0
  324. package/ui/uai-logo-black.svg +9 -0
  325. package/ui/uai-wheel.svg +9 -0
  326. package/bin/moondeck-station.mjs +0 -3
@@ -0,0 +1,116 @@
1
+ import type { TaskMachineRetireReason } from "../src/protocol";
2
+ import type { TaskAssignment } from "./cloud-credential-leases";
3
+
4
+ export type TaskFilesMachineRetirementReason = TaskMachineRetireReason;
5
+
6
+ export interface TaskFilesMachineRetirementRequest {
7
+ assignment: Readonly<TaskAssignment>;
8
+ reason: TaskFilesMachineRetirementReason;
9
+ error: Error;
10
+ }
11
+
12
+ /**
13
+ * Cutover-owned terminal path for a one-task machine whose Files boundary
14
+ * cannot be proved closed. The handler must synchronously quarantine local
15
+ * task work, then durably retry an authenticated exact-run machine-retirement
16
+ * request until the cloud acknowledges it or the machine dies. Exiting only
17
+ * the sidecar is explicitly insufficient: its systemd unit uses
18
+ * KillMode=process and leaves the task processes alive.
19
+ */
20
+ export type TaskFilesMachineRetirementHandler = (
21
+ request: TaskFilesMachineRetirementRequest,
22
+ ) => undefined;
23
+
24
+ let handler: TaskFilesMachineRetirementHandler | null = null;
25
+ let retiring: TaskFilesMachineRetirementRequest | null = null;
26
+ let damaged: Error | null = null;
27
+ let handedOff = false;
28
+
29
+ export function setTaskFilesMachineRetirementHandler(
30
+ next: TaskFilesMachineRetirementHandler | null,
31
+ ): void {
32
+ handler = next;
33
+ }
34
+
35
+ /** Files v1 must not be advertised, or activation entered, without this. */
36
+ export function taskFilesMachineRetirementReady(): boolean {
37
+ return handler !== null && retiring === null && damaged === null;
38
+ }
39
+
40
+ /** An unreadable same-boot outbox record may be an irreversible retirement
41
+ * request. With no safe exact run to reconstruct, keep every task-process
42
+ * admission closed for the rest of this boot. */
43
+ export function latchDamagedTaskFilesMachineRetirement(error: Error): void {
44
+ damaged ??= error;
45
+ }
46
+
47
+ /**
48
+ * The self provider calls this immediately around every task-process
49
+ * admission. Once terminal Files retirement wins, no in-flight command may
50
+ * repopulate the service cgroup after the retirement handler proves it empty.
51
+ */
52
+ export function assertTaskFilesMachineProcessAllowed(): void {
53
+ if (damaged !== null) {
54
+ throw new Error("task-machine Files retirement state is damaged", {
55
+ cause: damaged,
56
+ });
57
+ }
58
+ if (retiring !== null) {
59
+ throw new Error("task-machine Files retirement is pending");
60
+ }
61
+ }
62
+
63
+ export function requestTaskFilesMachineRetirement(
64
+ request: TaskFilesMachineRetirementRequest,
65
+ ): void {
66
+ if (damaged !== null) {
67
+ throw new Error("task-machine Files retirement state is damaged", {
68
+ cause: damaged,
69
+ });
70
+ }
71
+ if (!handler) {
72
+ throw new Error("task-machine Files retirement is not installed", {
73
+ cause: request.error,
74
+ });
75
+ }
76
+ if (retiring === null) {
77
+ // First exact failure wins before any callback: a reconnect/capability
78
+ // pass in the callback's first await can no longer advertise this machine.
79
+ retiring = request;
80
+ } else if (
81
+ retiring.assignment.taskId !== request.assignment.taskId ||
82
+ retiring.assignment.runEpoch !== request.assignment.runEpoch
83
+ ) {
84
+ return;
85
+ }
86
+ if (handedOff) return;
87
+ // A synchronous handoff failure must be retryable without reopening the
88
+ // capability or letting a later assignment replace the first failure.
89
+ // Once this returns, the handler owns durable retries itself.
90
+ const result: unknown = handler(retiring);
91
+ if (result !== undefined) {
92
+ // A JavaScript caller can evade the type even though `undefined` (rather
93
+ // than `void`) rejects async handlers in TypeScript. Observe a returned
94
+ // promise so its later rejection is not unhandled, but never count an
95
+ // asynchronous callback as the required synchronous handoff.
96
+ if (
97
+ typeof result === "object" &&
98
+ result !== null &&
99
+ "then" in result &&
100
+ typeof (result as { then?: unknown }).then === "function"
101
+ ) {
102
+ void Promise.resolve(result).catch(() => {});
103
+ }
104
+ throw new Error("task-machine Files retirement must be handed off synchronously");
105
+ }
106
+ handedOff = true;
107
+ }
108
+
109
+ /** Tests model a fresh machine process without weakening the production
110
+ * one-way latch. */
111
+ export function resetTaskFilesMachineRetirementForTests(): void {
112
+ handler = null;
113
+ retiring = null;
114
+ damaged = null;
115
+ handedOff = false;
116
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ accessSync,
3
+ chmodSync,
4
+ constants,
5
+ lstatSync,
6
+ mkdirSync,
7
+ statfsSync,
8
+ } from "node:fs";
9
+
10
+ export const TASK_FILES_MOUNT_S3_PATH = "/usr/bin/mount-s3";
11
+ export const TASK_FILES_FUSE_PATH = "/dev/fuse";
12
+ export const TASK_FILES_HELPER_PATHS = [
13
+ "/usr/bin/python3",
14
+ "/usr/bin/env",
15
+ "/usr/bin/mountpoint",
16
+ "/usr/bin/umount",
17
+ "/bin/sh",
18
+ ] as const;
19
+ export const TASK_FILES_RUNTIME_ROOT = "/run";
20
+ export const TASK_FILES_SESSION_ROOT = "/run/uai";
21
+
22
+ // linux/magic.h: credentials may be written only to a memory-backed runtime.
23
+ export const TMPFS_MAGIC = 0x01021994;
24
+
25
+ export type TaskFilesV1Readiness =
26
+ | { ready: true }
27
+ | {
28
+ ready: false;
29
+ reason:
30
+ | "not-task-machine"
31
+ | "not-root"
32
+ | "mount-s3-unavailable"
33
+ | "fuse-unavailable"
34
+ | "helper-unavailable"
35
+ | "run-not-tmpfs"
36
+ | "session-root-unsafe";
37
+ };
38
+
39
+ export interface TaskFilesV1ReadinessDeps {
40
+ getuid(): number;
41
+ access(path: string, mode: number): void;
42
+ prepareSessionRoot(): void;
43
+ statfs(path: string): { type: number | bigint };
44
+ }
45
+
46
+ function prepareSessionRoot(): void {
47
+ try {
48
+ mkdirSync(TASK_FILES_SESSION_ROOT, { mode: 0o700 });
49
+ } catch (error) {
50
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
51
+ }
52
+ const info = lstatSync(TASK_FILES_SESSION_ROOT);
53
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== 0) {
54
+ throw new Error("the Files session root is not a root-owned directory");
55
+ }
56
+ chmodSync(TASK_FILES_SESSION_ROOT, 0o700);
57
+ }
58
+
59
+ const DEFAULT_DEPS: TaskFilesV1ReadinessDeps = {
60
+ getuid: () => process.getuid?.() ?? -1,
61
+ access: accessSync,
62
+ prepareSessionRoot,
63
+ statfs: (path) => statfsSync(path),
64
+ };
65
+
66
+ /**
67
+ * Local half of the Files-v1 capability check. Cloud mode is irrelevant: a
68
+ * machine advertises what this process can safely do before activation. Keep
69
+ * this synchronous so the connect capability frame cannot race a later
70
+ * prerequisite probe.
71
+ */
72
+ export function taskFilesV1Readiness(
73
+ taskMachine: boolean,
74
+ deps: TaskFilesV1ReadinessDeps = DEFAULT_DEPS,
75
+ ): TaskFilesV1Readiness {
76
+ if (!taskMachine) return { ready: false, reason: "not-task-machine" };
77
+
78
+ let uid: number;
79
+ try {
80
+ uid = deps.getuid();
81
+ } catch {
82
+ return { ready: false, reason: "not-root" };
83
+ }
84
+ if (uid !== 0) return { ready: false, reason: "not-root" };
85
+
86
+ try {
87
+ deps.access(TASK_FILES_MOUNT_S3_PATH, constants.X_OK);
88
+ } catch {
89
+ return { ready: false, reason: "mount-s3-unavailable" };
90
+ }
91
+
92
+ try {
93
+ deps.access(TASK_FILES_FUSE_PATH, constants.R_OK | constants.W_OK);
94
+ } catch {
95
+ return { ready: false, reason: "fuse-unavailable" };
96
+ }
97
+
98
+ try {
99
+ for (const path of TASK_FILES_HELPER_PATHS) {
100
+ deps.access(path, constants.X_OK);
101
+ }
102
+ } catch {
103
+ return { ready: false, reason: "helper-unavailable" };
104
+ }
105
+
106
+ try {
107
+ if (Number(deps.statfs(TASK_FILES_RUNTIME_ROOT).type) !== TMPFS_MAGIC) {
108
+ return { ready: false, reason: "run-not-tmpfs" };
109
+ }
110
+ } catch {
111
+ return { ready: false, reason: "run-not-tmpfs" };
112
+ }
113
+
114
+ try {
115
+ deps.prepareSessionRoot();
116
+ // `/run` being tmpfs is insufficient: an existing `/run/uai` may be a
117
+ // bind mount onto persistent storage. Capability advertisement proves
118
+ // the exact directory in which the session will be written.
119
+ if (Number(deps.statfs(TASK_FILES_SESSION_ROOT).type) !== TMPFS_MAGIC) {
120
+ return { ready: false, reason: "session-root-unsafe" };
121
+ }
122
+ } catch {
123
+ return { ready: false, reason: "session-root-unsafe" };
124
+ }
125
+
126
+ return { ready: true };
127
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The port the runner's task gateway listens on (`UAI_MCP_GATEWAY_PORT`,
3
+ * 5877 by default): where task forwarders relay, and where a machine's
4
+ * sidecar serves its own task directly on loopback. A module of its own so
5
+ * the secrets proxy can name it without loading the gateway.
6
+ */
7
+
8
+ const DEFAULT_MCP_GATEWAY_PORT = 5877;
9
+
10
+ function gatewayPort(value: string | undefined): number {
11
+ if (value === undefined || !/^\d+$/.test(value)) {
12
+ return DEFAULT_MCP_GATEWAY_PORT;
13
+ }
14
+ const port = Number(value);
15
+ return Number.isSafeInteger(port) && port >= 1 && port <= 65_535
16
+ ? port
17
+ : DEFAULT_MCP_GATEWAY_PORT;
18
+ }
19
+
20
+ export const MCP_GATEWAY_PORT = gatewayPort(
21
+ process.env.UAI_MCP_GATEWAY_PORT,
22
+ );
@@ -0,0 +1,190 @@
1
+ import {
2
+ createHmac,
3
+ timingSafeEqual,
4
+ type KeyObject,
5
+ } from "node:crypto";
6
+
7
+ /**
8
+ * Binary, versioned task-forwarder handshake (ADR-153).
9
+ *
10
+ * The leading NUL cannot be the first byte of either HTTP/1.1 or TLS. That
11
+ * lets the shared gateway reject direct TLS while keeping the existing MCP
12
+ * HTTP listener on the same port. Length-prefixed fields keep unauthenticated
13
+ * input bounded before a task identity has been proved.
14
+ */
15
+ export const TASK_FORWARDER_PREAMBLE_MAGIC = Buffer.from([
16
+ 0x00, 0x55, 0x41, 0x49, 0x46, 0x57, 0x44, 0x01,
17
+ ]);
18
+ export const TASK_FORWARDER_CHALLENGE_MAGIC = Buffer.from("UAICHL01", "ascii");
19
+ export const TASK_FORWARDER_RESPONSE_MAGIC = Buffer.from("UAIRES01", "ascii");
20
+ export const TASK_FORWARDER_ACCEPT_MAGIC = Buffer.from("UAIOK001", "ascii");
21
+
22
+ export const TASK_FORWARDER_SECRET_BYTES = 32;
23
+ /** Numerical container uid reserved for the forwarder (neither root nor node). */
24
+ export const TASK_FORWARDER_UID = 65_532;
25
+ export const TASK_FORWARDER_NONCE_BYTES = 32;
26
+ export const TASK_FORWARDER_MAC_BYTES = 32;
27
+ export const TASK_FORWARDER_MAX_TASK_ID_BYTES = 128;
28
+ export const TASK_FORWARDER_MAX_GENERATION_BYTES = 64;
29
+ export const TASK_FORWARDER_PREAMBLE_HEADER_BYTES =
30
+ TASK_FORWARDER_PREAMBLE_MAGIC.length + 2;
31
+ export const TASK_FORWARDER_MAX_PREAMBLE_BYTES =
32
+ TASK_FORWARDER_PREAMBLE_HEADER_BYTES +
33
+ TASK_FORWARDER_MAX_TASK_ID_BYTES +
34
+ TASK_FORWARDER_MAX_GENERATION_BYTES;
35
+ export const TASK_FORWARDER_CHALLENGE_BYTES =
36
+ TASK_FORWARDER_CHALLENGE_MAGIC.length + TASK_FORWARDER_NONCE_BYTES;
37
+ export const TASK_FORWARDER_RESPONSE_BYTES =
38
+ TASK_FORWARDER_RESPONSE_MAGIC.length + TASK_FORWARDER_MAC_BYTES;
39
+
40
+ const FORWARDER_HMAC_DOMAIN = Buffer.from(
41
+ "uai-task-forwarder-v1\0",
42
+ "ascii",
43
+ );
44
+ const FIELD_SEPARATOR = Buffer.from([0]);
45
+ const FORWARDER_GENERATION = /^[A-Za-z0-9_-]{16,64}$/;
46
+
47
+ export interface TaskForwarderIdentity {
48
+ taskId: string;
49
+ generation: string;
50
+ }
51
+
52
+ export function encodeTaskForwarderPreamble(
53
+ identity: TaskForwarderIdentity,
54
+ ): Buffer {
55
+ const taskId = Buffer.from(identity.taskId, "utf8");
56
+ const generation = Buffer.from(identity.generation, "ascii");
57
+ if (
58
+ taskId.length < 1 ||
59
+ taskId.length > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
60
+ generation.length < 1 ||
61
+ generation.length > TASK_FORWARDER_MAX_GENERATION_BYTES ||
62
+ !FORWARDER_GENERATION.test(identity.generation)
63
+ ) {
64
+ throw new Error("invalid task forwarder identity");
65
+ }
66
+ return Buffer.concat([
67
+ TASK_FORWARDER_PREAMBLE_MAGIC,
68
+ Buffer.from([taskId.length, generation.length]),
69
+ taskId,
70
+ generation,
71
+ ]);
72
+ }
73
+
74
+ export function taskForwarderPreambleLength(header: Buffer): number | null {
75
+ if (
76
+ header.length !== TASK_FORWARDER_PREAMBLE_HEADER_BYTES ||
77
+ !header.subarray(0, TASK_FORWARDER_PREAMBLE_MAGIC.length).equals(
78
+ TASK_FORWARDER_PREAMBLE_MAGIC,
79
+ )
80
+ ) {
81
+ return null;
82
+ }
83
+ const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
84
+ const generationBytes =
85
+ header[TASK_FORWARDER_PREAMBLE_MAGIC.length + 1] ?? 0;
86
+ if (
87
+ taskIdBytes < 1 ||
88
+ taskIdBytes > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
89
+ generationBytes < 1 ||
90
+ generationBytes > TASK_FORWARDER_MAX_GENERATION_BYTES
91
+ ) {
92
+ return null;
93
+ }
94
+ return TASK_FORWARDER_PREAMBLE_HEADER_BYTES + taskIdBytes + generationBytes;
95
+ }
96
+
97
+ export function decodeTaskForwarderPreamble(
98
+ preamble: Buffer,
99
+ ): TaskForwarderIdentity | null {
100
+ const header = preamble.subarray(0, TASK_FORWARDER_PREAMBLE_HEADER_BYTES);
101
+ const expectedLength = taskForwarderPreambleLength(header);
102
+ if (expectedLength === null || preamble.length !== expectedLength) return null;
103
+
104
+ const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
105
+ const taskStart = TASK_FORWARDER_PREAMBLE_HEADER_BYTES;
106
+ const generationStart = taskStart + taskIdBytes;
107
+ const rawTaskId = preamble.subarray(taskStart, generationStart);
108
+ const rawGeneration = preamble.subarray(generationStart);
109
+ const taskId = rawTaskId.toString("utf8");
110
+ const generation = rawGeneration.toString("ascii");
111
+ // Both Node decoders replace or mask malformed bytes. Require exact
112
+ // round-trips so two wire identities can never alias the same HMAC input.
113
+ if (
114
+ !Buffer.from(taskId, "utf8").equals(rawTaskId) ||
115
+ !Buffer.from(generation, "ascii").equals(rawGeneration) ||
116
+ !FORWARDER_GENERATION.test(generation)
117
+ ) {
118
+ return null;
119
+ }
120
+ return { taskId, generation };
121
+ }
122
+
123
+ export function encodeTaskForwarderChallenge(nonce: Buffer): Buffer {
124
+ if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
125
+ throw new Error("invalid task forwarder nonce");
126
+ }
127
+ return Buffer.concat([TASK_FORWARDER_CHALLENGE_MAGIC, nonce]);
128
+ }
129
+
130
+ export function decodeTaskForwarderChallenge(
131
+ challenge: Buffer,
132
+ ): Buffer | null {
133
+ if (
134
+ challenge.length !== TASK_FORWARDER_CHALLENGE_BYTES ||
135
+ !challenge.subarray(0, TASK_FORWARDER_CHALLENGE_MAGIC.length).equals(
136
+ TASK_FORWARDER_CHALLENGE_MAGIC,
137
+ )
138
+ ) {
139
+ return null;
140
+ }
141
+ return Buffer.from(challenge.subarray(TASK_FORWARDER_CHALLENGE_MAGIC.length));
142
+ }
143
+
144
+ export function taskForwarderMac(
145
+ secret: Buffer | KeyObject,
146
+ identity: TaskForwarderIdentity,
147
+ nonce: Buffer,
148
+ ): Buffer {
149
+ if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
150
+ throw new Error("invalid task forwarder nonce");
151
+ }
152
+ return createHmac("sha256", secret)
153
+ .update(FORWARDER_HMAC_DOMAIN)
154
+ .update(identity.taskId, "utf8")
155
+ .update(FIELD_SEPARATOR)
156
+ .update(identity.generation, "ascii")
157
+ .update(FIELD_SEPARATOR)
158
+ .update(nonce)
159
+ .digest();
160
+ }
161
+
162
+ export function encodeTaskForwarderResponse(mac: Buffer): Buffer {
163
+ if (mac.length !== TASK_FORWARDER_MAC_BYTES) {
164
+ throw new Error("invalid task forwarder response");
165
+ }
166
+ return Buffer.concat([TASK_FORWARDER_RESPONSE_MAGIC, mac]);
167
+ }
168
+
169
+ export function decodeTaskForwarderResponse(response: Buffer): Buffer | null {
170
+ if (
171
+ response.length !== TASK_FORWARDER_RESPONSE_BYTES ||
172
+ !response.subarray(0, TASK_FORWARDER_RESPONSE_MAGIC.length).equals(
173
+ TASK_FORWARDER_RESPONSE_MAGIC,
174
+ )
175
+ ) {
176
+ return null;
177
+ }
178
+ return Buffer.from(response.subarray(TASK_FORWARDER_RESPONSE_MAGIC.length));
179
+ }
180
+
181
+ export function verifyTaskForwarderMac(
182
+ expected: Buffer,
183
+ provided: Buffer,
184
+ ): boolean {
185
+ return (
186
+ expected.length === TASK_FORWARDER_MAC_BYTES &&
187
+ provided.length === expected.length &&
188
+ timingSafeEqual(expected, provided)
189
+ );
190
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ activeLoaderHostTasks,
3
+ hostTaskProjectEnvSpec,
4
+ } from "./runtime-state";
5
+ import { taskEnvResolver } from "./task-env-source";
6
+ import { recoverTaskGatewayTask } from "./task-gateway-state";
7
+
8
+ export interface TaskGatewayRecoverySummary {
9
+ readonly recovered: number;
10
+ readonly failed: number;
11
+ }
12
+
13
+ /**
14
+ * Reattach value resolvers before the gateway starts listening. Persisted
15
+ * credentials load inactive, so a malformed row or missing identity stays
16
+ * fail-closed and its surviving forwarder cannot authenticate.
17
+ */
18
+ export function recoverActiveTaskGateways(): TaskGatewayRecoverySummary {
19
+ let recovered = 0;
20
+ let failed = 0;
21
+ for (const task of activeLoaderHostTasks()) {
22
+ try {
23
+ const spec = hostTaskProjectEnvSpec(task);
24
+ if (spec === null) {
25
+ throw new Error("loader task has no persisted project env spec");
26
+ }
27
+ // The generation serves the run its runtime was established for.
28
+ const runEpoch = task.runEpoch > 0 ? task.runEpoch : null;
29
+ const identity = recoverTaskGatewayTask(task.taskId, {
30
+ resolveEnv: taskEnvResolver(task.taskId, spec, runEpoch),
31
+ runEpoch,
32
+ });
33
+ if (identity === null) {
34
+ throw new Error("loader task has no valid persisted gateway identity");
35
+ }
36
+ recovered += 1;
37
+ } catch (error) {
38
+ failed += 1;
39
+ console.warn(
40
+ `[task-gateway] task ${task.taskId}: boot recovery stayed closed: ${
41
+ error instanceof Error ? error.message : String(error)
42
+ }`,
43
+ );
44
+ }
45
+ }
46
+ return { recovered, failed };
47
+ }