@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,332 @@
1
+ /**
2
+ * ADR-154 §3 on the sign-in box: write one capture as one immutable revision,
3
+ * exactly where the cloud's flow says, and never read it back.
4
+ *
5
+ * The cloud begins every capture (lib/credential-metadata.ts) and hands the
6
+ * box the revision's parameter names. The box checks them, builds the two
7
+ * parts from what the sign-in produced, and creates them, broker-only part
8
+ * first:
9
+ *
10
+ * - lease-readable: what a task may receive: a static key as entered, or an
11
+ * OAuth login's brokered copy (access token in, refresh token replaced);
12
+ * - broker-only (OAuth logins only): the full material, which only the
13
+ * broker reads and only the broker ever overwrites.
14
+ *
15
+ * Both are created, never overwritten (`putImmutable`), with tags naming the
16
+ * workspace and person the role policy checks. The answer carries names and
17
+ * versions, never material.
18
+ */
19
+ import {
20
+ ENGINE_CREDENTIAL_FILES,
21
+ ENGINE_ENV_CREDENTIAL_KEYS,
22
+ MAX_CREDENTIAL_VALUE_BYTES,
23
+ parseCaptureFlowWire,
24
+ serializeLeasedValue,
25
+ type CaptureFlowWire,
26
+ type CaptureResultWire,
27
+ type CredentialLocation,
28
+ } from "../src/cloud-credentials";
29
+ import { parseCustomConnectionMaterial, type CustomConnectionMaterial } from "../src/custom-connections";
30
+ import { ImmutableParameterAbortedError, storeFailureKind, type CredentialStore } from "./credential-store";
31
+ import { laneFor } from "./token-lanes/registry";
32
+ import {
33
+ emptyRefreshMeta,
34
+ refreshMetaFrom,
35
+ type CredentialFiles,
36
+ type RefreshMeta,
37
+ } from "./token-lanes/types";
38
+
39
+ /** What only the broker reads: the full material of an OAuth login. */
40
+ export interface BrokerEngineValue {
41
+ v: 1;
42
+ part: "broker";
43
+ location: CredentialLocation;
44
+ class: "engine";
45
+ lane: string;
46
+ files: CredentialFiles;
47
+ refresh: RefreshMeta;
48
+ }
49
+
50
+ export type EngineCaptureMaterial =
51
+ | { authKind: "env"; env: Record<string, string> }
52
+ | { authKind: "config-dir"; files: CredentialFiles }
53
+ | { authKind: "custom-connection"; connection: CustomConnectionMaterial };
54
+
55
+ export interface BuiltCapture {
56
+ lease: string;
57
+ broker: string | null;
58
+ expiresAt: number | null;
59
+ /** Metadata the lease part also carries (never material): an SSH key's
60
+ * public half, so a retry can tell which key the revision holds. The
61
+ * identity tags always win over these. */
62
+ leaseTags?: Record<string, string>;
63
+ }
64
+
65
+ export type ParsedCaptureFlow = CaptureFlowWire & { location: CredentialLocation };
66
+
67
+ export function parseCaptureFlow(value: unknown): ParsedCaptureFlow | null {
68
+ return parseCaptureFlowWire(value);
69
+ }
70
+
71
+ function onlyKeys(record: Record<string, string>, allowed: readonly string[]): boolean {
72
+ const keys = Object.keys(record);
73
+ return keys.length > 0 && keys.every((key) => allowed.includes(key));
74
+ }
75
+
76
+ /**
77
+ * The two parts of an AI account's capture, or an error the sign-in shows.
78
+ * The flow's broker part must exist exactly when the material rotates: a
79
+ * rotating login without one would leave its refresh token nowhere the
80
+ * broker can reach (and a task holding it would refresh it itself), and a
81
+ * static key needs none.
82
+ */
83
+ export function buildEngineCapture(
84
+ flow: ParsedCaptureFlow,
85
+ material: EngineCaptureMaterial,
86
+ ): BuiltCapture | { error: string } {
87
+ const { location } = flow;
88
+ if (location.class !== "engine") {
89
+ return { error: "This capture is not an AI account." };
90
+ }
91
+ const kind = location.engineKind!;
92
+ if (material.authKind === "custom-connection") {
93
+ const connection = parseCustomConnectionMaterial(material.connection);
94
+ if (!connection || connection.engine !== kind || flow.brokerParameter !== null) {
95
+ return { error: "unsupported" };
96
+ }
97
+ return {
98
+ lease: serializeLeasedValue({ v: 1, part: "lease", location, class: "engine", authKind: "custom-connection", connection }),
99
+ broker: null,
100
+ expiresAt: connection.expiresAt ?? null,
101
+ };
102
+ }
103
+ if (material.authKind === "env") {
104
+ if (!onlyKeys(material.env, ENGINE_ENV_CREDENTIAL_KEYS[kind] ?? [])) {
105
+ return { error: "This key is not one the engine reads." };
106
+ }
107
+ if (flow.brokerParameter !== null) {
108
+ return { error: "A pasted key does not rotate; it has no broker part." };
109
+ }
110
+ return {
111
+ lease: serializeLeasedValue({
112
+ v: 1,
113
+ part: "lease",
114
+ location,
115
+ class: "engine",
116
+ authKind: "env",
117
+ env: { ...material.env },
118
+ }),
119
+ broker: null,
120
+ expiresAt: null,
121
+ };
122
+ }
123
+ if (!onlyKeys(material.files, ENGINE_CREDENTIAL_FILES[kind] ?? [])) {
124
+ return { error: "The sign-in produced files the engine does not use." };
125
+ }
126
+ const lane = laneFor(
127
+ { kind, authKind: "config-dir", id: location.credentialId!, scope: location.scope },
128
+ material.files,
129
+ );
130
+ if (lane === null) {
131
+ if (flow.brokerParameter !== null) {
132
+ return { error: "This credential does not rotate; it has no broker part." };
133
+ }
134
+ return {
135
+ lease: serializeLeasedValue({
136
+ v: 1,
137
+ part: "lease",
138
+ location,
139
+ class: "engine",
140
+ authKind: "config-dir",
141
+ files: { ...material.files },
142
+ expiresAt: null,
143
+ }),
144
+ broker: null,
145
+ expiresAt: null,
146
+ };
147
+ }
148
+ if (flow.brokerParameter === null) {
149
+ return { error: "This sign-in rotates, and the cloud gave it nowhere to keep its refresh token." };
150
+ }
151
+ const expiresAt = lane.expiresAt(material.files);
152
+ const broker: BrokerEngineValue = {
153
+ v: 1,
154
+ part: "broker",
155
+ location,
156
+ class: "engine",
157
+ lane: lane.id,
158
+ files: { ...material.files },
159
+ refresh: { ...emptyRefreshMeta(lane.id), expiresAt },
160
+ };
161
+ return {
162
+ lease: serializeLeasedValue({
163
+ v: 1,
164
+ part: "lease",
165
+ location,
166
+ class: "engine",
167
+ authKind: "config-dir",
168
+ files: lane.brokeredCopy(material.files),
169
+ expiresAt,
170
+ }),
171
+ broker: JSON.stringify(broker),
172
+ expiresAt,
173
+ };
174
+ }
175
+
176
+ /** The tags every part carries: the identity a retry is checked against,
177
+ * and the workspace and person the role policy compares. */
178
+ export function captureTags(
179
+ flow: ParsedCaptureFlow,
180
+ part: "lease" | "broker",
181
+ ): Record<string, string> {
182
+ return {
183
+ ...(flow.orgId !== null ? { org: flow.orgId } : {}),
184
+ ...(flow.userId !== null ? { user: flow.userId } : {}),
185
+ uai_credential: flow.credentialId,
186
+ uai_revision: flow.revision,
187
+ uai_part: part,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Create both parts, broker-only first: a crash after it leaves a revision
193
+ * the cloud never promotes, which the sweep removes; a lease-readable part
194
+ * never exists without the refresh token it was brokered from. A retry after
195
+ * a lost answer finds its own create by its tags and carries on.
196
+ */
197
+ export async function writeCapture(
198
+ store: CredentialStore,
199
+ flow: ParsedCaptureFlow,
200
+ built: BuiltCapture,
201
+ options: { signal?: AbortSignal } = {},
202
+ ): Promise<CaptureResultWire> {
203
+ if (!store.putImmutable) {
204
+ throw new Error("This credential store cannot create immutable parameters.");
205
+ }
206
+ // A caller's deadline covers the writes too: past it nothing more is sent,
207
+ // and a part already sent is the cloud's to sweep with the revision.
208
+ const { signal } = options;
209
+ if (built.broker !== null) {
210
+ if (flow.brokerParameter === null) throw new Error("no broker part to write");
211
+ if (signal?.aborted) throw new ImmutableParameterAbortedError();
212
+ await store.putImmutable(flow.brokerParameter, built.broker, captureTags(flow, "broker"), { signal });
213
+ }
214
+ if (signal?.aborted) throw new ImmutableParameterAbortedError();
215
+ await store.putImmutable(
216
+ flow.leaseParameter,
217
+ built.lease,
218
+ { ...built.leaseTags, ...captureTags(flow, "lease") },
219
+ { signal },
220
+ );
221
+ return {
222
+ credentialId: flow.credentialId,
223
+ revision: flow.revision,
224
+ // Created, never overwritten by the box: version 1. Only the broker
225
+ // writes later versions.
226
+ leaseVersion: 1,
227
+ expiresAt: built.expiresAt,
228
+ };
229
+ }
230
+
231
+ export interface CaptureOutcome {
232
+ ok: boolean;
233
+ message: string;
234
+ /** Present exactly when `ok`. */
235
+ capture?: CaptureResultWire;
236
+ }
237
+
238
+ /**
239
+ * The sign-in box's sink for an AI account's capture: check the flow, build
240
+ * both parts, create them. Every failure is a message the sign-in shows, and
241
+ * none of them carries material.
242
+ */
243
+ export async function captureEngineAccount(
244
+ store: CredentialStore | null,
245
+ wire: CaptureFlowWire,
246
+ engine: string,
247
+ material: EngineCaptureMaterial,
248
+ engineLabel: string,
249
+ ): Promise<CaptureOutcome> {
250
+ const flow = parseCaptureFlow(wire);
251
+ if (!flow || flow.location.class !== "engine" || flow.location.engineKind !== engine) {
252
+ return { ok: false, message: "The sign-in's destination is invalid; nothing was kept." };
253
+ }
254
+ if (!store) {
255
+ return { ok: false, message: "This station has no credential store to hold the account." };
256
+ }
257
+ const built = buildEngineCapture(flow, material);
258
+ if ("error" in built) return { ok: false, message: built.error };
259
+ try {
260
+ const capture = await writeCapture(store, flow, built);
261
+ return { ok: true, message: `${engineLabel} signed in.`, capture };
262
+ } catch (err) {
263
+ // A category, never the error's words: a store failure can echo input.
264
+ console.warn(`[capture-flow] ${engine} ${flow.credentialId}/${flow.revision}: write failed (${storeFailureKind(err)})`);
265
+ return {
266
+ ok: false,
267
+ message: "The credential store refused the write; nothing was kept on this station.",
268
+ };
269
+ }
270
+ }
271
+
272
+
273
+ /**
274
+ * A stored broker-only part, exactly, for the revision at `expected`; null
275
+ * for anything else. Only the broker reads it (token-broker-revision.ts).
276
+ */
277
+ export function parseBrokerEngineValue(
278
+ raw: string,
279
+ expected: CredentialLocation,
280
+ ): BrokerEngineValue | null {
281
+ if (typeof raw !== "string" || new TextEncoder().encode(raw).length > MAX_CREDENTIAL_VALUE_BYTES) {
282
+ return null;
283
+ }
284
+ let parsed: unknown;
285
+ try {
286
+ parsed = JSON.parse(raw);
287
+ } catch {
288
+ return null;
289
+ }
290
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
291
+ const o = parsed as Record<string, unknown>;
292
+ const location = o.location as Record<string, unknown> | null;
293
+ if (
294
+ o.v !== 1 ||
295
+ o.part !== "broker" ||
296
+ o.class !== "engine" ||
297
+ expected.class !== "engine" ||
298
+ typeof o.lane !== "string" ||
299
+ !location ||
300
+ typeof location !== "object" ||
301
+ location.class !== expected.class ||
302
+ location.scope !== expected.scope ||
303
+ location.ownerId !== expected.ownerId ||
304
+ location.engineKind !== expected.engineKind ||
305
+ location.credentialId !== expected.credentialId ||
306
+ location.revision !== expected.revision
307
+ ) {
308
+ return null;
309
+ }
310
+ const allowed = ENGINE_CREDENTIAL_FILES[expected.engineKind ?? ""] ?? [];
311
+ const files = o.files as Record<string, unknown> | null;
312
+ if (
313
+ !files ||
314
+ typeof files !== "object" ||
315
+ Array.isArray(files) ||
316
+ Object.keys(files).length === 0 ||
317
+ !Object.entries(files).every(([name, text]) => allowed.includes(name) && typeof text === "string")
318
+ ) {
319
+ return null;
320
+ }
321
+ const refresh = refreshMetaFrom(o.refresh);
322
+ if (!refresh || refresh.lane !== o.lane) return null;
323
+ return {
324
+ v: 1,
325
+ part: "broker",
326
+ location: expected,
327
+ class: "engine",
328
+ lane: o.lane,
329
+ files: files as CredentialFiles,
330
+ refresh,
331
+ };
332
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Fail-closed boundary between the in-memory lease registry and the durable
3
+ * task credential cleanup procedure.
4
+ *
5
+ * The registry describes why its gate closed and whether the cleanup is
6
+ * selective or whole-task. The durable coordinator uses a smaller audit
7
+ * vocabulary and represents selectivity solely by the presence of
8
+ * `credentialIds`. Keep that lossy conversion here so a new registry reason
9
+ * cannot accidentally become a whole-task cleanup (or vice versa).
10
+ */
11
+ import type {
12
+ CredentialCleanupReason as LeaseCredentialCleanupReason,
13
+ CredentialCleanupRequest as LeaseCredentialCleanupRequest,
14
+ } from "./cloud-credential-leases";
15
+ import type {
16
+ TaskCredentialCleanupCoordinator,
17
+ TaskCredentialCleanupReason,
18
+ TaskCredentialCleanupRequest,
19
+ } from "./task-credential-cleanup";
20
+
21
+ export type CredentialCleanupRunnerKind = "owned" | "task-machine";
22
+
23
+ type LeaseCleanupMode = LeaseCredentialCleanupRequest["mode"];
24
+ type CleanupReasonByMode = Readonly<
25
+ Record<LeaseCleanupMode, TaskCredentialCleanupReason | null>
26
+ >;
27
+
28
+ /**
29
+ * `null` marks a reason/mode pair the registry must never emit. The `satisfies`
30
+ * clause makes additions to either the reason union or the mode union a
31
+ * compile-time decision at this boundary.
32
+ */
33
+ const CLEANUP_REASON_BY_MODE = {
34
+ starting: { selective: null, full: null },
35
+ recovery: { selective: null, full: "restart" },
36
+ superseded: { selective: null, full: "assignment" },
37
+ "minimum-revision": { selective: "removed", full: "removed" },
38
+ "manifest-narrowed": { selective: null, full: "removed" },
39
+ denied: { selective: null, full: "denied" },
40
+ expired: { selective: null, full: "expired" },
41
+ stopped: { selective: null, full: "stop" },
42
+ parked: { selective: null, full: "park" },
43
+ teardown: { selective: null, full: "teardown" },
44
+ // The registry emits this fail-closed fallback when its synchronous gate
45
+ // callback throws during assignment start/recovery. The durable vocabulary
46
+ // has no internal-callback-failure reason; `restart` is the conservative
47
+ // whole-task recovery category and, critically, lets the coordinator write
48
+ // its pending witness before retrying the quarantine.
49
+ "cleanup-failed": { selective: null, full: "restart" },
50
+ // A launch slot that could not be retired in its runtime (ADR-154 §6):
51
+ // the whole run's cleanup, in the same conservative category.
52
+ "fence-failed": { selective: null, full: "restart" },
53
+ // Shutdown ends assignments without a cleanup: the next process's
54
+ // recovery sweep runs it.
55
+ shutdown: { selective: null, full: null },
56
+ "lease-replaced": { selective: "removed", full: null },
57
+ } as const satisfies Readonly<
58
+ Record<LeaseCredentialCleanupReason, CleanupReasonByMode>
59
+ >;
60
+
61
+ function mappedReason(
62
+ reason: LeaseCredentialCleanupReason,
63
+ mode: LeaseCleanupMode,
64
+ ): TaskCredentialCleanupReason {
65
+ const mapped = CLEANUP_REASON_BY_MODE[reason][mode];
66
+ if (mapped === null) {
67
+ throw new Error(`invalid ${mode} lease credential cleanup reason: ${reason}`);
68
+ }
69
+ return mapped;
70
+ }
71
+
72
+ /** Convert one registry callback into the coordinator's durable request. */
73
+ export function adaptLeaseCredentialCleanupRequest(
74
+ request: Readonly<LeaseCredentialCleanupRequest>,
75
+ runnerKind: CredentialCleanupRunnerKind,
76
+ ): TaskCredentialCleanupRequest {
77
+ const reason = mappedReason(request.reason, request.mode);
78
+
79
+ if (request.mode === "selective") {
80
+ if (request.removeFilesSession) {
81
+ throw new Error("selective credential cleanup cannot remove the files session");
82
+ }
83
+ if (request.removed.length === 0) {
84
+ throw new Error("selective credential cleanup requires a removed credential");
85
+ }
86
+ // Several revisions of one stable credential can retire before a floor
87
+ // grant lands. Inventory is keyed by the stable id, so collapse those
88
+ // qualified identities rather than widening or rejecting the cleanup.
89
+ const credentialIds = [
90
+ ...new Set(request.removed.map((identity) => identity.credentialId)),
91
+ ];
92
+ return {
93
+ taskId: request.assignment.taskId,
94
+ runEpoch: request.assignment.runEpoch,
95
+ reason,
96
+ credentialIds,
97
+ };
98
+ }
99
+
100
+ if (request.removed.length !== 0) {
101
+ throw new Error("full credential cleanup cannot carry selective identities");
102
+ }
103
+ const expectedFilesSession = runnerKind === "task-machine";
104
+ if (request.removeFilesSession !== expectedFilesSession) {
105
+ throw new Error(
106
+ `${runnerKind} full credential cleanup has the wrong files-session scope`,
107
+ );
108
+ }
109
+ return {
110
+ taskId: request.assignment.taskId,
111
+ runEpoch: request.assignment.runEpoch,
112
+ reason,
113
+ };
114
+ }
115
+
116
+ /** Callback with the exact shape consumed by TaskCredentialLeaseRegistry. */
117
+ export function createLeaseCredentialCleanupAdapter(
118
+ coordinator: Pick<TaskCredentialCleanupCoordinator, "cleanup">,
119
+ runnerKind: CredentialCleanupRunnerKind,
120
+ ): (request: LeaseCredentialCleanupRequest) => Promise<void> {
121
+ return (request) =>
122
+ coordinator.cleanup(adaptLeaseCredentialCleanupRequest(request, runnerKind));
123
+ }