@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-062 shared files — host-side handler for the `filesOp` bridge command.
|
|
3
|
+
* Two roots per host: org files (per host+org) and personal files (per
|
|
4
|
+
* host+user), both under <workspaceRoot>/shared/. The cloud is a window
|
|
5
|
+
* only; this module is the sole writer host-side.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY: the cloud relays paths from browsers, and containers mount these
|
|
8
|
+
* same dirs — every path is treated as hostile. Two gates on every op:
|
|
9
|
+
* `resolveInsideRoot` (relative-only, ".."-free, resolved-prefix check) and
|
|
10
|
+
* `assertRealInsideRoot` (realpath containment, so a symlink the container
|
|
11
|
+
* planted inside a rw mount cannot point ops outside the root).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fsp } from "node:fs";
|
|
15
|
+
import { dirname, join, normalize, resolve, sep } from "node:path";
|
|
16
|
+
|
|
17
|
+
import type { FilesEntry, FilesOpInput, FilesOpValue } from "../src/protocol";
|
|
18
|
+
import { env } from "./env";
|
|
19
|
+
|
|
20
|
+
/** Max bytes returned per read chunk (base64 expands ~4/3; WS cap is 16MB). */
|
|
21
|
+
const MAX_READ_CHUNK = 4 * 1024 * 1024;
|
|
22
|
+
/** Max bytes accepted per write chunk. */
|
|
23
|
+
const MAX_WRITE_CHUNK = 8 * 1024 * 1024;
|
|
24
|
+
/** Max entries returned by a single list. */
|
|
25
|
+
const MAX_LIST_ENTRIES = 2_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Filesystem-safe directory name for an org/user id. Personal org ids are
|
|
29
|
+
* `personal:<userId>` — the colon breaks docker's short-syntax bind spec
|
|
30
|
+
* (src:dst:mode), so it is mapped to "_". MUST stay in lockstep with the
|
|
31
|
+
* identical mapping in scripts/agent/task-up.sh (shared_fs_name).
|
|
32
|
+
*/
|
|
33
|
+
export function sharedDirName(id: string): string {
|
|
34
|
+
return id.replace(/:/g, "_");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sharedRoot(scope: "org" | "me", orgId: string, userId: string): string {
|
|
38
|
+
return scope === "org"
|
|
39
|
+
? resolve(env.workspaceRoot, "shared", "orgs", sharedDirName(orgId))
|
|
40
|
+
: resolve(env.workspaceRoot, "shared", "users", sharedDirName(userId));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveInsideRoot(root: string, relPath: string): string {
|
|
44
|
+
// Relative paths only — an absolute path is rejected, never reinterpreted.
|
|
45
|
+
if (/^[/\\]/.test(relPath) || /^[A-Za-z]:/.test(relPath)) {
|
|
46
|
+
throw new Error("invalid path");
|
|
47
|
+
}
|
|
48
|
+
const cleaned = normalize(relPath);
|
|
49
|
+
if (
|
|
50
|
+
cleaned.split(/[/\\]/).some((seg) => seg === "..") ||
|
|
51
|
+
cleaned.includes("\0")
|
|
52
|
+
) {
|
|
53
|
+
throw new Error("invalid path");
|
|
54
|
+
}
|
|
55
|
+
const target = resolve(root, cleaned);
|
|
56
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
57
|
+
throw new Error("invalid path");
|
|
58
|
+
}
|
|
59
|
+
return target;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* String containment (resolveInsideRoot) can't see symlinks the container
|
|
64
|
+
* planted inside a rw mount — `root/evil → /` would pass it. Walk up from
|
|
65
|
+
* the target to its deepest EXISTING ancestor, realpath it, and require the
|
|
66
|
+
* REAL location to stay inside the real root (ADR-062 "no symlink escapes").
|
|
67
|
+
*/
|
|
68
|
+
async function assertRealInsideRoot(root: string, target: string): Promise<void> {
|
|
69
|
+
const realRoot = await fsp.realpath(root);
|
|
70
|
+
let probe = target;
|
|
71
|
+
for (;;) {
|
|
72
|
+
try {
|
|
73
|
+
const real = await fsp.realpath(probe);
|
|
74
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
75
|
+
throw new Error("invalid path");
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
80
|
+
const parent = dirname(probe);
|
|
81
|
+
if (parent === probe) throw new Error("invalid path");
|
|
82
|
+
probe = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function handleFilesOp(input: FilesOpInput): Promise<FilesOpValue> {
|
|
88
|
+
if (!input.orgId && input.scope === "org") throw new Error("missing orgId");
|
|
89
|
+
if (!input.userId && input.scope === "me") throw new Error("missing userId");
|
|
90
|
+
const root = sharedRoot(input.scope, input.orgId, input.userId);
|
|
91
|
+
await fsp.mkdir(root, { recursive: true });
|
|
92
|
+
const target = resolveInsideRoot(root, input.path ?? "");
|
|
93
|
+
await assertRealInsideRoot(root, target);
|
|
94
|
+
|
|
95
|
+
switch (input.op) {
|
|
96
|
+
case "list": {
|
|
97
|
+
const names = await fsp.readdir(target);
|
|
98
|
+
const entries: FilesEntry[] = [];
|
|
99
|
+
for (const name of names.slice(0, MAX_LIST_ENTRIES)) {
|
|
100
|
+
try {
|
|
101
|
+
const st = await fsp.lstat(join(target, name));
|
|
102
|
+
entries.push({
|
|
103
|
+
name,
|
|
104
|
+
dir: st.isDirectory(),
|
|
105
|
+
size: st.isDirectory() ? 0 : st.size,
|
|
106
|
+
mtimeMs: st.mtimeMs,
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
// Raced deletion — skip.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
entries.sort((a, b) =>
|
|
113
|
+
a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1,
|
|
114
|
+
);
|
|
115
|
+
return { entries };
|
|
116
|
+
}
|
|
117
|
+
case "read": {
|
|
118
|
+
const st = await fsp.stat(target);
|
|
119
|
+
if (st.isDirectory()) throw new Error("is a directory");
|
|
120
|
+
const offset = Math.max(0, input.offset ?? 0);
|
|
121
|
+
const length = Math.min(input.length ?? MAX_READ_CHUNK, MAX_READ_CHUNK);
|
|
122
|
+
const fh = await fsp.open(target, "r");
|
|
123
|
+
try {
|
|
124
|
+
const buf = Buffer.alloc(Math.min(length, Math.max(0, st.size - offset)));
|
|
125
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, offset);
|
|
126
|
+
return {
|
|
127
|
+
dataBase64: buf.subarray(0, bytesRead).toString("base64"),
|
|
128
|
+
size: st.size,
|
|
129
|
+
eof: offset + bytesRead >= st.size,
|
|
130
|
+
};
|
|
131
|
+
} finally {
|
|
132
|
+
await fh.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
case "write": {
|
|
136
|
+
if (target === root) throw new Error("invalid path");
|
|
137
|
+
const data = Buffer.from(input.dataBase64 ?? "", "base64");
|
|
138
|
+
if (data.length > MAX_WRITE_CHUNK) throw new Error("chunk too large");
|
|
139
|
+
await fsp.mkdir(resolve(target, ".."), { recursive: true });
|
|
140
|
+
await fsp.writeFile(target, data, { flag: input.append ? "a" : "w" });
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
case "mkdir": {
|
|
144
|
+
if (target === root) throw new Error("invalid path");
|
|
145
|
+
await fsp.mkdir(target, { recursive: true });
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
case "delete": {
|
|
149
|
+
if (target === root) throw new Error("invalid path");
|
|
150
|
+
await fsp.rm(target, { recursive: true, force: true });
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`unknown op: ${String(input.op)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The warm-pool claim's host half (docs/machine-warm-pool.md): the cloud's
|
|
3
|
+
* `sidecarAssign` names the task this pool machine now serves. The task id
|
|
4
|
+
* is appended to the sidecar unit's EnvironmentFile and the process exits
|
|
5
|
+
* with ASSIGN_RESTART_EXIT_CODE — systemd (`Restart=on-failure`) brings the
|
|
6
|
+
* unit back up in plain sidecar mode, the tested runner-restart path.
|
|
7
|
+
* Rewriting a root-owned file and restarting is deliberately boring: no
|
|
8
|
+
* runtime re-wiring of boot-time gates, no half-pool half-task states.
|
|
9
|
+
*/
|
|
10
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
import { sidecarPoolMode } from "./sidecar";
|
|
13
|
+
|
|
14
|
+
/** Anything but 0 and 3: 0 would not restart the unit, 3 is the lease-denied
|
|
15
|
+
* latch (`RestartPreventExitStatus=3`). */
|
|
16
|
+
export const ASSIGN_RESTART_EXIT_CODE = 64;
|
|
17
|
+
const EXIT_DELAY_MS = 500;
|
|
18
|
+
|
|
19
|
+
export function sidecarEnvFile(): string {
|
|
20
|
+
return process.env.UAI_SIDECAR_ENV_FILE ?? "/etc/uai/sidecar.env";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function applySidecarAssign(
|
|
24
|
+
taskId: string,
|
|
25
|
+
deps: {
|
|
26
|
+
envFile?: string;
|
|
27
|
+
exit?: (code: number) => void;
|
|
28
|
+
append?: (file: string, text: string) => void;
|
|
29
|
+
read?: (file: string) => string;
|
|
30
|
+
} = {},
|
|
31
|
+
): void {
|
|
32
|
+
if (!sidecarPoolMode()) {
|
|
33
|
+
throw new Error("not a pool machine (already assigned, or not a sidecar)");
|
|
34
|
+
}
|
|
35
|
+
if (!/^[a-z0-9]{1,32}$/i.test(taskId)) throw new Error("invalid task id");
|
|
36
|
+
const file = deps.envFile ?? sidecarEnvFile();
|
|
37
|
+
const read = deps.read ?? ((f: string) => readFileSync(f, "utf8"));
|
|
38
|
+
const current = read(file);
|
|
39
|
+
if (/^UAI_SIDECAR_TASK=/m.test(current)) {
|
|
40
|
+
throw new Error("this machine already carries a task assignment");
|
|
41
|
+
}
|
|
42
|
+
const append = deps.append ?? ((f: string, text: string) => appendFileSync(f, text));
|
|
43
|
+
append(file, `UAI_SIDECAR_TASK=${taskId}\n`);
|
|
44
|
+
// Ack first, then die: the delay lets the command reply flush to the
|
|
45
|
+
// bridge before the socket goes with the process.
|
|
46
|
+
const exit = deps.exit ?? ((code: number) => process.exit(code));
|
|
47
|
+
const timer = setTimeout(() => exit(ASSIGN_RESTART_EXIT_CODE), EXIT_DELAY_MS);
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A task machine's own guards, held by its sidecar (root) before the task
|
|
3
|
+
* runs anything there (ADR-133; ADR-153 §3, "Where the proxy runs"):
|
|
4
|
+
*
|
|
5
|
+
* - The runner's data directory is root's alone, so the task's user cannot
|
|
6
|
+
* read the store's database. Its values are sealed with a root-only key,
|
|
7
|
+
* but its key names and bindings would still be there to read.
|
|
8
|
+
* - The instance metadata service is closed to the task's user, so it can
|
|
9
|
+
* read neither the machine's user-data, which carries the sidecar's host
|
|
10
|
+
* token, nor the instance role's credentials.
|
|
11
|
+
*
|
|
12
|
+
* The image's prepare step sets both up too. The runner insists on them at
|
|
13
|
+
* every task-up, since an image that skipped or failed that step would
|
|
14
|
+
* otherwise let the task start without them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
/** Rejects the task user's traffic to the instance metadata service, with
|
|
21
|
+
* whichever iptables backend the kernel serves (as the proxy's chain does),
|
|
22
|
+
* adding the rule only when it is missing. */
|
|
23
|
+
export const METADATA_BLOCK_SCRIPT = [
|
|
24
|
+
"set -eu",
|
|
25
|
+
"ipt=iptables",
|
|
26
|
+
"if ! command -v iptables >/dev/null 2>&1 || ! iptables -S OUTPUT >/dev/null 2>&1; then ipt=iptables-legacy; fi",
|
|
27
|
+
'command -v "$ipt" >/dev/null 2>&1 || { echo "iptables is not installed" >&2; exit 1; }',
|
|
28
|
+
'"$ipt" -C OUTPUT -d 169.254.169.254 -m owner --uid-owner node -j REJECT 2>/dev/null \\',
|
|
29
|
+
' || "$ipt" -I OUTPUT -d 169.254.169.254 -m owner --uid-owner node -j REJECT',
|
|
30
|
+
].join("\n");
|
|
31
|
+
|
|
32
|
+
export interface SidecarGuardDeps {
|
|
33
|
+
dataDir: string;
|
|
34
|
+
/** Runs a script as this (root) process; throws when it fails. */
|
|
35
|
+
runScript(script: string): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultRunScript = (script: string): void => {
|
|
39
|
+
execFileSync("/bin/sh", ["-c", script], { stdio: ["ignore", "ignore", "pipe"], timeout: 15_000 });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Puts both guards in place, or says which could not be. */
|
|
43
|
+
export function guardSidecarMachine(deps: Partial<SidecarGuardDeps> & Pick<SidecarGuardDeps, "dataDir">): { ok: true } | { ok: false; reason: string } {
|
|
44
|
+
const runScript = deps.runScript ?? defaultRunScript;
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(deps.dataDir, { recursive: true, mode: 0o700 });
|
|
47
|
+
chmodSync(deps.dataDir, 0o700);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { ok: false, reason: `its data directory could not be made private (${error instanceof Error ? error.message : String(error)})` };
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
runScript(METADATA_BLOCK_SCRIPT);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const detail = error instanceof Error && "stderr" in error ? String((error as { stderr?: unknown }).stderr ?? "").trim() : "";
|
|
55
|
+
return { ok: false, reason: `the instance metadata service could not be closed to the task${detail ? ` (${detail})` : ""}` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-133: what a sidecar re-asserts when it starts on a machine whose task
|
|
3
|
+
* is ALREADY up — a restart of the unit, not a fresh launch.
|
|
4
|
+
*
|
|
5
|
+
* Task-up mounts the shared files and launches the editor once; nothing ran
|
|
6
|
+
* them again when the runner came back, so an operator's restart (smoke 10,
|
|
7
|
+
* 2026-09-10) left /workspace/files as two dead endpoints and the Editor tab
|
|
8
|
+
* empty for the rest of the task. The unit now keeps those processes alive
|
|
9
|
+
* across a stop (KillMode=process), and this pass covers what that cannot:
|
|
10
|
+
* a crash, an older unit, a SIGKILL. Both steps are idempotent — a healthy
|
|
11
|
+
* mount and a live code-server are left alone — so running it on every
|
|
12
|
+
* lease-ready costs one stat each.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { eq } from "drizzle-orm";
|
|
16
|
+
|
|
17
|
+
import * as schema from "../db/schema";
|
|
18
|
+
import { getDb } from "./db";
|
|
19
|
+
import { mountSharedFilesFromLease } from "./shared-files-s3";
|
|
20
|
+
import { sidecarTaskId } from "./sidecar";
|
|
21
|
+
import { reconstructPersistedTaskEnvironment } from "./task-environment";
|
|
22
|
+
import { installMachineProjectEnvLoader } from "./machine-project-env-loader";
|
|
23
|
+
import { activeSecretsProxy } from "./secrets-proxy/host";
|
|
24
|
+
import { taskViewProven } from "./secrets-proxy/sidecar-view";
|
|
25
|
+
import { reconcileSidecarTaskProxy } from "./secrets-proxy/task-trust";
|
|
26
|
+
import { ensureCodeServer } from "./task-environment/machine-task-up";
|
|
27
|
+
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
28
|
+
|
|
29
|
+
type PersistedTask = typeof schema.hostTasks.$inferSelect;
|
|
30
|
+
|
|
31
|
+
export interface SidecarResumeDeps {
|
|
32
|
+
taskId?: () => string | null;
|
|
33
|
+
persistedTask?: (taskId: string) => PersistedTask | undefined;
|
|
34
|
+
reconstruct?: (task: PersistedTask) => Promise<TaskEnvironmentHandle<unknown> | null>;
|
|
35
|
+
mount?: (
|
|
36
|
+
handle: TaskEnvironmentHandle<unknown>,
|
|
37
|
+
workspacePath: string,
|
|
38
|
+
task: { sharedFiles?: string },
|
|
39
|
+
) => Promise<string | null>;
|
|
40
|
+
/** ADR-154 Files v1: install and mount the exact recovered run's session
|
|
41
|
+
* from trusted host state. Unlike the legacy marker path, failure is
|
|
42
|
+
* terminal for this recovery setup and is not converted to a warning. */
|
|
43
|
+
prepareFiles?: (handle: TaskEnvironmentHandle<unknown>) => Promise<void>;
|
|
44
|
+
loader?: (handle: TaskEnvironmentHandle<unknown>) => Promise<void>;
|
|
45
|
+
editor?: (handle: TaskEnvironmentHandle<unknown>, workspacePath: string) => Promise<void>;
|
|
46
|
+
proxy?: typeof reconcileSidecarTaskProxy;
|
|
47
|
+
/** Remount the shared files from the v0 lease (the default). ADR-154: a
|
|
48
|
+
* task on cloud credentials remounts them from its own run's Files
|
|
49
|
+
* session instead, never from the runner's process-wide v0 lease. */
|
|
50
|
+
remountFiles?: boolean;
|
|
51
|
+
/** ADR-154: aborted when the run this resume is for ends (Stop, teardown,
|
|
52
|
+
* a newer run, shutdown): no step starts after. */
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SidecarResumeResult {
|
|
57
|
+
/** `no-task`: not a sidecar. `not-up`: the task has not come up on this
|
|
58
|
+
* machine yet (a fresh launch, or a start that a runner restart cut short —
|
|
59
|
+
* task-up does the work). `resumed`: the
|
|
60
|
+
* task was up before this runner started; its mounts and editor were
|
|
61
|
+
* re-asserted, with any per-step warning listed. */
|
|
62
|
+
outcome: "no-task" | "not-up" | "resumed" | "interrupted";
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The mode task-up mounted with, recorded in the files-mounted marker; null
|
|
67
|
+
* when the files were never mounted (mode off, or the mount failed then). */
|
|
68
|
+
function mountedModeFromMarker(bytes: Uint8Array | null): "ro" | "rw" | null {
|
|
69
|
+
if (bytes === null) return null;
|
|
70
|
+
return Buffer.from(bytes).toString("utf8").trim() === "rw" ? "rw" : "ro";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* ADR-153 machines, at the runner's start and before any command can reach
|
|
75
|
+
* the task: a task this machine has a row for, in whatever state (`starting`
|
|
76
|
+
* included), is held for this boot unless its hosts-view record proves it.
|
|
77
|
+
* A start driven again, or a resume, then finds the hold. Only task-up for a
|
|
78
|
+
* task with no row admits it fresh (src/index.ts).
|
|
79
|
+
*/
|
|
80
|
+
export function classifySidecarTaskView(
|
|
81
|
+
deps: Pick<SidecarResumeDeps, "taskId" | "persistedTask"> = {},
|
|
82
|
+
): void {
|
|
83
|
+
const taskId = (deps.taskId ?? sidecarTaskId)();
|
|
84
|
+
if (!taskId) return;
|
|
85
|
+
if ((deps.persistedTask ?? defaultPersistedTask)(taskId) === undefined) return;
|
|
86
|
+
// Asked for its record: one with none is held from here on.
|
|
87
|
+
taskViewProven(taskId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function resumeSidecarTask(
|
|
91
|
+
deps: SidecarResumeDeps = {},
|
|
92
|
+
): Promise<SidecarResumeResult> {
|
|
93
|
+
const taskId = (deps.taskId ?? sidecarTaskId)();
|
|
94
|
+
if (!taskId) return { outcome: "no-task", warnings: [] };
|
|
95
|
+
const task = (deps.persistedTask ?? defaultPersistedTask)(taskId);
|
|
96
|
+
// Only a task that came all the way up: one still starting (a runner that
|
|
97
|
+
// died mid-provision) is task-up's to finish, and resuming it would race
|
|
98
|
+
// that retry's loader, proxy steps and editor.
|
|
99
|
+
if (!task?.environmentLocator || task.statusMirror !== "running") return { outcome: "not-up", warnings: [] };
|
|
100
|
+
const handle = await (deps.reconstruct ?? reconstructPersistedTaskEnvironment)(task);
|
|
101
|
+
if (!handle) return { outcome: "not-up", warnings: [] };
|
|
102
|
+
const workspacePath = handle.descriptor.workspacePath;
|
|
103
|
+
const warnings: string[] = [];
|
|
104
|
+
const ended = (): boolean => deps.signal?.aborted === true;
|
|
105
|
+
const interrupted = (): SidecarResumeResult => ({ outcome: "interrupted", warnings });
|
|
106
|
+
|
|
107
|
+
if (deps.prepareFiles) {
|
|
108
|
+
if (ended()) return interrupted();
|
|
109
|
+
await deps.prepareFiles(handle);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (task.envMode === "loader") {
|
|
113
|
+
if (ended()) return interrupted();
|
|
114
|
+
try {
|
|
115
|
+
await (deps.loader ?? installMachineProjectEnvLoader)(handle);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// The loader remains fail-closed: no project value was baked into this
|
|
118
|
+
// machine. Keep repairing the editor/mounts, but make the degraded env
|
|
119
|
+
// channel visible to the sidecar's restart log.
|
|
120
|
+
warnings.push(
|
|
121
|
+
`project env loader: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
// ADR-153: the proxy's CA and pins again (a reboot keeps neither the
|
|
125
|
+
// chain nor this runner's record of any of it).
|
|
126
|
+
if (ended()) return interrupted();
|
|
127
|
+
const proxyWarning = await (deps.proxy ?? reconcileSidecarTaskProxy)(
|
|
128
|
+
handle,
|
|
129
|
+
activeSecretsProxy()?.runtime ?? null,
|
|
130
|
+
{ start: false, ...(deps.signal ? { isCurrent: () => !ended() } : {}) },
|
|
131
|
+
);
|
|
132
|
+
if (proxyWarning !== null) warnings.push(proxyWarning);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const marker =
|
|
136
|
+
deps.remountFiles === false
|
|
137
|
+
? null
|
|
138
|
+
: await handle.readWorkspaceFile(`${workspacePath}/.moondeck/files-mounted`).catch(() => null);
|
|
139
|
+
const mode = mountedModeFromMarker(marker);
|
|
140
|
+
if (mode) {
|
|
141
|
+
try {
|
|
142
|
+
const warning = await (deps.mount ?? mountSharedFilesFromLease)(handle, workspacePath, {
|
|
143
|
+
sharedFiles: mode,
|
|
144
|
+
});
|
|
145
|
+
if (warning) warnings.push(warning);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
warnings.push(
|
|
148
|
+
`shared files: ${error instanceof Error ? error.message : String(error)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ended()) return interrupted();
|
|
154
|
+
try {
|
|
155
|
+
await (deps.editor ?? defaultEditor)(handle, workspacePath);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
warnings.push(`editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
return { outcome: "resumed", warnings };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function defaultPersistedTask(taskId: string): PersistedTask | undefined {
|
|
163
|
+
return getDb()
|
|
164
|
+
.select()
|
|
165
|
+
.from(schema.hostTasks)
|
|
166
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
167
|
+
.get();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function defaultEditor(
|
|
171
|
+
handle: TaskEnvironmentHandle<unknown>,
|
|
172
|
+
workspacePath: string,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
// The theme rode task-up's preview env and is not persisted; the settings
|
|
175
|
+
// seed is skipped when the file already exists, so an existing task keeps
|
|
176
|
+
// the theme it started with.
|
|
177
|
+
return ensureCodeServer(handle, workspacePath, undefined);
|
|
178
|
+
}
|
package/lib/sidecar.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-133 sidecar mode: this runner process lives ON the task's machine and
|
|
3
|
+
* is that one task's host. Selected by the environment the machine image's
|
|
4
|
+
* `uai-sidecar.service` writes from EC2 user-data (hostless-implementation.md
|
|
5
|
+
* §8/§9). Everything the coordinator shape needed and a single-task machine
|
|
6
|
+
* does not — the local UI, the container runtime, host-level engine accounts,
|
|
7
|
+
* the machine provider, recovery sweeps over many tasks — asks here and
|
|
8
|
+
* stays off. Owned runners never set it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function sidecarTaskId(): string | null {
|
|
12
|
+
const value = process.env.UAI_SIDECAR_TASK?.trim();
|
|
13
|
+
return value && /^[a-z0-9]{1,32}$/i.test(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Warm-pool mode (docs/machine-warm-pool.md): a sidecar with no task yet.
|
|
18
|
+
* Same machine gates as sidecar mode, no lease (nothing to scope one to);
|
|
19
|
+
* the process waits for `sidecarAssign` to write `UAI_SIDECAR_TASK` into
|
|
20
|
+
* the unit env and restart it into plain sidecar mode. Once a task id is
|
|
21
|
+
* present, pool mode is over — even before the restart lands.
|
|
22
|
+
*/
|
|
23
|
+
export function sidecarPoolMode(): boolean {
|
|
24
|
+
return process.env.UAI_SIDECAR_POOL === "1" && sidecarTaskId() === null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sidecarMode(): boolean {
|
|
28
|
+
return sidecarTaskId() !== null || sidecarPoolMode();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A sidecar is ready to run its task only once it holds a lease AND the
|
|
33
|
+
* credential store has been reconciled under it — before that there is no
|
|
34
|
+
* engine account, GitHub token, or project env on the machine, and a task-up
|
|
35
|
+
* that raced the first grant would clone without credentials and spawn
|
|
36
|
+
* agents that cannot sign in (the dispatch arrives the moment the bridge
|
|
37
|
+
* connects; the lease answer is one round trip behind it). Resolved by
|
|
38
|
+
* main.ts after the first grant's reconciles settle; never on an owned host.
|
|
39
|
+
*/
|
|
40
|
+
let markReady: (() => void) | null = null;
|
|
41
|
+
let ready = false;
|
|
42
|
+
export const sidecarReady: Promise<void> = new Promise<void>((resolve) => {
|
|
43
|
+
markReady = resolve;
|
|
44
|
+
});
|
|
45
|
+
export function markSidecarReady(): void {
|
|
46
|
+
if (ready) return;
|
|
47
|
+
ready = true;
|
|
48
|
+
markReady?.();
|
|
49
|
+
markReady = null;
|
|
50
|
+
}
|
|
51
|
+
export function isSidecarReady(): boolean {
|
|
52
|
+
return ready;
|
|
53
|
+
}
|
|
54
|
+
/** How long a task-up waits for the lease before giving up. */
|
|
55
|
+
export const SIDECAR_READY_TIMEOUT_MS = 3 * 60_000;
|
|
56
|
+
export function awaitSidecarReady(timeoutMs = SIDECAR_READY_TIMEOUT_MS): Promise<void> {
|
|
57
|
+
if (!sidecarMode() || ready) return Promise.resolve();
|
|
58
|
+
return new Promise<void>((resolve, reject) => {
|
|
59
|
+
const timer = setTimeout(
|
|
60
|
+
() => reject(new Error("the task's machine has no credentials yet (no lease granted)")),
|
|
61
|
+
timeoutMs,
|
|
62
|
+
);
|
|
63
|
+
timer.unref?.();
|
|
64
|
+
void sidecarReady.then(() => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|