@moondeck/station 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/meta/_journal.json +223 -0
- package/db/schema.ts +850 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1453 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +891 -0
- package/lib/env.ts +195 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +131 -4
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5545 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
package/src/load-env.ts
ADDED
|
@@ -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
|
+
}
|