@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
@@ -0,0 +1,5 @@
1
+ /** ADR-164: the workspace switch is cloud-only. The host independently
2
+ * checks the concrete runtime identity; unknown and task machines refuse. */
3
+ export function isOwnedInternalConnectionRuntime(provider: string | null | undefined): boolean {
4
+ return provider === "docker-compose" || provider === "apple-container";
5
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Serialize asynchronous maintenance while coalescing callers for the same
3
+ * requested configuration. A different key always queues a distinct pass;
4
+ * this is stronger than a plain in-flight memo, which can let configuration C
5
+ * accidentally join an older B operation and then be declared reconciled.
6
+ */
7
+ export class KeyedPromiseTail<T> {
8
+ private tail: { key: string; promise: Promise<T> } | null = null;
9
+
10
+ run(key: string, operation: () => Promise<T>): Promise<T> {
11
+ if (this.tail?.key === key) return this.tail.promise;
12
+ const predecessor = this.tail?.promise;
13
+ let running!: Promise<T>;
14
+ running = (predecessor
15
+ ? predecessor.catch(() => undefined)
16
+ : Promise.resolve()
17
+ ).then(operation).finally(() => {
18
+ if (this.tail?.promise === running) this.tail = null;
19
+ });
20
+ this.tail = { key, promise: running };
21
+ return running;
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Whether this host was launched for development or tests, or is an
3
+ * installed product. An installed service leaves NODE_ENV unset, so "not
4
+ * production" is the wrong test; these markers are the positive signals of a
5
+ * development launch. Telemetry's default DSN (lib/obs) and any development
6
+ * override of where credentials are sent (lib/github-origin) both read this.
7
+ */
8
+
9
+ export type LaunchEnv = {
10
+ NODE_ENV?: string;
11
+ UAI_AGENT_FROM_REPO?: string;
12
+ UAI_LAUNCHED_FROM_REPO?: string;
13
+ };
14
+
15
+ /** A development or test launch: an explicit dev/test NODE_ENV, or a host
16
+ * started from a repository checkout (src/load-env.ts stamps that). */
17
+ export function isDevelopmentLaunch(env: LaunchEnv = process.env): boolean {
18
+ return (
19
+ env.NODE_ENV === "development" ||
20
+ env.NODE_ENV === "test" ||
21
+ env.UAI_AGENT_FROM_REPO === "1" ||
22
+ env.UAI_AGENT_FROM_REPO === "true" ||
23
+ env.UAI_LAUNCHED_FROM_REPO === "1"
24
+ );
25
+ }
package/lib/lease.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * ADR-133 task leases — the sidecar's AWS credentials.
3
+ *
4
+ * A task machine holds no long-lived AWS credential. At connect (and ten
5
+ * minutes before every expiry) the sidecar sends `lease.request` over the
6
+ * bridge; the cloud checks that this host row is the machine of a running
7
+ * task, assumes the `uai-task-machine` role with session tags for the task's
8
+ * workspace and owner, and answers `lease.grant` with the STS session. The
9
+ * session is held here in memory and handed to exactly two consumers:
10
+ *
11
+ * - the credential store's `aws` CLI calls (Parameter Store reads for engine
12
+ * accounts, GitHub tokens, MCP secrets, project env) through `leaseAwsEnv()`;
13
+ * - Mountpoint for Amazon S3 (shared files) through a `credential_process`
14
+ * that prints the current session — so a refreshed lease reaches a mount
15
+ * that is already up without remounting.
16
+ *
17
+ * Revocation is the next refresh being denied: `onDenied` fires and the
18
+ * sidecar exits, and the cloud's sweep terminates the machine. A lease is
19
+ * bound to one task and one machine; nothing here can ask for another's.
20
+ */
21
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
22
+ import { dirname } from "node:path";
23
+
24
+ import type { TaskLease } from "../src/protocol";
25
+
26
+ /** Re-request this long before the session expires. */
27
+ export const LEASE_REFRESH_LEAD_MS = 10 * 60_000;
28
+ /** A denied or unanswered request is retried this often until it lands. */
29
+ export const LEASE_RETRY_MS = 30_000;
30
+ /** The cloud must answer within this window or the request is retried. */
31
+ export const LEASE_REQUEST_TIMEOUT_MS = 20_000;
32
+
33
+ /** Where the `credential_process` command reads the live session. Root-only:
34
+ * the agents run as `node` and never see it. */
35
+ export const LEASE_CREDENTIALS_FILE = "/run/uai/lease-credentials.json";
36
+ /** The AWS config Mountpoint is pointed at (`AWS_CONFIG_FILE`). */
37
+ export const LEASE_AWS_CONFIG_FILE = "/run/uai/aws-config";
38
+ /**
39
+ * The `credential_process` line: `cat` of the JSON, run by the AWS CLI and
40
+ * by Mountpoint's CRT profile provider through `/bin/sh -c`. NOT a script
41
+ * under /run — Debian mounts /run `noexec`, so a helper there fails with
42
+ * EACCES even for root and both clients fall through to the instance
43
+ * profile, which has no S3 rights (live 2026-09-10, hostless smoke 6: every
44
+ * mount answered "assumed-role/uai-task-machine-instance … is not
45
+ * authorized to perform: s3:ListBucket").
46
+ */
47
+ export const LEASE_CREDENTIAL_PROCESS = `/bin/cat ${LEASE_CREDENTIALS_FILE}`;
48
+
49
+ export interface LeaseSender {
50
+ (frame: { kind: "lease.request"; opId: string; taskId: string }): boolean;
51
+ }
52
+
53
+ export interface LeaseManagerOptions {
54
+ taskId: string;
55
+ send: LeaseSender;
56
+ /** First grant — the store reconciles and the S3 mount wait for this. */
57
+ onFirstGrant?: (lease: TaskLease) => void;
58
+ /** Every grant, including refreshes. */
59
+ onGrant?: (lease: TaskLease) => void;
60
+ /** The cloud refused: the task is no longer ours. */
61
+ onDenied?: (error: string) => void;
62
+ /** Test seam: skip the /run/uai files. */
63
+ writeFiles?: boolean;
64
+ now?: () => number;
65
+ setTimer?: typeof setTimeout;
66
+ clearTimer?: typeof clearTimeout;
67
+ }
68
+
69
+ let current: TaskLease | null = null;
70
+
71
+ /** The AWS environment for a child process that should act under the lease
72
+ * (the credential store's CLI runner). Empty when no lease is held — the
73
+ * caller then runs with whatever the process has, which on a task machine
74
+ * is nothing. */
75
+ export function leaseAwsEnv(): Record<string, string> {
76
+ if (!current) return {};
77
+ return {
78
+ AWS_ACCESS_KEY_ID: current.accessKeyId,
79
+ AWS_SECRET_ACCESS_KEY: current.secretAccessKey,
80
+ AWS_SESSION_TOKEN: current.sessionToken,
81
+ AWS_REGION: current.region,
82
+ AWS_DEFAULT_REGION: current.region,
83
+ };
84
+ }
85
+
86
+ export function currentLease(): TaskLease | null {
87
+ return current;
88
+ }
89
+
90
+ /** Test seam. */
91
+ export function setCurrentLeaseForTesting(lease: TaskLease | null): void {
92
+ current = lease;
93
+ }
94
+
95
+ /** The JSON shape the AWS SDK credential_process contract expects. */
96
+ export function credentialProcessOutput(lease: TaskLease): string {
97
+ return JSON.stringify({
98
+ Version: 1,
99
+ AccessKeyId: lease.accessKeyId,
100
+ SecretAccessKey: lease.secretAccessKey,
101
+ SessionToken: lease.sessionToken,
102
+ Expiration: new Date(lease.expiresAt).toISOString(),
103
+ });
104
+ }
105
+
106
+ /** The AWS config file content for a lease: default profile, region, and
107
+ * the `credential_process` that serves the live session. Exported for the
108
+ * test that pins it. */
109
+ export function leaseAwsConfig(lease: Pick<TaskLease, "region">): string {
110
+ return `[default]\nregion = ${lease.region}\ncredential_process = ${LEASE_CREDENTIAL_PROCESS}\n`;
111
+ }
112
+
113
+ /** The org and user a current, unexpired lease was issued for — the credential store paths a
114
+ * sidecar may list (`/uai/orgs/<org>`, `/uai/users/<user>`; the task-machine
115
+ * policy grants nothing above them). Null on an owned host or before the
116
+ * first grant; `orgId` is null for a lease with no org scope. Absent from
117
+ * the lease when the cloud predates the field. */
118
+ export function leaseScope(
119
+ now: number = Date.now(),
120
+ ): { orgId: string | null; userId: string } | null {
121
+ if (
122
+ !current ||
123
+ current.expiresAt <= now ||
124
+ typeof current.userId !== "string"
125
+ ) {
126
+ return null;
127
+ }
128
+ return { orgId: current.orgId ?? null, userId: current.userId };
129
+ }
130
+
131
+ function writeLeaseFiles(lease: TaskLease): void {
132
+ mkdirSync(dirname(LEASE_CREDENTIALS_FILE), { recursive: true, mode: 0o700 });
133
+ writeFileSync(LEASE_CREDENTIALS_FILE, credentialProcessOutput(lease), { mode: 0o600 });
134
+ chmodSync(LEASE_CREDENTIALS_FILE, 0o600);
135
+ writeFileSync(LEASE_AWS_CONFIG_FILE, leaseAwsConfig(lease), { mode: 0o600 });
136
+ chmodSync(LEASE_AWS_CONFIG_FILE, 0o600);
137
+ }
138
+
139
+ export class LeaseManager {
140
+ readonly #opts: Required<Pick<LeaseManagerOptions, "now" | "setTimer" | "clearTimer">> &
141
+ LeaseManagerOptions;
142
+ #pending: { opId: string; timer: ReturnType<typeof setTimeout> } | null = null;
143
+ #refresh: ReturnType<typeof setTimeout> | null = null;
144
+ #granted = false;
145
+ #stopped = false;
146
+ #sequence = 0;
147
+
148
+ constructor(opts: LeaseManagerOptions) {
149
+ this.#opts = {
150
+ ...opts,
151
+ now: opts.now ?? Date.now,
152
+ setTimer: opts.setTimer ?? setTimeout,
153
+ clearTimer: opts.clearTimer ?? clearTimeout,
154
+ };
155
+ }
156
+
157
+ /** Ask now. Idempotent while a request is in flight. */
158
+ request(): void {
159
+ if (this.#stopped || this.#pending) return;
160
+ const opId = `lease-${this.#opts.taskId}-${++this.#sequence}-${this.#opts.now()}`;
161
+ const sent = this.#opts.send({ kind: "lease.request", opId, taskId: this.#opts.taskId });
162
+ const timer = this.#opts.setTimer(() => {
163
+ // Unanswered (the socket dropped, the cloud was mid-deploy): try again.
164
+ this.#pending = null;
165
+ this.#armRetry();
166
+ }, LEASE_REQUEST_TIMEOUT_MS);
167
+ timer.unref?.();
168
+ this.#pending = { opId, timer };
169
+ if (!sent) {
170
+ // Nothing on the wire; retry once the socket is back.
171
+ this.#opts.clearTimer(timer);
172
+ this.#pending = null;
173
+ this.#armRetry();
174
+ }
175
+ }
176
+
177
+ /** Bridge dispatch hands every `lease.*` frame here. */
178
+ handle(frame: { kind: "lease.grant"; opId: string; lease: TaskLease } | { kind: "lease.deny"; opId: string; error: string }): void {
179
+ if (!this.#pending || this.#pending.opId !== frame.opId) return;
180
+ this.#opts.clearTimer(this.#pending.timer);
181
+ this.#pending = null;
182
+ if (frame.kind === "lease.deny") {
183
+ current = null;
184
+ this.#opts.onDenied?.(frame.error);
185
+ return;
186
+ }
187
+ current = frame.lease;
188
+ if (this.#opts.writeFiles !== false) {
189
+ try {
190
+ writeLeaseFiles(frame.lease);
191
+ } catch (error) {
192
+ console.warn(
193
+ `[lease] could not write the credential_process files: ${error instanceof Error ? error.message : String(error)}`,
194
+ );
195
+ }
196
+ }
197
+ const first = !this.#granted;
198
+ this.#granted = true;
199
+ this.#armRefresh(frame.lease);
200
+ if (first) this.#opts.onFirstGrant?.(frame.lease);
201
+ this.#opts.onGrant?.(frame.lease);
202
+ }
203
+
204
+ /** The socket reconnected: whatever was pending is void; ask again. */
205
+ reconnected(): void {
206
+ if (this.#pending) {
207
+ this.#opts.clearTimer(this.#pending.timer);
208
+ this.#pending = null;
209
+ }
210
+ this.request();
211
+ }
212
+
213
+ stop(): void {
214
+ this.#stopped = true;
215
+ if (this.#pending) this.#opts.clearTimer(this.#pending.timer);
216
+ if (this.#refresh) this.#opts.clearTimer(this.#refresh);
217
+ this.#pending = null;
218
+ this.#refresh = null;
219
+ }
220
+
221
+ #armRefresh(lease: TaskLease): void {
222
+ if (this.#refresh) this.#opts.clearTimer(this.#refresh);
223
+ const delay = Math.max(1_000, lease.expiresAt - LEASE_REFRESH_LEAD_MS - this.#opts.now());
224
+ this.#refresh = this.#opts.setTimer(() => {
225
+ this.#refresh = null;
226
+ this.request();
227
+ }, delay);
228
+ this.#refresh.unref?.();
229
+ }
230
+
231
+ #armRetry(): void {
232
+ if (this.#stopped || this.#refresh) return;
233
+ this.#refresh = this.#opts.setTimer(() => {
234
+ this.#refresh = null;
235
+ this.request();
236
+ }, LEASE_RETRY_MS);
237
+ this.#refresh.unref?.();
238
+ }
239
+ }