@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,97 @@
1
+ /**
2
+ * Side-effect module: load `.env.local` then `.env` before host-agent modules
3
+ * read environment variables.
4
+ */
5
+
6
+ import { existsSync, realpathSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, resolve, sep } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ import { config as dotenvConfig } from "dotenv";
12
+
13
+ import { STATION } from "./station-identity";
14
+
15
+ const proc = process as unknown as {
16
+ loadEnvFile?: (path: string) => void;
17
+ };
18
+
19
+ // Real path, not the import path: pnpm workspace links reach this file via
20
+ // symlinks — a repo checkout must resolve to the checkout, and an installed
21
+ // tarball to its true node_modules location.
22
+ const here = (() => {
23
+ const raw = dirname(fileURLToPath(import.meta.url)); // <pkg>/src
24
+ try {
25
+ return realpathSync(raw);
26
+ } catch {
27
+ return raw;
28
+ }
29
+ })();
30
+
31
+ // An INSTALLED copy always lives under a node_modules directory. Without
32
+ // this guard, a tarball installed beneath some user's pnpm workspace that
33
+ // happens to contain a sibling host-agent/ dir was mistaken for this source
34
+ // checkout — suppressing telemetry AND selecting the wrong UAI_HOME
35
+ // (review finding). Packaged desktop resources contain no node_modules
36
+ // segment and no workspace above them; the repo checkout contains neither
37
+ // problem.
38
+ const installedCopy = here.split(sep).includes("node_modules");
39
+
40
+ // Resolve the same UAI_HOME as lib/env.ts, but independently: this module runs
41
+ // FIRST (before lib/env is imported) so it can't depend on it. .env.local is
42
+ // what *sets* UAI_DATA_DIR, so we can't key off that here — only an explicit
43
+ // UAI_HOME env, then repo detection, then ~/.uai (installed via npm).
44
+ function findUp(start: string, pred: (dir: string) => boolean): string | null {
45
+ let dir = start;
46
+ for (;;) {
47
+ if (pred(dir)) return dir;
48
+ const parent = dirname(dir);
49
+ if (parent === dir) return null;
50
+ dir = parent;
51
+ }
52
+ }
53
+
54
+ const repoRoot = installedCopy
55
+ ? null
56
+ : findUp(
57
+ here,
58
+ (d) =>
59
+ existsSync(resolve(d, "pnpm-workspace.yaml")) &&
60
+ existsSync(resolve(d, "host-agent")),
61
+ );
62
+
63
+ const uaiHome = process.env.UAI_HOME
64
+ ? resolve(process.env.UAI_HOME.replace(/^~(?=\/|$)/, homedir()))
65
+ : (repoRoot ?? resolve(homedir(), STATION.homeDirName));
66
+
67
+ // ADR-071: expose the repo-checkout detection computed above to the
68
+ // telemetry gate (lib/obs resolveHostDsn). `pnpm host-agent` from a fresh
69
+ // checkout (and the deleted desktop host app's dev launch) set neither NODE_ENV
70
+ // nor UAI_AGENT_FROM_REPO — this marker is the signal tied to the REAL
71
+ // launch path, so dev runs never default-activate crash reporting. Packaged
72
+ // installs (npm tarball, desktop resources) have no workspace file above
73
+ // them and stay unmarked.
74
+ if (repoRoot && !process.env.UAI_LAUNCHED_FROM_REPO) {
75
+ process.env.UAI_LAUNCHED_FROM_REPO = "1";
76
+ }
77
+
78
+ for (const file of [".env.local", ".env"]) {
79
+ const path = join(uaiHome, file);
80
+ if (!existsSync(path)) continue;
81
+ try {
82
+ if (proc.loadEnvFile) {
83
+ proc.loadEnvFile(path);
84
+ } else {
85
+ // process.loadEnvFile landed in Node 20.12; the package supports
86
+ // >=20. Silently skipping .env.local on 20.0–20.11 would ignore
87
+ // UAI_TELEMETRY_DISABLED — the telemetry kill switch MUST load on
88
+ // every supported Node, so fall back to dotenv (already a dep).
89
+ // Neither loader overrides pre-set process.env — same semantics.
90
+ dotenvConfig({ path });
91
+ }
92
+ } catch (err) {
93
+ console.warn(
94
+ `[host-agent] could not load ${file}: ${(err as Error).message}`,
95
+ );
96
+ }
97
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * ADR-121 machine tunnel routing, extracted from main.ts so its one expensive
3
+ * step is bounded and testable. Resolving a machine's address is a cloud API
4
+ * call — `aws ec2 describe-instances` through a spawned CLI, 0.5–1.5 s on the
5
+ * default t4g.medium host — and the cloud opens one tunnel per HTTP request,
6
+ * so a browser pane (noVNC ships dozens of unbundled modules) or the editor
7
+ * (code-server, more) paid that call dozens of times per load. Live
8
+ * 2026-09-06: "connecting, but very slow" on every AWS machine task, while
9
+ * Docker and apple-container hosts, whose routes are a local inspect, felt
10
+ * instant.
11
+ *
12
+ * The memo is per task, short (MACHINE_ROUTE_TTL_MS), and holds only a proven
13
+ * `running` route: a stopped or unknown machine is asked again on every open
14
+ * so a wake is seen promptly; a dial failure drops the entry so a machine that
15
+ * moved (a fresh public IP after stop/start) is re-described on the next open;
16
+ * a stop/down command drops it too — and bumps a per-task generation, so a
17
+ * description that was already in flight when the stop landed cannot
18
+ * re-memoise the pre-stop address. Concurrent cold opens share one resolution
19
+ * (the pane's first burst is half a dozen parallel requests), and a machine
20
+ * seen not running is not asked again for a few seconds (noVNC retries every
21
+ * couple of seconds against a stopped machine; without that, each retry was
22
+ * a CLI spawn). A refused dial invalidates at once. An address that
23
+ * black-holes (an instance stopped while memoised) answers nothing, so the
24
+ * route carries its own connect bound, MACHINE_DIAL_CONNECT_TIMEOUT_MS: both
25
+ * dial paths in main.ts arm it (`tunnel-dial.ts`) and the timeout surfaces as
26
+ * an ordinary dial error, which invalidates the entry exactly like a refusal.
27
+ * Before host 0.9.99 machine dials had no timeout of their own and a
28
+ * black-holed address hung on the kernel's SYN budget (~75 s macOS, ~127 s
29
+ * Linux) — longer than the memo TTL, so the TTL was never the real bound.
30
+ */
31
+
32
+ export type MachineTunnelRoute =
33
+ | { kind: "running"; ipv4Address: string }
34
+ | { kind: "unavailable" }
35
+ | { kind: "unknown"; detail: string };
36
+
37
+ export interface MachineTunnelRouteDeps<Task> {
38
+ reconstruct(task: Task): Promise<{
39
+ inspectRoute?: () => Promise<MachineTunnelRoute>;
40
+ } | null>;
41
+ now(): number;
42
+ ttlMs?: number;
43
+ }
44
+
45
+ export interface MachineTunnelRouteRequest {
46
+ taskId: string;
47
+ target: string;
48
+ containerPort?: number;
49
+ }
50
+
51
+ /** Long enough to cover a pane or editor load (dozens of opens inside a few
52
+ * seconds), short enough that a machine's address is never trusted past the
53
+ * next stop/start cycle by much; a failed dial drops it sooner. */
54
+ export const MACHINE_ROUTE_TTL_MS = 45_000;
55
+ /** How long a not-running answer is trusted before the machine is asked
56
+ * again — long enough to absorb a reconnecting pane, short enough that a wake
57
+ * is seen within seconds. */
58
+ export const MACHINE_ROUTE_NEGATIVE_TTL_MS = 5_000;
59
+ /** Connect-phase bound for a dial to a memoised machine address. An
60
+ * in-region machine answers a SYN in milliseconds; the cloud waits 30 s for
61
+ * the tunnel ack and the forward hop 45 s for the head, so 10 s is generous
62
+ * to a live machine and still leaves the caller a real answer (a 502 and a
63
+ * dropped memo) instead of the kernel's SYN retry budget. */
64
+ export const MACHINE_DIAL_CONNECT_TIMEOUT_MS = 10_000;
65
+
66
+ /** A resolved machine route: where to dial, and the connect bound the dial
67
+ * must carry because the address is memoised. */
68
+ export interface MachineTunnelRouteTarget {
69
+ host: string;
70
+ port: number;
71
+ connectTimeoutMs: number;
72
+ }
73
+
74
+ const memo = new Map<string, { ipv4Address: string; expiresAt: number }>();
75
+ const negative = new Map<string, number>();
76
+ const inFlight = new Map<string, Promise<string | null>>();
77
+ /** Bumped by every invalidation; a resolution only writes the memo if the
78
+ * generation it started under is still current. */
79
+ const generation = new Map<string, number>();
80
+
81
+ export async function resolveMachineTunnelRoute<
82
+ Task extends { codeServerPort?: number | null },
83
+ >(
84
+ request: MachineTunnelRouteRequest,
85
+ task: Task,
86
+ deps: MachineTunnelRouteDeps<Task>,
87
+ ): Promise<MachineTunnelRouteTarget | null> {
88
+ // The cheap refusal first: an unroutable request never costs an API call.
89
+ const port =
90
+ request.target === "editor"
91
+ ? (task.codeServerPort ?? null)
92
+ : (request.containerPort ?? null);
93
+ if (!port) return null;
94
+ const host = await machineAddress(request.taskId, task, deps);
95
+ return host === null
96
+ ? null
97
+ : { host, port, connectTimeoutMs: MACHINE_DIAL_CONNECT_TIMEOUT_MS };
98
+ }
99
+
100
+ async function machineAddress<Task>(
101
+ taskId: string,
102
+ task: Task,
103
+ deps: MachineTunnelRouteDeps<Task>,
104
+ ): Promise<string | null> {
105
+ const now = deps.now();
106
+ const hit = memo.get(taskId);
107
+ if (hit && hit.expiresAt > now) return hit.ipv4Address;
108
+ memo.delete(taskId);
109
+ const notRunningUntil = negative.get(taskId);
110
+ if (notRunningUntil !== undefined && notRunningUntil > now) return null;
111
+ negative.delete(taskId);
112
+ const pending = inFlight.get(taskId);
113
+ if (pending) return pending;
114
+ const startedUnder = generation.get(taskId) ?? 0;
115
+ const resolution = (async (): Promise<string | null> => {
116
+ const environment = await deps.reconstruct(task);
117
+ const route = environment?.inspectRoute
118
+ ? await environment.inspectRoute()
119
+ : null;
120
+ const settledAt = deps.now();
121
+ const stillCurrent = (generation.get(taskId) ?? 0) === startedUnder;
122
+ if (!route || route.kind !== "running") {
123
+ if (stillCurrent) {
124
+ negative.set(taskId, settledAt + MACHINE_ROUTE_NEGATIVE_TTL_MS);
125
+ }
126
+ return null;
127
+ }
128
+ if (stillCurrent) {
129
+ memo.set(taskId, {
130
+ ipv4Address: route.ipv4Address,
131
+ expiresAt: settledAt + (deps.ttlMs ?? MACHINE_ROUTE_TTL_MS),
132
+ });
133
+ }
134
+ return route.ipv4Address;
135
+ })().finally(() => {
136
+ inFlight.delete(taskId);
137
+ });
138
+ inFlight.set(taskId, resolution);
139
+ return resolution;
140
+ }
141
+
142
+ /** A dial to the memoised address failed, or the task was stopped, started or
143
+ * torn down: the next open must describe the machine again, and a description
144
+ * already in flight must not write what it learned before this point. */
145
+ export function invalidateMachineTunnelRoute(taskId: string): void {
146
+ memo.delete(taskId);
147
+ negative.delete(taskId);
148
+ generation.set(taskId, (generation.get(taskId) ?? 0) + 1);
149
+ }
150
+
151
+ export function resetMachineTunnelRoutesForTesting(): void {
152
+ memo.clear();
153
+ negative.clear();
154
+ inFlight.clear();
155
+ generation.clear();
156
+ }