@moondeck/station 0.0.1 → 1.0.1

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 (330) 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/0031_first_turn_receipts.sql +9 -0
  36. package/db/migrations/meta/_journal.json +230 -0
  37. package/db/schema.ts +871 -0
  38. package/images/standard/Dockerfile +347 -0
  39. package/images/standard/README.md +167 -0
  40. package/images/standard/container/code-server-settings.json +63 -0
  41. package/images/standard/container/corepack-version +1 -0
  42. package/images/standard/container/uai-init +704 -0
  43. package/images/standard/container/uai-materialize-runtimes +1527 -0
  44. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  45. package/images/standard/tool-versions +2 -0
  46. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  47. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  48. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  49. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  50. package/lib/agent-cli.ts +1507 -0
  51. package/lib/agent-errors.ts +22 -0
  52. package/lib/agent.ts +672 -0
  53. package/lib/agents/attach-compatibility.ts +51 -0
  54. package/lib/agents/claude.ts +918 -0
  55. package/lib/agents/codex-token-usage.fixture.json +130 -0
  56. package/lib/agents/codex.ts +1392 -0
  57. package/lib/agents/cursor-period-usage.ts +253 -0
  58. package/lib/agents/cursor.ts +562 -0
  59. package/lib/agents/dispatch.ts +78 -0
  60. package/lib/agents/durable-proc.ts +334 -0
  61. package/lib/agents/factory.ts +57 -0
  62. package/lib/agents/grok.ts +381 -0
  63. package/lib/agents/kimi.ts +329 -0
  64. package/lib/agents/machine-durable.ts +604 -0
  65. package/lib/agents/mock.ts +133 -0
  66. package/lib/agents/mode.ts +64 -0
  67. package/lib/agents/opencode.ts +508 -0
  68. package/lib/agents/output-source.ts +3 -0
  69. package/lib/agents/proc.ts +221 -0
  70. package/lib/agents/rate-limit.ts +337 -0
  71. package/lib/agents/registry.ts +182 -0
  72. package/lib/agents/session-state.ts +197 -0
  73. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  74. package/lib/agents/transport.ts +741 -0
  75. package/lib/agents/types.ts +254 -0
  76. package/lib/agents/uai.ts +106 -0
  77. package/lib/agents/usage.ts +44 -0
  78. package/lib/apple-one-off.ts +277 -0
  79. package/lib/apple-runtime-recycle.ts +237 -0
  80. package/lib/apple-uninstall-teardown.ts +224 -0
  81. package/lib/assignment-evidence.ts +94 -0
  82. package/lib/attachments.ts +48 -0
  83. package/lib/browser-testing.ts +1537 -0
  84. package/lib/capture-flow.ts +332 -0
  85. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  86. package/lib/cloud-credential-host.ts +512 -0
  87. package/lib/cloud-credential-leases.ts +2587 -0
  88. package/lib/cloud-credential-runtime.ts +1357 -0
  89. package/lib/cloud-state.ts +56 -0
  90. package/lib/codex-auth.ts +456 -0
  91. package/lib/command-db.ts +310 -0
  92. package/lib/compose-network-recovery.ts +283 -0
  93. package/lib/connection-secret-redaction.ts +25 -0
  94. package/lib/container-runtime.ts +1470 -0
  95. package/lib/credential-cleanup-inventory.ts +490 -0
  96. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  97. package/lib/credential-consumers/engines.ts +797 -0
  98. package/lib/credential-consumers/files.ts +934 -0
  99. package/lib/credential-consumers/github.ts +702 -0
  100. package/lib/credential-consumers/mcp.ts +389 -0
  101. package/lib/credential-consumers/task-files.ts +127 -0
  102. package/lib/credential-consumers/task-secrets.ts +10 -0
  103. package/lib/credential-store-pickup.ts +149 -0
  104. package/lib/credential-store-scope.ts +112 -0
  105. package/lib/credential-store.ts +932 -0
  106. package/lib/credentials-activation-authority.ts +709 -0
  107. package/lib/credentials-purge.ts +1006 -0
  108. package/lib/custom-connection-capture.ts +395 -0
  109. package/lib/custom-connection-launch.ts +384 -0
  110. package/lib/custom-connection-runtime.ts +141 -0
  111. package/lib/db.ts +80 -0
  112. package/lib/disk-pressure.ts +42 -0
  113. package/lib/docker-exec.ts +156 -0
  114. package/lib/engine-accounts.ts +1970 -0
  115. package/lib/engine-connect.ts +321 -0
  116. package/lib/engine-credential-env.ts +52 -0
  117. package/lib/engine-login.ts +2933 -0
  118. package/lib/engines.ts +1342 -0
  119. package/lib/enrollment-state.ts +251 -0
  120. package/lib/env-file.ts +246 -0
  121. package/lib/env-schema.ts +937 -0
  122. package/lib/env.ts +195 -0
  123. package/lib/first-turn-receipts.ts +62 -0
  124. package/lib/fs-aliases.ts +49 -0
  125. package/lib/git-clone-credentials.ts +334 -0
  126. package/lib/git-diff.ts +530 -0
  127. package/lib/git-identity.ts +715 -0
  128. package/lib/github-git-auth.ts +207 -0
  129. package/lib/github-origin.ts +28 -0
  130. package/lib/github-tokens.ts +2439 -0
  131. package/lib/host-cloud-url.ts +62 -0
  132. package/lib/host-config.ts +279 -0
  133. package/lib/host-env.ts +424 -0
  134. package/lib/host-logs.ts +965 -0
  135. package/lib/internal-connections.ts +5 -0
  136. package/lib/keyed-promise-tail.ts +23 -0
  137. package/lib/launch-mode.ts +25 -0
  138. package/lib/lease.ts +239 -0
  139. package/lib/legacy-host.ts +769 -0
  140. package/lib/legacy-runtime-retirement.ts +424 -0
  141. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  142. package/lib/machine-exec.ts +238 -0
  143. package/lib/machine-gateway-tunnel.ts +344 -0
  144. package/lib/machine-keys.ts +65 -0
  145. package/lib/machine-project-env-loader.ts +152 -0
  146. package/lib/machine-provider-aws.ts +923 -0
  147. package/lib/machine-provider-local.ts +338 -0
  148. package/lib/machine-provider.ts +90 -0
  149. package/lib/managed-activation-watcher.ts +72 -0
  150. package/lib/managed-install-owner-watcher.ts +55 -0
  151. package/lib/managed-operation-drain.ts +57 -0
  152. package/lib/managed-runtime.ts +4296 -0
  153. package/lib/managed-update-scheduler.ts +125 -0
  154. package/lib/mcp-capture.ts +865 -0
  155. package/lib/mcp-config-lock.ts +2 -0
  156. package/lib/mcp-connections.ts +959 -0
  157. package/lib/mcp-gateway.ts +2594 -0
  158. package/lib/obs.ts +512 -0
  159. package/lib/openrouter-budget.ts +247 -0
  160. package/lib/orchestrator.ts +11409 -0
  161. package/lib/person-capture.ts +327 -0
  162. package/lib/preview-ports.ts +85 -0
  163. package/lib/preview-sidecar.ts +331 -0
  164. package/lib/project-env-authority.ts +1319 -0
  165. package/lib/project-env-feature.ts +74 -0
  166. package/lib/project-env-legacy-tags.ts +130 -0
  167. package/lib/project-env-loader.ts +242 -0
  168. package/lib/project-env-replica.ts +2094 -0
  169. package/lib/project-env-schema-capture.ts +342 -0
  170. package/lib/project-env-staging.ts +148 -0
  171. package/lib/project-env-task-policy.ts +176 -0
  172. package/lib/project-env-task-replica.ts +1752 -0
  173. package/lib/release-feed.ts +61 -0
  174. package/lib/release-manifest.ts +710 -0
  175. package/lib/release-trust.ts +46 -0
  176. package/lib/repo-clone.ts +38 -0
  177. package/lib/routing/shadow.ts +261 -0
  178. package/lib/runner-boot.ts +10 -0
  179. package/lib/runtime-activation-tail.ts +279 -0
  180. package/lib/runtime-archive.ts +1216 -0
  181. package/lib/runtime-authority.ts +166 -0
  182. package/lib/runtime-guard.ts +36 -0
  183. package/lib/runtime-provider-state.ts +169 -0
  184. package/lib/runtime-reconnect.ts +86 -0
  185. package/lib/runtime-state.ts +1033 -0
  186. package/lib/safe-http.ts +364 -0
  187. package/lib/secrets-proxy/address.ts +95 -0
  188. package/lib/secrets-proxy/audit.ts +30 -0
  189. package/lib/secrets-proxy/ca-store.ts +200 -0
  190. package/lib/secrets-proxy/ca.ts +470 -0
  191. package/lib/secrets-proxy/container-steps.ts +138 -0
  192. package/lib/secrets-proxy/der.ts +273 -0
  193. package/lib/secrets-proxy/engine.ts +1633 -0
  194. package/lib/secrets-proxy/host.ts +93 -0
  195. package/lib/secrets-proxy/hostname.ts +57 -0
  196. package/lib/secrets-proxy/placeholders.ts +196 -0
  197. package/lib/secrets-proxy/policy.ts +150 -0
  198. package/lib/secrets-proxy/readiness.ts +148 -0
  199. package/lib/secrets-proxy/runtime.ts +582 -0
  200. package/lib/secrets-proxy/scrub.ts +559 -0
  201. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  202. package/lib/secrets-proxy/task-scripts.ts +262 -0
  203. package/lib/secrets-proxy/task-trust.ts +605 -0
  204. package/lib/secrets-proxy/trust-watch.ts +80 -0
  205. package/lib/secrets.ts +71 -0
  206. package/lib/shared-files-s3.ts +976 -0
  207. package/lib/shared-files.ts +156 -0
  208. package/lib/sidecar-assign.ts +49 -0
  209. package/lib/sidecar-guard.ts +58 -0
  210. package/lib/sidecar-resume.ts +178 -0
  211. package/lib/sidecar.ts +69 -0
  212. package/lib/signin-box-credentials.ts +465 -0
  213. package/lib/signin-box.ts +114 -0
  214. package/lib/skill-index-example.fixture.ts +56 -0
  215. package/lib/skill-layout.ts +417 -0
  216. package/lib/skills.ts +489 -0
  217. package/lib/ssh.ts +401 -0
  218. package/lib/standard-image-cleanup.ts +117 -0
  219. package/lib/standard-image-pin.ts +207 -0
  220. package/lib/standard-image.ts +1697 -0
  221. package/lib/stopped-task-status-queue.ts +120 -0
  222. package/lib/task-container-cli.ts +269 -0
  223. package/lib/task-credential-cleanup.ts +752 -0
  224. package/lib/task-diff-sandbox.ts +464 -0
  225. package/lib/task-diff.ts +91 -0
  226. package/lib/task-env-forwarder.ts +228 -0
  227. package/lib/task-env-source.ts +249 -0
  228. package/lib/task-environment/anchor.ts +31 -0
  229. package/lib/task-environment/apple-container.ts +860 -0
  230. package/lib/task-environment/docker.ts +1344 -0
  231. package/lib/task-environment/index.ts +466 -0
  232. package/lib/task-environment/legacy-adoption.ts +467 -0
  233. package/lib/task-environment/machine-environment.ts +61 -0
  234. package/lib/task-environment/machine-task-up.ts +1227 -0
  235. package/lib/task-environment/machine.ts +974 -0
  236. package/lib/task-environment/registry.ts +59 -0
  237. package/lib/task-environment/self-identity.ts +20 -0
  238. package/lib/task-environment/self.ts +566 -0
  239. package/lib/task-environment/types.ts +642 -0
  240. package/lib/task-environment/workspace-files.ts +685 -0
  241. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  242. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  243. package/lib/task-files-machine-retirement.ts +116 -0
  244. package/lib/task-files-readiness.ts +127 -0
  245. package/lib/task-gateway-port.ts +22 -0
  246. package/lib/task-gateway-protocol.ts +190 -0
  247. package/lib/task-gateway-recovery.ts +47 -0
  248. package/lib/task-gateway-state.ts +512 -0
  249. package/lib/task-identity.ts +19 -0
  250. package/lib/task-inventory.ts +682 -0
  251. package/lib/task-machine-credentials-activation.ts +95 -0
  252. package/lib/task-machine-swap-safety.ts +729 -0
  253. package/lib/task-person-credentials.ts +76 -0
  254. package/lib/task-project-ops-host.ts +147 -0
  255. package/lib/task-project-ops.ts +1062 -0
  256. package/lib/task-secret-staging.ts +16 -0
  257. package/lib/task-status.ts +46 -0
  258. package/lib/task-token.ts +46 -0
  259. package/lib/token-broker-adopt.ts +344 -0
  260. package/lib/token-broker-lambda.ts +105 -0
  261. package/lib/token-broker-local.ts +392 -0
  262. package/lib/token-broker-revision.ts +310 -0
  263. package/lib/token-broker-ssm.ts +525 -0
  264. package/lib/token-broker.ts +218 -0
  265. package/lib/token-lanes/openai-oauth.ts +123 -0
  266. package/lib/token-lanes/registry.ts +45 -0
  267. package/lib/token-lanes/types.ts +163 -0
  268. package/lib/token-lanes/xai-oauth.ts +165 -0
  269. package/lib/transcript.ts +128 -0
  270. package/lib/tunnel-registry.ts +220 -0
  271. package/lib/tunnel-runtime.ts +235 -0
  272. package/lib/ulid.ts +16 -0
  273. package/lib/user-bus-env.ts +116 -0
  274. package/lib/user-credentials-store.ts +435 -0
  275. package/lib/vnode-pressure.ts +142 -0
  276. package/package.json +115 -8
  277. package/runner/runner.mjs +442 -0
  278. package/scripts/agent/_common.sh +633 -0
  279. package/scripts/agent/env-hook.sh +626 -0
  280. package/scripts/agent/moondeck-cli.sh +5 -0
  281. package/scripts/agent/task-down.sh +291 -0
  282. package/scripts/agent/task-status.sh +70 -0
  283. package/scripts/agent/task-up.sh +3761 -0
  284. package/scripts/agent/uai-env-exec.sh +26 -0
  285. package/scripts/install/darwin.ts +1048 -0
  286. package/scripts/install/linux.ts +958 -0
  287. package/scripts/install/types.ts +78 -0
  288. package/scripts/install/util.ts +254 -0
  289. package/scripts/install/win.ts +143 -0
  290. package/src/activation-respawn.ts +155 -0
  291. package/src/apple-tunnel-route.ts +104 -0
  292. package/src/bridge-diagnostics.ts +39 -0
  293. package/src/capture-wire.ts +102 -0
  294. package/src/cli.ts +2585 -0
  295. package/src/cloud-credentials.ts +2144 -0
  296. package/src/cloud-origins.ts +23 -0
  297. package/src/credential-location.ts +248 -0
  298. package/src/credential-wire-validation.ts +25 -0
  299. package/src/custom-connections.ts +237 -0
  300. package/src/engine-account-paths.ts +11 -0
  301. package/src/engine-models.ts +39 -0
  302. package/src/engine-usage.ts +104 -0
  303. package/src/enrollment-next-steps.ts +24 -0
  304. package/src/event-outbox.ts +185 -0
  305. package/src/index.ts +2907 -0
  306. package/src/load-env.ts +97 -0
  307. package/src/machine-tunnel-route.ts +156 -0
  308. package/src/main.ts +6363 -0
  309. package/src/never-proxied-domains.ts +31 -0
  310. package/src/outbound-frame.ts +31 -0
  311. package/src/paths.ts +81 -0
  312. package/src/project-env-latest.ts +130 -0
  313. package/src/project-env-policy.ts +396 -0
  314. package/src/project-env-wording.ts +53 -0
  315. package/src/protocol.ts +5550 -0
  316. package/src/runtime-bootstrap.ts +165 -0
  317. package/src/station-identity.ts +96 -0
  318. package/src/task-env-names.ts +28 -0
  319. package/src/task-secrets.ts +75 -0
  320. package/src/tunnel-dial.ts +102 -0
  321. package/src/ui/server.ts +852 -0
  322. package/src/ui/types.ts +173 -0
  323. package/src/workspace-path-policy.ts +41 -0
  324. package/ui/app.js +1102 -0
  325. package/ui/index.html +90 -0
  326. package/ui/style.css +685 -0
  327. package/ui/uai-favicon.svg +21 -0
  328. package/ui/uai-logo-black.svg +9 -0
  329. package/ui/uai-wheel.svg +9 -0
  330. package/bin/moondeck-station.mjs +0 -3
