@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,254 @@
1
+ /**
2
+ * The agent abstraction.
3
+ *
4
+ * uai drives Claude Code and Codex in their structured modes (see
5
+ * docs/interaction-model.md). Both — plus a mock for testing — implement
6
+ * the same `AgentSession` interface, so the orchestrator and chat UI
7
+ * never branch on which CLI is behind a given agent.
8
+ *
9
+ * An "agent" is a roster entry on a project, not a hardcoded
10
+ * Claude/Codex pair. A project can run one agent or several; each has a
11
+ * stable `id` used for addressing (`@<id>`).
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ import type { EngineUsageWindow } from "../../src/protocol";
17
+ import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
18
+
19
+ /** The host-advertised adapter kind ("claude" | "codex" | "gemini" | …). */
20
+ export const AgentKind = z
21
+ .string()
22
+ .min(1)
23
+ .regex(/^[a-z0-9][a-z0-9_-]*$/, "kind must be lowercase letters, digits, _ or -");
24
+ export type AgentKind = z.infer<typeof AgentKind>;
25
+
26
+ /** A task agent (ADR-021/ADR-022) — materialised onto a task.
27
+ *
28
+ * `id` must be URL-safe (letters, digits, `_`, `-`) because it is the
29
+ * `@<id>` mention token. `kind` selects the adapter; `model` is passed
30
+ * through to the CLI. There is no `role` field. `initialPrompt` (if set)
31
+ * drives this agent's first turn at container-ready. */
32
+ /** Reference material (a link or inline document) materialised onto an agent
33
+ * from its persona + team (ADR-046). The host writes these to a per-agent
34
+ * SKILL.md at session start. */
35
+ export const RosterSkill = z.object({
36
+ name: z.string().min(1),
37
+ type: z.string().min(1), // 'link' | 'document' | 'package' (ADR-047)
38
+ value: z.string(),
39
+ // ADR-047 package skills: 'git' | 'cli' install mode + (git) repo subpath.
40
+ source: z.string().nullable().optional(),
41
+ subpath: z.string().nullable().optional(),
42
+ });
43
+ export type RosterSkill = z.infer<typeof RosterSkill>;
44
+
45
+ export const RosterAgent = z.object({
46
+ id: z
47
+ .string()
48
+ .min(1)
49
+ .regex(/^[A-Za-z0-9_-]+$/, "id must be url-safe: letters, digits, _ or -"),
50
+ label: z.string().min(1), // display name
51
+ kind: AgentKind,
52
+ model: z.string().min(1).optional(),
53
+ effort: z.string().min(1).optional(), // CLI reasoning level, passed through
54
+ // ADR-157 addendum §7: preferred cloud-leased AI account.
55
+ accountId: z
56
+ .string()
57
+ .min(1)
58
+ .max(128)
59
+ .regex(/^[A-Za-z0-9_.:-]+$/)
60
+ .optional(),
61
+ accountBinding: z.enum(["required", "preferred"]).optional(),
62
+ defaultPrompt: z.string().optional(), // persona
63
+ initialPrompt: z.string().optional(), // first-turn instructions, this task only
64
+ skills: z.array(RosterSkill).optional(), // ADR-046 reference material
65
+ permissions: z.array(z.string()).optional(), // ADR-048 `uai` CLI capabilities
66
+ // ADR-128: which upstream protocol a Uai-engine model speaks — resolved
67
+ // cloud-side from the model catalog; selects the delegated adapter.
68
+ uaiProvider: z.enum(["anthropic", "openai"]).optional(),
69
+ builtIn: z.literal("astronaut").optional(), // ADR-157: the cloud-built Astronaut only
70
+ // ADR-157 §3: signed into the agent's task token as `g` (see agentCliEnv).
71
+ tokenGeneration: z.string().min(1).max(64).optional(),
72
+ });
73
+ export type RosterAgent = z.infer<typeof RosterAgent>;
74
+
75
+ /** A task's agents — distinct ids so a mention routes unambiguously. May be
76
+ * empty (a scratchpad task with no crew). */
77
+ export const Roster = z
78
+ .array(RosterAgent)
79
+ .refine((agents) => new Set(agents.map((a) => a.id)).size === agents.length, {
80
+ message: "agent ids must be unique within a roster",
81
+ });
82
+ export type Roster = z.infer<typeof Roster>;
83
+
84
+ /**
85
+ * Parse a task's `agents` JSON into a validated Roster. Falls back to an
86
+ * empty roster if malformed.
87
+ */
88
+ export function parseRoster(raw: string): Roster {
89
+ try {
90
+ const parsed = Roster.safeParse(JSON.parse(raw));
91
+ return parsed.success ? parsed.data : [];
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Events an agent session emits as it works. The orchestrator persists
99
+ // these to `uai_messages` and streams them to the browser.
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Token + cost accounting for one turn, extracted from the agent CLI's result
104
+ * envelope where it reports it (Claude Code gives `usage` + `total_cost_usd`
105
+ * directly). Powers per-task cost visibility for self-hosted users and the
106
+ * metering pipeline for managed Uai-provided AI. All fields optional — an
107
+ * engine that doesn't report a dimension omits it.
108
+ */
109
+ export interface AgentUsage {
110
+ /** Model the CLI actually billed (may differ from the configured model, e.g. a sub-agent). */
111
+ model?: string;
112
+ inputTokens?: number;
113
+ outputTokens?: number;
114
+ cacheReadTokens?: number;
115
+ cacheCreateTokens?: number;
116
+ /** Total USD for the turn, as reported by the CLI (authoritative when present). */
117
+ costUsd?: number;
118
+ }
119
+
120
+ /** Secret-free account usage exactly as an engine CLI reported it. The
121
+ * orchestrator adds task/account identity only after proving the session is
122
+ * running on a cloud-leased account. */
123
+ export interface AgentAccountUsage {
124
+ status: "allowed" | "warning" | "limited";
125
+ resetsAt?: number;
126
+ usingOverage?: boolean;
127
+ windows: EngineUsageWindow[];
128
+ source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text" | "cursor-period-usage";
129
+ observedAt: number;
130
+ /** Which provider capacity this reading can prove usable again. Claude emits
131
+ * one event per named window; Codex snapshots cover every window. Internal
132
+ * runner metadata only: it is deliberately not part of engine.usage. */
133
+ coverage?:
134
+ | { kind: "window"; window: string }
135
+ | { kind: "all-windows" };
136
+ }
137
+
138
+ export type AgentEvent =
139
+ /** A chunk of streaming assistant text. Appended to the in-progress message. */
140
+ | { type: "message_delta"; text: string }
141
+ /** The assistant message finished. `text` is the full final text. */
142
+ | { type: "message_complete"; text: string }
143
+ /** The agent invoked a tool / ran a command. Rendered as a card. */
144
+ | { type: "tool_call"; id: string; title: string; detail: string }
145
+ /** ADR-083: the designated secretary invoked Uai's typed dispatch tool. */
146
+ | {
147
+ type: "dispatch";
148
+ dispatchId: string;
149
+ recipients: string[];
150
+ instruction: string;
151
+ }
152
+ /** The agent needs approval before proceeding. */
153
+ | { type: "permission_request"; id: string; title: string; detail: string }
154
+ /** The agent addressed another agent — uai routes this as a peer message. */
155
+ | { type: "peer_message"; toAgentId: string; text: string }
156
+ /** The provider turn is done; it may have absorbed multiple deliveries.
157
+ * `usage` carries this turn's token/cost accounting when the CLI reports it. */
158
+ | {
159
+ type: "turn_complete";
160
+ usage?: AgentUsage;
161
+ aborted?: boolean;
162
+ /** Logical deliveries combined into this one provider turn (default 1). */
163
+ settles?: number;
164
+ }
165
+ /** Account-level capacity reported by the engine CLI. This never contains
166
+ * credentials or provider account identifiers. */
167
+ | { type: "account_usage"; usage: AgentAccountUsage }
168
+ /** A nonterminal operation failed; leave the current message running. */
169
+ | { type: "notice"; message: string }
170
+ /** A recoverable error surfaced by the agent. `retryable` marks errors the
171
+ * orchestrator can fail over from — "rate_limit" triggers account rotation
172
+ * (ADR-076). */
173
+ | {
174
+ type: "error";
175
+ message: string;
176
+ /** Host-only full diagnostic for classification. Never put on the wire. */
177
+ diagnosticMessage?: string;
178
+ /** A short, scrubbed account of an upstream failure on a custom connection
179
+ * (HTTP status and an excerpt of its message), safe for the chat. Set only
180
+ * by the connection sanitizer (custom-connection-runtime.ts). */
181
+ detail?: string;
182
+ /** Provider RPC error code, when supplied. */
183
+ code?: number;
184
+ /** The adapter has ended the accepted turn, including before output. */
185
+ aborted?: boolean;
186
+ retryable?: "rate_limit";
187
+ /** Parsed from the same rate-limit error text, when unambiguous. */
188
+ resetsAt?: number;
189
+ }
190
+ /** The underlying process exited. */
191
+ | { type: "exit"; code: number };
192
+
193
+ export type AgentEventHandler = (event: AgentEvent) => void;
194
+
195
+ /**
196
+ * A live session with one agent. Implementations: ClaudeSession
197
+ * (stream-json), CodexSession (codex app-server), MockAgentSession.
198
+ */
199
+ export interface AgentSession {
200
+ /** The roster id this session is for. */
201
+ readonly agentId: string;
202
+ /** Which agent kind backs it ("claude" | "codex" | …). */
203
+ readonly kind: AgentKind;
204
+
205
+ /** Send a user (or routed peer) message into the agent. */
206
+ send(text: string): Promise<void>;
207
+
208
+ /** Interrupt the agent's current turn (ESC). Idempotent / safe when idle. */
209
+ interrupt(): Promise<void>;
210
+
211
+ /** Resolve a pending permission request. */
212
+ resolvePermission(
213
+ requestId: string,
214
+ decision: "accept" | "decline",
215
+ ): Promise<void>;
216
+
217
+ /** Subscribe to events. Returns an unsubscribe function. */
218
+ onEvent(handler: AgentEventHandler): () => void;
219
+
220
+ /** Tear the session down. Idempotent. */
221
+ close(): Promise<void>;
222
+ }
223
+
224
+ /**
225
+ * Constructs an AgentSession for a roster entry. The orchestrator is
226
+ * handed one of these; swapping mock ⇄ real adapters is changing the
227
+ * factory, nothing else.
228
+ */
229
+ export interface AgentSessionFactory {
230
+ create(args: {
231
+ taskId: string;
232
+ agent: RosterAgent;
233
+ /** Provider-owned process/session surface for this task environment. */
234
+ environment: TaskEnvironmentAgentSessionSurface;
235
+ /** Initial briefing — project.defaultPrompt — sent on session start. */
236
+ systemPreamble: string;
237
+ /** Host-derived restricted execution policy (ADR-083). Never supplied by
238
+ * the browser or stored on the roster entry. */
239
+ executionProfile?: "communicator" | "astronaut";
240
+ /** Durable runners are keyed by task+agent. Host-owned launch settings
241
+ * supply a compatibility key so restart recovery replaces, rather than
242
+ * attaches, a process launched with incompatible model/effort/policy. */
243
+ attachCompatibilityKey?: string;
244
+ /** ADR-048: extra per-agent process environment (e.g. this agent's own
245
+ * UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
246
+ agentEnv?: Record<string, string>;
247
+ /** ADR-128: engine config overrides a DELEGATING adapter prepends (codex
248
+ * `-c key=value`), e.g. the Uai Managed Engine's model-provider block
249
+ * that points codex at the inference gateway. Never from the roster. */
250
+ cliConfigOverrides?: string[];
251
+ /** Host-generated per-launch Claude settings; never from roster input. */
252
+ claudeSettings?: Record<string, unknown>;
253
+ }): Promise<AgentSession>;
254
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The Uai Managed Engine (ADR-128) — not a harness, a delegation.
3
+ *
4
+ * The customer picks "Uai Managed Engine" and one of Uai's models; under
5
+ * the hood this adapter spawns the EXISTING claude or codex adapter
6
+ * (chosen by the model's provider, resolved cloud-side into
7
+ * `agent.uaiProvider`) with the environment pointed at the inference
8
+ * gateway: base URL swapped, the agent's own task token as the API
9
+ * credential, and the host's real provider credentials explicitly
10
+ * BLANKED so a BYO host's Claude subscription can never be billed for a
11
+ * Uai-engine turn. The gateway meters usage server-side and the cloud
12
+ * debits credits — this file never sees money.
13
+ *
14
+ * The model slug passes through verbatim: the underlying CLI sends it as
15
+ * the request's `model`, and the gateway maps slug → upstream id.
16
+ */
17
+
18
+ import { STATION_DEFAULT_GATEWAY_URL } from "../../src/station-identity";
19
+ import { codexProviderOverrides, endpointAuthEnv } from "../custom-connection-launch";
20
+ import { factoryFor, register, supportsExecutionProfile } from "./registry";
21
+ import type { AgentSession, AgentSessionFactory } from "./types";
22
+
23
+ const DEFAULT_GATEWAY_URL = STATION_DEFAULT_GATEWAY_URL;
24
+
25
+ export function gatewayUrl(): string {
26
+ return process.env.UAI_INFERENCE_GATEWAY_URL || DEFAULT_GATEWAY_URL;
27
+ }
28
+
29
+ /** The env override set for a delegated session. Exported for tests. */
30
+ export function uaiGatewayEnv(
31
+ provider: "anthropic" | "openai",
32
+ url: string,
33
+ taskToken: string,
34
+ ): Record<string, string> {
35
+ return endpointAuthEnv(provider, url, taskToken, "bearer", "UAI_INFERENCE_TOKEN");
36
+ }
37
+
38
+ /**
39
+ * Codex does NOT follow `OPENAI_BASE_URL` / `OPENAI_API_KEY` for its default
40
+ * provider any more: with a ChatGPT login present it opens
41
+ * `wss://api.openai.com/v1/responses` directly and sends no bearer (live
42
+ * 2026-09-12: "401 Missing bearer or basic authentication", five
43
+ * reconnects, turn never answered). A dedicated model provider pins the
44
+ * gateway as the base URL, the HTTP Responses wire (the gateway serves
45
+ * `/v1/responses`, not WebSockets), and the task token via `env_key`;
46
+ * `requires_openai_auth=false` keeps the host's Codex login out of a
47
+ * gateway-billed session. Codex parses `-c` values as TOML, hence the
48
+ * quotes on strings.
49
+ */
50
+ export function uaiCodexOverrides(url: string): string[] {
51
+ const base = `${url.replace(/\/+$/, "")}/v1`;
52
+ return codexProviderOverrides(base, { provider: "uai", name: "Moondeck AI", envKey: "UAI_INFERENCE_TOKEN" });
53
+ }
54
+
55
+ const create: AgentSessionFactory["create"] = async (args): Promise<AgentSession> => {
56
+ const provider = args.agent.uaiProvider === "openai" ? "openai" : "anthropic";
57
+ const delegateKind = provider === "openai" ? "codex" : "claude";
58
+ const delegate = factoryFor(delegateKind);
59
+ if (!delegate) {
60
+ throw new Error(`uai engine: no ${delegateKind} adapter registered to delegate to`);
61
+ }
62
+ // ADR-157 §7: the runner checked the profile against the `uai` kind, but the
63
+ // delegate is what enforces it. Refuse rather than run an Astronaut on a
64
+ // delegate that would ignore the profile.
65
+ if (
66
+ args.executionProfile &&
67
+ !supportsExecutionProfile(delegateKind, args.executionProfile)
68
+ ) {
69
+ throw new Error(
70
+ `uai engine: the ${delegateKind} adapter cannot enforce execution profile "${args.executionProfile}"`,
71
+ );
72
+ }
73
+ const taskToken = args.agentEnv?.UAI_TASK_TOKEN;
74
+ if (!taskToken) {
75
+ // agentCliEnv mints one for every managed-engine agent, permissions or
76
+ // not; none here means the task has no owner, API url or CLI secret.
77
+ throw new Error(
78
+ "uai engine: this agent has no task token (the task has no CLI identity yet) — restart the task",
79
+ );
80
+ }
81
+ return delegate.create({
82
+ ...args,
83
+ agentEnv: {
84
+ ...(args.agentEnv ?? {}),
85
+ ...uaiGatewayEnv(provider, gatewayUrl(), taskToken),
86
+ },
87
+ ...(provider === "openai" ? { cliConfigOverrides: uaiCodexOverrides(gatewayUrl()) } : {}),
88
+ });
89
+ };
90
+
91
+ register({
92
+ kind: "uai",
93
+ label: "Moondeck AI",
94
+ // The model catalog is cloud data (ADR-128); the composer sources it from
95
+ // /api/org/inference/models, never from host capabilities. Advertising an
96
+ // empty list here simply marks the kind spawnable on this host.
97
+ supportedModels: () => [],
98
+ supportedEfforts: () => [],
99
+ // Always available: the engine needs no host-side credential — auth is
100
+ // the per-agent task token, billing is the org's credits.
101
+ available: () => true,
102
+ // ADR-157: `create` forwards the profile to the Claude or Codex delegate,
103
+ // and both enforce it.
104
+ executionProfiles: [{ id: "astronaut", mechanism: "uai-delegated-astronaut-v1" }],
105
+ create,
106
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared token/cost extraction for a stream-json `result` envelope (ADR-071
3
+ * metering). Claude Code reports snake_case `usage` plus `total_cost_usd` and
4
+ * per-model `modelUsage`. cursor-agent 2026.10.x reports camelCase counts and
5
+ * no cost: `{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens}`.
6
+ * Snake_case wins when a field is present, so Claude's path is unchanged.
7
+ *
8
+ * Codex (app-server protocol) and Kimi/Grok (plain text streams) report no
9
+ * token/cost data through this extractor.
10
+ */
11
+
12
+ import type { AgentUsage } from "./types";
13
+
14
+ function isObj(v: unknown): v is Record<string, unknown> {
15
+ return typeof v === "object" && v !== null;
16
+ }
17
+
18
+ function num(v: unknown): number | undefined {
19
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
20
+ }
21
+
22
+ /** Claude's name when it is a number, otherwise Cursor 2026.10's. */
23
+ function firstNum(claude: unknown, cursor: unknown): number | undefined {
24
+ const primary = num(claude);
25
+ return primary !== undefined ? primary : num(cursor);
26
+ }
27
+
28
+ /** Pull AgentUsage from a `result` line, or undefined when none is reported. */
29
+ export function extractResultUsage(
30
+ json: Record<string, unknown>,
31
+ ): AgentUsage | undefined {
32
+ const u = isObj(json.usage) ? json.usage : {};
33
+ const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
34
+ const usage: AgentUsage = {
35
+ model: models.length ? models.join(", ") : undefined,
36
+ inputTokens: firstNum(u.input_tokens, u.inputTokens),
37
+ outputTokens: firstNum(u.output_tokens, u.outputTokens),
38
+ cacheReadTokens: firstNum(u.cache_read_input_tokens, u.cacheReadTokens),
39
+ cacheCreateTokens: firstNum(u.cache_creation_input_tokens, u.cacheWriteTokens),
40
+ costUsd: num(json.total_cost_usd),
41
+ };
42
+ // Nothing usable reported → omit rather than send an empty object.
43
+ return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
44
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * A one-off container on the Apple runtime (ADR-106), and nothing of it left
3
+ * behind (#p2jdax).
4
+ *
5
+ * `container run --rm` removes only a container that started. A run the
6
+ * runtime refuses leaves a created-never-started container, and those wedge
7
+ * the runtime's apiserver: `run` and `delete` hang while `list` and
8
+ * `inspect` still answer, until the runtime's services are recycled, which
9
+ * restarts every Apple task (apple-runtime-recycle.ts). A client killed at
10
+ * its bound (no answer in time, output past its bound) can leave one too, or
11
+ * a container that goes on running what it was abandoned for. So every
12
+ * one-off container is named in the closed grammar the recycle's sweep
13
+ * deletes, with a random part no other run's name has, labelled as Uai's,
14
+ * and labelled with a token only its own run carries; and its run is
15
+ * settled after it ends:
16
+ *
17
+ * - absent (the runtime says it is not found): done;
18
+ * - its own (Uai's label and its run's token, on exactly that name):
19
+ * deleted, then proven gone; deleted again if it is not; still there, it
20
+ * is what wedges the runtime, so the runtime's remedy is asked for, and it
21
+ * is proven gone after. Only the runtime's own activation restarts its
22
+ * services (it recovers every task after); all other work, and every
23
+ * watch, asks the runtime's owner for that activation instead
24
+ * (askRuntimeOwnerOnce);
25
+ * - of that name but not its own: never touched (a name alone is no proof);
26
+ * - unobservable (the runtime does not answer): nothing is deleted or
27
+ * recycled on a guess.
28
+ *
29
+ * Whatever is left unproven is watched a while longer, and so is the name of
30
+ * a run whose client never reported its end (its container can still be
31
+ * created after): each look that finds it its own settles it again, and the
32
+ * watch ends only once it is proven gone or removed, or its time is up.
33
+ */
34
+
35
+ import { randomBytes } from "node:crypto";
36
+
37
+ import { MAINTENANCE_CONTAINER_LABEL, type AppleRecycleOutcome } from "./apple-runtime-recycle";
38
+
39
+ /** The label only one run's container carries: its token. */
40
+ export const MAINTENANCE_TOKEN_LABEL = "io.moondeck.maintenance.token";
41
+
42
+ /** One run of the Apple `container` CLI. */
43
+ export interface AppleCliResult {
44
+ code: number | null;
45
+ stdout: string;
46
+ stderr: string;
47
+ }
48
+
49
+ export type AppleCli = (args: string[], timeoutMs: number) => Promise<AppleCliResult>;
50
+
51
+ export interface AppleOneOffDeps {
52
+ cli: AppleCli;
53
+ /** The runtime's remedy for a container of a run's own that will not go
54
+ * (see the module comment). */
55
+ recycle: () => Promise<AppleRecycleOutcome>;
56
+ /** Tests only: how the watch waits. */
57
+ delay?: (ms: number) => Promise<void>;
58
+ }
59
+
60
+ /** One one-off container: its name, and the token only its run carries. */
61
+ export interface AppleOneOff {
62
+ readonly name: string;
63
+ readonly token: string;
64
+ }
65
+
66
+ /** What a one-off container's name may name, within the recycle sweep's
67
+ * grammar (MAINTENANCE_CONTAINER_NAME_RE). */
68
+ const PURPOSE = /^[a-z]+(?:-[a-z]+)*$/;
69
+ const INSPECT_TIMEOUT_MS = 15_000;
70
+ const DELETE_TIMEOUT_MS = 30_000;
71
+ const DELETE_ATTEMPTS = 2;
72
+ /** A watched name is looked for again after each of these waits. */
73
+ const WATCH_MS: readonly number[] = [5_000, 15_000, 60_000, 180_000, 600_000];
74
+
75
+ let sequence = 0;
76
+
77
+ /** A new one-off container: a name no other run has had, and its token. */
78
+ export function newAppleOneOff(purpose: string): AppleOneOff {
79
+ if (!PURPOSE.test(purpose)) throw new Error(`a malformed one-off container purpose: ${purpose}`);
80
+ sequence += 1;
81
+ return {
82
+ name: `moondeck-maint-${purpose}-${process.pid}-${sequence}-${randomBytes(8).toString("hex")}`,
83
+ token: randomBytes(16).toString("hex"),
84
+ };
85
+ }
86
+
87
+ /** The `container run` arguments that name a one-off container, mark it
88
+ * Uai's (what the recycle's sweep goes by) and give it its run's token
89
+ * (what alone authorizes deleting it here). */
90
+ export function appleOneOffRunArgs(oneOff: AppleOneOff): string[] {
91
+ return [
92
+ "--name",
93
+ oneOff.name,
94
+ "-l",
95
+ `${MAINTENANCE_CONTAINER_LABEL}=1`,
96
+ "-l",
97
+ `${MAINTENANCE_TOKEN_LABEL}=${oneOff.token}`,
98
+ ];
99
+ }
100
+
101
+ /** What one inspection of a one-off container's name finds. */
102
+ export type AppleOneOffPresence = "ours" | "absent" | "foreign" | "unknown";
103
+
104
+ /**
105
+ * One observation of `oneOff`'s name: absent only on the runtime's own "not
106
+ * found"; its own only with Uai's label and its run's token on exactly that
107
+ * container; of that name without them, foreign; anything else (no answer,
108
+ * an answer that does not parse), unknown.
109
+ */
110
+ export async function appleOneOffPresence(cli: AppleCli, oneOff: AppleOneOff): Promise<AppleOneOffPresence> {
111
+ const { name } = oneOff;
112
+ const inspected = await cli(["inspect", name], INSPECT_TIMEOUT_MS);
113
+ if (inspected.code === 0) {
114
+ try {
115
+ const parsed = JSON.parse(inspected.stdout) as Array<{
116
+ id?: unknown;
117
+ configuration?: { labels?: Record<string, unknown> };
118
+ }>;
119
+ if (!Array.isArray(parsed) || parsed.length !== 1 || parsed[0]?.id !== name) return "unknown";
120
+ const labels = parsed[0]?.configuration?.labels;
121
+ return labels?.[MAINTENANCE_CONTAINER_LABEL] === "1" && labels?.[MAINTENANCE_TOKEN_LABEL] === oneOff.token
122
+ ? "ours"
123
+ : "foreign";
124
+ } catch {
125
+ return "unknown";
126
+ }
127
+ }
128
+ if (
129
+ inspected.code !== null &&
130
+ inspected.stdout.trim() === "" &&
131
+ inspected.stderr.split("\n").some((line) => line.trim() === `Error: container not found: ${name}`)
132
+ ) {
133
+ return "absent";
134
+ }
135
+ return "unknown";
136
+ }
137
+
138
+ /** How a one-off container's run was settled. */
139
+ export type AppleOneOffSettlement =
140
+ /** Nothing of that name is there. */
141
+ | "absent"
142
+ /** Its own was there: deleted, and proven gone. */
143
+ | "removed"
144
+ /** Its own would not go: the runtime's remedy took it, and it is proven
145
+ * gone. */
146
+ | "recycled"
147
+ /** Of that name but not its own: left untouched. */
148
+ | "foreign"
149
+ /** Neither proven gone nor removed. */
150
+ | "unproven";
151
+
152
+ /**
153
+ * Settle `oneOff`'s container once (see the module comment). Never
154
+ * rejects.
155
+ */
156
+ export async function settleAppleOneOff(deps: AppleOneOffDeps, oneOff: AppleOneOff): Promise<AppleOneOffSettlement> {
157
+ const { name } = oneOff;
158
+ try {
159
+ let presence = await appleOneOffPresence(deps.cli, oneOff);
160
+ if (presence === "absent") return "absent";
161
+ // Only its own is ever deleted.
162
+ for (let attempt = 0; attempt < DELETE_ATTEMPTS && presence === "ours"; attempt += 1) {
163
+ await deps.cli(["delete", "--force", name], DELETE_TIMEOUT_MS);
164
+ presence = await appleOneOffPresence(deps.cli, oneOff);
165
+ }
166
+ if (presence === "absent") return "removed";
167
+ if (presence !== "ours") return leftAlone(name, presence);
168
+ // Its own, and it will not go: what wedges the runtime.
169
+ console.warn(`[host-agent] one-off container ${name} could not be deleted; asking for the Apple runtime's remedy`);
170
+ const outcome = await deps.recycle();
171
+ presence = await appleOneOffPresence(deps.cli, oneOff);
172
+ if (presence === "absent") return "recycled";
173
+ console.warn(
174
+ `[host-agent] one-off container ${name} is still there (${
175
+ outcome.recycled ? "the runtime's services were recycled" : outcome.reason
176
+ }); it is watched, and removed once it can be`,
177
+ );
178
+ return "unproven";
179
+ } catch (error) {
180
+ console.warn(
181
+ `[host-agent] one-off container ${name} could not be settled: ${error instanceof Error ? error.message : String(error)}`,
182
+ );
183
+ return "unproven";
184
+ }
185
+ }
186
+
187
+ function leftAlone(name: string, presence: "foreign" | "unknown"): AppleOneOffSettlement {
188
+ if (presence === "foreign") {
189
+ console.warn(`[host-agent] a container named ${name} is not this run's own; leaving it untouched`);
190
+ return "foreign";
191
+ }
192
+ console.warn(`[host-agent] could not observe one-off container ${name}; nothing is deleted or recycled on a guess`);
193
+ return "unproven";
194
+ }
195
+
196
+ /**
197
+ * The runtime's remedy for work outside its activation: ask the runtime's
198
+ * owner, at most once for one run, to recheck the runtime
199
+ * (container-runtime.ts requestContainerRuntimeRecheck). Its activation
200
+ * restarts the services of a runtime it finds wedged, then recovers every
201
+ * task; this never restarts them itself.
202
+ */
203
+ export function askRuntimeOwnerOnce(
204
+ recheck: (reason: string) => boolean,
205
+ reason: string,
206
+ ): () => Promise<Extract<AppleRecycleOutcome, { recycled: false }>> {
207
+ let asked: boolean | null = null;
208
+ return async () => {
209
+ asked ??= recheck(reason);
210
+ return {
211
+ recycled: false,
212
+ reason: asked ? "its owner was asked to recheck the runtime" : "no owner of the runtime was there to ask",
213
+ };
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Whether a run's settlement leaves its name to watch: anything unproven,
219
+ * and, when its client never reported its end (`lateCreation`: its
220
+ * container can still be created after), anything short of removed.
221
+ */
222
+ export function appleOneOffNeedsWatch(settled: AppleOneOffSettlement, lateCreation: boolean): boolean {
223
+ if (settled === "removed" || settled === "recycled") return false;
224
+ return settled === "unproven" || lateCreation;
225
+ }
226
+
227
+ /**
228
+ * Watch `oneOff`'s name: look for it again after each wait, and settle it
229
+ * whenever it is found its own; one that cannot be observed, or is not its
230
+ * own (when `lateCreation`, the name may be its own's later), is looked for
231
+ * again. Resolves once it is removed or proven gone (absent ends the watch
232
+ * only where nothing of the run can be created after), or the watch's time
233
+ * is up; never rejects.
234
+ */
235
+ export async function watchAppleOneOff(
236
+ deps: AppleOneOffDeps,
237
+ oneOff: AppleOneOff,
238
+ options: { lateCreation: boolean },
239
+ ): Promise<AppleOneOffSettlement> {
240
+ const delay = deps.delay ?? defaultDelay;
241
+ let last: AppleOneOffSettlement = "unproven";
242
+ for (const wait of WATCH_MS) {
243
+ await delay(wait);
244
+ let presence: AppleOneOffPresence;
245
+ try {
246
+ presence = await appleOneOffPresence(deps.cli, oneOff);
247
+ } catch {
248
+ presence = "unknown";
249
+ }
250
+ if (presence === "absent" || presence === "foreign") {
251
+ if (!options.lateCreation) return presence;
252
+ last = presence;
253
+ continue;
254
+ }
255
+ if (presence === "unknown") {
256
+ last = "unproven";
257
+ continue;
258
+ }
259
+ const settled = await settleAppleOneOff(deps, oneOff);
260
+ if (settled === "removed" || settled === "recycled") return settled;
261
+ if (settled !== "unproven" && !options.lateCreation) return settled;
262
+ last = settled;
263
+ }
264
+ return last;
265
+ }
266
+
267
+ function defaultDelay(ms: number): Promise<void> {
268
+ return new Promise((resolve) => {
269
+ const timer = setTimeout(resolve, ms);
270
+ timer.unref?.();
271
+ });
272
+ }
273
+
274
+ /** Tests only: the name sequence, from the start. */
275
+ export function resetAppleOneOffNamesForTests(): void {
276
+ sequence = 0;
277
+ }