@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,490 @@
1
+ /**
2
+ * ADR-154's durable, non-secret inventory of task-local credential paths.
3
+ *
4
+ * Writers remember a path before placing credential material there. Cleanup
5
+ * later removes the row only after it has independently verified the path is
6
+ * absent. This module records the witness; it deliberately does not perform
7
+ * cleanup or inspect credential contents.
8
+ */
9
+ import { and, asc, eq, lte, sql } from "drizzle-orm";
10
+
11
+ import { idPathSegment } from "../src/cloud-credentials";
12
+ import type {
13
+ HostCredentialCleanupPath,
14
+ HostCredentialCleanupPending,
15
+ HostCredentialCleanupRestop,
16
+ HostLeasedEngineRuntime,
17
+ } from "../db/schema";
18
+ import { getDb, schema, type Db } from "./db";
19
+ import { assertSafeHostTaskId } from "./task-identity";
20
+ import {
21
+ LEASE_AWS_CONFIG_FILE,
22
+ LEASE_CREDENTIALS_FILE,
23
+ } from "./lease";
24
+ import {
25
+ serializeTaskEnvironmentLocator,
26
+ type TaskEnvironmentLocator,
27
+ } from "./task-environment/types";
28
+
29
+ type InventoryExecutor = Pick<Db, "select" | "insert" | "delete">;
30
+
31
+ export interface CredentialCleanupPathKey {
32
+ taskId: string;
33
+ runEpoch: number;
34
+ runtime: TaskEnvironmentLocator;
35
+ lane: CredentialCleanupLane;
36
+ path: string;
37
+ }
38
+
39
+ export type CredentialCleanupLane =
40
+ | "engine-auth"
41
+ | "github"
42
+ | "ssh"
43
+ | "files-session";
44
+
45
+ export type CredentialCleanupPendingReason =
46
+ | "removed"
47
+ | "denied"
48
+ | "expired"
49
+ | "assignment"
50
+ | "restart"
51
+ | "stop"
52
+ | "park"
53
+ | "teardown";
54
+
55
+ const CLEANUP_PENDING_REASONS = new Set<CredentialCleanupPendingReason>([
56
+ "removed",
57
+ "denied",
58
+ "expired",
59
+ "assignment",
60
+ "restart",
61
+ "stop",
62
+ "park",
63
+ "teardown",
64
+ ]);
65
+
66
+ export interface CredentialCleanupPendingInput {
67
+ taskId: string;
68
+ runEpoch: number;
69
+ reason: CredentialCleanupPendingReason;
70
+ }
71
+
72
+ export interface CredentialCleanupPathInput extends CredentialCleanupPathKey {
73
+ credentialId: string;
74
+ }
75
+
76
+ function positiveRunEpoch(value: number): void {
77
+ if (!Number.isSafeInteger(value) || value < 1) {
78
+ throw new Error("credential cleanup run epoch must be a positive safe integer");
79
+ }
80
+ }
81
+
82
+ function normalizedPending(input: CredentialCleanupPendingInput): void {
83
+ assertSafeHostTaskId(input.taskId);
84
+ positiveRunEpoch(input.runEpoch);
85
+ if (!CLEANUP_PENDING_REASONS.has(input.reason)) {
86
+ throw new Error("credential cleanup reason is invalid");
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Durable fail-closed witness written before cleanup closes any gate or starts
92
+ * any asynchronous work. A newer assignment can only raise the recorded
93
+ * epoch; a delayed older cleanup must never move the boot-recovery floor back.
94
+ */
95
+ export function rememberCredentialCleanupPending(
96
+ input: CredentialCleanupPendingInput,
97
+ db: InventoryExecutor = getDb(),
98
+ now = Date.now(),
99
+ ): void {
100
+ normalizedPending(input);
101
+ db.insert(schema.hostCredentialCleanupPending)
102
+ .values({
103
+ taskId: input.taskId,
104
+ runEpoch: input.runEpoch,
105
+ reason: input.reason,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ })
109
+ .onConflictDoUpdate({
110
+ target: schema.hostCredentialCleanupPending.taskId,
111
+ set: {
112
+ runEpoch: sql`max(${schema.hostCredentialCleanupPending.runEpoch}, excluded.run_epoch)`,
113
+ reason: sql`CASE WHEN excluded.run_epoch >= ${schema.hostCredentialCleanupPending.runEpoch} THEN excluded.reason ELSE ${schema.hostCredentialCleanupPending.reason} END`,
114
+ updatedAt: now,
115
+ },
116
+ })
117
+ .run();
118
+ }
119
+
120
+ export function listCredentialCleanupPending(
121
+ db: InventoryExecutor = getDb(),
122
+ ): HostCredentialCleanupPending[] {
123
+ return db
124
+ .select()
125
+ .from(schema.hostCredentialCleanupPending)
126
+ .orderBy(asc(schema.hostCredentialCleanupPending.taskId))
127
+ .all();
128
+ }
129
+
130
+ /** Clear only a marker no newer than the cleanup that just succeeded. */
131
+ export function forgetCredentialCleanupPending(
132
+ taskId: string,
133
+ throughRunEpoch: number,
134
+ db: InventoryExecutor = getDb(),
135
+ ): void {
136
+ assertSafeHostTaskId(taskId);
137
+ positiveRunEpoch(throughRunEpoch);
138
+ db.delete(schema.hostCredentialCleanupPending)
139
+ .where(
140
+ and(
141
+ eq(schema.hostCredentialCleanupPending.taskId, taskId),
142
+ lte(schema.hostCredentialCleanupPending.runEpoch, throughRunEpoch),
143
+ ),
144
+ )
145
+ .run();
146
+ }
147
+
148
+ /** An engine account's file, or the intermediate its writer stages
149
+ * (`.uai-tmp`) before renaming it into place. */
150
+ const CONNECTION_AUTH_PATH = /^\/home\/node\/\.uai-connection\/[a-f0-9]{64}(?:\/(?:auth\.json|config\.toml|aws-config|aws-credentials))?$/;
151
+ const ENGINE_AUTH_PATH = /^\/home\/node\/(?:\.codex(?:-acct-[A-Za-z0-9_-]+)?(?:-agent-[A-Za-z0-9_-]+)?|\.grok(?:-acct-[A-Za-z0-9_-]+)?|\.local\/share\/opencode(?:-acct-[A-Za-z0-9_-]+)?)\/auth\.json(?:\.uai-tmp)?$/;
152
+ /** A file a writer stages (`.uai-tmp`) before renaming it into place. */
153
+ function andStaged(...paths: string[]): string[] {
154
+ return paths.flatMap((path) => [path, `${path}.uai-tmp`]);
155
+ }
156
+ const GITHUB_PATHS = new Set(
157
+ andStaged("/home/node/.config/gh/hosts.yml", "/home/node/.config/gh/uai.gitconfig"),
158
+ );
159
+ const SSH_PATHS = new Set(
160
+ andStaged(
161
+ "/home/node/.ssh/id_ed25519",
162
+ "/home/node/.ssh/id_ed25519.pub",
163
+ "/home/node/.ssh/allowed_signers",
164
+ "/home/node/.ssh/uai-signing.gitconfig",
165
+ "/home/node/.ssh/uai-transport.gitconfig",
166
+ ),
167
+ );
168
+ const FILES_SESSION_PATHS = new Set([
169
+ LEASE_CREDENTIALS_FILE,
170
+ `${LEASE_CREDENTIALS_FILE}.uai-tmp`,
171
+ LEASE_AWS_CONFIG_FILE,
172
+ `${LEASE_AWS_CONFIG_FILE}.uai-tmp`,
173
+ ]);
174
+
175
+ /** Destructive cleanup accepts only lane-owned files. The executor repeats
176
+ * this check after reconstructing the runtime and resolves every parent
177
+ * symlink-safely before deletion; persistence never turns an arbitrary task
178
+ * path into cleanup authority. */
179
+ export function assertCredentialCleanupPath(
180
+ lane: CredentialCleanupLane,
181
+ value: string,
182
+ ): void {
183
+ if (
184
+ typeof value !== "string" ||
185
+ value.length === 0 ||
186
+ value.length > 4096 ||
187
+ value.includes("\0") ||
188
+ value.split("/").some((part) => part === "." || part === "..")
189
+ ) {
190
+ throw new Error("credential cleanup path is invalid");
191
+ }
192
+ const allowed =
193
+ (lane === "engine-auth" && (ENGINE_AUTH_PATH.test(value) || CONNECTION_AUTH_PATH.test(value))) ||
194
+ (lane === "github" && GITHUB_PATHS.has(value)) ||
195
+ (lane === "ssh" && SSH_PATHS.has(value)) ||
196
+ (lane === "files-session" && FILES_SESSION_PATHS.has(value));
197
+ if (!allowed) {
198
+ throw new Error("credential cleanup path is outside its managed lane");
199
+ }
200
+ }
201
+
202
+ function normalizedKey(input: CredentialCleanupPathKey): {
203
+ taskId: string;
204
+ runEpoch: number;
205
+ runtimeLocator: string;
206
+ lane: CredentialCleanupLane;
207
+ path: string;
208
+ } {
209
+ assertSafeHostTaskId(input.taskId);
210
+ positiveRunEpoch(input.runEpoch);
211
+ assertCredentialCleanupPath(input.lane, input.path);
212
+ return {
213
+ taskId: input.taskId,
214
+ runEpoch: input.runEpoch,
215
+ runtimeLocator: serializeTaskEnvironmentLocator(input.runtime),
216
+ lane: input.lane,
217
+ path: input.path,
218
+ };
219
+ }
220
+
221
+ /** Record before writing the credential path. Re-recording the same exact
222
+ * runtime path is idempotent and preserves its first-seen timestamp. */
223
+ export function rememberCredentialCleanupPath(
224
+ input: CredentialCleanupPathInput,
225
+ db: InventoryExecutor = getDb(),
226
+ now = Date.now(),
227
+ ): void {
228
+ // The shared path encoder has the contract's exact bounded id grammar.
229
+ idPathSegment(input.credentialId);
230
+ const key = normalizedKey(input);
231
+ db.insert(schema.hostCredentialCleanupPaths)
232
+ .values({
233
+ ...key,
234
+ credentialId: input.credentialId,
235
+ createdAt: now,
236
+ updatedAt: now,
237
+ })
238
+ .onConflictDoUpdate({
239
+ target: [
240
+ schema.hostCredentialCleanupPaths.taskId,
241
+ schema.hostCredentialCleanupPaths.runEpoch,
242
+ schema.hostCredentialCleanupPaths.runtimeLocator,
243
+ schema.hostCredentialCleanupPaths.path,
244
+ ],
245
+ set: { credentialId: input.credentialId, updatedAt: now },
246
+ })
247
+ .run();
248
+ }
249
+
250
+ /** All outstanding cleanup work for a task, oldest assignment first. */
251
+ export function listCredentialCleanupPaths(
252
+ taskId: string,
253
+ db: InventoryExecutor = getDb(),
254
+ ): HostCredentialCleanupPath[] {
255
+ assertSafeHostTaskId(taskId);
256
+ return db
257
+ .select()
258
+ .from(schema.hostCredentialCleanupPaths)
259
+ .where(eq(schema.hostCredentialCleanupPaths.taskId, taskId))
260
+ .orderBy(
261
+ asc(schema.hostCredentialCleanupPaths.runEpoch),
262
+ asc(schema.hostCredentialCleanupPaths.runtimeLocator),
263
+ asc(schema.hostCredentialCleanupPaths.path),
264
+ )
265
+ .all();
266
+ }
267
+
268
+ /** Boot recovery must not depend on a current manifest or even a surviving
269
+ * host-task row. Return the entire non-secret worklist in deterministic order. */
270
+ export function listAllCredentialCleanupPaths(
271
+ db: InventoryExecutor = getDb(),
272
+ ): HostCredentialCleanupPath[] {
273
+ return db
274
+ .select()
275
+ .from(schema.hostCredentialCleanupPaths)
276
+ .orderBy(
277
+ asc(schema.hostCredentialCleanupPaths.taskId),
278
+ asc(schema.hostCredentialCleanupPaths.runEpoch),
279
+ asc(schema.hostCredentialCleanupPaths.runtimeLocator),
280
+ asc(schema.hostCredentialCleanupPaths.path),
281
+ )
282
+ .all();
283
+ }
284
+
285
+ /**
286
+ * Value-free boot worklist. A marker keeps a task recoverable even when a
287
+ * crash happened before its first path was inventoried, or after every path
288
+ * was deleted but before in-memory material was dropped.
289
+ */
290
+ export function listCredentialCleanupRecoveryTasks(
291
+ db: InventoryExecutor = getDb(),
292
+ ): Array<{ taskId: string; runEpoch: number }> {
293
+ const tasks = new Map<string, number>();
294
+ for (const pending of listCredentialCleanupPending(db)) {
295
+ tasks.set(pending.taskId, pending.runEpoch);
296
+ }
297
+ for (const row of listAllCredentialCleanupPaths(db)) {
298
+ tasks.set(row.taskId, Math.max(tasks.get(row.taskId) ?? 0, row.runEpoch));
299
+ }
300
+ // A leased engine on an env-token account leaves no path: the record of
301
+ // where it ran is the evidence its task needs a cleanup after a crash.
302
+ for (const row of listAllLeasedEngineRuntimes(db)) {
303
+ tasks.set(row.taskId, Math.max(tasks.get(row.taskId) ?? 0, row.runEpoch));
304
+ }
305
+ // A container the boot sweep started and has not yet proven stopped again
306
+ // is owed its stop, whatever paths remain.
307
+ for (const row of listAllCredentialCleanupRestops(db)) {
308
+ tasks.set(row.taskId, Math.max(tasks.get(row.taskId) ?? 0, row.runEpoch));
309
+ }
310
+ return [...tasks]
311
+ .sort(([left], [right]) => left.localeCompare(right))
312
+ .map(([taskId, runEpoch]) => ({ taskId, runEpoch }));
313
+ }
314
+
315
+ /** Persistence primitive for the later cleanup executor. The executor is
316
+ * responsible for proving absence before it calls this. */
317
+ export function forgetCredentialCleanupPath(
318
+ input: CredentialCleanupPathKey,
319
+ db: InventoryExecutor = getDb(),
320
+ ): void {
321
+ const key = normalizedKey(input);
322
+ db.delete(schema.hostCredentialCleanupPaths)
323
+ .where(
324
+ and(
325
+ eq(schema.hostCredentialCleanupPaths.taskId, key.taskId),
326
+ eq(schema.hostCredentialCleanupPaths.runEpoch, key.runEpoch),
327
+ eq(
328
+ schema.hostCredentialCleanupPaths.runtimeLocator,
329
+ key.runtimeLocator,
330
+ ),
331
+ eq(schema.hostCredentialCleanupPaths.lane, key.lane),
332
+ eq(schema.hostCredentialCleanupPaths.path, key.path),
333
+ ),
334
+ )
335
+ .run();
336
+ }
337
+
338
+ /**
339
+ * ADR-154 §6: record, before a leased engine starts in it, the runtime it
340
+ * starts in for this run, so the cleanup proves its engines stopped there
341
+ * even when they left no file to inventory. Idempotent.
342
+ */
343
+ export function rememberLeasedEngineRuntime(
344
+ input: { taskId: string; runEpoch: number; runtime: TaskEnvironmentLocator },
345
+ db: InventoryExecutor = getDb(),
346
+ now = Date.now(),
347
+ ): void {
348
+ assertSafeHostTaskId(input.taskId);
349
+ positiveRunEpoch(input.runEpoch);
350
+ db.insert(schema.hostLeasedEngineRuntimes)
351
+ .values({
352
+ taskId: input.taskId,
353
+ runEpoch: input.runEpoch,
354
+ runtimeLocator: serializeTaskEnvironmentLocator(input.runtime),
355
+ createdAt: now,
356
+ })
357
+ .onConflictDoNothing()
358
+ .run();
359
+ }
360
+
361
+ /** Every runtime a leased engine of the task was started in, oldest run first. */
362
+ export function listLeasedEngineRuntimes(
363
+ taskId: string,
364
+ db: InventoryExecutor = getDb(),
365
+ ): HostLeasedEngineRuntime[] {
366
+ assertSafeHostTaskId(taskId);
367
+ return db
368
+ .select()
369
+ .from(schema.hostLeasedEngineRuntimes)
370
+ .where(eq(schema.hostLeasedEngineRuntimes.taskId, taskId))
371
+ .orderBy(
372
+ asc(schema.hostLeasedEngineRuntimes.runEpoch),
373
+ asc(schema.hostLeasedEngineRuntimes.runtimeLocator),
374
+ )
375
+ .all();
376
+ }
377
+
378
+ /** Every runtime a leased engine of any task was started in. */
379
+ export function listAllLeasedEngineRuntimes(
380
+ db: InventoryExecutor = getDb(),
381
+ ): HostLeasedEngineRuntime[] {
382
+ return db
383
+ .select()
384
+ .from(schema.hostLeasedEngineRuntimes)
385
+ .orderBy(
386
+ asc(schema.hostLeasedEngineRuntimes.taskId),
387
+ asc(schema.hostLeasedEngineRuntimes.runEpoch),
388
+ asc(schema.hostLeasedEngineRuntimes.runtimeLocator),
389
+ )
390
+ .all();
391
+ }
392
+
393
+ /** After a whole-task cleanup proved every engine of these runs stopped in
394
+ * these runtimes: forget them, never a newer run's. */
395
+ export function forgetLeasedEngineRuntimes(
396
+ taskId: string,
397
+ throughRunEpoch: number,
398
+ runtimeLocators: readonly string[],
399
+ db: InventoryExecutor = getDb(),
400
+ ): void {
401
+ assertSafeHostTaskId(taskId);
402
+ positiveRunEpoch(throughRunEpoch);
403
+ for (const runtimeLocator of runtimeLocators) {
404
+ db.delete(schema.hostLeasedEngineRuntimes)
405
+ .where(
406
+ and(
407
+ eq(schema.hostLeasedEngineRuntimes.taskId, taskId),
408
+ lte(schema.hostLeasedEngineRuntimes.runEpoch, throughRunEpoch),
409
+ eq(schema.hostLeasedEngineRuntimes.runtimeLocator, runtimeLocator),
410
+ ),
411
+ )
412
+ .run();
413
+ }
414
+ }
415
+
416
+ /**
417
+ * The boot sweep's owed re-stop, recorded BEFORE it starts a stopped app
418
+ * container to clean it (`host_credential_cleanup_restops`). Idempotent.
419
+ */
420
+ export function rememberCredentialCleanupRestop(
421
+ input: { taskId: string; runEpoch: number; runtimeLocator: string; containerId: string },
422
+ db: InventoryExecutor = getDb(),
423
+ now = Date.now(),
424
+ ): void {
425
+ assertSafeHostTaskId(input.taskId);
426
+ positiveRunEpoch(input.runEpoch);
427
+ if (!/^[0-9a-f]{64}$/.test(input.containerId)) {
428
+ throw new Error("an owed re-stop names a container by its full id");
429
+ }
430
+ if (input.runtimeLocator.length === 0) {
431
+ throw new Error("an owed re-stop names its runtime");
432
+ }
433
+ db.insert(schema.hostCredentialCleanupRestops)
434
+ .values({
435
+ taskId: input.taskId,
436
+ containerId: input.containerId,
437
+ runEpoch: input.runEpoch,
438
+ runtimeLocator: input.runtimeLocator,
439
+ createdAt: now,
440
+ })
441
+ .onConflictDoNothing()
442
+ .run();
443
+ }
444
+
445
+ /** The task's owed re-stops, oldest first. */
446
+ export function listCredentialCleanupRestops(
447
+ taskId: string,
448
+ db: InventoryExecutor = getDb(),
449
+ ): HostCredentialCleanupRestop[] {
450
+ assertSafeHostTaskId(taskId);
451
+ return db
452
+ .select()
453
+ .from(schema.hostCredentialCleanupRestops)
454
+ .where(eq(schema.hostCredentialCleanupRestops.taskId, taskId))
455
+ .orderBy(
456
+ asc(schema.hostCredentialCleanupRestops.createdAt),
457
+ asc(schema.hostCredentialCleanupRestops.containerId),
458
+ )
459
+ .all();
460
+ }
461
+
462
+ /** Every owed re-stop of any task. */
463
+ export function listAllCredentialCleanupRestops(
464
+ db: InventoryExecutor = getDb(),
465
+ ): HostCredentialCleanupRestop[] {
466
+ return db
467
+ .select()
468
+ .from(schema.hostCredentialCleanupRestops)
469
+ .orderBy(
470
+ asc(schema.hostCredentialCleanupRestops.taskId),
471
+ asc(schema.hostCredentialCleanupRestops.containerId),
472
+ )
473
+ .all();
474
+ }
475
+
476
+ /** Only once this exact container is proven stopped again, or gone. */
477
+ export function forgetCredentialCleanupRestop(
478
+ input: { taskId: string; containerId: string },
479
+ db: InventoryExecutor = getDb(),
480
+ ): void {
481
+ assertSafeHostTaskId(input.taskId);
482
+ db.delete(schema.hostCredentialCleanupRestops)
483
+ .where(
484
+ and(
485
+ eq(schema.hostCredentialCleanupRestops.taskId, input.taskId),
486
+ eq(schema.hostCredentialCleanupRestops.containerId, input.containerId),
487
+ ),
488
+ )
489
+ .run();
490
+ }