@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,116 @@
|
|
|
1
|
+
import type { TaskMachineRetireReason } from "../src/protocol";
|
|
2
|
+
import type { TaskAssignment } from "./cloud-credential-leases";
|
|
3
|
+
|
|
4
|
+
export type TaskFilesMachineRetirementReason = TaskMachineRetireReason;
|
|
5
|
+
|
|
6
|
+
export interface TaskFilesMachineRetirementRequest {
|
|
7
|
+
assignment: Readonly<TaskAssignment>;
|
|
8
|
+
reason: TaskFilesMachineRetirementReason;
|
|
9
|
+
error: Error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cutover-owned terminal path for a one-task machine whose Files boundary
|
|
14
|
+
* cannot be proved closed. The handler must synchronously quarantine local
|
|
15
|
+
* task work, then durably retry an authenticated exact-run machine-retirement
|
|
16
|
+
* request until the cloud acknowledges it or the machine dies. Exiting only
|
|
17
|
+
* the sidecar is explicitly insufficient: its systemd unit uses
|
|
18
|
+
* KillMode=process and leaves the task processes alive.
|
|
19
|
+
*/
|
|
20
|
+
export type TaskFilesMachineRetirementHandler = (
|
|
21
|
+
request: TaskFilesMachineRetirementRequest,
|
|
22
|
+
) => undefined;
|
|
23
|
+
|
|
24
|
+
let handler: TaskFilesMachineRetirementHandler | null = null;
|
|
25
|
+
let retiring: TaskFilesMachineRetirementRequest | null = null;
|
|
26
|
+
let damaged: Error | null = null;
|
|
27
|
+
let handedOff = false;
|
|
28
|
+
|
|
29
|
+
export function setTaskFilesMachineRetirementHandler(
|
|
30
|
+
next: TaskFilesMachineRetirementHandler | null,
|
|
31
|
+
): void {
|
|
32
|
+
handler = next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Files v1 must not be advertised, or activation entered, without this. */
|
|
36
|
+
export function taskFilesMachineRetirementReady(): boolean {
|
|
37
|
+
return handler !== null && retiring === null && damaged === null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** An unreadable same-boot outbox record may be an irreversible retirement
|
|
41
|
+
* request. With no safe exact run to reconstruct, keep every task-process
|
|
42
|
+
* admission closed for the rest of this boot. */
|
|
43
|
+
export function latchDamagedTaskFilesMachineRetirement(error: Error): void {
|
|
44
|
+
damaged ??= error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The self provider calls this immediately around every task-process
|
|
49
|
+
* admission. Once terminal Files retirement wins, no in-flight command may
|
|
50
|
+
* repopulate the service cgroup after the retirement handler proves it empty.
|
|
51
|
+
*/
|
|
52
|
+
export function assertTaskFilesMachineProcessAllowed(): void {
|
|
53
|
+
if (damaged !== null) {
|
|
54
|
+
throw new Error("task-machine Files retirement state is damaged", {
|
|
55
|
+
cause: damaged,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (retiring !== null) {
|
|
59
|
+
throw new Error("task-machine Files retirement is pending");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function requestTaskFilesMachineRetirement(
|
|
64
|
+
request: TaskFilesMachineRetirementRequest,
|
|
65
|
+
): void {
|
|
66
|
+
if (damaged !== null) {
|
|
67
|
+
throw new Error("task-machine Files retirement state is damaged", {
|
|
68
|
+
cause: damaged,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!handler) {
|
|
72
|
+
throw new Error("task-machine Files retirement is not installed", {
|
|
73
|
+
cause: request.error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (retiring === null) {
|
|
77
|
+
// First exact failure wins before any callback: a reconnect/capability
|
|
78
|
+
// pass in the callback's first await can no longer advertise this machine.
|
|
79
|
+
retiring = request;
|
|
80
|
+
} else if (
|
|
81
|
+
retiring.assignment.taskId !== request.assignment.taskId ||
|
|
82
|
+
retiring.assignment.runEpoch !== request.assignment.runEpoch
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (handedOff) return;
|
|
87
|
+
// A synchronous handoff failure must be retryable without reopening the
|
|
88
|
+
// capability or letting a later assignment replace the first failure.
|
|
89
|
+
// Once this returns, the handler owns durable retries itself.
|
|
90
|
+
const result: unknown = handler(retiring);
|
|
91
|
+
if (result !== undefined) {
|
|
92
|
+
// A JavaScript caller can evade the type even though `undefined` (rather
|
|
93
|
+
// than `void`) rejects async handlers in TypeScript. Observe a returned
|
|
94
|
+
// promise so its later rejection is not unhandled, but never count an
|
|
95
|
+
// asynchronous callback as the required synchronous handoff.
|
|
96
|
+
if (
|
|
97
|
+
typeof result === "object" &&
|
|
98
|
+
result !== null &&
|
|
99
|
+
"then" in result &&
|
|
100
|
+
typeof (result as { then?: unknown }).then === "function"
|
|
101
|
+
) {
|
|
102
|
+
void Promise.resolve(result).catch(() => {});
|
|
103
|
+
}
|
|
104
|
+
throw new Error("task-machine Files retirement must be handed off synchronously");
|
|
105
|
+
}
|
|
106
|
+
handedOff = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Tests model a fresh machine process without weakening the production
|
|
110
|
+
* one-way latch. */
|
|
111
|
+
export function resetTaskFilesMachineRetirementForTests(): void {
|
|
112
|
+
handler = null;
|
|
113
|
+
retiring = null;
|
|
114
|
+
damaged = null;
|
|
115
|
+
handedOff = false;
|
|
116
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accessSync,
|
|
3
|
+
chmodSync,
|
|
4
|
+
constants,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
statfsSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
|
|
10
|
+
export const TASK_FILES_MOUNT_S3_PATH = "/usr/bin/mount-s3";
|
|
11
|
+
export const TASK_FILES_FUSE_PATH = "/dev/fuse";
|
|
12
|
+
export const TASK_FILES_HELPER_PATHS = [
|
|
13
|
+
"/usr/bin/python3",
|
|
14
|
+
"/usr/bin/env",
|
|
15
|
+
"/usr/bin/mountpoint",
|
|
16
|
+
"/usr/bin/umount",
|
|
17
|
+
"/bin/sh",
|
|
18
|
+
] as const;
|
|
19
|
+
export const TASK_FILES_RUNTIME_ROOT = "/run";
|
|
20
|
+
export const TASK_FILES_SESSION_ROOT = "/run/uai";
|
|
21
|
+
|
|
22
|
+
// linux/magic.h: credentials may be written only to a memory-backed runtime.
|
|
23
|
+
export const TMPFS_MAGIC = 0x01021994;
|
|
24
|
+
|
|
25
|
+
export type TaskFilesV1Readiness =
|
|
26
|
+
| { ready: true }
|
|
27
|
+
| {
|
|
28
|
+
ready: false;
|
|
29
|
+
reason:
|
|
30
|
+
| "not-task-machine"
|
|
31
|
+
| "not-root"
|
|
32
|
+
| "mount-s3-unavailable"
|
|
33
|
+
| "fuse-unavailable"
|
|
34
|
+
| "helper-unavailable"
|
|
35
|
+
| "run-not-tmpfs"
|
|
36
|
+
| "session-root-unsafe";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface TaskFilesV1ReadinessDeps {
|
|
40
|
+
getuid(): number;
|
|
41
|
+
access(path: string, mode: number): void;
|
|
42
|
+
prepareSessionRoot(): void;
|
|
43
|
+
statfs(path: string): { type: number | bigint };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function prepareSessionRoot(): void {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(TASK_FILES_SESSION_ROOT, { mode: 0o700 });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
51
|
+
}
|
|
52
|
+
const info = lstatSync(TASK_FILES_SESSION_ROOT);
|
|
53
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== 0) {
|
|
54
|
+
throw new Error("the Files session root is not a root-owned directory");
|
|
55
|
+
}
|
|
56
|
+
chmodSync(TASK_FILES_SESSION_ROOT, 0o700);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_DEPS: TaskFilesV1ReadinessDeps = {
|
|
60
|
+
getuid: () => process.getuid?.() ?? -1,
|
|
61
|
+
access: accessSync,
|
|
62
|
+
prepareSessionRoot,
|
|
63
|
+
statfs: (path) => statfsSync(path),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Local half of the Files-v1 capability check. Cloud mode is irrelevant: a
|
|
68
|
+
* machine advertises what this process can safely do before activation. Keep
|
|
69
|
+
* this synchronous so the connect capability frame cannot race a later
|
|
70
|
+
* prerequisite probe.
|
|
71
|
+
*/
|
|
72
|
+
export function taskFilesV1Readiness(
|
|
73
|
+
taskMachine: boolean,
|
|
74
|
+
deps: TaskFilesV1ReadinessDeps = DEFAULT_DEPS,
|
|
75
|
+
): TaskFilesV1Readiness {
|
|
76
|
+
if (!taskMachine) return { ready: false, reason: "not-task-machine" };
|
|
77
|
+
|
|
78
|
+
let uid: number;
|
|
79
|
+
try {
|
|
80
|
+
uid = deps.getuid();
|
|
81
|
+
} catch {
|
|
82
|
+
return { ready: false, reason: "not-root" };
|
|
83
|
+
}
|
|
84
|
+
if (uid !== 0) return { ready: false, reason: "not-root" };
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
deps.access(TASK_FILES_MOUNT_S3_PATH, constants.X_OK);
|
|
88
|
+
} catch {
|
|
89
|
+
return { ready: false, reason: "mount-s3-unavailable" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
deps.access(TASK_FILES_FUSE_PATH, constants.R_OK | constants.W_OK);
|
|
94
|
+
} catch {
|
|
95
|
+
return { ready: false, reason: "fuse-unavailable" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
for (const path of TASK_FILES_HELPER_PATHS) {
|
|
100
|
+
deps.access(path, constants.X_OK);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
return { ready: false, reason: "helper-unavailable" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (Number(deps.statfs(TASK_FILES_RUNTIME_ROOT).type) !== TMPFS_MAGIC) {
|
|
108
|
+
return { ready: false, reason: "run-not-tmpfs" };
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return { ready: false, reason: "run-not-tmpfs" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
deps.prepareSessionRoot();
|
|
116
|
+
// `/run` being tmpfs is insufficient: an existing `/run/uai` may be a
|
|
117
|
+
// bind mount onto persistent storage. Capability advertisement proves
|
|
118
|
+
// the exact directory in which the session will be written.
|
|
119
|
+
if (Number(deps.statfs(TASK_FILES_SESSION_ROOT).type) !== TMPFS_MAGIC) {
|
|
120
|
+
return { ready: false, reason: "session-root-unsafe" };
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
return { ready: false, reason: "session-root-unsafe" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { ready: true };
|
|
127
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port the runner's task gateway listens on (`UAI_MCP_GATEWAY_PORT`,
|
|
3
|
+
* 5877 by default): where task forwarders relay, and where a machine's
|
|
4
|
+
* sidecar serves its own task directly on loopback. A module of its own so
|
|
5
|
+
* the secrets proxy can name it without loading the gateway.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MCP_GATEWAY_PORT = 5877;
|
|
9
|
+
|
|
10
|
+
function gatewayPort(value: string | undefined): number {
|
|
11
|
+
if (value === undefined || !/^\d+$/.test(value)) {
|
|
12
|
+
return DEFAULT_MCP_GATEWAY_PORT;
|
|
13
|
+
}
|
|
14
|
+
const port = Number(value);
|
|
15
|
+
return Number.isSafeInteger(port) && port >= 1 && port <= 65_535
|
|
16
|
+
? port
|
|
17
|
+
: DEFAULT_MCP_GATEWAY_PORT;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MCP_GATEWAY_PORT = gatewayPort(
|
|
21
|
+
process.env.UAI_MCP_GATEWAY_PORT,
|
|
22
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHmac,
|
|
3
|
+
timingSafeEqual,
|
|
4
|
+
type KeyObject,
|
|
5
|
+
} from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Binary, versioned task-forwarder handshake (ADR-153).
|
|
9
|
+
*
|
|
10
|
+
* The leading NUL cannot be the first byte of either HTTP/1.1 or TLS. That
|
|
11
|
+
* lets the shared gateway reject direct TLS while keeping the existing MCP
|
|
12
|
+
* HTTP listener on the same port. Length-prefixed fields keep unauthenticated
|
|
13
|
+
* input bounded before a task identity has been proved.
|
|
14
|
+
*/
|
|
15
|
+
export const TASK_FORWARDER_PREAMBLE_MAGIC = Buffer.from([
|
|
16
|
+
0x00, 0x55, 0x41, 0x49, 0x46, 0x57, 0x44, 0x01,
|
|
17
|
+
]);
|
|
18
|
+
export const TASK_FORWARDER_CHALLENGE_MAGIC = Buffer.from("UAICHL01", "ascii");
|
|
19
|
+
export const TASK_FORWARDER_RESPONSE_MAGIC = Buffer.from("UAIRES01", "ascii");
|
|
20
|
+
export const TASK_FORWARDER_ACCEPT_MAGIC = Buffer.from("UAIOK001", "ascii");
|
|
21
|
+
|
|
22
|
+
export const TASK_FORWARDER_SECRET_BYTES = 32;
|
|
23
|
+
/** Numerical container uid reserved for the forwarder (neither root nor node). */
|
|
24
|
+
export const TASK_FORWARDER_UID = 65_532;
|
|
25
|
+
export const TASK_FORWARDER_NONCE_BYTES = 32;
|
|
26
|
+
export const TASK_FORWARDER_MAC_BYTES = 32;
|
|
27
|
+
export const TASK_FORWARDER_MAX_TASK_ID_BYTES = 128;
|
|
28
|
+
export const TASK_FORWARDER_MAX_GENERATION_BYTES = 64;
|
|
29
|
+
export const TASK_FORWARDER_PREAMBLE_HEADER_BYTES =
|
|
30
|
+
TASK_FORWARDER_PREAMBLE_MAGIC.length + 2;
|
|
31
|
+
export const TASK_FORWARDER_MAX_PREAMBLE_BYTES =
|
|
32
|
+
TASK_FORWARDER_PREAMBLE_HEADER_BYTES +
|
|
33
|
+
TASK_FORWARDER_MAX_TASK_ID_BYTES +
|
|
34
|
+
TASK_FORWARDER_MAX_GENERATION_BYTES;
|
|
35
|
+
export const TASK_FORWARDER_CHALLENGE_BYTES =
|
|
36
|
+
TASK_FORWARDER_CHALLENGE_MAGIC.length + TASK_FORWARDER_NONCE_BYTES;
|
|
37
|
+
export const TASK_FORWARDER_RESPONSE_BYTES =
|
|
38
|
+
TASK_FORWARDER_RESPONSE_MAGIC.length + TASK_FORWARDER_MAC_BYTES;
|
|
39
|
+
|
|
40
|
+
const FORWARDER_HMAC_DOMAIN = Buffer.from(
|
|
41
|
+
"uai-task-forwarder-v1\0",
|
|
42
|
+
"ascii",
|
|
43
|
+
);
|
|
44
|
+
const FIELD_SEPARATOR = Buffer.from([0]);
|
|
45
|
+
const FORWARDER_GENERATION = /^[A-Za-z0-9_-]{16,64}$/;
|
|
46
|
+
|
|
47
|
+
export interface TaskForwarderIdentity {
|
|
48
|
+
taskId: string;
|
|
49
|
+
generation: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function encodeTaskForwarderPreamble(
|
|
53
|
+
identity: TaskForwarderIdentity,
|
|
54
|
+
): Buffer {
|
|
55
|
+
const taskId = Buffer.from(identity.taskId, "utf8");
|
|
56
|
+
const generation = Buffer.from(identity.generation, "ascii");
|
|
57
|
+
if (
|
|
58
|
+
taskId.length < 1 ||
|
|
59
|
+
taskId.length > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
|
|
60
|
+
generation.length < 1 ||
|
|
61
|
+
generation.length > TASK_FORWARDER_MAX_GENERATION_BYTES ||
|
|
62
|
+
!FORWARDER_GENERATION.test(identity.generation)
|
|
63
|
+
) {
|
|
64
|
+
throw new Error("invalid task forwarder identity");
|
|
65
|
+
}
|
|
66
|
+
return Buffer.concat([
|
|
67
|
+
TASK_FORWARDER_PREAMBLE_MAGIC,
|
|
68
|
+
Buffer.from([taskId.length, generation.length]),
|
|
69
|
+
taskId,
|
|
70
|
+
generation,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function taskForwarderPreambleLength(header: Buffer): number | null {
|
|
75
|
+
if (
|
|
76
|
+
header.length !== TASK_FORWARDER_PREAMBLE_HEADER_BYTES ||
|
|
77
|
+
!header.subarray(0, TASK_FORWARDER_PREAMBLE_MAGIC.length).equals(
|
|
78
|
+
TASK_FORWARDER_PREAMBLE_MAGIC,
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
|
|
84
|
+
const generationBytes =
|
|
85
|
+
header[TASK_FORWARDER_PREAMBLE_MAGIC.length + 1] ?? 0;
|
|
86
|
+
if (
|
|
87
|
+
taskIdBytes < 1 ||
|
|
88
|
+
taskIdBytes > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
|
|
89
|
+
generationBytes < 1 ||
|
|
90
|
+
generationBytes > TASK_FORWARDER_MAX_GENERATION_BYTES
|
|
91
|
+
) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return TASK_FORWARDER_PREAMBLE_HEADER_BYTES + taskIdBytes + generationBytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function decodeTaskForwarderPreamble(
|
|
98
|
+
preamble: Buffer,
|
|
99
|
+
): TaskForwarderIdentity | null {
|
|
100
|
+
const header = preamble.subarray(0, TASK_FORWARDER_PREAMBLE_HEADER_BYTES);
|
|
101
|
+
const expectedLength = taskForwarderPreambleLength(header);
|
|
102
|
+
if (expectedLength === null || preamble.length !== expectedLength) return null;
|
|
103
|
+
|
|
104
|
+
const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
|
|
105
|
+
const taskStart = TASK_FORWARDER_PREAMBLE_HEADER_BYTES;
|
|
106
|
+
const generationStart = taskStart + taskIdBytes;
|
|
107
|
+
const rawTaskId = preamble.subarray(taskStart, generationStart);
|
|
108
|
+
const rawGeneration = preamble.subarray(generationStart);
|
|
109
|
+
const taskId = rawTaskId.toString("utf8");
|
|
110
|
+
const generation = rawGeneration.toString("ascii");
|
|
111
|
+
// Both Node decoders replace or mask malformed bytes. Require exact
|
|
112
|
+
// round-trips so two wire identities can never alias the same HMAC input.
|
|
113
|
+
if (
|
|
114
|
+
!Buffer.from(taskId, "utf8").equals(rawTaskId) ||
|
|
115
|
+
!Buffer.from(generation, "ascii").equals(rawGeneration) ||
|
|
116
|
+
!FORWARDER_GENERATION.test(generation)
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { taskId, generation };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function encodeTaskForwarderChallenge(nonce: Buffer): Buffer {
|
|
124
|
+
if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
|
|
125
|
+
throw new Error("invalid task forwarder nonce");
|
|
126
|
+
}
|
|
127
|
+
return Buffer.concat([TASK_FORWARDER_CHALLENGE_MAGIC, nonce]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function decodeTaskForwarderChallenge(
|
|
131
|
+
challenge: Buffer,
|
|
132
|
+
): Buffer | null {
|
|
133
|
+
if (
|
|
134
|
+
challenge.length !== TASK_FORWARDER_CHALLENGE_BYTES ||
|
|
135
|
+
!challenge.subarray(0, TASK_FORWARDER_CHALLENGE_MAGIC.length).equals(
|
|
136
|
+
TASK_FORWARDER_CHALLENGE_MAGIC,
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return Buffer.from(challenge.subarray(TASK_FORWARDER_CHALLENGE_MAGIC.length));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function taskForwarderMac(
|
|
145
|
+
secret: Buffer | KeyObject,
|
|
146
|
+
identity: TaskForwarderIdentity,
|
|
147
|
+
nonce: Buffer,
|
|
148
|
+
): Buffer {
|
|
149
|
+
if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
|
|
150
|
+
throw new Error("invalid task forwarder nonce");
|
|
151
|
+
}
|
|
152
|
+
return createHmac("sha256", secret)
|
|
153
|
+
.update(FORWARDER_HMAC_DOMAIN)
|
|
154
|
+
.update(identity.taskId, "utf8")
|
|
155
|
+
.update(FIELD_SEPARATOR)
|
|
156
|
+
.update(identity.generation, "ascii")
|
|
157
|
+
.update(FIELD_SEPARATOR)
|
|
158
|
+
.update(nonce)
|
|
159
|
+
.digest();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function encodeTaskForwarderResponse(mac: Buffer): Buffer {
|
|
163
|
+
if (mac.length !== TASK_FORWARDER_MAC_BYTES) {
|
|
164
|
+
throw new Error("invalid task forwarder response");
|
|
165
|
+
}
|
|
166
|
+
return Buffer.concat([TASK_FORWARDER_RESPONSE_MAGIC, mac]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function decodeTaskForwarderResponse(response: Buffer): Buffer | null {
|
|
170
|
+
if (
|
|
171
|
+
response.length !== TASK_FORWARDER_RESPONSE_BYTES ||
|
|
172
|
+
!response.subarray(0, TASK_FORWARDER_RESPONSE_MAGIC.length).equals(
|
|
173
|
+
TASK_FORWARDER_RESPONSE_MAGIC,
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return Buffer.from(response.subarray(TASK_FORWARDER_RESPONSE_MAGIC.length));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function verifyTaskForwarderMac(
|
|
182
|
+
expected: Buffer,
|
|
183
|
+
provided: Buffer,
|
|
184
|
+
): boolean {
|
|
185
|
+
return (
|
|
186
|
+
expected.length === TASK_FORWARDER_MAC_BYTES &&
|
|
187
|
+
provided.length === expected.length &&
|
|
188
|
+
timingSafeEqual(expected, provided)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activeLoaderHostTasks,
|
|
3
|
+
hostTaskProjectEnvSpec,
|
|
4
|
+
} from "./runtime-state";
|
|
5
|
+
import { taskEnvResolver } from "./task-env-source";
|
|
6
|
+
import { recoverTaskGatewayTask } from "./task-gateway-state";
|
|
7
|
+
|
|
8
|
+
export interface TaskGatewayRecoverySummary {
|
|
9
|
+
readonly recovered: number;
|
|
10
|
+
readonly failed: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reattach value resolvers before the gateway starts listening. Persisted
|
|
15
|
+
* credentials load inactive, so a malformed row or missing identity stays
|
|
16
|
+
* fail-closed and its surviving forwarder cannot authenticate.
|
|
17
|
+
*/
|
|
18
|
+
export function recoverActiveTaskGateways(): TaskGatewayRecoverySummary {
|
|
19
|
+
let recovered = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
for (const task of activeLoaderHostTasks()) {
|
|
22
|
+
try {
|
|
23
|
+
const spec = hostTaskProjectEnvSpec(task);
|
|
24
|
+
if (spec === null) {
|
|
25
|
+
throw new Error("loader task has no persisted project env spec");
|
|
26
|
+
}
|
|
27
|
+
// The generation serves the run its runtime was established for.
|
|
28
|
+
const runEpoch = task.runEpoch > 0 ? task.runEpoch : null;
|
|
29
|
+
const identity = recoverTaskGatewayTask(task.taskId, {
|
|
30
|
+
resolveEnv: taskEnvResolver(task.taskId, spec, runEpoch),
|
|
31
|
+
runEpoch,
|
|
32
|
+
});
|
|
33
|
+
if (identity === null) {
|
|
34
|
+
throw new Error("loader task has no valid persisted gateway identity");
|
|
35
|
+
}
|
|
36
|
+
recovered += 1;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
failed += 1;
|
|
39
|
+
console.warn(
|
|
40
|
+
`[task-gateway] task ${task.taskId}: boot recovery stayed closed: ${
|
|
41
|
+
error instanceof Error ? error.message : String(error)
|
|
42
|
+
}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { recovered, failed };
|
|
47
|
+
}
|