@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,22 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ import { captureHostError, scrubHostString } from "./obs";
4
+
5
+ /** Provider errors can quote requests. Keep chat copy short and on one line;
6
+ * diagnostic reporting retains the full length with the host's redaction. */
7
+ export function agentErrorMessage(message: string): string {
8
+ const line = stripVTControlCharacters(scrubHostString(message))
9
+ .replace(/[\s\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
10
+ return line.length > 300 ? `${line.slice(0, 299).trimEnd()}…` : line;
11
+ }
12
+
13
+ export function reportAgentError(
14
+ message: string,
15
+ context: { taskId: string; agentId: string; engine: string; rpcCode?: number },
16
+ { capture = true }: { capture?: boolean } = {},
17
+ ): void {
18
+ const diagnostic = scrubHostString(message);
19
+ console.warn(`[agent] ${context.taskId}/${context.agentId}: ${diagnostic}`);
20
+ // This uses the existing disclosure/opt-out gate and Sentry scrubber.
21
+ if (capture) captureHostError(new Error(diagnostic), context);
22
+ }
package/lib/agent.ts ADDED
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Typed wrapper around the host-agent shell scripts.
3
+ *
4
+ * The agent's wire contract (docs/agent.md):
5
+ * success → stdout: { ok: true, data: ... }
6
+ * failure → stderr: { ok: false, error: { code, message, step?, exit_code? } }
7
+ * non-zero exit on failure.
8
+ *
9
+ * This module shells out to scripts/agent/*.sh (or UAI_AGENT_DIR/*.sh in
10
+ * production), pipes UAI_DB_PATH + UAI_DATA_DIR into the child, parses
11
+ * the JSON, and returns typed result objects — or throws AgentError.
12
+ *
13
+ * Same-process for MVP (per ADR-004). When uai is extracted to a hosted
14
+ * product, this module becomes a JSON-RPC client over a websocket; the
15
+ * surface stays the same.
16
+ */
17
+
18
+ import { spawn } from "node:child_process";
19
+ import { chmodSync, existsSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { dirname, join, resolve } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { z } from "zod";
24
+
25
+ import {
26
+ createTaskEnvironmentDownCommandDb,
27
+ createTaskDownCommandDb,
28
+ createTaskStatusCommandDb,
29
+ createTaskUpCommandDb,
30
+ removeCommandDb,
31
+ } from "./command-db";
32
+ import { env } from "./env";
33
+ import { containerRuntimeStandardAssetsRoot } from "./container-runtime";
34
+ import { getDecryptedForProject } from "./host-env";
35
+ import { PreviewPortRuntimesSchema } from "./preview-ports";
36
+ import { getHostTask } from "./runtime-state";
37
+ import { ENGINE_CREDENTIAL_ENV_NAMES } from "./engine-credential-env";
38
+ import { activeSecretsProxy } from "./secrets-proxy/host";
39
+ import { closeTaskUpTrustSteps, finishTaskUpTrustSteps, writeTaskUpTrustSteps } from "./secrets-proxy/task-trust";
40
+ import { removeTaskIdentity, writeTaskIdentity } from "./ssh";
41
+ import type { TaskGitCredentialHold } from "./credential-consumers/github";
42
+ import type { TaskWriteEnvironment } from "./credential-consumers/task-files";
43
+ import { serveCloneCredentials, type CloneCredentialService } from "./git-clone-credentials";
44
+ import { taskGatewayHost } from "./mcp-gateway";
45
+ import type { ProjectEnvMode } from "./task-environment/types";
46
+ import { isProjectEnvSchemaObservation, type ProjectEnvSchemaObservation, type TaskDownInput, type TaskLaunchInput } from "../src/protocol";
47
+
48
+ export type { TaskLaunchInput } from "../src/protocol";
49
+
50
+ /**
51
+ * ADR-154 §12: a start on cloud credentials. Only ever whole: the hold and
52
+ * the runtime-ready writes come together, so no such start reaches its
53
+ * runtime without what the runner writes into it.
54
+ */
55
+ export interface LeasedTaskUp {
56
+ /**
57
+ * The owner's GitHub sign-in and SSH key, held for this start
58
+ * (`holdTaskGitCredentials`). The host-side clone reaches them only
59
+ * through this process's own sockets, for as long as task-up runs; a hold
60
+ * taken back aborts task-up. No key file is written and the runner's own
61
+ * store is never read.
62
+ */
63
+ clone: TaskGitCredentialHold;
64
+ /**
65
+ * The task's runtime exists and nothing in it has run yet (task-up pauses
66
+ * before uai-init, a machine before its clones): write what the runner
67
+ * writes into it, its owner's GitHub configuration and SSH key recorded
68
+ * for cleanup, before anything that may need them runs. A rejection stops
69
+ * the start.
70
+ */
71
+ runtimeReady: (environment: TaskWriteEnvironment, signal: AbortSignal) => Promise<void>;
72
+ /** This exact start carries a task-machine Files session. Machine task-up
73
+ * skips its legacy copy/mount path only in this case; owned leased tasks do
74
+ * not receive a Files session and keep their normal Files behavior. */
75
+ filesV1?: true;
76
+ }
77
+
78
+ /** Refuses a start on cloud credentials that is not whole, or not this
79
+ * start's: before anything is provisioned, cloned or written. */
80
+ export function assertLeasedTaskUp(
81
+ leased: LeasedTaskUp | undefined,
82
+ task: { id: string; runEpoch?: number },
83
+ ): void {
84
+ if (leased === undefined) return;
85
+ if (!leased.clone || typeof leased.runtimeReady !== "function") {
86
+ throw new Error("a start on cloud credentials needs its hold and its runtime-ready writes together");
87
+ }
88
+ if (leased.clone.assignment.taskId !== task.id || leased.clone.assignment.runEpoch !== task.runEpoch) {
89
+ throw new Error("the credentials held for this start are another task's or another run's");
90
+ }
91
+ }
92
+
93
+ export interface TaskUpCredentials {
94
+ /** Path to the task owner's private, ephemeral Git credential cache. */
95
+ githubCredentialSocket?: string;
96
+ /** ADR-154 §12: a task on cloud credentials. */
97
+ leased?: LeasedTaskUp;
98
+ /** Fresh ADR-153 generation credential. The raw bytes are forwarded only
99
+ * over stdin and are never serialized into a task row, argv, or env. */
100
+ taskGateway?: {
101
+ generation: string;
102
+ secret: Uint8Array;
103
+ };
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Locate the agent scripts. They ship inside the package, so by default we
108
+ // resolve them relative to this module — which works identically in a repo
109
+ // checkout (host-agent/scripts/agent) and an npm install
110
+ // (node_modules/@runuai/host/scripts/agent). An explicit UAI_AGENT_DIR
111
+ // overrides this (e.g. hacking on scripts against a running host).
112
+ // ---------------------------------------------------------------------------
113
+
114
+ function packageAgentScriptsDir(): string {
115
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "agent");
116
+ }
117
+
118
+ function agentDir(): string {
119
+ return env.agentDir ?? packageAgentScriptsDir();
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Result schemas. Each command shares the success envelope and produces a
124
+ // command-specific `data` payload.
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const ErrorPayload = z.object({
128
+ code: z.string(),
129
+ message: z.string(),
130
+ step: z.string().optional(),
131
+ exit_code: z.number().optional(),
132
+ });
133
+
134
+ const SuccessEnvelope = <T extends z.ZodTypeAny>(data: T) =>
135
+ z.object({ ok: z.literal(true), data });
136
+
137
+ const FailureEnvelope = z.object({
138
+ ok: z.literal(false),
139
+ error: ErrorPayload,
140
+ });
141
+
142
+ // task-up
143
+ const TaskUpData = z.object({
144
+ composeProject: z.string(),
145
+ worktreePath: z.string(),
146
+ codeServerPort: z.number().int().positive().optional(),
147
+ previewPorts: PreviewPortRuntimesSchema.optional(),
148
+ /** Degraded start: uai-init failed twice; surfaced as a feed system note. */
149
+ initWarning: z.string().optional(),
150
+ /** ADR-153: whether this start put the secrets proxy's CA, and its pins,
151
+ * into the task. Absent from a task-up that predates them. */
152
+ secretsProxyTrust: z.enum(["installed", "failed", "skipped"]).optional(),
153
+ secretsProxyPins: z.enum(["installed", "failed", "skipped"]).optional(),
154
+ /** ADR-153 §4: each project's schema observation, as the runner stored it
155
+ * before anything in the task started (a task machine reports its own). */
156
+ envSchemas: z.array(z.custom<ProjectEnvSchemaObservation>(isProjectEnvSchemaObservation)).optional(),
157
+ });
158
+ export type TaskUpResult = z.infer<typeof TaskUpData>;
159
+
160
+ // task-down
161
+ /** task-down.sh destroys the task's runtime AND its directory (worktree +
162
+ * task-private Git data), so it refuses without this explicit terminal
163
+ * intent. Only the cloud taskDown reaches these two calls: the handler in
164
+ * src/index.ts, and a credential cleanup under that same command
165
+ * (`reason: "teardown"`, task-credential-cleanup.ts). */
166
+ const TASK_DOWN_TERMINAL_ENV = Object.freeze({ UAI_TASK_DOWN_TERMINAL: "1" });
167
+
168
+ const TaskDownData = z
169
+ .object({
170
+ status: z.string().optional(),
171
+ alreadyGone: z.boolean().optional(),
172
+ })
173
+ .passthrough();
174
+ export type TaskDownResult = z.infer<typeof TaskDownData>;
175
+
176
+ // task-status
177
+ const TaskStatusData = z.object({
178
+ composeRunning: z.boolean(),
179
+ containers: z.array(z.string()),
180
+ worktreePresent: z.boolean(),
181
+ });
182
+ export type TaskStatusResult = z.infer<typeof TaskStatusData>;
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Error type. All call sites should `catch (err)` and use `AgentError.is`.
186
+ // ---------------------------------------------------------------------------
187
+
188
+ export class AgentError extends Error {
189
+ override readonly name = "AgentError";
190
+ readonly code: string;
191
+ readonly step: string | undefined;
192
+ readonly exitCode: number | undefined;
193
+
194
+ constructor(
195
+ code: string,
196
+ message: string,
197
+ options: { step?: string; exitCode?: number; cause?: unknown } = {},
198
+ ) {
199
+ super(
200
+ message,
201
+ options.cause === undefined ? undefined : { cause: options.cause },
202
+ );
203
+ this.code = code;
204
+ this.step = options.step;
205
+ this.exitCode = options.exitCode;
206
+ }
207
+
208
+ static is(err: unknown): err is AgentError {
209
+ return err instanceof AgentError;
210
+ }
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Internal: run an agent script and parse the JSON envelope.
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /** How long a withdrawn script has to end on SIGTERM before SIGKILL. */
218
+ const WITHDRAWN_KILL_GRACE_MS = 5_000;
219
+
220
+ /** What a start on cloud credentials says when they were withdrawn under it. */
221
+ export const CREDENTIALS_WITHDRAWN_MESSAGE =
222
+ "The task's credentials were withdrawn while it was starting: removed, refused or replaced in the cloud. Start the task again.";
223
+ /** How often task-up's handshake directory is looked at for its events. */
224
+ const HANDSHAKE_POLL_MS = 100;
225
+
226
+ /**
227
+ * The environment every host script (task-up, task-down, task-status) runs
228
+ * with: the runner's own, then every root a script derives a path from,
229
+ * resolved here and passed explicitly, so no script falls back to a default.
230
+ * Projects, tasks, browsers and shared files live under the workspace root,
231
+ * credentials under the owner's home. Rehearsal #2 (2026-11-02): the station's
232
+ * root defaults to ~/.moondeck (env.ts) but nothing passed it, so task-up
233
+ * built under _common.sh's own fallback, ~/.uai, and docker.ts refused the
234
+ * stack it returned. The packaged-runtime check in
235
+ * scripts/host-runtime/workspace-root-smoke.mjs calls this from an archive.
236
+ */
237
+ export function agentScriptEnv(
238
+ commandDbPath: string,
239
+ runnerEnv: NodeJS.ProcessEnv = process.env,
240
+ extraEnv: Record<string, string> = {},
241
+ ): NodeJS.ProcessEnv {
242
+ return {
243
+ ...runnerEnv,
244
+ UAI_DB_PATH: commandDbPath,
245
+ UAI_DATA_DIR: env.dataDir,
246
+ UAI_WORKSPACE_ROOT: env.workspaceRoot,
247
+ UAI_OWNER_HOME: process.env.UAI_OWNER_HOME?.trim() || homedir(),
248
+ UAI_STANDARD_CONTAINER_ASSETS_DIR:
249
+ containerRuntimeStandardAssetsRoot() ??
250
+ join(packageAgentScriptsDir(), "..", "..", "images", "standard", "container"),
251
+ ...extraEnv,
252
+ };
253
+ }
254
+
255
+ async function runAgent<T extends z.ZodTypeAny>(
256
+ scriptName: string,
257
+ args: string[],
258
+ dataSchema: T,
259
+ commandDbPath: string,
260
+ extraEnv: Record<string, string> = {},
261
+ stdinPayload?: string,
262
+ /** Aborted when what the script runs on is withdrawn (ADR-154): the
263
+ * script and everything it started, its process group, end. */
264
+ withdrawn?: AbortSignal,
265
+ /** Names the script never inherits from the runner's own environment
266
+ * (ADR-154: a start on cloud credentials gets none of the runner's). */
267
+ withoutRunnerEnv: ReadonlySet<string> = new Set(),
268
+ ): Promise<z.infer<T>> {
269
+ const scriptPath = resolve(agentDir(), scriptName);
270
+ const runnerEnv = Object.fromEntries(
271
+ Object.entries(process.env).filter(([name]) => !withoutRunnerEnv.has(name)),
272
+ );
273
+
274
+ const child = spawn(scriptPath, args, {
275
+ // Its own process group when it may be withdrawn, so ending it ends
276
+ // every git and container CLI it started.
277
+ detached: withdrawn !== undefined,
278
+ stdio: [stdinPayload === undefined ? "ignore" : "pipe", "pipe", "pipe"],
279
+ env: agentScriptEnv(commandDbPath, runnerEnv, extraEnv),
280
+ });
281
+
282
+ let stdoutBuf = "";
283
+ let stderrBuf = "";
284
+ child.stdout?.on("data", (chunk) => {
285
+ stdoutBuf += chunk.toString("utf8");
286
+ });
287
+ child.stderr?.on("data", (chunk) => {
288
+ stderrBuf += chunk.toString("utf8");
289
+ });
290
+ if (stdinPayload !== undefined && child.stdin) {
291
+ // A script that exits before consuming stdin may close the pipe first.
292
+ // The process exit/error below is authoritative; do not turn EPIPE into an
293
+ // unhandled stream error in the host agent.
294
+ child.stdin.on("error", () => {});
295
+ child.stdin.end(stdinPayload);
296
+ }
297
+
298
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
299
+ const endGroup = (signal: NodeJS.Signals): void => {
300
+ try {
301
+ if (child.pid !== undefined) process.kill(-child.pid, signal);
302
+ } catch {
303
+ // Already gone.
304
+ }
305
+ };
306
+ const onWithdrawn = (): void => {
307
+ endGroup("SIGTERM");
308
+ killTimer = setTimeout(() => endGroup("SIGKILL"), WITHDRAWN_KILL_GRACE_MS);
309
+ };
310
+ if (withdrawn?.aborted) onWithdrawn();
311
+ else withdrawn?.addEventListener("abort", onWithdrawn, { once: true });
312
+
313
+ let exitCode: number;
314
+ try {
315
+ exitCode = await new Promise((res, rej) => {
316
+ child.on("error", rej);
317
+ child.on("close", (code) => res(code ?? 0));
318
+ });
319
+ } finally {
320
+ withdrawn?.removeEventListener("abort", onWithdrawn);
321
+ clearTimeout(killTimer);
322
+ }
323
+ if (withdrawn?.aborted) {
324
+ endGroup("SIGKILL");
325
+ throw new AgentError("CREDENTIALS_WITHDRAWN", CREDENTIALS_WITHDRAWN_MESSAGE, { exitCode });
326
+ }
327
+
328
+ // Try to parse whichever stream has the JSON. On failure the agent puts
329
+ // its envelope on stderr; on success, on stdout.
330
+ if (exitCode !== 0) {
331
+ // Log the full stderr so the dev terminal shows the underlying tool's
332
+ // output (docker's stderr, git's stderr, etc.) — the shell agent only
333
+ // surfaces the code + step on the structured envelope.
334
+ if (stderrBuf.trim()) {
335
+ console.error(
336
+ `[uai-agent] ${scriptName} stderr (exit ${exitCode}):\n${stderrBuf.trim()}`,
337
+ );
338
+ }
339
+ const parsed = tryParseLastJsonLine(stderrBuf);
340
+ if (parsed) {
341
+ const failure = FailureEnvelope.safeParse(parsed);
342
+ if (failure.success) {
343
+ throw new AgentError(
344
+ failure.data.error.code,
345
+ composeFailureMessage(failure.data.error.message, stderrBuf),
346
+ {
347
+ step: failure.data.error.step,
348
+ exitCode: failure.data.error.exit_code ?? exitCode,
349
+ },
350
+ );
351
+ }
352
+ }
353
+ throw new AgentError(
354
+ "AGENT_UNKNOWN_FAILURE",
355
+ `${scriptName} exited ${exitCode}: ${stderrBuf.trim() || "no stderr"}`,
356
+ { exitCode },
357
+ );
358
+ }
359
+
360
+ const parsed = tryParseLastJsonLine(stdoutBuf);
361
+ if (!parsed) {
362
+ throw new AgentError(
363
+ "AGENT_BAD_OUTPUT",
364
+ `${scriptName} stdout was not JSON: ${stdoutBuf.trim().slice(0, 200)}`,
365
+ { exitCode },
366
+ );
367
+ }
368
+ const envelope = SuccessEnvelope(dataSchema).safeParse(parsed);
369
+ if (!envelope.success) {
370
+ throw new AgentError(
371
+ "AGENT_BAD_OUTPUT",
372
+ `${scriptName} returned unexpected shape: ${envelope.error.message}`,
373
+ { exitCode },
374
+ );
375
+ }
376
+ return envelope.data.data;
377
+ }
378
+
379
+ /**
380
+ * The agent may log info lines to stderr alongside the JSON envelope.
381
+ * On stdout we expect a single trailing JSON object; on stderr the
382
+ * envelope is the last line. This helper takes the last non-empty line
383
+ * and parses it as JSON.
384
+ */
385
+ function tryParseLastJsonLine(buf: string): unknown {
386
+ const trimmed = buf.trim();
387
+ if (!trimmed) return null;
388
+ const lines = trimmed.split(/\r?\n/);
389
+ for (let i = lines.length - 1; i >= 0; i--) {
390
+ const line = (lines[i] ?? "").trim();
391
+ if (!line) continue;
392
+ try {
393
+ return JSON.parse(line);
394
+ } catch {
395
+ // Keep walking back; agent log lines are not JSON.
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+
401
+ /**
402
+ * The FAILING TOOL's output from a script's stderr — the docker/git/apt lines
403
+ * that actually say WHY a step failed, dropping our own `[uai-agent] step:`
404
+ * breadcrumbs and the `{"ok":false,…}` envelope. The bash envelope's message
405
+ * is generic ("agent step failed"); this is what makes it specific so the
406
+ * reason reaches the cloud UI instead of only the host log — useless when the
407
+ * host is a remote user's machine (live 2026-07-22, "compose_up_failed" with
408
+ * no why). Bounded to the last few lines so a build log doesn't flood the UI.
409
+ */
410
+ /**
411
+ * The operator-facing failure text: the envelope's message LEADS and the
412
+ * tool output follows. The scripts' emit_err messages are curated
413
+ * instructions ("close another running task, then press Resume") —
414
+ * replacing them with the stderr tail buried exactly the sentence the
415
+ * operator needed under VZ/git noise (live 2026-08-17, twice in one day).
416
+ * The tool detail still ships, because a message alone can lack the WHY
417
+ * (the 2026-07-22 lesson the old detail-first ordering encoded).
418
+ */
419
+ export function composeFailureMessage(
420
+ envelopeMessage: string,
421
+ stderrBuf: string,
422
+ ): string {
423
+ const detail = toolStderrDetail(stderrBuf);
424
+ return detail ? `${envelopeMessage}\n${detail}` : envelopeMessage;
425
+ }
426
+
427
+ export function toolStderrDetail(stderr: string): string {
428
+ const lines = stderr
429
+ .split(/\r?\n/)
430
+ .map((l) => l.replace(/\s+$/, ""))
431
+ .filter((l) => l.trim().length > 0)
432
+ .filter((l) => !l.startsWith("[uai-agent"))
433
+ .filter((l) => !l.trimStart().startsWith('{"ok"'));
434
+ return lines
435
+ .slice(-6)
436
+ .map((l) => (l.length > 300 ? `${l.slice(0, 300)}…` : l))
437
+ .join("\n");
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Public API. One function per agent command.
442
+ // ---------------------------------------------------------------------------
443
+
444
+ export const agent = {
445
+ async taskUp(
446
+ input: TaskLaunchInput,
447
+ credentials: TaskUpCredentials = {},
448
+ envMode: ProjectEnvMode = "baked",
449
+ /** The task's runtime as the provider reaches it once task-up has
450
+ * created it: where `credentials.leased.runtimeReady` writes. */
451
+ runtimeEnvironment?: () => TaskWriteEnvironment,
452
+ ): Promise<TaskUpResult> {
453
+ const leased = credentials.leased;
454
+ if (leased && credentials.githubCredentialSocket) {
455
+ throw new Error("a task on cloud credentials never uses the station's own GitHub credential");
456
+ }
457
+ assertLeasedTaskUp(leased, input.task);
458
+ if (leased && !runtimeEnvironment) {
459
+ throw new Error("this runtime cannot pause task-up for its runtime-ready writes");
460
+ }
461
+ const commandDbPath = createTaskUpCommandDb(input);
462
+ // Materialize the creator's per-user SSH key for commit signing and the
463
+ // explicit no-GitHub-connection transport fallback (ADR-029). A task on
464
+ // cloud credentials gets no key file: the runner writes the task's key
465
+ // itself, and serves the clone's from memory.
466
+ const identityDir = leased ? null : writeTaskIdentity(input.task.id, input.task.ownerUserId);
467
+ let cloneService: CloneCredentialService | null = null;
468
+ // ADR-154 §12: task-up says where it is with event files in a private
469
+ // directory (event-clone-done, then event-runtime-ready), never on the
470
+ // stderr its subprocesses share, and finds the runtime-ready answer
471
+ // there. Each event is taken once, in order. The runtime-ready writes
472
+ // are one operation, abortable by the hold, joined before task-up's
473
+ // outcome is returned whatever it is.
474
+ const handshakeDir = leased ? mkdtempSync(join("/tmp", "uai-task-handshake-")) : null;
475
+ if (handshakeDir) chmodSync(handshakeDir, 0o700);
476
+ let cloneDone = false;
477
+ let runtimeReadyWrites: Promise<void> | null = null;
478
+ const answer = (value: "continue" | "abort"): void => {
479
+ try {
480
+ const staged = join(handshakeDir!, ".runtime-ready");
481
+ writeFileSync(staged, `${value}\n`, { mode: 0o600 });
482
+ renameSync(staged, join(handshakeDir!, "runtime-ready"));
483
+ } catch {
484
+ // task-up already ended: nothing waits for the answer.
485
+ }
486
+ };
487
+ const takeEvents = (): void => {
488
+ if (!handshakeDir) return;
489
+ if (!cloneDone && existsSync(join(handshakeDir, "event-clone-done"))) {
490
+ cloneDone = true;
491
+ // The host's clones and fetches are over: stop serving them.
492
+ void cloneService?.close();
493
+ }
494
+ if (runtimeReadyWrites || !existsSync(join(handshakeDir, "event-runtime-ready"))) return;
495
+ if (!cloneDone) {
496
+ // Out of order: not task-up's.
497
+ runtimeReadyWrites = Promise.resolve();
498
+ answer("abort");
499
+ return;
500
+ }
501
+ runtimeReadyWrites = (async () => {
502
+ let value: "continue" | "abort" = "abort";
503
+ try {
504
+ const signal = leased!.clone.signal;
505
+ if (signal.aborted) throw new Error("the task's credentials were withdrawn");
506
+ await leased!.runtimeReady(runtimeEnvironment!(), signal);
507
+ value = "continue";
508
+ } catch (error) {
509
+ console.warn(
510
+ `[uai-agent] task ${input.task.id}: runtime-ready writes failed: ${error instanceof Error ? error.message : String(error)}`,
511
+ );
512
+ }
513
+ answer(value);
514
+ })();
515
+ };
516
+ const poller = handshakeDir ? setInterval(takeEvents, HANDSHAKE_POLL_MS) : null;
517
+ try {
518
+ // Per-(project, key) env values must NEVER enter task-up's host process
519
+ // environment. Besides ordinary app names, projects can declare names such
520
+ // as BASH_ENV, PATH, GIT_CONFIG_*, or UAI_*; exposing those before the host
521
+ // clone would let container-owned configuration influence host commands or
522
+ // read another credential boundary. Send one JSON object over stdin
523
+ // instead. task-up feeds it directly to Compose as an in-memory override,
524
+ // so values reach only the task container and are never written to disk.
525
+ // On a key collision the LEAD project (position 0) wins: iterate in
526
+ // descending position order so position-0 assignments apply last.
527
+ const projectEnv: Record<string, string> = {};
528
+ if (envMode === "baked") {
529
+ const orderedProjects = [...input.projects].sort(
530
+ (a, b) => b.position - a.position,
531
+ );
532
+ for (const project of orderedProjects) {
533
+ Object.assign(projectEnv, getDecryptedForProject(project.id));
534
+ }
535
+ }
536
+ const extraEnv: Record<string, string> = {
537
+ UAI_PROJECT_ENV_MODE: envMode,
538
+ };
539
+ if (handshakeDir) extraEnv.UAI_TASK_HANDSHAKE_DIR = handshakeDir;
540
+ if (identityDir) extraEnv.UAI_TASK_IDENTITY_DIR = identityDir;
541
+ // Only a non-secret socket path crosses into Bash. The token itself was
542
+ // seeded over stdin and never enters argv, env, project data, Compose, or
543
+ // the task container.
544
+ if (credentials.githubCredentialSocket) {
545
+ extraEnv.UAI_GITHUB_CREDENTIAL_SOCKET =
546
+ credentials.githubCredentialSocket;
547
+ }
548
+ if (leased) {
549
+ cloneService = await serveCloneCredentials(leased.clone);
550
+ Object.assign(extraEnv, cloneService.env);
551
+ }
552
+ let stdinPayload: string;
553
+ if (envMode === "loader") {
554
+ const gateway = credentials.taskGateway;
555
+ if (!gateway || gateway.secret.byteLength !== 32) {
556
+ throw new Error(
557
+ "loader task is missing its gateway generation credential",
558
+ );
559
+ }
560
+ extraEnv.UAI_TASK_GATEWAY_GENERATION = gateway.generation;
561
+ extraEnv.UAI_TASK_GATEWAY_HOST = await taskGatewayHost();
562
+ // ADR-153: what task-up installs before anything runs in the task.
563
+ // Public material only; absent under the kill switch.
564
+ // The start holds the task's root writes until its result is
565
+ // recorded, so no older writer (a recovery, a re-pin) lands over
566
+ // them while uai-init runs.
567
+ const secretsProxy = activeSecretsProxy();
568
+ if (secretsProxy !== null) {
569
+ // task-up reports each step's outcome into it as it goes, so the
570
+ // task's readiness is exact even while uai-init already runs.
571
+ const stepsDir = await writeTaskUpTrustSteps(env.dataDir, input.task.id, secretsProxy.runtime);
572
+ if (stepsDir !== null) extraEnv.UAI_SECRETS_PROXY_DIR = stepsDir;
573
+ }
574
+ const secretCopy = Buffer.from(gateway.secret);
575
+ try {
576
+ stdinPayload = secretCopy.toString("base64");
577
+ } finally {
578
+ secretCopy.fill(0);
579
+ }
580
+ } else {
581
+ stdinPayload = JSON.stringify(projectEnv);
582
+ }
583
+ let result: TaskUpResult;
584
+ try {
585
+ result = await runAgent(
586
+ "task-up.sh",
587
+ [input.task.id],
588
+ TaskUpData,
589
+ commandDbPath,
590
+ extraEnv,
591
+ stdinPayload,
592
+ leased?.clone.signal,
593
+ // ADR-154 §6: no default slot. A start on cloud credentials gets
594
+ // the runner's own engine sign-ins in neither its env nor files.
595
+ leased ? ENGINE_CREDENTIAL_ENV_NAMES : undefined,
596
+ );
597
+ } catch (error) {
598
+ finishTaskUpTrustSteps(env.dataDir, input.task.id, null);
599
+ throw error;
600
+ }
601
+ // What this start actually installed. A start that did not get the CA
602
+ // in is retried by the trust watcher once the task runs.
603
+ finishTaskUpTrustSteps(env.dataDir, input.task.id, result);
604
+ if (result.secretsProxyTrust === "failed") activeSecretsProxy()?.runtime.requestTrustDelivery();
605
+ return result;
606
+ } finally {
607
+ if (poller) clearInterval(poller);
608
+ // Joined, whatever task-up's outcome: nothing it began still writes
609
+ // into the task once this returns.
610
+ await runtimeReadyWrites;
611
+ await cloneService?.close();
612
+ if (handshakeDir) rmSync(handshakeDir, { recursive: true, force: true });
613
+ closeTaskUpTrustSteps(env.dataDir, input.task.id);
614
+ removeCommandDb(commandDbPath);
615
+ removeTaskIdentity(input.task.id);
616
+ }
617
+ },
618
+
619
+ async taskDown(input: TaskDownInput): Promise<TaskDownResult> {
620
+ const commandDbPath = createTaskDownCommandDb(
621
+ input,
622
+ getHostTask(input.taskId),
623
+ );
624
+ try {
625
+ return await runAgent(
626
+ "task-down.sh",
627
+ [input.taskId],
628
+ TaskDownData,
629
+ commandDbPath,
630
+ TASK_DOWN_TERMINAL_ENV,
631
+ );
632
+ } finally {
633
+ removeCommandDb(commandDbPath);
634
+ }
635
+ },
636
+
637
+ /** ADR-101 Phase C: idempotent teardown from a durable environment locator.
638
+ * Cloud command metadata is intentionally unnecessary at this boundary. */
639
+ async taskEnvironmentDown(taskId: string): Promise<TaskDownResult> {
640
+ const commandDbPath = createTaskEnvironmentDownCommandDb(
641
+ taskId,
642
+ getHostTask(taskId),
643
+ );
644
+ try {
645
+ return await runAgent(
646
+ "task-down.sh",
647
+ [taskId],
648
+ TaskDownData,
649
+ commandDbPath,
650
+ TASK_DOWN_TERMINAL_ENV,
651
+ );
652
+ } finally {
653
+ removeCommandDb(commandDbPath);
654
+ }
655
+ },
656
+
657
+ async taskStatus(taskId: string): Promise<TaskStatusResult> {
658
+ const commandDbPath = createTaskStatusCommandDb(taskId, getHostTask(taskId));
659
+ try {
660
+ return await runAgent(
661
+ "task-status.sh",
662
+ [taskId],
663
+ TaskStatusData,
664
+ commandDbPath,
665
+ );
666
+ } finally {
667
+ removeCommandDb(commandDbPath);
668
+ }
669
+ },
670
+ };
671
+
672
+ export type Agent = typeof agent;