@moondeck/station 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/0031_first_turn_receipts.sql +9 -0
- package/db/migrations/meta/_journal.json +230 -0
- package/db/schema.ts +871 -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 +1470 -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 +937 -0
- package/lib/env.ts +195 -0
- package/lib/first-turn-receipts.ts +62 -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 +115 -8
- 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/bridge-diagnostics.ts +39 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2585 -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/enrollment-next-steps.ts +24 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2907 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6363 -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 +5550 -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/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectEnvMode,
|
|
3
|
+
TaskEnvironmentLocator,
|
|
4
|
+
TaskEnvironmentProvider,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Provider registry kept instance-based for tests and future multi-provider
|
|
9
|
+
* hosts. Registration is explicit; reconstructing an unknown/newer provider
|
|
10
|
+
* fails closed rather than guessing Docker.
|
|
11
|
+
*/
|
|
12
|
+
export class TaskEnvironmentRegistry<
|
|
13
|
+
TInput,
|
|
14
|
+
TCredentials,
|
|
15
|
+
TResult,
|
|
16
|
+
TTeardownResult = void,
|
|
17
|
+
> {
|
|
18
|
+
readonly #providers = new Map<
|
|
19
|
+
string,
|
|
20
|
+
TaskEnvironmentProvider<TInput, TCredentials, TResult, TTeardownResult>
|
|
21
|
+
>();
|
|
22
|
+
|
|
23
|
+
register(
|
|
24
|
+
provider: TaskEnvironmentProvider<
|
|
25
|
+
TInput,
|
|
26
|
+
TCredentials,
|
|
27
|
+
TResult,
|
|
28
|
+
TTeardownResult
|
|
29
|
+
>,
|
|
30
|
+
): () => void {
|
|
31
|
+
if (this.#providers.has(provider.kind)) {
|
|
32
|
+
throw new Error(`task environment provider ${provider.kind} is already registered`);
|
|
33
|
+
}
|
|
34
|
+
this.#providers.set(provider.kind, provider);
|
|
35
|
+
return () => {
|
|
36
|
+
if (this.#providers.get(provider.kind) === provider) {
|
|
37
|
+
this.#providers.delete(provider.kind);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
provider(
|
|
43
|
+
kind: string,
|
|
44
|
+
): TaskEnvironmentProvider<TInput, TCredentials, TResult, TTeardownResult> {
|
|
45
|
+
const provider = this.#providers.get(kind);
|
|
46
|
+
if (!provider) {
|
|
47
|
+
throw new Error(`task environment provider ${kind} is unavailable`);
|
|
48
|
+
}
|
|
49
|
+
return provider;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
reconstruct(locator: TaskEnvironmentLocator, envMode: ProjectEnvMode = "baked") {
|
|
53
|
+
return this.provider(locator.provider).reconstruct(locator, envMode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
kinds(): string[] {
|
|
57
|
+
return [...this.#providers.keys()].sort();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `self` provider's fixed names: a Uai Cloud machine's own task, run by
|
|
3
|
+
* the sidecar on that machine (ADR-133). A module with no imports, so the
|
|
4
|
+
* secrets proxy can name them without loading the provider, which reaches it
|
|
5
|
+
* back through the agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const SELF_TASK_ENVIRONMENT_PROVIDER = "self";
|
|
9
|
+
|
|
10
|
+
/** The task user every agent, editor, and shell runs as. */
|
|
11
|
+
export const SELF_TASK_USER = "node";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A task process's own view of the hosts on its machine (ADR-153): the
|
|
15
|
+
* machine's /etc/hosts plus the secrets proxy's pins, which the root
|
|
16
|
+
* sidecar's own processes must never resolve. Every process the sidecar
|
|
17
|
+
* starts as the task user gets it bind-mounted over /etc/hosts, in a mount
|
|
18
|
+
* namespace of its own that still follows the machine's mounts.
|
|
19
|
+
*/
|
|
20
|
+
export const TASK_HOSTS_VIEW_PATH = "/run/uai/task-hosts";
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-133 sidecar mode: the task environment IS this machine.
|
|
3
|
+
*
|
|
4
|
+
* The runner runs on the task's own EC2 machine (`uai-host run --task`), so
|
|
5
|
+
* there is no container to exec into and no ssh hop to make: every request
|
|
6
|
+
* runs as a local child process, as user `node` (the runner itself is root
|
|
7
|
+
* so it can drop privileges, mount S3, and own the workspace), in the same
|
|
8
|
+
* shape the machine provider sends over ssh. `machine-task-up.ts` (clone,
|
|
9
|
+
* editor, browser) and `agents/machine-durable.ts` (durable agent sessions)
|
|
10
|
+
* take a handle, not a transport, so they run unchanged on top of this one.
|
|
11
|
+
*
|
|
12
|
+
* What is deliberately NOT here: launch/stop/terminate. The cloud owns the
|
|
13
|
+
* machine's lifecycle (hostless-implementation.md §7); `stop` and `teardown`
|
|
14
|
+
* are no-ops because a parked or closed task ends with the cloud stopping or
|
|
15
|
+
* terminating the instance this process lives on.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
import { buildRemoteCommand } from "../machine-exec";
|
|
21
|
+
import {
|
|
22
|
+
applyProjectEnvProfile,
|
|
23
|
+
PROJECT_ENV_PROFILE_ENV_NAMES,
|
|
24
|
+
} from "../runtime-authority";
|
|
25
|
+
import { capturedCliExec, streamingCliExec } from "./docker";
|
|
26
|
+
import type { MachineTaskInput } from "./machine";
|
|
27
|
+
import {
|
|
28
|
+
assertTaskEnvironmentExecRequest,
|
|
29
|
+
assertTaskEnvironmentProcessRequest,
|
|
30
|
+
refusePrivilegedExec,
|
|
31
|
+
assertTaskEnvironmentSessionRequest,
|
|
32
|
+
assertWorkspaceScopedPath,
|
|
33
|
+
type TaskEnvironmentCallOptions,
|
|
34
|
+
type TaskEnvironmentReadOptions,
|
|
35
|
+
type TaskEnvironmentCopyRequest,
|
|
36
|
+
type TaskEnvironmentDescriptor,
|
|
37
|
+
type TaskEnvironmentDetachedSessionRequest,
|
|
38
|
+
type TaskEnvironmentExecRequest,
|
|
39
|
+
type TaskEnvironmentExecResult,
|
|
40
|
+
type TaskEnvironmentHandle,
|
|
41
|
+
type TaskEnvironmentLocator,
|
|
42
|
+
type TaskEnvironmentPort,
|
|
43
|
+
type TaskEnvironmentProcess,
|
|
44
|
+
type ProjectEnvMode,
|
|
45
|
+
type TaskEnvironmentProvider,
|
|
46
|
+
type TaskEnvironmentProvisionRequest,
|
|
47
|
+
type TaskEnvironmentProvisioned,
|
|
48
|
+
type TaskEnvironmentRecoveryContext,
|
|
49
|
+
type TaskEnvironmentRecoveryResult,
|
|
50
|
+
type TaskEnvironmentSessionRequest,
|
|
51
|
+
type TaskEnvironmentSpawnRequest,
|
|
52
|
+
type TaskEnvironmentStatus,
|
|
53
|
+
TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
|
|
54
|
+
} from "./types";
|
|
55
|
+
import {
|
|
56
|
+
BASELINE_PATH,
|
|
57
|
+
ETC_ENVIRONMENT_PATH,
|
|
58
|
+
parseEtcEnvironment,
|
|
59
|
+
withoutEngineCredentials,
|
|
60
|
+
withoutLaunchSecrets,
|
|
61
|
+
} from "./machine-environment";
|
|
62
|
+
import { SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH } from "./self-identity";
|
|
63
|
+
import { readBoundedFile, workspacePathParts } from "./workspace-files";
|
|
64
|
+
import { assertTaskFilesMachineProcessAllowed } from "../task-files-machine-retirement";
|
|
65
|
+
|
|
66
|
+
export { ETC_ENVIRONMENT_PATH, parseEtcEnvironment, SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A terminal Files failure latches before its synchronous handler runs. Check
|
|
70
|
+
* on both sides of a caller's own admission callback so that callback cannot
|
|
71
|
+
* trigger retirement and then let this launch repopulate the cgroup the
|
|
72
|
+
* handler just emptied. JavaScript runs the final check and node spawn in one
|
|
73
|
+
* turn, so no other callback can interleave between them.
|
|
74
|
+
*/
|
|
75
|
+
function admitTaskMachineCall(admit?: () => void): void {
|
|
76
|
+
assertTaskFilesMachineProcessAllowed();
|
|
77
|
+
admit?.();
|
|
78
|
+
assertTaskFilesMachineProcessAllowed();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Binds the view, then runs the rest of its argv. With no view yet (nothing
|
|
83
|
+
* pinned so far: a start whose CA failed, the proxy off), it first makes one,
|
|
84
|
+
* a copy of the machine's file, so every task process has the view bound
|
|
85
|
+
* from its start and a later pin, rewritten into it in place, reaches them
|
|
86
|
+
* all. Made whole under another name and linked in, so it never shows half
|
|
87
|
+
* written and never replaces one made meanwhile. A view that cannot be
|
|
88
|
+
* bound fails the start: the task never runs with the proxy half in place.
|
|
89
|
+
*/
|
|
90
|
+
const TASK_VIEW_SCRIPT = [
|
|
91
|
+
"PATH=/usr/sbin:/usr/bin:/sbin:/bin; view=$1; shift",
|
|
92
|
+
'if [ ! -f "$view" ]; then',
|
|
93
|
+
' umask 022; dir=${view%/*}; mkdir -p "$dir"',
|
|
94
|
+
' tmp=$(mktemp "$dir/.task-hosts.XXXXXX") || exit 126',
|
|
95
|
+
' cat /etc/hosts > "$tmp" && chmod 0644 "$tmp" && ln "$tmp" "$view" 2>/dev/null; rm -f "$tmp"',
|
|
96
|
+
"fi",
|
|
97
|
+
'mount --bind "$view" /etc/hosts || exit 126',
|
|
98
|
+
'exec "$@"',
|
|
99
|
+
].join("\n");
|
|
100
|
+
|
|
101
|
+
const SAFE_TASK_ID = /^[a-z0-9]{1,32}$/i;
|
|
102
|
+
|
|
103
|
+
export interface SelfTaskEnvironmentLocatorValue {
|
|
104
|
+
taskId: string;
|
|
105
|
+
/** Absolute workspace path on this machine. */
|
|
106
|
+
workspacePath: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SelfTaskEnvironmentDeps {
|
|
110
|
+
spawn?: typeof nodeSpawn;
|
|
111
|
+
/** Whether this process may drop privileges with setpriv (root). Tests and
|
|
112
|
+
* a non-root dev run keep everything as the current user. */
|
|
113
|
+
privileged?: () => boolean;
|
|
114
|
+
/** The machine-wide environment every request runs under (beneath its own
|
|
115
|
+
* `env`). Defaults to /etc/environment — see `readEtcEnvironment`. */
|
|
116
|
+
etcEnvironment?: () => Readonly<Record<string, string>>;
|
|
117
|
+
/** Tests only: start an anchored launch without the drop to the task
|
|
118
|
+
* user, so the supervisor and what it carries are the test user's own. No
|
|
119
|
+
* isolation boundary: never a runner's (ADR-154 §6). */
|
|
120
|
+
anchorWithoutIsolationForTests?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* /etc/environment (parseEtcEnvironment). The machine provider gets it for
|
|
125
|
+
* free — sshd applies pam_env to every session — but a local
|
|
126
|
+
* `setpriv --reset-env … sh -lc` sees neither pam nor the file, so
|
|
127
|
+
* `PLAYWRIGHT_BROWSERS_PATH` from the payload image never reached an agent
|
|
128
|
+
* on the sidecar and every task re-downloaded Chromium (2026-09-10, smoke 6).
|
|
129
|
+
*/
|
|
130
|
+
let etcEnvironmentCache: Record<string, string> | null = null;
|
|
131
|
+
/** The parsed /etc/environment, read once per process (the image writes it
|
|
132
|
+
* at bake time; nothing edits it on a task machine). Empty when absent. */
|
|
133
|
+
export function readEtcEnvironment(): Record<string, string> {
|
|
134
|
+
if (etcEnvironmentCache) return etcEnvironmentCache;
|
|
135
|
+
try {
|
|
136
|
+
etcEnvironmentCache = parseEtcEnvironment(readFileSync(ETC_ENVIRONMENT_PATH, "utf8"));
|
|
137
|
+
} catch {
|
|
138
|
+
etcEnvironmentCache = {};
|
|
139
|
+
}
|
|
140
|
+
return etcEnvironmentCache;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function selfTaskEnvironmentLocator(
|
|
144
|
+
value: SelfTaskEnvironmentLocatorValue,
|
|
145
|
+
): TaskEnvironmentLocator {
|
|
146
|
+
return {
|
|
147
|
+
schemaVersion: TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
|
|
148
|
+
provider: SELF_TASK_ENVIRONMENT_PROVIDER,
|
|
149
|
+
value: { taskId: value.taskId, workspacePath: value.workspacePath },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function parseSelfTaskEnvironmentLocator(
|
|
154
|
+
locator: TaskEnvironmentLocator,
|
|
155
|
+
): SelfTaskEnvironmentLocatorValue {
|
|
156
|
+
if (
|
|
157
|
+
locator.schemaVersion !== TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION ||
|
|
158
|
+
locator.provider !== SELF_TASK_ENVIRONMENT_PROVIDER ||
|
|
159
|
+
typeof locator.value !== "object" ||
|
|
160
|
+
locator.value === null
|
|
161
|
+
) {
|
|
162
|
+
throw new Error("not a self task environment locator");
|
|
163
|
+
}
|
|
164
|
+
const value = locator.value as Record<string, unknown>;
|
|
165
|
+
if (
|
|
166
|
+
typeof value.taskId !== "string" ||
|
|
167
|
+
!SAFE_TASK_ID.test(value.taskId) ||
|
|
168
|
+
typeof value.workspacePath !== "string" ||
|
|
169
|
+
!value.workspacePath.startsWith("/") ||
|
|
170
|
+
value.workspacePath.includes("\0")
|
|
171
|
+
) {
|
|
172
|
+
throw new Error("malformed self task environment locator");
|
|
173
|
+
}
|
|
174
|
+
return { taskId: value.taskId, workspacePath: value.workspacePath };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The local command for a request: the same `cd … && exec env … argv` shell
|
|
179
|
+
* line the machine provider ships over ssh, run through setpriv when this
|
|
180
|
+
* process is root and the request wants a task user. Values ride argv here
|
|
181
|
+
* exactly as they ride the ssh command line on a machine task — the machine
|
|
182
|
+
* is one task's trust domain (machine-exec.ts) — except a leased engine's
|
|
183
|
+
* credentials, which its admission carries to the injector this line runs
|
|
184
|
+
* after its drop (ADR-154 §6).
|
|
185
|
+
*/
|
|
186
|
+
export function selfCommand(
|
|
187
|
+
request: {
|
|
188
|
+
argv: readonly string[];
|
|
189
|
+
cwd?: string;
|
|
190
|
+
user?: string;
|
|
191
|
+
inheritEnv?: readonly string[];
|
|
192
|
+
env?: Readonly<Record<string, string>>;
|
|
193
|
+
anchor?: { readonly prefix: readonly [string, ...string[]]; readonly injector?: readonly string[] };
|
|
194
|
+
},
|
|
195
|
+
mode: "interactive" | "detached",
|
|
196
|
+
privileged: boolean,
|
|
197
|
+
): { command: string; args: string[] } {
|
|
198
|
+
const line = buildRemoteCommand({ ...request, detached: mode === "detached" });
|
|
199
|
+
const outerProfileEnv = PROJECT_ENV_PROFILE_ENV_NAMES.flatMap((name) => {
|
|
200
|
+
const value = request.env?.[name];
|
|
201
|
+
return value === undefined ? [] : [`${name}=${value}`];
|
|
202
|
+
});
|
|
203
|
+
const shellCommand =
|
|
204
|
+
outerProfileEnv.length === 0
|
|
205
|
+
? { command: "/bin/sh", args: ["-lc", line] }
|
|
206
|
+
: {
|
|
207
|
+
command: "/usr/bin/env",
|
|
208
|
+
args: [...outerProfileEnv, "/bin/sh", "-lc", line],
|
|
209
|
+
};
|
|
210
|
+
const user = request.user ?? SELF_TASK_USER;
|
|
211
|
+
// ADR-154 §6: an anchored process starts under the engine supervisor, as
|
|
212
|
+
// root inside the task's view, which then runs the drop to the task user
|
|
213
|
+
// itself ("-": setpriv drops it). What the launch carries is read by the
|
|
214
|
+
// injector right after that drop, into the environment setpriv reset.
|
|
215
|
+
const anchor = request.anchor ? [...request.anchor.prefix, "-", "--"] : [];
|
|
216
|
+
const injector = request.anchor?.injector ?? [];
|
|
217
|
+
if (request.anchor && user === "root") {
|
|
218
|
+
throw new Error("an anchored process runs as a task user, never as root");
|
|
219
|
+
}
|
|
220
|
+
if (privileged && user !== "root") {
|
|
221
|
+
return {
|
|
222
|
+
command: "/usr/bin/unshare",
|
|
223
|
+
args: [
|
|
224
|
+
"--mount",
|
|
225
|
+
"--propagation",
|
|
226
|
+
"slave",
|
|
227
|
+
"--",
|
|
228
|
+
"/bin/sh",
|
|
229
|
+
"-c",
|
|
230
|
+
TASK_VIEW_SCRIPT,
|
|
231
|
+
"uai-task-view",
|
|
232
|
+
TASK_HOSTS_VIEW_PATH,
|
|
233
|
+
...anchor,
|
|
234
|
+
"/usr/bin/setpriv",
|
|
235
|
+
`--reuid=${user}`,
|
|
236
|
+
`--regid=${user}`,
|
|
237
|
+
"--init-groups",
|
|
238
|
+
"--reset-env",
|
|
239
|
+
"--",
|
|
240
|
+
...injector,
|
|
241
|
+
shellCommand.command,
|
|
242
|
+
...shellCommand.args,
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (anchor.length > 0) {
|
|
247
|
+
return {
|
|
248
|
+
command: anchor[0]!,
|
|
249
|
+
args: [...anchor.slice(1), ...injector, shellCommand.command, ...shellCommand.args],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return shellCommand;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* ADR-154 §6: what an anchored launch starts from, instead of this runner's
|
|
257
|
+
* own environment (its host credential, its AWS keys): neither the engine
|
|
258
|
+
* supervisor nor the engine it starts inherits anything of the runner's.
|
|
259
|
+
* The drop resets it again for the task user; the launch's own line and
|
|
260
|
+
* login shell give the engine the rest.
|
|
261
|
+
*/
|
|
262
|
+
const ANCHORED_LAUNCH_ENV: NodeJS.ProcessEnv = Object.freeze({ PATH: BASELINE_PATH });
|
|
263
|
+
|
|
264
|
+
class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
265
|
+
readonly descriptor: TaskEnvironmentDescriptor;
|
|
266
|
+
readonly durableIdentity: string;
|
|
267
|
+
/** ADR-154 §6: a request's environment rides a local shell line, so a
|
|
268
|
+
* leased launch's engine credentials travel in its admission. */
|
|
269
|
+
readonly credentialDelivery = "admission";
|
|
270
|
+
readonly #value: SelfTaskEnvironmentLocatorValue;
|
|
271
|
+
readonly #spawn: typeof nodeSpawn;
|
|
272
|
+
readonly #privileged: () => boolean;
|
|
273
|
+
readonly #etcEnvironment: () => Readonly<Record<string, string>>;
|
|
274
|
+
readonly #anchorWithoutIsolation: boolean;
|
|
275
|
+
|
|
276
|
+
constructor(
|
|
277
|
+
locator: TaskEnvironmentLocator,
|
|
278
|
+
deps: SelfTaskEnvironmentDeps,
|
|
279
|
+
envMode: ProjectEnvMode,
|
|
280
|
+
) {
|
|
281
|
+
this.#value = parseSelfTaskEnvironmentLocator(locator);
|
|
282
|
+
this.#spawn = deps.spawn ?? nodeSpawn;
|
|
283
|
+
this.#privileged =
|
|
284
|
+
deps.privileged ?? (() => typeof process.getuid === "function" && process.getuid() === 0);
|
|
285
|
+
this.#etcEnvironment = deps.etcEnvironment ?? readEtcEnvironment;
|
|
286
|
+
this.#anchorWithoutIsolation = deps.anchorWithoutIsolationForTests === true;
|
|
287
|
+
this.durableIdentity = `self:${this.#value.taskId}`;
|
|
288
|
+
this.descriptor = {
|
|
289
|
+
taskId: this.#value.taskId,
|
|
290
|
+
locator,
|
|
291
|
+
envMode,
|
|
292
|
+
workspacePath: this.#value.workspacePath,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async readWorkspaceFile(path: string, options?: TaskEnvironmentReadOptions): Promise<Uint8Array | null> {
|
|
297
|
+
assertWorkspaceScopedPath(this.descriptor, path);
|
|
298
|
+
admitTaskMachineCall(options?.admit);
|
|
299
|
+
return readBoundedFile(this.descriptor.workspacePath, workspacePathParts(this.descriptor, path), options);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async writeWorkspaceFile(path: string, bytes: Uint8Array, options?: TaskEnvironmentCallOptions): Promise<void> {
|
|
303
|
+
assertWorkspaceScopedPath(this.descriptor, path);
|
|
304
|
+
// Through the task user, so the file is owned like everything else the
|
|
305
|
+
// agents touch (a root-owned file inside the workspace is a trap).
|
|
306
|
+
const result = await this.exec({
|
|
307
|
+
argv: ["/bin/sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1"', "write", path],
|
|
308
|
+
projectEnvProfile: "maintenance",
|
|
309
|
+
stdin: bytes,
|
|
310
|
+
timeoutMs: 120_000,
|
|
311
|
+
maxOutputBytes: 1024 * 1024,
|
|
312
|
+
...(options?.admit ? { admit: options.admit } : {}),
|
|
313
|
+
});
|
|
314
|
+
if (result.exitCode !== 0) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`workspace write failed: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** ADR-154 §6: anchored requests start under the engine supervisor, only
|
|
322
|
+
* where it runs as root and the engine as the task user: otherwise the
|
|
323
|
+
* supervisor, and what a launch carries, would be the task user's own. */
|
|
324
|
+
get anchors(): boolean {
|
|
325
|
+
return this.#privileged() || this.#anchorWithoutIsolation;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The command for `request`, and the environment it starts with: an
|
|
329
|
+
* anchored one starts from the explicit baseline, never this runner's,
|
|
330
|
+
* and only where the supervisor is isolated from the task user. */
|
|
331
|
+
#command(
|
|
332
|
+
request: Parameters<typeof selfCommand>[0],
|
|
333
|
+
mode: "interactive" | "detached",
|
|
334
|
+
): { command: string; args: string[]; env?: NodeJS.ProcessEnv } {
|
|
335
|
+
if (request.anchor && !this.anchors) {
|
|
336
|
+
throw new Error("an anchored launch needs the drop to the task user, as root");
|
|
337
|
+
}
|
|
338
|
+
const { command, args } = selfCommand(request, mode, this.#privileged());
|
|
339
|
+
return request.anchor ? { command, args, env: ANCHORED_LAUNCH_ENV } : { command, args };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The request with the machine-wide environment beneath its own: what an
|
|
343
|
+
* ssh session on a machine task gets from pam_env, applied here by hand,
|
|
344
|
+
* never an engine's credential (ADR-154 §6), and for an anchored launch,
|
|
345
|
+
* whose secrets come from its admission, no secret of the machine's at
|
|
346
|
+
* all: its environment rides the local command line. */
|
|
347
|
+
#withMachineEnv<
|
|
348
|
+
T extends {
|
|
349
|
+
env?: Readonly<Record<string, string>>;
|
|
350
|
+
projectEnvProfile: "maintenance" | "task";
|
|
351
|
+
anchor?: unknown;
|
|
352
|
+
},
|
|
353
|
+
>(request: T): T {
|
|
354
|
+
const etc = this.#etcEnvironment();
|
|
355
|
+
const base = request.anchor ? withoutLaunchSecrets(etc) : withoutEngineCredentials(etc);
|
|
356
|
+
const withMachineEnv =
|
|
357
|
+
Object.keys(base).length === 0
|
|
358
|
+
? request
|
|
359
|
+
: { ...request, env: { ...base, ...request.env } };
|
|
360
|
+
return applyProjectEnvProfile(
|
|
361
|
+
withMachineEnv,
|
|
362
|
+
this.descriptor.envMode ?? "baked",
|
|
363
|
+
"/bin/bash",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
|
|
368
|
+
assertTaskEnvironmentExecRequest(request);
|
|
369
|
+
refusePrivilegedExec(request);
|
|
370
|
+
const scoped = this.#withMachineEnv(request);
|
|
371
|
+
const { command, args, env } = this.#command(scoped, "interactive");
|
|
372
|
+
admitTaskMachineCall(request.admit);
|
|
373
|
+
return capturedCliExec(this.#spawn, command, args, scoped, env);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async spawn(request: TaskEnvironmentSpawnRequest): Promise<TaskEnvironmentProcess> {
|
|
377
|
+
assertTaskEnvironmentProcessRequest(request);
|
|
378
|
+
const scoped = this.#withMachineEnv(request);
|
|
379
|
+
const { command, args, env } = this.#command(scoped, "interactive");
|
|
380
|
+
admitTaskMachineCall(request.admit);
|
|
381
|
+
return streamingCliExec(this.#spawn, command, args, scoped, env);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async spawnSession(request: TaskEnvironmentSessionRequest): Promise<TaskEnvironmentProcess> {
|
|
385
|
+
assertTaskEnvironmentSessionRequest(request);
|
|
386
|
+
const session = this.#withMachineEnv({
|
|
387
|
+
...request,
|
|
388
|
+
cwd: request.cwd ?? this.#value.workspacePath,
|
|
389
|
+
});
|
|
390
|
+
const { command, args, env } = this.#command(session, "interactive");
|
|
391
|
+
admitTaskMachineCall(request.admit);
|
|
392
|
+
return streamingCliExec(this.#spawn, command, args, session, env);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async launchDetachedSession(
|
|
396
|
+
request: TaskEnvironmentDetachedSessionRequest,
|
|
397
|
+
): Promise<TaskEnvironmentExecResult> {
|
|
398
|
+
assertTaskEnvironmentSessionRequest(request);
|
|
399
|
+
const session = this.#withMachineEnv({
|
|
400
|
+
...request,
|
|
401
|
+
cwd: request.cwd ?? this.#value.workspacePath,
|
|
402
|
+
});
|
|
403
|
+
const { command, args, env } = this.#command(session, "detached");
|
|
404
|
+
admitTaskMachineCall(request.admit);
|
|
405
|
+
return capturedCliExec(
|
|
406
|
+
this.#spawn,
|
|
407
|
+
command,
|
|
408
|
+
args,
|
|
409
|
+
{
|
|
410
|
+
...session,
|
|
411
|
+
timeoutMs: session.launchTimeoutMs,
|
|
412
|
+
},
|
|
413
|
+
env,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async copy(request: TaskEnvironmentCopyRequest): Promise<void> {
|
|
418
|
+
if (
|
|
419
|
+
!request.source.startsWith("/") ||
|
|
420
|
+
!request.destination.startsWith("/") ||
|
|
421
|
+
request.source.includes("\0") ||
|
|
422
|
+
request.destination.includes("\0")
|
|
423
|
+
) {
|
|
424
|
+
throw new Error("self copy paths must be absolute and NUL-free");
|
|
425
|
+
}
|
|
426
|
+
// Both sides are this machine. A directory source lands as the
|
|
427
|
+
// destination's CONTENTS, the way the machine provider's tar stream
|
|
428
|
+
// does: engine-account provisioning pre-creates the destination, and a
|
|
429
|
+
// plain `cp -a dir dest` into an existing dest nests `dest/dir/…`, so
|
|
430
|
+
// CODEX_HOME / GROK_HOME pointed at a dir without auth.json and both
|
|
431
|
+
// engines ran signed out and silent (live 2026-09-10, first codex + grok
|
|
432
|
+
// agents on a task machine). A file source copies as before. The result
|
|
433
|
+
// is chowned to the task user when this process is root, so the agents
|
|
434
|
+
// can read what was copied in for them.
|
|
435
|
+
const copy =
|
|
436
|
+
'if [ -d "$1" ]; then mkdir -p "$2" && cp -a "$1/." "$2/"; ' +
|
|
437
|
+
'else mkdir -p "$(dirname "$2")" && cp -a "$1" "$2"; fi';
|
|
438
|
+
const script =
|
|
439
|
+
request.direction === "into" && this.#privileged()
|
|
440
|
+
? `${copy} && chown -R ${SELF_TASK_USER}:${SELF_TASK_USER} "$2"`
|
|
441
|
+
: copy;
|
|
442
|
+
const result = await this.exec({
|
|
443
|
+
argv: ["/bin/sh", "-c", script, "copy", request.source, request.destination],
|
|
444
|
+
user: "root",
|
|
445
|
+
projectEnvProfile: "maintenance",
|
|
446
|
+
timeoutMs: 5 * 60_000,
|
|
447
|
+
maxOutputBytes: 64 * 1024,
|
|
448
|
+
...(request.admit ? { admit: request.admit } : {}),
|
|
449
|
+
});
|
|
450
|
+
if (result.exitCode !== 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`self copy failed: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async ports(): Promise<TaskEnvironmentPort[]> {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Every tunnel dials loopback: the editor and preview servers listen on
|
|
462
|
+
* this machine, and the runner that answers the tunnel is this process. */
|
|
463
|
+
async inspectRoute(): Promise<{ kind: "running"; ipv4Address: string }> {
|
|
464
|
+
return { kind: "running", ipv4Address: "127.0.0.1" };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async status(): Promise<TaskEnvironmentStatus> {
|
|
468
|
+
return {
|
|
469
|
+
state: "running",
|
|
470
|
+
instances: [this.durableIdentity],
|
|
471
|
+
workspacePresent: existsSync(this.#value.workspacePath),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async recover(
|
|
476
|
+
context: TaskEnvironmentRecoveryContext,
|
|
477
|
+
): Promise<TaskEnvironmentRecoveryResult> {
|
|
478
|
+
await context.maintenanceReady;
|
|
479
|
+
if (!context.isCurrent()) {
|
|
480
|
+
return { outcome: "deferred", detail: "runtime generation superseded" };
|
|
481
|
+
}
|
|
482
|
+
await context.prepareConsumers?.();
|
|
483
|
+
context.allowConsumers?.();
|
|
484
|
+
await context.ensureConsumersStarted?.();
|
|
485
|
+
if (!context.isCurrent()) {
|
|
486
|
+
await context.quarantineConsumers?.();
|
|
487
|
+
return { outcome: "deferred", detail: "runtime generation superseded" };
|
|
488
|
+
}
|
|
489
|
+
return { outcome: "recovered", status: await this.status() };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Parking is the cloud stopping this instance; nothing to do locally. */
|
|
493
|
+
async stop(): Promise<void> {}
|
|
494
|
+
|
|
495
|
+
/** Closing is the cloud terminating this instance; the workspace dies
|
|
496
|
+
* with it. Nothing is deleted here so a close that races a late read (a
|
|
497
|
+
* final diff, a transcript flush) still finds the files. */
|
|
498
|
+
async teardown(): Promise<void> {}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function createSelfTaskEnvironmentProvider(
|
|
502
|
+
deps: SelfTaskEnvironmentDeps = {},
|
|
503
|
+
): TaskEnvironmentProvider<MachineTaskInput, void, TaskEnvironmentDescriptor> {
|
|
504
|
+
return {
|
|
505
|
+
kind: SELF_TASK_ENVIRONMENT_PROVIDER,
|
|
506
|
+
|
|
507
|
+
// Takes the machine input shape so `createMachineHostTaskEnvironmentProvider`
|
|
508
|
+
// can sit on top unchanged; image/cpus/memory describe a machine the cloud
|
|
509
|
+
// already launched and are ignored here.
|
|
510
|
+
async provision(
|
|
511
|
+
request: TaskEnvironmentProvisionRequest<MachineTaskInput, void>,
|
|
512
|
+
): Promise<TaskEnvironmentProvisioned<TaskEnvironmentDescriptor>> {
|
|
513
|
+
const { taskId, input } = request;
|
|
514
|
+
if (!SAFE_TASK_ID.test(taskId)) {
|
|
515
|
+
throw new Error("self provisioning refused an unsafe task id");
|
|
516
|
+
}
|
|
517
|
+
const workspacePath = input.workspacePath ?? "/workspace";
|
|
518
|
+
const locator = selfTaskEnvironmentLocator({ taskId, workspacePath });
|
|
519
|
+
await request.onPrepared?.(locator);
|
|
520
|
+
const handle = new SelfTaskEnvironmentHandle(
|
|
521
|
+
locator,
|
|
522
|
+
deps,
|
|
523
|
+
request.envMode ?? "loader",
|
|
524
|
+
);
|
|
525
|
+
const prepare = await handle.exec({
|
|
526
|
+
argv: ["/bin/sh", "-c", 'mkdir -p "$1" && chown node:node "$1"', "prepare", workspacePath],
|
|
527
|
+
user: "root",
|
|
528
|
+
projectEnvProfile: "maintenance",
|
|
529
|
+
timeoutMs: 30_000,
|
|
530
|
+
maxOutputBytes: 64 * 1024,
|
|
531
|
+
});
|
|
532
|
+
if (prepare.exitCode !== 0) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`workspace preparation failed: ${Buffer.from(prepare.stderr).toString("utf8").trim()}`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (input.repo) {
|
|
538
|
+
const clone = await handle.exec({
|
|
539
|
+
argv: [
|
|
540
|
+
"/usr/bin/git",
|
|
541
|
+
"clone",
|
|
542
|
+
...(input.repo.branch ? ["--branch", input.repo.branch] : []),
|
|
543
|
+
input.repo.url,
|
|
544
|
+
workspacePath,
|
|
545
|
+
],
|
|
546
|
+
projectEnvProfile: "maintenance",
|
|
547
|
+
timeoutMs: 10 * 60_000,
|
|
548
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
549
|
+
});
|
|
550
|
+
if (clone.exitCode !== 0) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`clone failed: ${Buffer.from(clone.stderr).toString("utf8").trim()}`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return { handle, result: handle.descriptor };
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
async reconstruct(
|
|
560
|
+
locator: TaskEnvironmentLocator,
|
|
561
|
+
envMode: ProjectEnvMode = "baked",
|
|
562
|
+
): Promise<TaskEnvironmentHandle> {
|
|
563
|
+
return new SelfTaskEnvironmentHandle(locator, deps, envMode);
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|