package/db/schema.ts ADDED
@@ -0,0 +1,871 @@
1
+ /**
2
+ * Host-local SQLite schema (ADR-020).
3
+ *
4
+ * The cloud DB owns metadata and lifecycle. Each host owns only the runtime
5
+ * fields needed to recover containers, discover local ports, and tunnel
6
+ * editor/preview traffic.
7
+ */
8
+
9
+ import { sql } from "drizzle-orm";
10
+ import {
11
+ blob,
12
+ check,
13
+ index,
14
+ integer,
15
+ primaryKey,
16
+ sqliteTable,
17
+ text,
18
+ } from "drizzle-orm/sqlite-core";
19
+
20
+ export const hostTasks = sqliteTable(
21
+ "uai_host_tasks",
22
+ {
23
+ taskId: text("task_id").primaryKey(),
24
+ // ADR-154: the v1 epoch whose runtime was successfully established.
25
+ // Admission's monotonic floor lives separately so deleting this recovery
26
+ // row can never make an older dispatch current again. 0 means no v1
27
+ // runtime has been proven for this row (including every legacy row).
28
+ runEpoch: integer("run_epoch").notNull().default(0),
29
+ codeServerPort: integer("code_server_port"),
30
+ previewPorts: text("preview_ports").notNull().default("[]"),
31
+ composeProject: text("compose_project"),
32
+ worktreePath: text("worktree_path"),
33
+ // ADR-101 Phase C. Null rows predate the provider seam and are inferred as
34
+ // Docker Compose only when their legacy runtime identity is complete.
35
+ environmentProvider: text("environment_provider"),
36
+ environmentLocator: text("environment_locator"),
37
+ // ADR-153 rollout fence. Null rows predate the loader and mean `baked`.
38
+ envMode: text("env_mode"),
39
+ // Value-free resolver metadata for rebuilding the project-env gateway
40
+ // after a runner restart, before the cloud re-sends task-up.
41
+ projectEnvSpec: text("project_env_spec"),
42
+ // ADR-154 Files v1: the normalized mode from the cloud's task launch.
43
+ // Recovery must never trust the task-writable `.uai/files-mounted`
44
+ // marker as authority for what may be mounted.
45
+ sharedFilesMode: text("shared_files_mode", {
46
+ enum: ["off", "ro", "rw"],
47
+ }),
48
+ statusMirror: text("status_mirror"),
49
+ // Owner + project slugs — received in TaskLaunchInput at task-up and
50
+ // persisted here so the local UI (ADR-028) can show "who/what" without a
51
+ // cloud round-trip. The cloud stays the audit authority.
52
+ ownerUserId: text("owner_user_id"),
53
+ ownerEmail: text("owner_email"),
54
+ ownerName: text("owner_name"),
55
+ projectSlugs: text("project_slugs").notNull().default("[]"),
56
+ // Lead project first. Retained for legacy/baked machine-task agent spawns;
57
+ // loader tasks resolve only declared names through projectEnvSpec.
58
+ projectIds: text("project_ids").notNull().default("[]"),
59
+ // ADR-131 D3: which engine-account scope this task resolves credentials
60
+ // from ("org:<orgId>" on a shared host; null = "host"). Persisted so a
61
+ // host restart re-provisions the same scope before the cloud re-sends it.
62
+ engineAccountScope: text("engine_account_scope"),
63
+ lockedAt: integer("locked_at", { mode: "number" }),
64
+ startedAt: integer("started_at", { mode: "number" }),
65
+ endedAt: integer("ended_at", { mode: "number" }),
66
+ updatedAt: integer("updated_at", { mode: "number" })
67
+ .notNull()
68
+ .default(sql`(unixepoch() * 1000)`),
69
+ },
70
+ (table) => ({
71
+ runEpochCheck: check(
72
+ "uai_host_tasks_run_epoch",
73
+ sql`${table.runEpoch} >= 0`,
74
+ ),
75
+ environmentPair: check(
76
+ "uai_host_tasks_environment_pair",
77
+ sql`(${table.environmentProvider} IS NULL) = (${table.environmentLocator} IS NULL)`,
78
+ ),
79
+ sharedFilesModeCheck: check(
80
+ "uai_host_tasks_shared_files_mode",
81
+ sql`${table.sharedFilesMode} IS NULL OR ${table.sharedFilesMode} IN ('off', 'ro', 'rw')`,
82
+ ),
83
+ }),
84
+ );
85
+
86
+ export type HostTask = typeof hostTasks.$inferSelect;
87
+ export type NewHostTask = typeof hostTasks.$inferInsert;
88
+
89
+ // ADR-154's non-secret assignment authority. It intentionally has no foreign
90
+ // key to uai_host_tasks: terminal-history retention and orphan GC may remove a
91
+ // runtime row, but neither is allowed to erase the highest epoch this runner
92
+ // has admitted or reopen a closed assignment.
93
+ export const hostTaskRunAssignments = sqliteTable(
94
+ "host_task_run_assignments",
95
+ {
96
+ taskId: text("task_id").primaryKey(),
97
+ highestRunEpoch: integer("highest_run_epoch").notNull(),
98
+ assignmentOpen: integer("assignment_open", { mode: "boolean" })
99
+ .notNull()
100
+ .default(true),
101
+ createdAt: integer("created_at", { mode: "number" })
102
+ .notNull()
103
+ .default(sql`(unixepoch() * 1000)`),
104
+ updatedAt: integer("updated_at", { mode: "number" })
105
+ .notNull()
106
+ .default(sql`(unixepoch() * 1000)`),
107
+ },
108
+ (table) => ({
109
+ runEpochCheck: check(
110
+ "host_task_run_assignments_epoch",
111
+ sql`${table.highestRunEpoch} >= 1`,
112
+ ),
113
+ assignmentOpenCheck: check(
114
+ "host_task_run_assignments_open",
115
+ sql`${table.assignmentOpen} IN (0, 1)`,
116
+ ),
117
+ taskIdCheck: check(
118
+ "host_task_run_assignments_task_id",
119
+ sql`length(${table.taskId}) > 0`,
120
+ ),
121
+ }),
122
+ );
123
+
124
+ export type HostTaskRunAssignment =
125
+ typeof hostTaskRunAssignments.$inferSelect;
126
+ export type NewHostTaskRunAssignment =
127
+ typeof hostTaskRunAssignments.$inferInsert;
128
+
129
+ // ADR-154: values never land here. This is only the durable witness needed
130
+ // to remove a task-local credential file after a runner crash. The exact
131
+ // runtime locator is part of the key because a later run may recreate the
132
+ // same task id on a different container or VM.
133
+ export const hostCredentialCleanupPaths = sqliteTable(
134
+ "host_credential_cleanup_paths",
135
+ {
136
+ taskId: text("task_id").notNull(),
137
+ runEpoch: integer("run_epoch").notNull(),
138
+ runtimeLocator: text("runtime_locator").notNull(),
139
+ lane: text("lane", {
140
+ enum: ["engine-auth", "github", "ssh", "files-session"],
141
+ }).notNull(),
142
+ path: text("path").notNull(),
143
+ credentialId: text("credential_id").notNull(),
144
+ createdAt: integer("created_at", { mode: "number" })
145
+ .notNull()
146
+ .default(sql`(unixepoch() * 1000)`),
147
+ updatedAt: integer("updated_at", { mode: "number" })
148
+ .notNull()
149
+ .default(sql`(unixepoch() * 1000)`),
150
+ },
151
+ (table) => ({
152
+ pk: primaryKey({
153
+ columns: [
154
+ table.taskId,
155
+ table.runEpoch,
156
+ table.runtimeLocator,
157
+ table.path,
158
+ ],
159
+ }),
160
+ taskCredentialIdx: index(
161
+ "host_credential_cleanup_paths_task_credential_idx",
162
+ ).on(table.taskId, table.runEpoch, table.credentialId),
163
+ runEpochCheck: check(
164
+ "host_credential_cleanup_paths_run_epoch",
165
+ sql`${table.runEpoch} >= 1`,
166
+ ),
167
+ identityCheck: check(
168
+ "host_credential_cleanup_paths_identity",
169
+ sql`length(${table.taskId}) > 0 AND length(${table.runtimeLocator}) > 0 AND length(${table.credentialId}) > 0`,
170
+ ),
171
+ laneCheck: check(
172
+ "host_credential_cleanup_paths_lane",
173
+ sql`${table.lane} IN ('engine-auth', 'github', 'ssh', 'files-session')`,
174
+ ),
175
+ absolutePathCheck: check(
176
+ "host_credential_cleanup_paths_absolute_path",
177
+ sql`substr(${table.path}, 1, 1) = '/'`,
178
+ ),
179
+ }),
180
+ );
181
+
182
+ export type HostCredentialCleanupPath =
183
+ typeof hostCredentialCleanupPaths.$inferSelect;
184
+ export type NewHostCredentialCleanupPath =
185
+ typeof hostCredentialCleanupPaths.$inferInsert;
186
+
187
+ // A task remains fail-closed across a host crash even when cleanup has not yet
188
+ // reached (or has already deleted) an inventoried path. This row contains no
189
+ // credential value; it is cleared only after the entire cleanup procedure has
190
+ // completed successfully through the recorded epoch.
191
+ export const hostCredentialCleanupPending = sqliteTable(
192
+ "host_credential_cleanup_pending",
193
+ {
194
+ taskId: text("task_id").primaryKey(),
195
+ runEpoch: integer("run_epoch").notNull(),
196
+ reason: text("reason", {
197
+ enum: [
198
+ "removed",
199
+ "denied",
200
+ "expired",
201
+ "assignment",
202
+ "restart",
203
+ "stop",
204
+ "park",
205
+ "teardown",
206
+ ],
207
+ }).notNull(),
208
+ createdAt: integer("created_at", { mode: "number" })
209
+ .notNull()
210
+ .default(sql`(unixepoch() * 1000)`),
211
+ updatedAt: integer("updated_at", { mode: "number" })
212
+ .notNull()
213
+ .default(sql`(unixepoch() * 1000)`),
214
+ },
215
+ (table) => ({
216
+ runEpochCheck: check(
217
+ "host_credential_cleanup_pending_run_epoch",
218
+ sql`${table.runEpoch} >= 1`,
219
+ ),
220
+ reasonCheck: check(
221
+ "host_credential_cleanup_pending_reason",
222
+ sql`${table.reason} IN ('removed', 'denied', 'expired', 'assignment', 'restart', 'stop', 'park', 'teardown')`,
223
+ ),
224
+ taskIdCheck: check(
225
+ "host_credential_cleanup_pending_task_id",
226
+ sql`length(${table.taskId}) > 0`,
227
+ ),
228
+ }),
229
+ );
230
+
231
+ export type HostCredentialCleanupPending =
232
+ typeof hostCredentialCleanupPending.$inferSelect;
233
+ export type NewHostCredentialCleanupPending =
234
+ typeof hostCredentialCleanupPending.$inferInsert;
235
+
236
+ // ADR-154 §6: every runtime a leased engine of a task's run was started in,
237
+ // recorded before it starts. An env-token engine leaves no credential file
238
+ // for the cleanup inventory to name, yet the cleanup must prove it stopped
239
+ // wherever it ran, an older runtime included. Value-free.
240
+ export const hostLeasedEngineRuntimes = sqliteTable(
241
+ "host_leased_engine_runtimes",
242
+ {
243
+ taskId: text("task_id").notNull(),
244
+ runEpoch: integer("run_epoch").notNull(),
245
+ runtimeLocator: text("runtime_locator").notNull(),
246
+ createdAt: integer("created_at", { mode: "number" })
247
+ .notNull()
248
+ .default(sql`(unixepoch() * 1000)`),
249
+ },
250
+ (table) => ({
251
+ pk: primaryKey({
252
+ columns: [table.taskId, table.runEpoch, table.runtimeLocator],
253
+ }),
254
+ runEpochCheck: check(
255
+ "host_leased_engine_runtimes_run_epoch",
256
+ sql`${table.runEpoch} >= 1`,
257
+ ),
258
+ identityCheck: check(
259
+ "host_leased_engine_runtimes_identity",
260
+ sql`length(${table.taskId}) > 0 AND length(${table.runtimeLocator}) > 0`,
261
+ ),
262
+ }),
263
+ );
264
+
265
+ export type HostLeasedEngineRuntime =
266
+ typeof hostLeasedEngineRuntimes.$inferSelect;
267
+
268
+ // The boot sweep's owed re-stops (2026-10-30): a stopped app container the
269
+ // sweep starts only to remove its credential files is recorded here, by its
270
+ // exact id and runtime, BEFORE it starts. Only proof that this container is
271
+ // stopped again, or gone, retires the row; removing a credential path never
272
+ // does. A crash after the start, a start that timed out yet started it, or a
273
+ // stop that failed all leave the row for the next cleanup of the task to
274
+ // settle first. Value-free.
275
+ export const hostCredentialCleanupRestops = sqliteTable(
276
+ "host_credential_cleanup_restops",
277
+ {
278
+ taskId: text("task_id").notNull(),
279
+ containerId: text("container_id").notNull(),
280
+ runEpoch: integer("run_epoch").notNull(),
281
+ runtimeLocator: text("runtime_locator").notNull(),
282
+ createdAt: integer("created_at", { mode: "number" })
283
+ .notNull()
284
+ .default(sql`(unixepoch() * 1000)`),
285
+ },
286
+ (table) => ({
287
+ pk: primaryKey({ columns: [table.taskId, table.containerId] }),
288
+ runEpochCheck: check(
289
+ "host_credential_cleanup_restops_run_epoch",
290
+ sql`${table.runEpoch} >= 1`,
291
+ ),
292
+ identityCheck: check(
293
+ "host_credential_cleanup_restops_identity",
294
+ sql`length(${table.taskId}) > 0 AND length(${table.containerId}) > 0 AND length(${table.runtimeLocator}) > 0`,
295
+ ),
296
+ }),
297
+ );
298
+
299
+ export type HostCredentialCleanupRestop =
300
+ typeof hostCredentialCleanupRestops.$inferSelect;
301
+
302
+ // Singleton host-local settings row (ADR-100). Runtime switches that must be
303
+ // visible before SQLite opens remain in `.env.local`; retention policy belongs
304
+ // here because it governs owner-facing local history only. Correctness
305
+ // tombstones and orphan-GC evidence are never controlled by this setting.
306
+ export const hostSettings = sqliteTable(
307
+ "host_settings",
308
+ {
309
+ id: text("id").primaryKey().default("host"),
310
+ retainTerminalHistory: integer("retain_terminal_history", {
311
+ mode: "boolean",
312
+ })
313
+ .notNull()
314
+ .default(true),
315
+ updatedAt: integer("updated_at", { mode: "number" })
316
+ .notNull()
317
+ .default(sql`(unixepoch() * 1000)`),
318
+ },
319
+ (table) => ({
320
+ singleton: check("host_settings_singleton", sql`${table.id} = 'host'`),
321
+ retentionBoolean: check(
322
+ "host_settings_retention_boolean",
323
+ sql`${table.retainTerminalHistory} IN (0, 1)`,
324
+ ),
325
+ }),
326
+ );
327
+
328
+ export type HostSettings = typeof hostSettings.$inferSelect;
329
+ export type NewHostSettings = typeof hostSettings.$inferInsert;
330
+
331
+ // Host-side task lifecycle event log (ADR-028). HostEvent (the agent-output
332
+ // stream) is forwarded to the cloud, not stored; this table is the local
333
+ // `/api/events` feed — appended on task.created/started/ended (+ ship).
334
+ export const hostEvents = sqliteTable(
335
+ "uai_host_events",
336
+ {
337
+ id: text("id").primaryKey(), // ulid
338
+ taskId: text("task_id").notNull(),
339
+ kind: text("kind").notNull(), // task.created | task.started | task.ended | task.ship
340
+ ts: integer("ts", { mode: "number" })
341
+ .notNull()
342
+ .default(sql`(unixepoch() * 1000)`),
343
+ detail: text("detail"),
344
+ },
345
+ (t) => ({
346
+ taskIdIdx: index("uai_host_events_task_id_idx").on(t.taskId),
347
+ }),
348
+ );
349
+
350
+ export type HostEventRow = typeof hostEvents.$inferSelect;
351
+ export type NewHostEventRow = typeof hostEvents.$inferInsert;
352
+
353
+ // GitHub user refresh token, encrypted at rest with the host master key
354
+ // (ADR-027). The ONLY place a GitHub secret lives at rest. Access tokens are
355
+ // never stored — they go straight into the container's gh config.
356
+ export const githubTokens = sqliteTable("host_github_tokens", {
357
+ userId: text("user_id").primaryKey(),
358
+ installationId: integer("installation_id").notNull(),
359
+ /** Generation of the cloud binding whose credential this row contains.
360
+ * Readers trust the token only while it matches the durable mutation fence
361
+ * below, so a crash between claiming a newer generation and deleting this
362
+ * row leaves a safely inert old ciphertext rather than a usable old token. */
363
+ generation: integer("generation").notNull().default(0),
364
+ // ADR-033: holds a long-lived ACCESS token when kind="access", or a refresh
365
+ // token when kind="refresh" (legacy ADR-027). Encrypted at rest either way.
366
+ refreshTokenCt: blob("refresh_token_ct").notNull(),
367
+ refreshTokenNonce: blob("refresh_token_nonce").notNull(),
368
+ refreshTokenExpiresAt: integer("refresh_token_expires_at"),
369
+ // "access" (non-expiring, ADR-033) | "refresh" (expiring, ADR-027).
370
+ kind: text("kind").notNull().default("refresh"),
371
+ updatedAt: integer("updated_at").notNull(),
372
+ // ADR-134 slice 2: the credential-store parameter this row mirrors to.
373
+ storeRef: text("store_ref"),
374
+ });
375
+
376
+ export type GithubToken = typeof githubTokens.$inferSelect;
377
+ export type NewGithubToken = typeof githubTokens.$inferInsert;
378
+
379
+ // Durable monotonic tombstone for GitHub credential mutations. Kept separate
380
+ // from the token row so a disconnect can reject a delayed older connect even
381
+ // after the encrypted credential itself has been deleted.
382
+ export const githubCredentialGenerations = sqliteTable(
383
+ "host_github_credential_generations",
384
+ {
385
+ userId: text("user_id").primaryKey(),
386
+ generation: integer("generation").notNull().default(0),
387
+ mutation: text("mutation").notNull().default("set"),
388
+ updatedAt: integer("updated_at").notNull(),
389
+ },
390
+ );
391
+
392
+ // Per-user ed25519 SSH key (ADR-029). Generated ON the host; the private key
393
+ // lives encrypted at rest with the host master key (same secret-blind pattern
394
+ // as host_github_tokens — ADR-015). The cloud only ever sees `public_key`,
395
+ // which the user pastes into GitHub. Keyed by the internal cloud user id.
396
+ export const sshKeys = sqliteTable("host_ssh_keys", {
397
+ userId: text("user_id").primaryKey(),
398
+ publicKey: text("public_key").notNull(), // OpenSSH "ssh-ed25519 AAAA… uai-<user>"
399
+ privateKeyCt: blob("private_key_ct").notNull(),
400
+ privateKeyNonce: blob("private_key_nonce").notNull(),
401
+ createdAt: integer("created_at").notNull(),
402
+ updatedAt: integer("updated_at").notNull(),
403
+ // ADR-134 slice 2: the credential-store parameter this row mirrors to.
404
+ storeRef: text("store_ref"),
405
+ });
406
+
407
+ export type SshKey = typeof sshKeys.$inferSelect;
408
+ export type NewSshKey = typeof sshKeys.$inferInsert;
409
+
410
+ // Per-(project, key) environment variable values, encrypted at rest with the
411
+ // host master key (same secret-blind pattern as host_ssh_keys /
412
+ // host_github_tokens — ADR-015). The cloud only ever sees KEY NAMES, never
413
+ // values (values are write-only). Set by project/org members on the project
414
+ // settings page (grouped by host); at task-up each project on the task gets ITS
415
+ // (projectId, key) values injected under the key name. The same key name is
416
+ // independent per project, and a project's values are independent per host.
417
+ export const hostProjectEnv = sqliteTable(
418
+ "host_project_env",
419
+ {
420
+ projectId: text("project_id").notNull(),
421
+ key: text("key").notNull(),
422
+ valueEnc: text("value_enc").notNull(), // sealAesGcm output (base64 ct.nonce)
423
+ updatedAt: integer("updated_at", { mode: "number" })
424
+ .notNull()
425
+ .default(sql`(unixepoch() * 1000)`),
426
+ // ADR-134 slice 3: the credential-store parameter this row mirrors to.
427
+ storeRef: text("store_ref"),
428
+ // The project's org, learned from an env.var.set/list frame that carries
429
+ // it (cloud from 2026-09-10) or from a stored value that does. It tags the
430
+ // mirrored parameter (`org`, `project`) so a task machine's tag-scoped
431
+ // lease can read it (ADR-133). Null until the cloud has said.
432
+ orgId: text("org_id"),
433
+ },
434
+ (t) => ({
435
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
436
+ }),
437
+ );
438
+
439
+ export type HostProjectEnv = typeof hostProjectEnv.$inferSelect;
440
+ export type NewHostProjectEnv = typeof hostProjectEnv.$inferInsert;
441
+
442
+ // ADR-153: value-free authority records live beside (not inside) the legacy
443
+ // encrypted value row. A delete therefore keeps its binding/head forever,
444
+ // while a live owned-runner head always refers to that row.
445
+ export const hostProjectEnvRecords = sqliteTable(
446
+ "host_project_env_records",
447
+ {
448
+ projectId: text("project_id").notNull(),
449
+ key: text("key").notNull(),
450
+ revision: integer("revision").notNull(),
451
+ generation: integer("generation").notNull(),
452
+ tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
453
+ bindingJson: text("binding_json"),
454
+ policyEpoch: integer("policy_epoch"),
455
+ listingGeneration: integer("listing_generation").notNull(),
456
+ updatedAt: integer("updated_at", { mode: "number" })
457
+ .notNull()
458
+ .default(sql`(unixepoch() * 1000)`),
459
+ },
460
+ (t) => ({
461
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
462
+ }),
463
+ );
464
+
465
+ export const hostProjectEnvProjects = sqliteTable("host_project_env_projects", {
466
+ projectId: text("project_id").primaryKey(),
467
+ recordVersion: integer("record_version").notNull().default(0),
468
+ });
469
+
470
+ // Singleton activation, serving gate and ordered-delivery cursor. `modernWrite`
471
+ // is raised only inside a mutation transaction so the permanent legacy table
472
+ // triggers can distinguish this build from an old process still writing it.
473
+ export const hostProjectEnvAuthority = sqliteTable("host_project_env_authority", {
474
+ id: text("id").primaryKey().default("host"),
475
+ activated: integer("activated", { mode: "boolean" }).notNull().default(false),
476
+ gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(true),
477
+ streamId: text("stream_id"),
478
+ appliedSequence: integer("applied_sequence").notNull().default(0),
479
+ attemptId: text("attempt_id"),
480
+ attemptLegacyVersion: integer("attempt_legacy_version"),
481
+ legacyVersion: integer("legacy_version").notNull().default(0),
482
+ policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
483
+ modernWrite: integer("modern_write", { mode: "boolean" }).notNull().default(false),
484
+ updatedAt: integer("updated_at", { mode: "number" })
485
+ .notNull()
486
+ .default(sql`(unixepoch() * 1000)`),
487
+ });
488
+
489
+ // A receipt contains a keyed request MAC and a value-free result. In
490
+ // particular, a set's plaintext is never replayable from SQLite.
491
+ export const hostProjectEnvDeliveryReceipts = sqliteTable(
492
+ "host_project_env_delivery_receipts",
493
+ {
494
+ streamId: text("stream_id").notNull(),
495
+ sequence: integer("sequence").notNull(),
496
+ requestKind: text("request_kind").notNull(),
497
+ requestMac: text("request_mac").notNull(),
498
+ ok: integer("ok", { mode: "boolean" }).notNull(),
499
+ stateJson: text("state_json"),
500
+ recordVersion: integer("record_version"),
501
+ code: text("code"),
502
+ createdAt: integer("created_at", { mode: "number" })
503
+ .notNull()
504
+ .default(sql`(unixepoch() * 1000)`),
505
+ },
506
+ (t) => ({ pk: primaryKey({ columns: [t.streamId, t.sequence] }) }),
507
+ );
508
+
509
+ export const hostProjectEnvLegacyJournal = sqliteTable(
510
+ "host_project_env_legacy_journal",
511
+ {
512
+ id: integer("id").primaryKey({ autoIncrement: true }),
513
+ projectId: text("project_id").notNull(),
514
+ key: text("key").notNull(),
515
+ mutation: text("mutation").notNull(),
516
+ createdAt: integer("created_at", { mode: "number" })
517
+ .notNull()
518
+ .default(sql`(unixepoch() * 1000)`),
519
+ },
520
+ (t) => ({ keyIdx: index("host_project_env_legacy_journal_key_idx").on(t.projectId, t.key) }),
521
+ );
522
+
523
+ // ADR-153 Uai Cloud task-machine replica. These tables are deliberately
524
+ // separate from the owned-runner authority above: a sidecar stages a complete
525
+ // task snapshot while serving nothing, then promotes it atomically. In
526
+ // particular, materialising a cloud value must not fire the owned authority's
527
+ // permanent legacy-write triggers.
528
+ export const hostProjectEnvReplicaAuthority = sqliteTable(
529
+ "host_project_env_replica_authority",
530
+ {
531
+ id: text("id").primaryKey().default("replica"),
532
+ activated: integer("activated", { mode: "boolean" }).notNull().default(false),
533
+ gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(false),
534
+ streamId: text("stream_id"),
535
+ appliedSequence: integer("applied_sequence").notNull().default(0),
536
+ attemptId: text("attempt_id"),
537
+ orgId: text("org_id"),
538
+ policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
539
+ updatedAt: integer("updated_at", { mode: "number" })
540
+ .notNull()
541
+ .default(sql`(unixepoch() * 1000)`),
542
+ },
543
+ (t) => ({
544
+ singleton: check("host_project_env_replica_authority_singleton", sql`${t.id} = 'replica'`),
545
+ booleans: check(
546
+ "host_project_env_replica_authority_booleans",
547
+ sql`${t.activated} IN (0, 1) AND ${t.gateOpen} IN (0, 1)`,
548
+ ),
549
+ cursor: check("host_project_env_replica_authority_cursor", sql`${t.appliedSequence} >= 0`),
550
+ stream: check(
551
+ "host_project_env_replica_authority_stream",
552
+ sql`(${t.activated} = 0 AND ${t.streamId} IS NULL AND ${t.appliedSequence} = 0 AND ${t.attemptId} IS NULL AND ${t.orgId} IS NULL AND ${t.gateOpen} = 0) OR (${t.activated} = 1 AND ${t.streamId} IS NOT NULL AND ${t.orgId} IS NOT NULL)`,
553
+ ),
554
+ gate: check(
555
+ "host_project_env_replica_authority_gate",
556
+ sql`${t.gateOpen} = 0 OR ${t.attemptId} IS NULL`,
557
+ ),
558
+ }),
559
+ );
560
+
561
+ export const hostProjectEnvReplicaProjects = sqliteTable(
562
+ "host_project_env_replica_projects",
563
+ {
564
+ projectId: text("project_id").primaryKey(),
565
+ recordVersion: integer("record_version").notNull(),
566
+ },
567
+ (t) => ({
568
+ version: check("host_project_env_replica_projects_version", sql`${t.recordVersion} >= 0`),
569
+ }),
570
+ );
571
+
572
+ const replicaRecordColumns = () => ({
573
+ projectId: text("project_id").notNull(),
574
+ key: text("key").notNull(),
575
+ revision: integer("revision").notNull(),
576
+ generation: integer("generation").notNull(),
577
+ tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
578
+ valueRefJson: text("value_ref_json"),
579
+ bindingJson: text("binding_json"),
580
+ policyEpoch: integer("policy_epoch"),
581
+ listingGeneration: integer("listing_generation").notNull(),
582
+ materialization: text("materialization").notNull(),
583
+ materializationReason: text("materialization_reason"),
584
+ updatedAt: integer("updated_at", { mode: "number" })
585
+ .notNull()
586
+ .default(sql`(unixepoch() * 1000)`),
587
+ });
588
+
589
+ export const hostProjectEnvReplicaRecords = sqliteTable(
590
+ "host_project_env_replica_records",
591
+ replicaRecordColumns(),
592
+ (t) => ({
593
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
594
+ head: check(
595
+ "host_project_env_replica_records_head",
596
+ sql`(${t.tombstone} = 0 AND ${t.revision} >= 1 AND ${t.generation} >= 1 AND ${t.valueRefJson} IS NOT NULL) OR (${t.tombstone} = 1 AND ${t.revision} = 1 AND ${t.generation} = 0 AND ${t.valueRefJson} IS NULL) OR (${t.tombstone} = 1 AND ${t.revision} >= 2 AND ${t.generation} >= 2 AND ${t.valueRefJson} IS NULL)`,
597
+ ),
598
+ binding: check(
599
+ "host_project_env_replica_records_binding",
600
+ sql`(${t.generation} = 0 AND ${t.bindingJson} IS NULL AND ${t.policyEpoch} IS NULL AND ${t.listingGeneration} >= 2 AND (${t.listingGeneration} % 2) = 0) OR (${t.generation} > 0 AND ${t.policyEpoch} >= 1)`,
601
+ ),
602
+ listing: check(
603
+ "host_project_env_replica_records_listing",
604
+ sql`${t.listingGeneration} >= 0`,
605
+ ),
606
+ boolean: check(
607
+ "host_project_env_replica_records_boolean",
608
+ sql`${t.tombstone} IN (0, 1)`,
609
+ ),
610
+ materialization: check(
611
+ "host_project_env_replica_records_materialization",
612
+ sql`(${t.materialization} = 'installed' AND ${t.tombstone} = 0 AND ${t.materializationReason} IS NULL) OR (${t.materialization} IN ('withheld', 'unavailable') AND ${t.materializationReason} IS NOT NULL)`,
613
+ ),
614
+ }),
615
+ );
616
+
617
+ export const hostProjectEnvReplicaValues = sqliteTable(
618
+ "host_project_env_replica_values",
619
+ {
620
+ projectId: text("project_id").notNull(),
621
+ key: text("key").notNull(),
622
+ revision: integer("revision").notNull(),
623
+ generation: integer("generation").notNull(),
624
+ valueRefJson: text("value_ref_json").notNull(),
625
+ valueEnc: text("value_enc").notNull(),
626
+ updatedAt: integer("updated_at", { mode: "number" })
627
+ .notNull()
628
+ .default(sql`(unixepoch() * 1000)`),
629
+ },
630
+ (t) => ({
631
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
632
+ head: check(
633
+ "host_project_env_replica_values_head",
634
+ sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
635
+ ),
636
+ }),
637
+ );
638
+
639
+ export const hostProjectEnvReplicaStageProjects = sqliteTable(
640
+ "host_project_env_replica_stage_projects",
641
+ {
642
+ attemptId: text("attempt_id").notNull(),
643
+ projectId: text("project_id").notNull(),
644
+ recordVersion: integer("record_version"),
645
+ lastKey: text("last_key"),
646
+ complete: integer("complete", { mode: "boolean" }).notNull().default(false),
647
+ },
648
+ (t) => ({
649
+ pk: primaryKey({ columns: [t.attemptId, t.projectId] }),
650
+ version: check(
651
+ "host_project_env_replica_stage_projects_version",
652
+ sql`${t.recordVersion} IS NULL OR ${t.recordVersion} >= 0`,
653
+ ),
654
+ completeBoolean: check(
655
+ "host_project_env_replica_stage_projects_complete",
656
+ sql`${t.complete} IN (0, 1)`,
657
+ ),
658
+ }),
659
+ );
660
+
661
+ export const hostProjectEnvReplicaStageRecords = sqliteTable(
662
+ "host_project_env_replica_stage_records",
663
+ {
664
+ attemptId: text("attempt_id").notNull(),
665
+ ...replicaRecordColumns(),
666
+ present: integer("present", { mode: "boolean" }).notNull().default(false),
667
+ },
668
+ (t) => ({
669
+ pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
670
+ head: check(
671
+ "host_project_env_replica_stage_records_head",
672
+ sql`(${t.tombstone} = 0 AND ${t.revision} >= 1 AND ${t.generation} >= 1 AND ${t.valueRefJson} IS NOT NULL) OR (${t.tombstone} = 1 AND ${t.revision} = 1 AND ${t.generation} = 0 AND ${t.valueRefJson} IS NULL) OR (${t.tombstone} = 1 AND ${t.revision} >= 2 AND ${t.generation} >= 2 AND ${t.valueRefJson} IS NULL)`,
673
+ ),
674
+ binding: check(
675
+ "host_project_env_replica_stage_records_binding",
676
+ sql`(${t.generation} = 0 AND ${t.bindingJson} IS NULL AND ${t.policyEpoch} IS NULL AND ${t.listingGeneration} >= 2 AND (${t.listingGeneration} % 2) = 0) OR (${t.generation} > 0 AND ${t.policyEpoch} >= 1)`,
677
+ ),
678
+ listing: check(
679
+ "host_project_env_replica_stage_records_listing",
680
+ sql`${t.listingGeneration} >= 0`,
681
+ ),
682
+ boolean: check(
683
+ "host_project_env_replica_stage_records_boolean",
684
+ sql`${t.tombstone} IN (0, 1) AND ${t.present} IN (0, 1)`,
685
+ ),
686
+ materialization: check(
687
+ "host_project_env_replica_stage_records_materialization",
688
+ sql`(${t.materialization} = 'installed' AND ${t.tombstone} = 0 AND ${t.materializationReason} IS NULL) OR (${t.materialization} IN ('withheld', 'unavailable') AND ${t.materializationReason} IS NOT NULL)`,
689
+ ),
690
+ }),
691
+ );
692
+
693
+ export const hostProjectEnvReplicaStageValues = sqliteTable(
694
+ "host_project_env_replica_stage_values",
695
+ {
696
+ attemptId: text("attempt_id").notNull(),
697
+ projectId: text("project_id").notNull(),
698
+ key: text("key").notNull(),
699
+ revision: integer("revision").notNull(),
700
+ generation: integer("generation").notNull(),
701
+ valueRefJson: text("value_ref_json").notNull(),
702
+ valueEnc: text("value_enc").notNull(),
703
+ updatedAt: integer("updated_at", { mode: "number" })
704
+ .notNull()
705
+ .default(sql`(unixepoch() * 1000)`),
706
+ },
707
+ (t) => ({
708
+ pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
709
+ head: check(
710
+ "host_project_env_replica_stage_values_head",
711
+ sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
712
+ ),
713
+ }),
714
+ );
715
+
716
+ export const hostProjectEnvReplicaReceipts = sqliteTable(
717
+ "host_project_env_replica_receipts",
718
+ {
719
+ streamId: text("stream_id").notNull(),
720
+ sequence: integer("sequence").notNull(),
721
+ requestMac: text("request_mac").notNull(),
722
+ resultJson: text("result_json"),
723
+ recordVersion: integer("record_version"),
724
+ ok: integer("ok", { mode: "boolean" }).notNull(),
725
+ code: text("code"),
726
+ createdAt: integer("created_at", { mode: "number" })
727
+ .notNull()
728
+ .default(sql`(unixepoch() * 1000)`),
729
+ },
730
+ (t) => ({
731
+ pk: primaryKey({ columns: [t.streamId, t.sequence] }),
732
+ sequenceCheck: check(
733
+ "host_project_env_replica_receipts_sequence",
734
+ sql`${t.sequence} >= 1`,
735
+ ),
736
+ booleanCheck: check(
737
+ "host_project_env_replica_receipts_boolean",
738
+ sql`${t.ok} IN (0, 1)`,
739
+ ),
740
+ outcome: check(
741
+ "host_project_env_replica_receipts_outcome",
742
+ sql`(${t.ok} = 1 AND ${t.resultJson} IS NOT NULL AND ${t.recordVersion} IS NOT NULL AND ${t.code} IS NULL) OR (${t.ok} = 0 AND ${t.resultJson} IS NULL AND ${t.recordVersion} IS NULL AND ${t.code} IS NOT NULL)`,
743
+ ),
744
+ }),
745
+ );
746
+
747
+ // User-authed MCP connections (ADR-057). One row per cloud connection id; all
748
+ // secret material (static header, OAuth client secret, PKCE verifier, tokens)
749
+ // is sealed with the host master key in single-column `ct.nonce` base64 form
750
+ // (same packing as host_project_env). The cloud only ever holds the non-secret
751
+ // metadata; acks never echo secrets back.
752
+ // ---------------------------------------------------------------------------
753
+ // host_agent_sessions — ADR-061 durable agent sessions. One row per
754
+ // (task, agent): where the live in-container runner's session dir is and how
755
+ // far the host has consumed its outbox. The byte offset is what turns a host
756
+ // restart into "resume tailing" instead of "kill and respawn"; attachability
757
+ // itself is judged live off the runner's heartbeat file, not this row.
758
+ // ---------------------------------------------------------------------------
759
+
760
+ export const hostAgentSessions = sqliteTable(
761
+ "host_agent_sessions",
762
+ {
763
+ taskId: text("task_id").notNull(),
764
+ agentId: text("agent_id").notNull(),
765
+ // Host-side absolute path of the session dir (on the workspace mount,
766
+ // identical in-container — task-up bind-mounts the workspace at its own
767
+ // host path).
768
+ sessionDir: text("session_dir").notNull(),
769
+ containerName: text("container_name").notNull(),
770
+ kind: text("kind").notNull(), // "claude" | "codex" | ...
771
+ // Attach only when the process was launched under the same host-owned
772
+ // policy generation. Null preserves legacy attach behavior for ordinary
773
+ // agents; Secretary transitions use an explicit key.
774
+ attachCompatibilityKey: text("attach_compatibility_key"),
775
+ outboxOffset: integer("outbox_offset", { mode: "number" })
776
+ .notNull()
777
+ .default(0),
778
+ status: text("status").notNull().default("running"), // running|closed
779
+ // ADR-078: Codex thread id (from thread/start). Persisted so a respawned
780
+ // app-server after a host restart resumes the SAME thread (thread/resume)
781
+ // instead of losing the conversation. Null for non-codex kinds / pre-resume.
782
+ codexThreadId: text("codex_thread_id"),
783
+ createdAt: integer("created_at", { mode: "number" }).notNull(),
784
+ updatedAt: integer("updated_at", { mode: "number" }).notNull(),
785
+ },
786
+ (t) => ({
787
+ pk: primaryKey({ columns: [t.taskId, t.agentId] }),
788
+ }),
789
+ );
790
+
791
+ export type HostAgentSession = typeof hostAgentSessions.$inferSelect;
792
+
793
+ export const mcpConnections = sqliteTable("host_mcp_connections", {
794
+ id: text("id").primaryKey(), // cloud uai_mcp_connections id
795
+ userId: text("user_id").notNull(),
796
+ url: text("url").notNull(),
797
+ // "oauth" | "token" | "none"
798
+ authKind: text("auth_kind").notNull(),
799
+ // OAuth machinery discovered at probe time.
800
+ tokenEndpoint: text("token_endpoint"),
801
+ redirectUri: text("redirect_uri"),
802
+ clientId: text("client_id"),
803
+ clientSecretEnc: text("client_secret_enc"),
804
+ // PKCE verifier held between probe and oauth.complete.
805
+ pkceVerifierEnc: text("pkce_verifier_enc"),
806
+ // Sealed JSON: {"headerName","headerValue"} (token kind) or
807
+ // {"accessToken","refreshToken","expiresAt"} (oauth kind). Null for "none".
808
+ secretEnc: text("secret_enc"),
809
+ status: text("status").notNull().default("pending"), // pending|connected
810
+ scopes: text("scopes"),
811
+ updatedAt: integer("updated_at", { mode: "number" }).notNull(),
812
+ // ADR-134 slice 2: the credential-store parameter this row mirrors to.
813
+ storeRef: text("store_ref"),
814
+ });
815
+
816
+ export type McpConnection = typeof mcpConnections.$inferSelect;
817
+
818
+ // Extra (non-default) engine accounts (ADR-076). One row per additional
819
+ // credential for an engine kind, beyond the single legacy slot (.env.local /
820
+ // ~/.codex / ~/.local/share/opencode) which is synthesized as the "default"
821
+ // account and NOT stored here. Secret material is sealed with the host master
822
+ // key (same secret-blind pattern as host_github_tokens — ADR-015):
823
+ // - authKind "env": secretEnc = sealed JSON of env vars, e.g.
824
+ // {"CLAUDE_CODE_OAUTH_TOKEN":"sk-ant-oat…"}.
825
+ // - authKind "config-dir": configDir = the host dir holding this account's
826
+ // auth files (an isolated ~/.codex-acct-<id> etc.);
827
+ // secretEnc is null (the dir IS the secret at rest).
828
+ // Rotation state (cooldown / last-used) is in-memory only, not persisted.
829
+ export const engineAccounts = sqliteTable("host_engine_accounts", {
830
+ id: text("id").primaryKey(), // ulid
831
+ kind: text("kind").notNull(), // "claude" | "codex" | "opencode" | …
832
+ label: text("label").notNull(), // display, e.g. "work"
833
+ authKind: text("auth_kind").notNull(), // "env" | "config-dir"
834
+ secretEnc: text("secret_enc"), // sealed JSON (env kind); null for config-dir
835
+ configDir: text("config_dir"), // host dir (config-dir kind); null for env
836
+ // ADR-131 D3: "host" (owner-level, default) or "org:<orgId>" (a workspace's
837
+ // account on a shared host). Resolution is by exact scope.
838
+ scope: text("scope").notNull().default("host"),
839
+ // Manage-v1: priority within (scope, kind) — tasks pick the first healthy
840
+ // account in ascending position; ties break on created_at.
841
+ position: integer("position", { mode: "number" }).notNull().default(0),
842
+ // ADR-134: the credential-store parameter this row mirrors to (null on an
843
+ // owned host, or before the first successful mirror). The store is the
844
+ // truth when configured; this row is the local cache.
845
+ storeRef: text("store_ref"),
846
+ createdAt: integer("created_at", { mode: "number" }).notNull(),
847
+ });
848
+
849
+ export type EngineAccountRow = typeof engineAccounts.$inferSelect;
850
+ export type NewEngineAccountRow = typeof engineAccounts.$inferInsert;
851
+
852
+ // First-turn dedup: ids of first-turn deliveries this station has accepted, so
853
+ // a cloud that crashed before recording its own ack can redeliver without a
854
+ // second opening turn. Written only after the turn was accepted.
855
+ export const hostFirstTurnReceipts = sqliteTable(
856
+ "host_first_turn_receipts",
857
+ {
858
+ taskId: text("task_id").notNull(),
859
+ firstTurnId: text("first_turn_id").notNull(),
860
+ createdAt: integer("created_at", { mode: "number" })
861
+ .notNull()
862
+ .default(sql`(unixepoch() * 1000)`),
863
+ },
864
+ (t) => ({
865
+ pk: primaryKey({ columns: [t.taskId, t.firstTurnId] }),
866
+ identity: check(
867
+ "host_first_turn_receipts_identity",
868
+ sql`length(${t.taskId}) > 0 AND length(${t.firstTurnId}) > 0`,
869
+ ),
870
+ }),
871
+ );