@moondeck/station 0.0.0-stage → 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 (325) 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 +131 -4
  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
@@ -0,0 +1,334 @@
1
+ import { StringDecoder } from "node:string_decoder";
2
+ import { agentOutputSource } from "./output-source";
3
+ /**
4
+ * DurableProcess (ADR-061) — LineProcess's durable twin. Instead of holding
5
+ * the agent CLI's pipes, it talks to the in-container runner
6
+ * (runner/runner.mjs) through two append-only JSONL files on the
7
+ * task-workspace bind mount:
8
+ *
9
+ * writeLine() → append to <sessionDir>/inbox.jsonl
10
+ * onLine() ← poll-tail <sessionDir>/outbox.jsonl from a byte offset
11
+ *
12
+ * The CLI's lifetime is decoupled from this object: dropping it (host
13
+ * restart) leaves the runner and CLI running; `attach()` with the persisted
14
+ * outbox offset resumes exactly where consumption stopped. `close()` is the
15
+ * real teardown — it sends the runner a stop control.
16
+ *
17
+ * Exit is observed via the runner's `{"__uai":"exit"}` meta line, with a
18
+ * stale-heartbeat check as the backstop for a killed container. Meta lines
19
+ * never reach onLine handlers — adapters see the same protocol stream
20
+ * LineProcess gave them.
21
+ */
22
+
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ import {
26
+ appendContainedFile,
27
+ containedModifiedAt,
28
+ NotARegularFileError,
29
+ readContainedFrom,
30
+ WorkspacePathRefusedError,
31
+ } from "../task-environment/workspace-files";
32
+
33
+ import type { ExitHandler, LineHandler } from "./proc";
34
+
35
+ const POLL_MS = 75;
36
+ /** Runner beats every 5 s; 25 s of silence without an exit meta = dead. */
37
+ const HEARTBEAT_STALE_MS = 25_000;
38
+ /** After close(), how long to wait for the exit meta before giving up. */
39
+ const CLOSE_GRACE_MS = 8_000;
40
+
41
+ /** Host-side absolute path of the bundled in-container runner script. */
42
+ export function runnerScriptPath(): string {
43
+ return fileURLToPath(new URL("../../runner/runner.mjs", import.meta.url));
44
+ }
45
+
46
+ export interface DurableProcessOptions {
47
+ /** Session dir as seen FROM THE HOST (on the workspace bind mount). */
48
+ hostSessionDir: string;
49
+ /** The same directory as the task's workspace root and its components
50
+ * below it: every file in it is reached never through a link the task
51
+ * put there (#cb3j0z). It exists before the runner is launched. */
52
+ sessionFiles: { root: string; parts: readonly string[] };
53
+ /** Settles once that directory has been made, where only the task's
54
+ * runtime can make it (macOS, #cb3j0z): no inbox write, the stop
55
+ * included, is attempted before. Absent: it exists already. */
56
+ ready?: Promise<void>;
57
+ /** Provider-owned detached launch. Omit to attach to an existing runner. */
58
+ launch?: () => Promise<{
59
+ exitCode: number | null;
60
+ stderr: Uint8Array;
61
+ }>;
62
+ /** Resume consumption from this outbox byte offset (attach path). */
63
+ initialOutboxOffset?: number;
64
+ /** Called after each poll batch whose lines were delivered — persist this
65
+ * offset so a host restart resumes here instead of replaying. */
66
+ onOffsetAdvance?: (offset: number) => void;
67
+ /** Called the moment close() is requested (before the runner has actually
68
+ * died) — lets the owner mark the session unattachable immediately, so a
69
+ * re-create during the teardown grace window spawns fresh instead of
70
+ * attaching to a dying runner. */
71
+ onCloseRequested?: () => void;
72
+ /** Mirrors LineProcess: log raw traffic when UAI_DEBUG_AGENTS is set. */
73
+ debugLabel?: string;
74
+ }
75
+
76
+ interface RunnerMeta {
77
+ __uai: string;
78
+ code?: number | null;
79
+ stderrTail?: string;
80
+ }
81
+
82
+ export class DurableProcess {
83
+ readonly durableSessionDir: string;
84
+ private readonly root: string;
85
+ private readonly outbox: readonly string[];
86
+ private readonly inbox: readonly string[];
87
+ private readonly heartbeat: readonly string[];
88
+
89
+ private readonly decoder = new StringDecoder("utf8");
90
+ private lineOffset = 0;
91
+ private readonly lineHandlers = new Set<LineHandler>();
92
+ private readonly exitHandlers = new Set<ExitHandler>();
93
+
94
+ private offset: number;
95
+ private lineBuf = "";
96
+ private stderrBuf = "";
97
+ private closed = false;
98
+ private detached = false;
99
+ private sawSpawnMeta = false;
100
+ private readonly startedAt = Date.now();
101
+ private poller: ReturnType<typeof setInterval> | null = null;
102
+ private closeTimer: ReturnType<typeof setTimeout> | null = null;
103
+ private inboxChain: Promise<void> = Promise.resolve();
104
+ private readonly onOffsetAdvance: ((offset: number) => void) | null;
105
+ private readonly onCloseRequested: (() => void) | null;
106
+ private readonly debug: string | null;
107
+
108
+ constructor(opts: DurableProcessOptions) {
109
+ this.durableSessionDir = opts.hostSessionDir;
110
+ this.root = opts.sessionFiles.root;
111
+ this.outbox = [...opts.sessionFiles.parts, "outbox.jsonl"];
112
+ this.inbox = [...opts.sessionFiles.parts, "inbox.jsonl"];
113
+ this.heartbeat = [...opts.sessionFiles.parts, "heartbeat"];
114
+ this.offset = opts.initialOutboxOffset ?? 0;
115
+ this.lineOffset = this.offset;
116
+ this.onOffsetAdvance = opts.onOffsetAdvance ?? null;
117
+ this.onCloseRequested = opts.onCloseRequested ?? null;
118
+ if (opts.ready) {
119
+ this.inboxChain = opts.ready.then(
120
+ () => undefined,
121
+ () => undefined,
122
+ );
123
+ }
124
+ this.debug =
125
+ opts.debugLabel && process.env.UAI_DEBUG_AGENTS ? opts.debugLabel : null;
126
+
127
+ if (opts.launch) {
128
+ void opts.launch().then(
129
+ (result) => {
130
+ const stderr = Buffer.from(result.stderr).toString("utf8");
131
+ if (stderr) this.stderrBuf = (this.stderrBuf + stderr).slice(-8192);
132
+ if (result.exitCode !== 0) this.finish(null);
133
+ },
134
+ (error: unknown) => {
135
+ this.stderrBuf = (
136
+ this.stderrBuf +
137
+ (error instanceof Error ? error.message : String(error))
138
+ ).slice(-8192);
139
+ this.finish(null);
140
+ },
141
+ );
142
+ } else {
143
+ this.sawSpawnMeta = true; // attach: the runner pre-exists
144
+ }
145
+
146
+ this.poller = setInterval(() => this.poll(), POLL_MS);
147
+ }
148
+
149
+ private log(msg: string): void {
150
+ console.error(`[uai-agent ${this.debug}] ${msg}`);
151
+ }
152
+
153
+ // ---- outbox tail ---------------------------------------------------------
154
+
155
+ private poll(): void {
156
+ if (this.closed || this.detached) return;
157
+ const before = this.offset;
158
+ try {
159
+ for (;;) {
160
+ const read = readContainedFrom(this.root, this.outbox, this.offset, 256 * 1024);
161
+ if (read === null) break; // outbox not created yet
162
+ if (read.bytes.length === 0) break;
163
+ this.offset += read.bytes.length;
164
+ this.lineBuf += this.decoder.write(read.bytes);
165
+ this.drainLines();
166
+ if (this.offset >= read.size) break;
167
+ }
168
+ } catch (error) {
169
+ if (error instanceof WorkspacePathRefusedError || error instanceof NotARegularFileError) {
170
+ // A link, or anything but a regular file, where the outbox is: the
171
+ // task broke its own session, and it ends (#cb3j0z).
172
+ this.finish(null);
173
+ return;
174
+ }
175
+ // Anything else may pass: the heartbeat decides, below.
176
+ }
177
+ if (this.offset !== before && this.onOffsetAdvance && !this.closed) {
178
+ try {
179
+ this.onOffsetAdvance(this.lineOffset);
180
+ } catch {
181
+ // Persistence hiccup — the next batch retries with a larger offset.
182
+ }
183
+ }
184
+ this.checkLiveness();
185
+ }
186
+
187
+ private drainLines(): void {
188
+ let nl: number;
189
+ while ((nl = this.lineBuf.indexOf("\n")) >= 0) {
190
+ const rawLine = this.lineBuf.slice(0, nl);
191
+ this.lineOffset += Buffer.byteLength(rawLine) + 1;
192
+ const line = rawLine.trim();
193
+ this.lineBuf = this.lineBuf.slice(nl + 1);
194
+ if (line.length === 0) continue;
195
+ if (line.startsWith('{"__uai"')) {
196
+ this.handleMeta(line);
197
+ continue;
198
+ }
199
+ if (this.debug) this.log(`<- ${line.slice(0, 1000)}`);
200
+ for (const h of this.lineHandlers) {
201
+ try {
202
+ agentOutputSource.run(this.durableSessionDir + ":" + this.lineOffset, () => h(line));
203
+ } catch {
204
+ // A broken handler must not wedge the tail loop.
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ private handleMeta(line: string): void {
211
+ let meta: RunnerMeta;
212
+ try {
213
+ meta = JSON.parse(line) as RunnerMeta;
214
+ } catch {
215
+ return;
216
+ }
217
+ if (meta.__uai === "spawn") {
218
+ this.sawSpawnMeta = true;
219
+ } else if (meta.__uai === "exit") {
220
+ if (typeof meta.stderrTail === "string" && meta.stderrTail) {
221
+ this.stderrBuf = meta.stderrTail.slice(-8192);
222
+ }
223
+ if (this.debug) this.log(`exit meta: code ${meta.code ?? null}`);
224
+ this.finish(typeof meta.code === "number" ? meta.code : null);
225
+ }
226
+ }
227
+
228
+ /** Backstop: no exit meta, but the runner stopped beating → it's gone. */
229
+ private checkLiveness(): void {
230
+ if (!this.sawSpawnMeta) {
231
+ // Runner never wrote anything; give the spawn a grace window.
232
+ if (Date.now() - this.startedAt > HEARTBEAT_STALE_MS) this.finish(null);
233
+ return;
234
+ }
235
+ let beat: number | null = null;
236
+ try {
237
+ beat = containedModifiedAt(this.root, this.heartbeat);
238
+ } catch {
239
+ beat = null;
240
+ }
241
+ if (beat !== null) {
242
+ if (Date.now() - beat > HEARTBEAT_STALE_MS) this.finish(null);
243
+ } else if (Date.now() - this.startedAt > HEARTBEAT_STALE_MS) {
244
+ this.finish(null);
245
+ }
246
+ }
247
+
248
+ private finish(code: number | null): void {
249
+ if (this.closed) return;
250
+ this.closed = true;
251
+ if (this.poller) clearInterval(this.poller);
252
+ if (this.closeTimer) clearTimeout(this.closeTimer);
253
+ for (const h of this.exitHandlers) h(code);
254
+ }
255
+
256
+ // ---- LineProcess-compatible surface ---------------------------------------
257
+
258
+ onLine(handler: LineHandler): void {
259
+ this.lineHandlers.add(handler);
260
+ }
261
+
262
+ onExit(handler: ExitHandler): void {
263
+ this.exitHandlers.add(handler);
264
+ }
265
+
266
+ /**
267
+ * Serialise `value` as one JSONL line appended to the runner's inbox.
268
+ * A runner-filtered metadata line immediately before it supplies the local
269
+ * timestamp without changing the protocol frame forwarded to the CLI. Old
270
+ * runners already ignore unknown `__uai` lines, so attached sessions remain
271
+ * wire-compatible across the host update.
272
+ *
273
+ * `__uai` MUST stay the first key of the metadata object. Both runners filter
274
+ * with `line.startsWith('{"__uai"')` — a byte prefix, not a parse — so
275
+ * serialising the keys in any other order emits `{"ts":…`, which the runner
276
+ * then forwards to the CLI's stdin as a user turn. No type in this file
277
+ * expresses that, and a `toEqual` assertion cannot see it; the raw-prefix
278
+ * check in `durable-proc.test.ts` is what pins it.
279
+ */
280
+ writeLine(value: unknown): void {
281
+ if (this.closed || this.detached) return;
282
+ const json = JSON.stringify(value);
283
+ const inputMeta = JSON.stringify({ __uai: "input", ts: Date.now() });
284
+ if (this.debug) this.log(`-> ${json.slice(0, 1000)}`);
285
+ // Chain appends so concurrent writes can't interleave bytes.
286
+ this.inboxChain = this.inboxChain
287
+ .then(() => appendContainedFile(this.root, this.inbox, Buffer.from(`${inputMeta}\n${json}\n`)))
288
+ .catch(() => {
289
+ // Disk error — liveness checks will surface a dead session.
290
+ });
291
+ }
292
+
293
+ get stderrTail(): string {
294
+ return this.stderrBuf;
295
+ }
296
+
297
+ get isClosed(): boolean {
298
+ return this.closed;
299
+ }
300
+
301
+ /** Consumed-outbox byte offset — persist this to survive host restarts. */
302
+ get outboxOffset(): number {
303
+ return this.lineOffset;
304
+ }
305
+
306
+ /**
307
+ * Stop consuming WITHOUT touching the runner — the host-restart path in
308
+ * miniature (a replaced consumer, a shutting-down host). The CLI lives on;
309
+ * a later attach() resumes from the persisted offset.
310
+ */
311
+ detach(): void {
312
+ if (this.detached || this.closed) return;
313
+ this.detached = true;
314
+ if (this.poller) clearInterval(this.poller);
315
+ if (this.closeTimer) clearTimeout(this.closeTimer);
316
+ }
317
+
318
+ /** Real teardown: ask the runner to stop its CLI, then observe the exit. */
319
+ async close(): Promise<void> {
320
+ if (this.closed || this.detached) return;
321
+ try {
322
+ this.onCloseRequested?.();
323
+ } catch {
324
+ // Bookkeeping only — never blocks the teardown itself.
325
+ }
326
+ try {
327
+ await this.inboxChain;
328
+ await appendContainedFile(this.root, this.inbox, Buffer.from('{"__uai":"stop"}\n'));
329
+ } catch {
330
+ // Inbox unwritable — fall through to the grace timer.
331
+ }
332
+ this.closeTimer = setTimeout(() => this.finish(null), CLOSE_GRACE_MS);
333
+ }
334
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * realAgentFactory — builds a real AgentSession per roster agent, resolving
3
+ * the adapter from the plug-in registry by `agent.kind` (ADR-021).
4
+ *
5
+ * Importing this module pulls in `./claude` and `./codex` purely for their
6
+ * side effect: each calls `register()` at load, populating the registry.
7
+ * Adding a kind is then "drop a module + import it for its side effect" — no
8
+ * change here. Unknown kinds fail loudly so a typo in a roster surfaces as an
9
+ * error rather than silently falling back to the wrong CLI.
10
+ *
11
+ * Adapters run their CLI through the task environment's provider-owned
12
+ * session surface; the agent's `model` (if set) is passed through. Swapping
13
+ * mock ⇄ real is changing which factory the orchestrator is constructed with
14
+ * — see `getOrchestrator`.
15
+ */
16
+
17
+ // Side-effect imports: register the built-in adapters with the registry.
18
+ import "./claude";
19
+ import "./codex";
20
+ import "./kimi";
21
+ import "./grok";
22
+ import "./cursor";
23
+ import "./opencode";
24
+ import "./uai";
25
+
26
+ import { agentClisReady } from "../standard-image";
27
+ import { factoryFor, supportsExecutionProfile } from "./registry";
28
+ import type { AgentSession, AgentSessionFactory } from "./types";
29
+
30
+ export const realAgentFactory: AgentSessionFactory = {
31
+ create: async (args): Promise<AgentSession> => {
32
+ const factory = factoryFor(args.agent.kind);
33
+ if (!factory) {
34
+ throw new Error(
35
+ `no agent adapter registered for kind "${args.agent.kind}"`,
36
+ );
37
+ }
38
+ if (
39
+ args.executionProfile &&
40
+ !supportsExecutionProfile(args.agent.kind, args.executionProfile)
41
+ ) {
42
+ throw new Error(
43
+ `agent adapter "${args.agent.kind}" cannot enforce execution profile "${args.executionProfile}"`,
44
+ );
45
+ }
46
+ // Don't spawn a CLI until the shared-volume agent CLIs are reconciled:
47
+ // at boot the CLI auto-upgrade briefly removes then reinstalls codex/claude,
48
+ // and a resume that races that window dies with "No codex executable found
49
+ // for nodejs X". Post-boot this is already resolved (instant).
50
+ // Fail closed: the maintenance commands have their own process timeouts
51
+ // and settle this latch even on failure. A second timeout here would let a
52
+ // CLI spawn enter the shared volume while boot reconciliation still owns
53
+ // its remove/reinstall window.
54
+ await agentClisReady;
55
+ return factory.create(args);
56
+ },
57
+ };