@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,424 @@
1
+ /**
2
+ * Per-(project, key) environment variable store.
3
+ *
4
+ * Project/org members set KEY=VALUE pairs scoped to a (projectId, key) on this
5
+ * host; values are encrypted at rest with the host master key (same
6
+ * secret-blind pattern as host_ssh_keys / host_github_tokens — ADR-015). The
7
+ * cloud + UI only ever see KEY NAMES, never values (values are write-only). The
8
+ * same key name (e.g. DATABASE_URL) is independent per project, and a project's
9
+ * values are independent per host. At task-up each project on the task gets ITS
10
+ * (projectId, key) values injected into the task container env under the key
11
+ * name (see lib/agent.ts + scripts/agent/task-up.sh).
12
+ *
13
+ * `value_enc` is a single TEXT column carrying both halves of the AES-GCM
14
+ * sealed value as "<ct-base64>.<nonce-base64>".
15
+ */
16
+
17
+ import { and, eq, isNotNull } from "drizzle-orm";
18
+
19
+ import {
20
+ credentialStorePrefix,
21
+ isParameterPathSegment,
22
+ parameterName,
23
+ queueStoreWrite,
24
+ type CredentialStore,
25
+ } from "./credential-store";
26
+ import { isReservedTaskEnvName } from "../src/task-env-names";
27
+ import { getDb, schema } from "./db";
28
+ import { sealAesGcm, openAesGcm } from "./secrets";
29
+ import { signInBoxMode } from "./signin-box";
30
+
31
+ // Process-local epochs are enough for loader snapshots: every value mutation
32
+ // also happens in this process, and a host restart rebuilds both the epoch map
33
+ // and every task resolver. Keeping the counter per project lets a task ignore
34
+ // unrelated projects without retaining decrypted values in a shared cache.
35
+ const projectEnvRevisions = new Map<string, number>();
36
+
37
+ // ADR-153: what a task's view decides moves with more than values: a
38
+ // binding, and the store's gate opening or closing (a value write outside
39
+ // the records closes it too). One process-local counter covers them all.
40
+ let projectEnvAuthorityRevisionCount = 0;
41
+
42
+ export function noteProjectEnvValueMutation(projectId: string): void {
43
+ projectEnvRevisions.set(
44
+ projectId,
45
+ (projectEnvRevisions.get(projectId) ?? 0) + 1,
46
+ );
47
+ // On an activated store, a value written outside its records (an old
48
+ // frame, the credential store) closes the gate by trigger, which every
49
+ // task's view depends on, not only this project's.
50
+ projectEnvAuthorityRevisionCount += 1;
51
+ emitProjectEnvChange();
52
+ }
53
+
54
+ const projectEnvChangeListeners = new Set<() => void>();
55
+
56
+ /** Calls `listener` after any change to project env values, records,
57
+ * bindings or the store's gate; returns its removal. A listener that throws
58
+ * is skipped, never the change. */
59
+ export function onProjectEnvChange(listener: () => void): () => void {
60
+ projectEnvChangeListeners.add(listener);
61
+ return () => {
62
+ projectEnvChangeListeners.delete(listener);
63
+ };
64
+ }
65
+
66
+ function emitProjectEnvChange(): void {
67
+ for (const listener of projectEnvChangeListeners) {
68
+ try {
69
+ listener();
70
+ } catch {
71
+ // A listener's failure is its own.
72
+ }
73
+ }
74
+ }
75
+
76
+ /** Current process-local value revision for one project's env. */
77
+ export function projectEnvRevision(projectId: string): number {
78
+ return projectEnvRevisions.get(projectId) ?? 0;
79
+ }
80
+
81
+
82
+ export function noteProjectEnvAuthorityChange(): void {
83
+ projectEnvAuthorityRevisionCount += 1;
84
+ emitProjectEnvChange();
85
+ }
86
+
87
+ /** A task's project env changed outside this runner's store (ADR-154: the
88
+ * cloud's task replica, or the composer starting or ending serving it):
89
+ * whoever follows changes (the proxy's trust delivery) looks again. The
90
+ * store's own revisions do not move; nothing caches a view of the cloud's. */
91
+ export function noteProjectEnvViewChange(): void {
92
+ emitProjectEnvChange();
93
+ }
94
+
95
+ /** Current process-local revision of the store's records and gate. */
96
+ export function projectEnvAuthorityRevision(): number {
97
+ return projectEnvAuthorityRevisionCount;
98
+ }
99
+
100
+ /** Pack a sealed value (ct + nonce) into the single TEXT column. */
101
+ export function sealProjectEnvValueForStorage(value: string): string {
102
+ const sealed = sealAesGcm(value);
103
+ return `${sealed.ct.toString("base64")}.${sealed.nonce.toString("base64")}`;
104
+ }
105
+
106
+ /** Reverse {@link sealProjectEnvValueForStorage}; throws if malformed. */
107
+ export function openProjectEnvValueFromStorage(valueEnc: string): string {
108
+ const dot = valueEnc.indexOf(".");
109
+ if (dot === -1) throw new Error("malformed value_enc");
110
+ const ct = Buffer.from(valueEnc.slice(0, dot), "base64");
111
+ const nonce = Buffer.from(valueEnc.slice(dot + 1), "base64");
112
+ return openAesGcm(ct, nonce);
113
+ }
114
+
115
+ /** Set (insert or overwrite) a project env var, encrypting the value at rest.
116
+ * `orgId` is the project's org when the cloud sent it (see `noteProjectOrg`). */
117
+ export function setProjectEnvVar(
118
+ projectId: string,
119
+ key: string,
120
+ value: string,
121
+ orgId?: string,
122
+ ): void {
123
+ const now = Date.now();
124
+ getDb()
125
+ .insert(schema.hostProjectEnv)
126
+ .values({
127
+ projectId,
128
+ key,
129
+ valueEnc: sealProjectEnvValueForStorage(value),
130
+ updatedAt: now,
131
+ ...(orgId ? { orgId } : {}),
132
+ })
133
+ .onConflictDoUpdate({
134
+ target: [schema.hostProjectEnv.projectId, schema.hostProjectEnv.key],
135
+ set: { valueEnc: sealProjectEnvValueForStorage(value), updatedAt: now, ...(orgId ? { orgId } : {}) },
136
+ })
137
+ .run();
138
+ noteProjectEnvValueMutation(projectId);
139
+ if (orgId) noteProjectOrg(projectId, orgId, { except: key });
140
+ mirrorProjectEnvToStore(projectId, key);
141
+ }
142
+
143
+ /**
144
+ * The cloud told us which org owns `projectId` (an env.var.set/list frame
145
+ * carrying `orgId`). Every row of that project mirrored before the org was
146
+ * known gets the org and is re-tagged in the store, so a task machine's
147
+ * tag-scoped lease can read it (ADR-133). Returns the rows updated.
148
+ */
149
+ export function noteProjectOrg(
150
+ projectId: string,
151
+ orgId: string,
152
+ opts: { except?: string } = {},
153
+ ): number {
154
+ const rows = getDb()
155
+ .select({ key: schema.hostProjectEnv.key, orgId: schema.hostProjectEnv.orgId, storeRef: schema.hostProjectEnv.storeRef })
156
+ .from(schema.hostProjectEnv)
157
+ .where(eq(schema.hostProjectEnv.projectId, projectId))
158
+ .all()
159
+ .filter((row) => row.key !== opts.except && row.orgId !== orgId);
160
+ for (const row of rows) {
161
+ getDb()
162
+ .update(schema.hostProjectEnv)
163
+ .set({ orgId })
164
+ .where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, row.key)))
165
+ .run();
166
+ if (row.storeRef) {
167
+ const name = row.storeRef;
168
+ queueStoreWrite(`tag project env ${projectId}/${row.key}`, async (store) => {
169
+ await tagProjectEnvParameter(store, name, projectId, orgId);
170
+ });
171
+ }
172
+ }
173
+ return rows.length;
174
+ }
175
+
176
+ /** The org recorded for a project, or null while the cloud has not said. */
177
+ export function projectEnvOrgId(projectId: string): string | null {
178
+ const row = getDb()
179
+ .select({ orgId: schema.hostProjectEnv.orgId })
180
+ .from(schema.hostProjectEnv)
181
+ .where(and(eq(schema.hostProjectEnv.projectId, projectId), isNotNull(schema.hostProjectEnv.orgId)))
182
+ .get();
183
+ return row?.orgId ?? null;
184
+ }
185
+
186
+ /** The tags a project env parameter carries: the lease policy's `org`, plus
187
+ * `project` for operators reading the store. */
188
+ export function projectEnvTags(projectId: string, orgId: string): Record<string, string> {
189
+ return { org: orgId, project: projectId };
190
+ }
191
+
192
+ const tagWarned = new Set<string>();
193
+ /**
194
+ * Tag a mirrored parameter for the task-machine lease. Best-effort: the
195
+ * value is already in the store, and a host whose policy predates
196
+ * `ssm:AddTagsToResource` (host-agent/scripts/credential-store/
197
+ * bootstrap-aws.sh) must keep mirroring; it warns once per parameter and a
198
+ * later set/list retries.
199
+ */
200
+ export async function tagProjectEnvParameter(
201
+ store: Pick<CredentialStore, "tag">,
202
+ name: string,
203
+ projectId: string,
204
+ orgId: string,
205
+ ): Promise<boolean> {
206
+ try {
207
+ await store.tag(name, projectEnvTags(projectId, orgId));
208
+ tagWarned.delete(name);
209
+ return true;
210
+ } catch (err) {
211
+ if (!tagWarned.has(name)) {
212
+ tagWarned.add(name);
213
+ console.warn(
214
+ `[project-env] credential store: tagging ${name} failed (${err instanceof Error ? err.message : String(err)}) — a task machine cannot read it until it carries org=${orgId}; re-run bootstrap-aws.sh for ssm:AddTagsToResource`,
215
+ );
216
+ }
217
+ return false;
218
+ }
219
+ }
220
+
221
+ /** Remove a project env var (idempotent). */
222
+ export function deleteProjectEnvVar(projectId: string, key: string): void {
223
+ const result = getDb()
224
+ .delete(schema.hostProjectEnv)
225
+ .where(
226
+ and(
227
+ eq(schema.hostProjectEnv.projectId, projectId),
228
+ eq(schema.hostProjectEnv.key, key),
229
+ ),
230
+ )
231
+ .run();
232
+ if (result.changes > 0) {
233
+ noteProjectEnvValueMutation(projectId);
234
+ mirrorProjectEnvDeletionToStore(projectId, key);
235
+ }
236
+ }
237
+
238
+ /** Queue only the credential-store side of a committed local deletion. */
239
+ export function mirrorProjectEnvDeletionToStore(
240
+ projectId: string,
241
+ key: string,
242
+ ): void {
243
+ const name = projectEnvParameterName(projectId, key);
244
+ if (name) {
245
+ queueStoreWrite(`delete project env ${projectId}/${key}`, (store) =>
246
+ store.delete(name),
247
+ );
248
+ }
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Credential store mirror (ADR-134 slice 3): `<prefix>/projects/<projectId>/env/<KEY>`.
253
+ // The set/delete frames carry no org, and a project id is org-bound in the
254
+ // cloud, so the project is the owner segment. Owned hosts: no-ops.
255
+ // ---------------------------------------------------------------------------
256
+
257
+ export interface StoredProjectEnv {
258
+ v: 1;
259
+ kind: "project-env";
260
+ projectId: string;
261
+ key: string;
262
+ value: string;
263
+ updatedAt: number;
264
+ /** The project's org once the cloud has said (ADR-133 tag scope); a
265
+ * replacement host inherits it with the value. */
266
+ orgId?: string;
267
+ }
268
+
269
+ /** Null when the project id or key cannot be a parameter path segment (a
270
+ * key with a character SSM refuses is kept local and logged once). */
271
+ export function projectEnvParameterName(projectId: string, key: string): string | null {
272
+ if (!isParameterPathSegment(projectId) || !isParameterPathSegment(key)) return null;
273
+ return parameterName(credentialStorePrefix(), "projects", projectId, "env", key);
274
+ }
275
+
276
+ /** The row as it is mirrored (value decrypted). Never logged. */
277
+ export function projectEnvStoreValue(projectId: string, key: string): StoredProjectEnv | null {
278
+ const row = getDb()
279
+ .select()
280
+ .from(schema.hostProjectEnv)
281
+ .where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
282
+ .get();
283
+ if (!row) return null;
284
+ return {
285
+ v: 1,
286
+ kind: "project-env",
287
+ projectId,
288
+ key,
289
+ value: openProjectEnvValueFromStorage(row.valueEnc),
290
+ updatedAt: row.updatedAt,
291
+ ...(row.orgId ? { orgId: row.orgId } : {}),
292
+ };
293
+ }
294
+
295
+ export function setProjectEnvStoreRef(projectId: string, key: string, storeRef: string): void {
296
+ getDb()
297
+ .update(schema.hostProjectEnv)
298
+ .set({ storeRef })
299
+ .where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
300
+ .run();
301
+ }
302
+
303
+ export function listProjectEnvStoreRows(): Array<{ projectId: string; key: string; storeRef: string | null }> {
304
+ return getDb()
305
+ .select({
306
+ projectId: schema.hostProjectEnv.projectId,
307
+ key: schema.hostProjectEnv.key,
308
+ storeRef: schema.hostProjectEnv.storeRef,
309
+ })
310
+ .from(schema.hostProjectEnv)
311
+ .all();
312
+ }
313
+
314
+ export function mirrorProjectEnvToStore(projectId: string, key: string): void {
315
+ // ADR-136 sign-in box: no local rows; env.var.* is a store passthrough
316
+ // (lib/signin-box-credentials.ts).
317
+ if (signInBoxMode()) return;
318
+ const name = projectEnvParameterName(projectId, key);
319
+ if (!name) return;
320
+ queueStoreWrite(`mirror project env ${projectId}/${key}`, async (store) => {
321
+ const value = projectEnvStoreValue(projectId, key);
322
+ if (!value) return;
323
+ await store.put(name, JSON.stringify(value));
324
+ setProjectEnvStoreRef(projectId, key, name);
325
+ if (value.orgId) await tagProjectEnvParameter(store, name, projectId, value.orgId);
326
+ });
327
+ }
328
+
329
+ /** Reconcile import: a value this host lacks. Existing rows win. */
330
+ export function importProjectEnvFromStore(value: StoredProjectEnv, storeRef: string): boolean {
331
+ const result = getDb()
332
+ .insert(schema.hostProjectEnv)
333
+ .values({
334
+ projectId: value.projectId,
335
+ key: value.key,
336
+ valueEnc: sealProjectEnvValueForStorage(value.value),
337
+ updatedAt: value.updatedAt,
338
+ storeRef,
339
+ ...(value.orgId ? { orgId: value.orgId } : {}),
340
+ })
341
+ .onConflictDoNothing({ target: [schema.hostProjectEnv.projectId, schema.hostProjectEnv.key] })
342
+ .run();
343
+ if (result.changes > 0) noteProjectEnvValueMutation(value.projectId);
344
+ return result.changes > 0;
345
+ }
346
+
347
+ /** Reconcile: the row's parameter vanished elsewhere; drop the row only. */
348
+ export function deleteProjectEnvLocal(projectId: string, key: string): void {
349
+ const result = getDb()
350
+ .delete(schema.hostProjectEnv)
351
+ .where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
352
+ .run();
353
+ if (result.changes > 0) noteProjectEnvValueMutation(projectId);
354
+ }
355
+
356
+ /** Sorted list of a project's env var KEY NAMES (never the values). */
357
+ export function listProjectEnvKeys(projectId: string): string[] {
358
+ return getDb()
359
+ .select({ key: schema.hostProjectEnv.key })
360
+ .from(schema.hostProjectEnv)
361
+ .where(eq(schema.hostProjectEnv.projectId, projectId))
362
+ .all()
363
+ .map((row) => row.key)
364
+ .sort();
365
+ }
366
+
367
+ /**
368
+ * Decrypt + return a project's env vars as KEY→VALUE. Used at task-up injection
369
+ * (lib/agent.ts) — the decrypted values live only on the trusted host (ADR-015)
370
+ * and go straight into the task container's child env, never logged. Resilient:
371
+ * a single value that fails to decrypt is logged + skipped rather than aborting
372
+ * the whole task launch.
373
+ */
374
+ export function getDecryptedForProject(projectId: string): Record<string, string> {
375
+ const rows = getDb()
376
+ .select({
377
+ key: schema.hostProjectEnv.key,
378
+ valueEnc: schema.hostProjectEnv.valueEnc,
379
+ })
380
+ .from(schema.hostProjectEnv)
381
+ .where(eq(schema.hostProjectEnv.projectId, projectId))
382
+ .all();
383
+ const out: Record<string, string> = {};
384
+ for (const row of rows) {
385
+ try {
386
+ out[row.key] = openProjectEnvValueFromStorage(row.valueEnc);
387
+ } catch (err) {
388
+ console.warn(
389
+ `[host-env] skipping ${projectId}/${row.key}: decrypt failed (${
390
+ err instanceof Error ? err.message : String(err)
391
+ })`,
392
+ );
393
+ }
394
+ }
395
+ return out;
396
+ }
397
+
398
+ /**
399
+ * Names a project may declare but a task must never receive from project env —
400
+ * the same list task-up.sh's `is_reserved_task_env_name` enforces for
401
+ * container tasks: git/gh plumbing, loader and shell hooks, toolchain config,
402
+ * the host's own `UAI_*` namespace, engine credentials, and the browser path.
403
+ */
404
+ export { isReservedTaskEnvName } from "../src/task-env-names";
405
+
406
+ /**
407
+ * Rollout-only project env for an ADR-121 machine task whose persisted mode
408
+ * is still `baked`: every project's decrypted values, lead project winning a
409
+ * key collision, minus the reserved names above. Loader tasks must use the
410
+ * declaration-pinned gateway instead. Live 2026-09-09: a machine task on Uai
411
+ * Cloud had the Infisical identity in the store and the host, and the agent
412
+ * never saw it, because only the compose path injected project env.
413
+ */
414
+ export function projectEnvForTask(projectIds: readonly string[]): Record<string, string> {
415
+ const merged: Record<string, string> = {};
416
+ for (const projectId of [...projectIds].reverse()) {
417
+ Object.assign(merged, getDecryptedForProject(projectId));
418
+ }
419
+ const out: Record<string, string> = {};
420
+ for (const [key, value] of Object.entries(merged)) {
421
+ if (!isReservedTaskEnvName(key)) out[key] = value;
422
+ }
423
+ return out;
424
+ }