@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,133 @@
1
+ /**
2
+ * MockAgentSession — a fake AgentSession for building and testing the
3
+ * chat pipeline without Docker or a real CLI.
4
+ *
5
+ * On send(), it streams a few `message_delta` chunks, sometimes a
6
+ * `tool_call` card, then `message_complete` + `turn_complete`. Enough
7
+ * shape that the orchestrator, SSE stream, and chat UI can all be
8
+ * exercised end to end. The real ClaudeSession / CodexSession adapters
9
+ * replace this behind the identical interface.
10
+ */
11
+
12
+ import { newId } from "../ulid";
13
+ import type {
14
+ AgentEvent,
15
+ AgentEventHandler,
16
+ AgentKind,
17
+ AgentSession,
18
+ AgentSessionFactory,
19
+ RosterAgent,
20
+ } from "./types";
21
+
22
+ /** Tunable so tests can run instantly while dev feels lifelike. */
23
+ const DELTA_INTERVAL_MS = Number(process.env.UAI_MOCK_DELTA_MS ?? 60);
24
+
25
+ export class MockAgentSession implements AgentSession {
26
+ readonly agentId: string;
27
+ readonly kind: AgentKind;
28
+
29
+ private readonly handlers = new Set<AgentEventHandler>();
30
+ private readonly timers = new Set<ReturnType<typeof setTimeout>>();
31
+ private closed = false;
32
+ private turn = 0;
33
+
34
+ constructor(args: { agent: RosterAgent; systemPreamble: string }) {
35
+ this.agentId = args.agent.id;
36
+ this.kind = args.agent.kind;
37
+ }
38
+
39
+ onEvent(handler: AgentEventHandler): () => void {
40
+ this.handlers.add(handler);
41
+ return () => this.handlers.delete(handler);
42
+ }
43
+
44
+ private emit(event: AgentEvent): void {
45
+ if (this.closed) return;
46
+ for (const h of this.handlers) h(event);
47
+ }
48
+
49
+ /** schedule(fn, ms) — like setTimeout but cleaned up on close(). */
50
+ private schedule(fn: () => void, ms: number): void {
51
+ const t = setTimeout(() => {
52
+ this.timers.delete(t);
53
+ if (!this.closed) fn();
54
+ }, ms);
55
+ this.timers.add(t);
56
+ }
57
+
58
+ async send(text: string): Promise<void> {
59
+ if (this.closed) return;
60
+ this.turn += 1;
61
+
62
+ // Compose a canned reply that echoes a little of the input so the
63
+ // feed visibly reflects what was sent.
64
+ const trimmed = text.trim().replace(/\s+/g, " ");
65
+ const echo = trimmed.length > 60 ? `${trimmed.slice(0, 57)}…` : trimmed;
66
+ const reply =
67
+ `[mock ${this.agentId}] received: "${echo}". ` +
68
+ `This is turn ${this.turn}. Wiring up the real ${this.kind} ` +
69
+ `adapter is the next step — until then I just echo.`;
70
+
71
+ const words = reply.split(" ");
72
+ let cursor = 0;
73
+ let assembled = "";
74
+
75
+ // Stream the reply word-by-word.
76
+ const streamNext = (): void => {
77
+ if (cursor >= words.length) {
78
+ this.emit({ type: "message_complete", text: assembled });
79
+ // Halfway through the conversation, fake a tool call so the
80
+ // card rendering path gets exercised.
81
+ if (this.turn === 1) {
82
+ this.schedule(() => {
83
+ this.emit({
84
+ type: "tool_call",
85
+ id: newId(),
86
+ title: "Read",
87
+ detail: "README.md (mock)",
88
+ });
89
+ this.schedule(() => this.emit({ type: "turn_complete" }), 200);
90
+ }, 150);
91
+ } else {
92
+ this.emit({ type: "turn_complete" });
93
+ }
94
+ return;
95
+ }
96
+ const chunk = (cursor === 0 ? "" : " ") + words[cursor];
97
+ assembled += chunk;
98
+ cursor += 1;
99
+ this.emit({ type: "message_delta", text: chunk });
100
+ this.schedule(streamNext, DELTA_INTERVAL_MS);
101
+ };
102
+
103
+ // Small initial "thinking" delay before the first token.
104
+ this.schedule(streamNext, DELTA_INTERVAL_MS * 2);
105
+ }
106
+
107
+ async interrupt(): Promise<void> {
108
+ if (this.closed) return;
109
+ // Stop any in-flight streaming and end the turn — the mock's ESC.
110
+ for (const t of this.timers) clearTimeout(t);
111
+ this.timers.clear();
112
+ this.emit({ type: "turn_complete" });
113
+ }
114
+
115
+ async resolvePermission(): Promise<void> {
116
+ // The mock never raises permission requests, so nothing to resolve.
117
+ }
118
+
119
+ async close(): Promise<void> {
120
+ if (this.closed) return;
121
+ this.closed = true;
122
+ for (const t of this.timers) clearTimeout(t);
123
+ this.timers.clear();
124
+ this.emit({ type: "exit", code: 0 });
125
+ this.handlers.clear();
126
+ }
127
+ }
128
+
129
+ /** Factory that hands back MockAgentSessions. */
130
+ export const mockAgentFactory: AgentSessionFactory = {
131
+ create: async ({ agent, systemPreamble }) =>
132
+ new MockAgentSession({ agent, systemPreamble }),
133
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Which agent adapters this host runs — `UAI_AGENTS`.
3
+ *
4
+ * REAL IS THE DEFAULT. It did not used to be: the check was
5
+ * `UAI_AGENTS === "real" ? real : mock`, so every value except that one
6
+ * literal — including unset — silently selected the echo mock. That was a
7
+ * sane default while the adapters were unproven and the only hosts were the
8
+ * author's, but it shipped to npm unchanged. A user installs `@runuai/host`,
9
+ * enrolls, starts a task, and the host looks perfectly healthy while every
10
+ * agent echoes canned text. Nothing in the logs or `uai-host status` said so.
11
+ *
12
+ * The desktop host app (since deleted) never hit it because it hardcoded
13
+ * `UAI_AGENTS: "real"` — the npm path had no equivalent, so
14
+ * the bug was invisible to exactly the people who could reproduce it.
15
+ *
16
+ * So: a host runs real agents unless it is explicitly told not to. `mock` is
17
+ * the opt-in for tests and for dev boxes with no Docker, and an unrecognised
18
+ * value resolves to `real` with a warning rather than quietly downgrading —
19
+ * a typo must never cost someone a day of echoed output again.
20
+ */
21
+
22
+ export type AgentMode = "real" | "mock";
23
+
24
+ /** Values that select the in-process echo mock. */
25
+ const MOCK_VALUES = new Set(["mock", "fake", "echo"]);
26
+ /** Values that select the real Claude/Codex adapters (the default). */
27
+ const REAL_VALUES = new Set(["real", "prod", "production"]);
28
+
29
+ export interface AgentModeResolution {
30
+ mode: AgentMode;
31
+ /** Set when the raw value was neither recognised nor empty. */
32
+ warning?: string;
33
+ }
34
+
35
+ /**
36
+ * Resolve `UAI_AGENTS` to a mode. Pure and case/whitespace insensitive so a
37
+ * stray `UAI_AGENTS=Mock ` from a hand-edited .env.local behaves as written.
38
+ */
39
+ export function resolveAgentMode(
40
+ raw: string | undefined = process.env.UAI_AGENTS,
41
+ ): AgentModeResolution {
42
+ const value = (raw ?? "").trim().toLowerCase();
43
+ if (value === "") return { mode: "real" };
44
+ if (MOCK_VALUES.has(value)) return { mode: "mock" };
45
+ if (REAL_VALUES.has(value)) return { mode: "real" };
46
+ return {
47
+ mode: "real",
48
+ warning:
49
+ `unrecognised UAI_AGENTS=${JSON.stringify(raw)} — running REAL agents. ` +
50
+ `Use "mock" for the echo agent, or unset it.`,
51
+ };
52
+ }
53
+
54
+ /** The mode this process is running in. */
55
+ export function agentMode(): AgentMode {
56
+ return resolveAgentMode().mode;
57
+ }
58
+
59
+ /** Echo/mock sessions cannot execute the in-task structured-dispatch CLI. */
60
+ export function canAdvertiseTypedSecretaryDispatch(
61
+ raw: string | undefined = process.env.UAI_AGENTS,
62
+ ): boolean {
63
+ return resolveAgentMode(raw).mode === "real";
64
+ }
@@ -0,0 +1,508 @@
1
+ /**
2
+ * OpencodeSession — a real AgentSession backed by the OpenCode CLI's
3
+ * non-interactive `run` mode, executed inside the task environment (ADR-077):
4
+ *
5
+ * ~/.opencode/bin/opencode run \
6
+ * --format json --auto --model <provider/model> [--variant <effort>] \
7
+ * [--session <id>] "<prompt>"
8
+ *
9
+ * Like Kimi/Grok/Cursor (ADR-066/067/068) OpenCode is **one-shot per turn**:
10
+ * `run` executes a single prompt to completion, streams newline-delimited JSON
11
+ * events to stdout, and exits. Multi-turn continuity is `--session <id>` (the
12
+ * id is captured from the first turn's event stream). Each `send()` spawns a
13
+ * fresh provider process; sends are serialized so a second message can't race a
14
+ * running turn.
15
+ *
16
+ * `--format json` vocabulary (one object per line, `type` field):
17
+ * {"type":"step_start", ...} → (ignored)
18
+ * {"type":"text","part":{"text":"…"}} → message_complete
19
+ * {"type":"tool_use","part":{"tool","state":{…}}} → tool_call
20
+ * {"type":"step_finish","part":{"cost","tokens":…}} → (accumulated usage)
21
+ * {"type":"error","error":{…}} → error
22
+ * Every event carries a top-level `sessionID` (captured for --session). The
23
+ * turn ends when the process exits (turn_complete, with accumulated usage).
24
+ *
25
+ * AUTH is the operator's OpenCode provider keys in
26
+ * `~/.local/share/opencode/auth.json`, docker-cp'd into the container at
27
+ * task-up (like Codex's ~/.codex) — never the cloud (ADR-015). `--auto`
28
+ * auto-approves tool permissions (the container is the isolation boundary), so
29
+ * there are no permission prompts to resolve. ADR-076: `OPENCODE_DATA_DIR` in
30
+ * agentEnv selects which account's data dir (auth) this exec uses.
31
+ */
32
+ import { existsSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { join } from "node:path";
35
+
36
+ import { requireContainerRuntimeOperational } from "../runtime-guard";
37
+ import type {
38
+ TaskEnvironmentAgentSessionSurface,
39
+ TaskEnvironmentSessionRequest,
40
+ } from "../task-environment/types";
41
+ import { newId } from "../ulid";
42
+ import {
43
+ AGENT_SESSION_OUTPUT_BUFFER_BYTES,
44
+ spawnEnvironmentProcess,
45
+ type AgentEnvironmentProcess,
46
+ } from "./proc";
47
+ import { rateLimitErrorUsage } from "./rate-limit";
48
+ import { register } from "./registry";
49
+ import type {
50
+ AgentEvent,
51
+ AgentEventHandler,
52
+ AgentKind,
53
+ AgentSession,
54
+ AgentUsage,
55
+ RosterAgent,
56
+ } from "./types";
57
+
58
+ /** Installed by the image's opencode installer (opencode.ai/install); absolute
59
+ * so a non-login provider process doesn't depend on PATH. */
60
+ const OPENCODE_BIN = "/home/node/.opencode/bin/opencode";
61
+
62
+ // OpenCode is multi-provider: models are `provider/model` strings. This is a
63
+ // curated slice keyed to OpenCode Zen (the `opencode/…` gateway that the
64
+ // connect picker defaults to — OpenCode's own coding-benchmarked catalog);
65
+ // any provider/model the operator's auth.json can drive is still typable in
66
+ // the composer (unlisted values round-trip). If you bring native provider
67
+ // keys instead of Zen, type e.g. `anthropic/claude-sonnet-5` directly.
68
+ // UPDATE alongside lib/agent-catalog.ts. Order = display order. Deprecated Zen
69
+ // models are intentionally omitted (they 4xx): the gpt-5.x-codex/gpt-5-codex
70
+ // line (retired 2026-07-23), glm-5, minimax-m2.5, kimi-k2.5.
71
+ const OPENCODE_MODELS = [
72
+ // Anthropic (Claude).
73
+ "opencode/claude-sonnet-5",
74
+ "opencode/claude-opus-5",
75
+ "opencode/claude-opus-4-8",
76
+ "opencode/claude-opus-4-7",
77
+ "opencode/claude-opus-4-6",
78
+ "opencode/claude-opus-4-5",
79
+ "opencode/claude-sonnet-4-6",
80
+ "opencode/claude-sonnet-4-5",
81
+ "opencode/claude-haiku-4-5",
82
+ "opencode/claude-fable-5",
83
+ // OpenAI (GPT).
84
+ "opencode/gpt-5.6-sol",
85
+ "opencode/gpt-5.6-terra",
86
+ "opencode/gpt-5.6-luna",
87
+ "opencode/gpt-5.5",
88
+ "opencode/gpt-5.5-pro",
89
+ "opencode/gpt-5.4",
90
+ "opencode/gpt-5.4-pro",
91
+ "opencode/gpt-5.4-mini",
92
+ "opencode/gpt-5.4-nano",
93
+ "opencode/gpt-5.3-codex",
94
+ "opencode/gpt-5.3-codex-spark",
95
+ "opencode/gpt-5.2",
96
+ "opencode/gpt-5.1",
97
+ "opencode/gpt-5",
98
+ "opencode/gpt-5-nano",
99
+ // Google (Gemini).
100
+ "opencode/gemini-3.6-flash",
101
+ "opencode/gemini-3.5-flash",
102
+ "opencode/gemini-3.5-flash-lite",
103
+ "opencode/gemini-3.1-pro",
104
+ "opencode/gemini-3-flash",
105
+ // xAI (Grok).
106
+ "opencode/grok-4.5",
107
+ "opencode/grok-build-0.1",
108
+ // Open-weight — Moonshot Kimi.
109
+ "opencode/kimi-k2.7-code",
110
+ "opencode/kimi-k2.6",
111
+ // Open-weight — Alibaba Qwen.
112
+ "opencode/qwen3.7-max",
113
+ "opencode/qwen3.7-plus",
114
+ "opencode/qwen3.6-plus",
115
+ "opencode/qwen3.5-plus",
116
+ // Open-weight — DeepSeek.
117
+ "opencode/deepseek-v4-pro",
118
+ "opencode/deepseek-v4-flash",
119
+ // Open-weight — Z.ai GLM.
120
+ "opencode/glm-5.2",
121
+ "opencode/glm-5.1",
122
+ // Open-weight — MiniMax.
123
+ "opencode/minimax-m3",
124
+ "opencode/minimax-m2.7",
125
+ // Free / stealth (time-limited; some retain data during their free window).
126
+ "opencode/big-pickle",
127
+ "opencode/deepseek-v4-flash-free",
128
+ "opencode/mimo-v2.5-free",
129
+ "opencode/laguna-s-2.1-free",
130
+ "opencode/ling-3.0-flash-free",
131
+ "opencode/north-mini-code-free",
132
+ "opencode/nemotron-3-ultra-free",
133
+ ];
134
+ const OPENCODE_DEFAULT_MODEL = "opencode/claude-sonnet-5";
135
+
136
+ // Reasoning effort is a provider-specific `--variant`, not a fixed list, so the
137
+ // picker offers none; a typed effort is still passed through.
138
+ const OPENCODE_EFFORTS: string[] = [];
139
+
140
+ /** Host path to the operator's OpenCode auth — its presence gates the engine
141
+ * (mirrors Codex's ~/.codex/auth.json check). */
142
+ function opencodeAuthPath(): string {
143
+ return join(
144
+ process.env.UAI_OWNER_HOME?.trim() || homedir(),
145
+ ".local",
146
+ "share",
147
+ "opencode",
148
+ "auth.json",
149
+ );
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Pure protocol mapping — one JSON line → AgentEvent[] (+ session id / usage).
154
+ // ---------------------------------------------------------------------------
155
+
156
+ function isObj(v: unknown): v is Record<string, unknown> {
157
+ return typeof v === "object" && v !== null;
158
+ }
159
+
160
+ function str(v: unknown, fallback = ""): string {
161
+ return typeof v === "string" ? v : fallback;
162
+ }
163
+
164
+ function num(v: unknown): number | undefined {
165
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
166
+ }
167
+
168
+ export interface MappedOpencodeLine {
169
+ events: AgentEvent[];
170
+ /** The session id, when this line carried one (for --session continuity). */
171
+ sessionId?: string;
172
+ /** Usage accounting, when this line was a step_finish. */
173
+ usage?: AgentUsage;
174
+ }
175
+
176
+ /** Best-effort one-line detail for a tool card from a tool_use part. */
177
+ function toolDetail(state: unknown): string {
178
+ if (!isObj(state)) return "";
179
+ const title = state.title;
180
+ if (typeof title === "string" && title.length > 0) return title;
181
+ const input = state.input;
182
+ if (typeof input === "string") return input;
183
+ try {
184
+ return input !== undefined ? JSON.stringify(input) : "";
185
+ } catch {
186
+ return "";
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Map a single OpenCode `run --format json` line. Non-JSON lines and events
192
+ * with no user-facing meaning (step_start) map to no events. Total + pure.
193
+ */
194
+ export function mapOpencodeLine(
195
+ line: string,
196
+ now: number = Date.now(),
197
+ ): MappedOpencodeLine {
198
+ const trimmed = line.trim();
199
+ if (!trimmed.startsWith("{")) return { events: [] };
200
+ let msg: Record<string, unknown>;
201
+ try {
202
+ const parsed: unknown = JSON.parse(trimmed);
203
+ if (!isObj(parsed)) return { events: [] };
204
+ msg = parsed;
205
+ } catch {
206
+ return { events: [] };
207
+ }
208
+
209
+ const sessionId =
210
+ typeof msg.sessionID === "string" ? msg.sessionID : undefined;
211
+ const type = str(msg.type);
212
+ const part = isObj(msg.part) ? msg.part : {};
213
+
214
+ if (type === "text") {
215
+ const text = str(part.text);
216
+ return text
217
+ ? { events: [{ type: "message_complete", text }], sessionId }
218
+ : { events: [], sessionId };
219
+ }
220
+
221
+ if (type === "tool_use") {
222
+ const state = isObj(part.state) ? part.state : {};
223
+ return {
224
+ events: [
225
+ {
226
+ type: "tool_call",
227
+ id: str(part.callID) || str(part.id) || newId(),
228
+ title: str(part.tool, "tool"),
229
+ detail: toolDetail(state),
230
+ },
231
+ ],
232
+ sessionId,
233
+ };
234
+ }
235
+
236
+ if (type === "step_finish") {
237
+ const tokens = isObj(part.tokens) ? part.tokens : {};
238
+ const cache = isObj(tokens.cache) ? tokens.cache : {};
239
+ const usage: AgentUsage = {
240
+ inputTokens: num(tokens.input),
241
+ outputTokens: num(tokens.output),
242
+ cacheReadTokens: num(cache.read),
243
+ cacheCreateTokens: num(cache.write),
244
+ costUsd: num(part.cost),
245
+ };
246
+ return { events: [], sessionId, usage };
247
+ }
248
+
249
+ if (type === "error") {
250
+ const err = isObj(msg.error) ? msg.error : {};
251
+ const data = isObj(err.data) ? err.data : {};
252
+ const message =
253
+ str(data.message) || str(err.name) || "opencode error";
254
+ const accountUsage = rateLimitErrorUsage(message, now);
255
+ return {
256
+ events: accountUsage
257
+ ? [
258
+ { type: "account_usage", usage: accountUsage },
259
+ {
260
+ type: "error",
261
+ message,
262
+ retryable: "rate_limit",
263
+ ...(accountUsage.resetsAt === undefined
264
+ ? {}
265
+ : { resetsAt: accountUsage.resetsAt }),
266
+ },
267
+ ]
268
+ : [
269
+ { type: "error", message },
270
+ ],
271
+ sessionId,
272
+ };
273
+ }
274
+
275
+ return { events: [], sessionId };
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Session
280
+ // ---------------------------------------------------------------------------
281
+
282
+ /** Injectable spawn seam (tests provide a fake). */
283
+ export type Spawner = (
284
+ request: TaskEnvironmentSessionRequest,
285
+ ) => AgentEnvironmentProcess;
286
+
287
+ /** Sum this turn's step usage into a running total. */
288
+ function addUsage(acc: AgentUsage, u: AgentUsage): AgentUsage {
289
+ const add = (a?: number, b?: number): number | undefined =>
290
+ a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);
291
+ return {
292
+ inputTokens: add(acc.inputTokens, u.inputTokens),
293
+ outputTokens: add(acc.outputTokens, u.outputTokens),
294
+ cacheReadTokens: add(acc.cacheReadTokens, u.cacheReadTokens),
295
+ cacheCreateTokens: add(acc.cacheCreateTokens, u.cacheCreateTokens),
296
+ costUsd: add(acc.costUsd, u.costUsd),
297
+ };
298
+ }
299
+
300
+ export class OpencodeSession implements AgentSession {
301
+ readonly agentId: string;
302
+ readonly kind: AgentKind = "opencode";
303
+
304
+ private readonly environment: TaskEnvironmentAgentSessionSurface;
305
+ private readonly model?: string;
306
+ private readonly effort?: string;
307
+ private readonly systemPreamble: string;
308
+ private readonly agentEnv: Record<string, string>;
309
+ private readonly spawner: Spawner;
310
+
311
+ private readonly handlers = new Set<AgentEventHandler>();
312
+ private sessionId: string | null = null;
313
+ private current: AgentEnvironmentProcess | null = null;
314
+ private queue: Promise<void> = Promise.resolve();
315
+ private sentPreamble = false;
316
+ private closed = false;
317
+
318
+ constructor(args: {
319
+ taskId: string;
320
+ agent: RosterAgent;
321
+ environment: TaskEnvironmentAgentSessionSurface;
322
+ systemPreamble: string;
323
+ agentEnv?: Record<string, string>;
324
+ spawner?: Spawner;
325
+ }) {
326
+ this.agentId = args.agent.id;
327
+ this.environment = args.environment;
328
+ this.model = args.agent.model;
329
+ this.effort = args.agent.effort;
330
+ this.systemPreamble = args.systemPreamble;
331
+ this.agentEnv = args.agentEnv ?? {};
332
+ this.spawner =
333
+ args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
334
+ }
335
+
336
+ onEvent(handler: AgentEventHandler): () => void {
337
+ this.handlers.add(handler);
338
+ return () => this.handlers.delete(handler);
339
+ }
340
+
341
+ private emit(event: AgentEvent): void {
342
+ if (this.closed && event.type !== "exit") return;
343
+ for (const h of this.handlers) h(event);
344
+ }
345
+
346
+ /** One-shot per turn; serialize so a second message queues behind the
347
+ * running one instead of spawning a racing `opencode run`. */
348
+ async send(text: string): Promise<void> {
349
+ if (this.closed) return;
350
+ const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
351
+ this.queue = turn;
352
+ return turn;
353
+ }
354
+
355
+ private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
356
+ const argv = [
357
+ OPENCODE_BIN,
358
+ "run",
359
+ "--format",
360
+ "json",
361
+ // Auto-approve permissions not explicitly denied — the container is the
362
+ // sandbox, and headless has no interactive approver.
363
+ "--auto",
364
+ ];
365
+ if (this.model) argv.push("--model", this.model);
366
+ if (this.effort) argv.push("--variant", this.effort);
367
+ // Continue the same OpenCode session across turns (captured from the first
368
+ // turn's stream). First turn has none → a fresh session.
369
+ if (this.sessionId) argv.push("--session", this.sessionId);
370
+ // The prompt is positional; put it LAST so it's never mistaken for a flag.
371
+ argv.push(prompt);
372
+ return {
373
+ argv: argv as [string, ...string[]],
374
+ projectEnvProfile: "task",
375
+ user: "node",
376
+ env: this.agentEnv,
377
+ maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
378
+ };
379
+ }
380
+
381
+ private async runTurn(text: string): Promise<void> {
382
+ if (this.closed) return;
383
+ requireContainerRuntimeOperational();
384
+ // `run` has no system-prompt flag, so fold the channel briefing into the
385
+ // FIRST turn's prompt (later turns carry it via the resumed session).
386
+ const prompt =
387
+ !this.sentPreamble && this.systemPreamble.trim().length > 0
388
+ ? `${this.systemPreamble}\n\n---\n\n${text}`
389
+ : text;
390
+ this.sentPreamble = true;
391
+
392
+ const child = this.spawner(this.buildRequest(prompt));
393
+ this.current = child;
394
+
395
+ let turnUsage: AgentUsage = {};
396
+ let sawUsage = false;
397
+ // Whether the JSON stream already surfaced an error this turn. `opencode
398
+ // run` reports a provider/gateway failure as a `{type:"error"}` event AND
399
+ // then exits 1 — without this we'd double-report one failure (e.g.
400
+ // "Unexpected server error" followed by a bare "opencode exited 1").
401
+ let sawError = false;
402
+ let buf = "";
403
+ const consume = (chunk: string): void => {
404
+ buf += chunk;
405
+ let nl: number;
406
+ while ((nl = buf.indexOf("\n")) >= 0) {
407
+ const line = buf.slice(0, nl);
408
+ buf = buf.slice(nl + 1);
409
+ const { events, sessionId, usage } = mapOpencodeLine(line);
410
+ if (sessionId) this.sessionId = sessionId;
411
+ if (usage) {
412
+ turnUsage = addUsage(turnUsage, usage);
413
+ sawUsage = true;
414
+ }
415
+ for (const e of events) {
416
+ if (e.type === "error") sawError = true;
417
+ this.emit(e);
418
+ }
419
+ }
420
+ };
421
+ child.stdout?.on("data", (b: Buffer) => consume(b.toString("utf8")));
422
+ let stderr = "";
423
+ child.stderr?.on("data", (b: Buffer) => {
424
+ stderr += b.toString("utf8");
425
+ });
426
+
427
+ await new Promise<void>((resolve) => {
428
+ child.on("exit", (code) => {
429
+ this.current = null;
430
+ if (buf.trim().length > 0) consume("\n"); // flush a trailing line
431
+ if (!this.closed) {
432
+ // Only surface the bare exit code when the stream didn't already
433
+ // explain the failure (see sawError) — otherwise it's a redundant
434
+ // second error card for the same turn.
435
+ if (code !== 0 && !sawError) {
436
+ const tail = stderr.trim().slice(-500);
437
+ const message = `opencode exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
438
+ const accountUsage = rateLimitErrorUsage(message);
439
+ if (accountUsage) {
440
+ this.emit({ type: "account_usage", usage: accountUsage });
441
+ this.emit({
442
+ type: "error",
443
+ message,
444
+ retryable: "rate_limit",
445
+ ...(accountUsage.resetsAt === undefined
446
+ ? {}
447
+ : { resetsAt: accountUsage.resetsAt }),
448
+ });
449
+ } else {
450
+ this.emit({ type: "error", message });
451
+ }
452
+ }
453
+ this.emit({
454
+ type: "turn_complete",
455
+ usage: sawUsage ? turnUsage : undefined,
456
+ });
457
+ }
458
+ resolve();
459
+ });
460
+ child.on("error", (err) => {
461
+ this.current = null;
462
+ if (!this.closed) {
463
+ this.emit({
464
+ type: "error",
465
+ message: `opencode spawn failed: ${err.message}`,
466
+ });
467
+ this.emit({ type: "turn_complete" });
468
+ }
469
+ resolve();
470
+ });
471
+ });
472
+ }
473
+
474
+ async interrupt(): Promise<void> {
475
+ // Kill the running turn's exec. Best-effort — the in-container `opencode`
476
+ // may finish the current tool, but no further output is emitted.
477
+ this.current?.kill("SIGKILL");
478
+ }
479
+
480
+ // `--auto` approves tools (the container is the sandbox), so no permission
481
+ // requests are ever emitted — nothing to resolve.
482
+ async resolvePermission(): Promise<void> {
483
+ /* no-op */
484
+ }
485
+
486
+ async close(): Promise<void> {
487
+ if (this.closed) return;
488
+ this.closed = true;
489
+ this.current?.kill("SIGKILL");
490
+ this.current = null;
491
+ this.emit({ type: "exit", code: 0 });
492
+ this.handlers.clear();
493
+ }
494
+ }
495
+
496
+ // Register the OpenCode adapter at module load (ADR-021/077).
497
+ register({
498
+ kind: "opencode",
499
+ label: "OpenCode",
500
+ supportedModels: () => [...OPENCODE_MODELS],
501
+ defaultModel: OPENCODE_DEFAULT_MODEL,
502
+ supportedEfforts: () => [...OPENCODE_EFFORTS],
503
+ // Gated on the operator's OpenCode auth (copied into containers at task-up).
504
+ // No auth → the engine isn't advertised.
505
+ available: () => existsSync(opencodeAuthPath()),
506
+ create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
507
+ new OpencodeSession({ taskId, agent, environment, systemPreamble, agentEnv }),
508
+ });