@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,104 @@
1
+ /**
2
+ * Build the secret-free account usage report sent to Uai Cloud.
3
+ *
4
+ * A leased env account carries both a variable name and its secret value in
5
+ * runner memory. Billing is deliberately inferred from the NAME alone. Some
6
+ * file-backed accounts use the same `auth.json` name for a subscription and
7
+ * an API key, so they carry no hint.
8
+ *
9
+ * The hint names the revision it was read from, and is given only when that is
10
+ * the revision the reporting session runs on: after a re-paste, a lease
11
+ * refresh can hand the runner newer material than an agent is still using.
12
+ */
13
+
14
+ import type { EngineUsageFrame, EngineUsageWindow } from "./protocol";
15
+
16
+ type EngineUsageBilling = NonNullable<EngineUsageFrame["billing"]>;
17
+
18
+ export interface LeasedBillingAccount {
19
+ credentialId: string;
20
+ revision: string;
21
+ engineKind: string;
22
+ authKind: "env" | "config-dir";
23
+ env: Readonly<Record<string, string>> | null;
24
+ }
25
+
26
+ export interface EngineUsageObservation {
27
+ status: EngineUsageFrame["status"];
28
+ resetsAt?: number;
29
+ usingOverage?: boolean;
30
+ windows: EngineUsageWindow[];
31
+ source: EngineUsageFrame["source"];
32
+ observedAt: number;
33
+ }
34
+
35
+ const BILLING_BY_VARIABLE: Readonly<
36
+ Record<string, Readonly<Record<string, EngineUsageBilling>>>
37
+ > = Object.freeze({
38
+ claude: Object.freeze({
39
+ // Capture files every pasted Claude secret that is NOT `sk-ant-api…` under
40
+ // this name (engine-accounts.ts `pastedSecretMaterial`), so the name alone
41
+ // also covers a token whose prefix told capture nothing, which the cloud
42
+ // left unknown. Subscription is still right for a report: usage is read
43
+ // from a session that worked, and Claude Code authenticates this variable
44
+ // only as an OAuth (subscription) token, so a session that ran on it was
45
+ // one. Do not "correct" it to unknown.
46
+ CLAUDE_CODE_OAUTH_TOKEN: "subscription",
47
+ ANTHROPIC_API_KEY: "api-key",
48
+ }),
49
+ cursor: Object.freeze({ CURSOR_API_KEY: "api-key" }),
50
+ grok: Object.freeze({ XAI_API_KEY: "api-key" }),
51
+ });
52
+
53
+ /** The billing lane when one leased variable name tells it unambiguously. */
54
+ export function billingFromLeasedVariableName(
55
+ account: LeasedBillingAccount | null | undefined,
56
+ ): EngineUsageBilling | undefined {
57
+ if (!account || account.authKind !== "env" || account.env === null) {
58
+ return undefined;
59
+ }
60
+ const names = Object.keys(account.env);
61
+ if (names.length !== 1) return undefined;
62
+ return BILLING_BY_VARIABLE[account.engineKind]?.[names[0]!];
63
+ }
64
+
65
+ /** Construct the exact wire frame for one adapter usage observation.
66
+ * `revision` is the one the reporting session runs on; a leased account at
67
+ * any other revision gives no hint. */
68
+ export function buildEngineUsageFrame(input: {
69
+ reportId: string;
70
+ taskId: string;
71
+ agentId: string;
72
+ engine: EngineUsageFrame["engine"];
73
+ credentialId: string;
74
+ revision?: string;
75
+ usage: EngineUsageObservation;
76
+ leasedAccount?: LeasedBillingAccount | null;
77
+ }): EngineUsageFrame {
78
+ const { usage } = input;
79
+ const account =
80
+ input.revision !== undefined &&
81
+ input.leasedAccount?.credentialId === input.credentialId &&
82
+ input.leasedAccount.revision === input.revision &&
83
+ input.leasedAccount.engineKind === input.engine
84
+ ? input.leasedAccount
85
+ : null;
86
+ const billing = billingFromLeasedVariableName(account);
87
+ return {
88
+ kind: "engine.usage",
89
+ reportId: input.reportId,
90
+ taskId: input.taskId,
91
+ agentId: input.agentId,
92
+ engine: input.engine,
93
+ credentialId: input.credentialId,
94
+ ...(billing === undefined ? {} : { billing, revision: account!.revision }),
95
+ status: usage.status,
96
+ ...(usage.resetsAt === undefined ? {} : { resetsAt: usage.resetsAt }),
97
+ ...(usage.usingOverage === undefined
98
+ ? {}
99
+ : { usingOverage: usage.usingOverage }),
100
+ windows: usage.windows,
101
+ source: usage.source,
102
+ observedAt: usage.observedAt,
103
+ };
104
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What `moondeck-station enroll` / `setup` tells the person to do once the
3
+ * machine is enrolled. Run by hand, the next step is theirs: install and
4
+ * start the service, or restart it. Run by the installer (which exports
5
+ * UAI_FROM_INSTALLER=1 for its enrollment call), the installer registers and
6
+ * starts the service itself right after, so telling the person to do it was
7
+ * a stale instruction (the owner's public-install test, 2026-11-03).
8
+ *
9
+ * An environment variable rather than a flag: the installer the cloud serves
10
+ * may run a station runtime older than this one, which ignores an unknown
11
+ * variable but would refuse an unknown flag.
12
+ */
13
+ import { STATION } from "./station-identity";
14
+
15
+ export const FROM_INSTALLER_ENV = "UAI_FROM_INSTALLER";
16
+
17
+ export function enrollmentNextSteps(env: NodeJS.ProcessEnv = process.env): Array<{ text: string; command?: string; note?: string }> {
18
+ if (env[FROM_INSTALLER_ENV] === "1") return [];
19
+ return [
20
+ { text: "Start the station:" },
21
+ { text: "", command: ` ${STATION.command} install && ${STATION.command} start`, note: " (first time)" },
22
+ { text: "", command: ` ${STATION.command} restart`, note: " (already installed)" },
23
+ ];
24
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * ADR-103: the host-side event outbox.
3
+ *
4
+ * Every HostEvent gets a monotonic seq and its serialized wire frame is held
5
+ * here until the cloud acks it. The bridge connection drains the outbox in
6
+ * order — live traffic and reconnect replay are the same code path, so a
7
+ * WSS blip (a cloud deploy) can no longer drop events on the floor.
8
+ *
9
+ * Bounded, in-memory. A host PROCESS restart still loses unsent events —
10
+ * that path is ADR-061's (the restarted host reattaches to the container
11
+ * and resumes streaming); this outbox closes the disconnect gap only.
12
+ */
13
+
14
+ import { MAX_OUTBOUND_FRAME_BYTES } from "./outbound-frame";
15
+ import type { HostEvent } from "./protocol";
16
+
17
+ export interface OutboxEntry {
18
+ seq: number;
19
+ /** The full serialized `{kind:"event", seq, event}` frame, ready to send. */
20
+ raw: string;
21
+ /** Exact UTF-8 wire bytes retained by this frame. */
22
+ bytes: number;
23
+ }
24
+
25
+ export interface EventOutboxSink {
26
+ readonly bufferedAmount: number;
27
+ send(raw: string): void;
28
+ }
29
+
30
+ export type EventOutboxTransmitResult =
31
+ | "empty"
32
+ | "backpressured"
33
+ | "send_failed";
34
+
35
+ /** Overflow bounds. Generous: a disconnect longer than this many events is a
36
+ * real outage, not a deploy blip, and dropping OLDEST keeps the tail — the
37
+ * most recent turn — intact for replay. */
38
+ const MAX_ENTRIES = 10_000;
39
+ const MAX_BYTES = 32 * 1024 * 1024;
40
+ // The one ceiling on anything the runner sends (src/outbound-frame.ts), under
41
+ // the authenticated cloud receiver's. Task diffs and message_complete events
42
+ // are not yet field-bounded, so rolling hosts must retain one legitimate
43
+ // frame above the ordinary outbox/transport budgets. Anything larger is
44
+ // refused here, counted as dropped: it would never reach the cloud, and could
45
+ // otherwise put the host into a permanent reconnect loop.
46
+ export const MAX_EVENT_FRAME_BYTES = MAX_OUTBOUND_FRAME_BYTES;
47
+
48
+ export class EventOutbox {
49
+ private entries: OutboxEntry[] = [];
50
+ private totalBytes = 0;
51
+ private nextSeq = 1;
52
+ /** Highest seq ever transmitted on any connection. */
53
+ private sentUpTo = 0;
54
+ /** Entries dropped by overflow since the last drain — for one loud log. */
55
+ private droppedSinceDrain = 0;
56
+
57
+ /** Serialize + append. Returns the entry's seq. */
58
+ enqueue(event: HostEvent): number {
59
+ const seq = this.nextSeq++;
60
+ const raw = JSON.stringify({ kind: "event", seq, event });
61
+ const bytes = Buffer.byteLength(raw, "utf8");
62
+ if (bytes > MAX_EVENT_FRAME_BYTES) {
63
+ this.droppedSinceDrain += 1;
64
+ return seq;
65
+ }
66
+ this.entries.push({ seq, raw, bytes });
67
+ this.totalBytes += bytes;
68
+ while (
69
+ this.entries.length > MAX_ENTRIES ||
70
+ (this.totalBytes > MAX_BYTES && this.entries.length > 1)
71
+ ) {
72
+ const dropped = this.entries.shift();
73
+ if (!dropped) break;
74
+ this.totalBytes -= dropped.bytes;
75
+ // Only a NEVER-SENT entry is a real loss; an unacked-but-sent one most
76
+ // likely landed and just lost its ack.
77
+ if (dropped.seq > this.sentUpTo) this.droppedSinceDrain += 1;
78
+ }
79
+ return seq;
80
+ }
81
+
82
+ /** Cumulative ack: forget everything up to and including `seq`. */
83
+ ack(seq: number): void {
84
+ let i = 0;
85
+ for (const entry of this.entries) {
86
+ if (entry.seq > seq) break;
87
+ this.totalBytes -= entry.bytes;
88
+ i += 1;
89
+ }
90
+ if (i > 0) this.entries.splice(0, i);
91
+ if (seq > this.sentUpTo) this.sentUpTo = seq;
92
+ }
93
+
94
+ /**
95
+ * Rewind the transmit cursor for a reconnect (ADR-103 resume handshake).
96
+ * `afterSeq` is the cloud's watermark: everything newer re-sends on the
97
+ * next drain. `null` = the cloud has no state for this boot — replay
98
+ * nothing; only never-transmitted entries go out.
99
+ */
100
+ resume(afterSeq: number | null): void {
101
+ if (afterSeq !== null && afterSeq < this.sentUpTo) this.sentUpTo = afterSeq;
102
+ }
103
+
104
+ /**
105
+ * Entries due for transmission, in order. The caller sends them and the
106
+ * cursor advances — drain is idempotent per entry until `resume` rewinds.
107
+ */
108
+ drain(): OutboxEntry[] {
109
+ const due = this.entries.filter((e) => e.seq > this.sentUpTo);
110
+ const last = due[due.length - 1];
111
+ if (last) this.sentUpTo = last.seq;
112
+ return due;
113
+ }
114
+
115
+ /** Oldest due entry without advancing the transmit cursor. */
116
+ nextDue(): OutboxEntry | null {
117
+ return this.entries.find((entry) => entry.seq > this.sentUpTo) ?? null;
118
+ }
119
+
120
+ /** Advance only the exact next due entry after the caller queues it. */
121
+ markSent(seq: number): boolean {
122
+ const due = this.nextDue();
123
+ if (!due || due.seq !== seq) return false;
124
+ this.sentUpTo = seq;
125
+ return true;
126
+ }
127
+
128
+ /** Never-sent entries lost to overflow since the last call; resets. */
129
+ takeDroppedCount(): number {
130
+ const n = this.droppedSinceDrain;
131
+ this.droppedSinceDrain = 0;
132
+ return n;
133
+ }
134
+
135
+ get size(): number {
136
+ return this.entries.length;
137
+ }
138
+
139
+ get bytes(): number {
140
+ return this.totalBytes;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Queue due frames only until the transport reaches its high-water mark.
146
+ * Entries advance one at a time after `send` accepts them, so a slow socket
147
+ * leaves the remainder replayable in the bounded outbox instead of moving the
148
+ * whole backlog into the WebSocket implementation's own buffer.
149
+ */
150
+ export function transmitEventOutbox(
151
+ outbox: EventOutbox,
152
+ sink: EventOutboxSink,
153
+ maxBufferedBytes: number,
154
+ ): EventOutboxTransmitResult {
155
+ let projectedBufferedBytes = sink.bufferedAmount;
156
+ while (true) {
157
+ const entry = outbox.nextDue();
158
+ if (!entry) return "empty";
159
+ if (
160
+ maxBufferedBytes <= 0 ||
161
+ (projectedBufferedBytes > 0 &&
162
+ (projectedBufferedBytes >= maxBufferedBytes ||
163
+ entry.bytes > maxBufferedBytes - projectedBufferedBytes))
164
+ ) {
165
+ return "backpressured";
166
+ }
167
+ try {
168
+ sink.send(entry.raw);
169
+ } catch {
170
+ return "send_failed";
171
+ }
172
+ if (!outbox.markSent(entry.seq)) return "send_failed";
173
+ projectedBufferedBytes = Math.max(
174
+ sink.bufferedAmount,
175
+ projectedBufferedBytes + entry.bytes,
176
+ );
177
+
178
+ // A rolling-compatible frame above the normal high-water budget gets one
179
+ // empty transport to itself. Do not depend on a sink updating
180
+ // `bufferedAmount` synchronously before protecting the following frame.
181
+ if (entry.bytes > maxBufferedBytes) {
182
+ return outbox.nextDue() ? "backpressured" : "empty";
183
+ }
184
+ }
185
+ }