@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,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-level task boundary (ADR-101 Phase C).
|
|
3
|
+
*
|
|
4
|
+
* Providers own machine/container details. Generic orchestration receives an
|
|
5
|
+
* opaque, durable locator and a handle with bounded argv-based operations; it
|
|
6
|
+
* never receives a Docker socket, Compose project, or generic remote shell.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION = 1 as const;
|
|
10
|
+
|
|
11
|
+
/** Durable rollout generation for project environment delivery (ADR-153). */
|
|
12
|
+
export type ProjectEnvMode = "baked" | "loader";
|
|
13
|
+
|
|
14
|
+
/** Every process launch is explicitly classified at the type boundary. */
|
|
15
|
+
export type ProjectEnvProfile = "maintenance" | "task";
|
|
16
|
+
|
|
17
|
+
export interface TaskEnvironmentLocator {
|
|
18
|
+
schemaVersion: typeof TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION;
|
|
19
|
+
/** Stable provider registry key, for example `docker-compose`. */
|
|
20
|
+
provider: string;
|
|
21
|
+
/** Provider-owned, JSON-serializable reconstruction payload. */
|
|
22
|
+
value: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TaskEnvironmentDescriptor {
|
|
26
|
+
taskId: string;
|
|
27
|
+
locator: TaskEnvironmentLocator;
|
|
28
|
+
/** Persisted independently from the provider locator for rollout safety. */
|
|
29
|
+
envMode?: ProjectEnvMode;
|
|
30
|
+
/** Absolute path as observed inside the environment. */
|
|
31
|
+
workspacePath: string;
|
|
32
|
+
codeServerPort?: number;
|
|
33
|
+
previewPorts?: Array<{ name: string; hostPort: number }>;
|
|
34
|
+
initWarning?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TaskEnvironmentExecRequest {
|
|
38
|
+
/** Executable followed by literal argv. Shell command strings are forbidden. */
|
|
39
|
+
argv: readonly [string, ...string[]];
|
|
40
|
+
cwd?: string;
|
|
41
|
+
user?: string;
|
|
42
|
+
/** Host environment names inherited by name, without copying values into argv. */
|
|
43
|
+
inheritEnv?: readonly string[];
|
|
44
|
+
env?: Readonly<Record<string, string>>;
|
|
45
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
46
|
+
stdin?: Uint8Array;
|
|
47
|
+
timeoutMs: number;
|
|
48
|
+
maxOutputBytes: number;
|
|
49
|
+
/**
|
|
50
|
+
* Run with every capability, as root only: the secrets proxy's NAT step
|
|
51
|
+
* needs NET_ADMIN, which a task container does not have (ADR-153). Docker
|
|
52
|
+
* only (`docker exec --privileged`, one process, the container keeps its
|
|
53
|
+
* default capabilities); every other provider refuses it.
|
|
54
|
+
*/
|
|
55
|
+
privileged?: true;
|
|
56
|
+
/**
|
|
57
|
+
* ADR-154 §6: start the process under the engine supervisor, admitted by
|
|
58
|
+
* the runner and owned until a cleanup kills it. The provider runs, as
|
|
59
|
+
* root, the anchor's prefix, the task user the process would have run as
|
|
60
|
+
* (or "-" when the provider drops to it itself), "--", then the process
|
|
61
|
+
* as it would have run it; the supervisor drops to that user. A request
|
|
62
|
+
* that must run as root is never anchored.
|
|
63
|
+
*/
|
|
64
|
+
anchor?: TaskEnvironmentAnchor;
|
|
65
|
+
/**
|
|
66
|
+
* Called by the provider at the last moment before the process starts,
|
|
67
|
+
* after anything it awaits first (a machine looks up its address);
|
|
68
|
+
* throwing refuses the exec unstarted. The secrets proxy's root-step
|
|
69
|
+
* fence rides on it, so a step's exec cannot start once its operation is
|
|
70
|
+
* no longer live, however long that lookup takes (ADR-153).
|
|
71
|
+
*/
|
|
72
|
+
admit?: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TaskEnvironmentExecResult {
|
|
76
|
+
exitCode: number | null;
|
|
77
|
+
signal: string | null;
|
|
78
|
+
stdout: Uint8Array;
|
|
79
|
+
stderr: Uint8Array;
|
|
80
|
+
stdoutTruncated: boolean;
|
|
81
|
+
stderrTruncated: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TaskEnvironmentSpawnRequest {
|
|
85
|
+
argv: readonly [string, ...string[]];
|
|
86
|
+
cwd?: string;
|
|
87
|
+
user?: string;
|
|
88
|
+
/** Host environment names inherited by name, without copying values into argv. */
|
|
89
|
+
inheritEnv?: readonly string[];
|
|
90
|
+
env?: Readonly<Record<string, string>>;
|
|
91
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
92
|
+
timeoutMs: number;
|
|
93
|
+
maxOutputBytes: number;
|
|
94
|
+
/** ADR-154 §6: see `TaskEnvironmentExecRequest.anchor`. */
|
|
95
|
+
anchor?: TaskEnvironmentAnchor;
|
|
96
|
+
/** See `TaskEnvironmentExecRequest.admit`. */
|
|
97
|
+
admit?: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ADR-154 §6: the engine supervisor a process starts under
|
|
102
|
+
* (credential-consumers/engine-anchor.ts): its argv up to the user it drops
|
|
103
|
+
* to.
|
|
104
|
+
*/
|
|
105
|
+
export interface TaskEnvironmentAnchor {
|
|
106
|
+
readonly prefix: readonly [string, ...string[]];
|
|
107
|
+
/**
|
|
108
|
+
* When the launch carries engine credentials in its admission: what a
|
|
109
|
+
* provider that drops to the task user itself (anchor user "-") runs
|
|
110
|
+
* right after that drop, before the process; it reads them from the
|
|
111
|
+
* supervisor's hand-off. A provider whose supervisor drops (a named user)
|
|
112
|
+
* ignores it: the supervisor hands them to the engine itself.
|
|
113
|
+
*/
|
|
114
|
+
readonly injector?: readonly [string, ...string[]];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Long-lived process request used only by agent-session adapters. Its
|
|
119
|
+
* lifetime is owned by the session and therefore has no wall-clock timeout;
|
|
120
|
+
* unread stdout/stderr remains bounded by `maxOutputBytes`.
|
|
121
|
+
*/
|
|
122
|
+
export interface TaskEnvironmentSessionRequest {
|
|
123
|
+
argv: readonly [string, ...string[]];
|
|
124
|
+
cwd?: string;
|
|
125
|
+
user?: string;
|
|
126
|
+
inheritEnv?: readonly string[];
|
|
127
|
+
env?: Readonly<Record<string, string>>;
|
|
128
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
129
|
+
maxOutputBytes: number;
|
|
130
|
+
/** ADR-154 §6: see `TaskEnvironmentExecRequest.anchor`. */
|
|
131
|
+
anchor?: TaskEnvironmentAnchor;
|
|
132
|
+
/** See `TaskEnvironmentExecRequest.admit`. */
|
|
133
|
+
admit?: () => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A bounded control-plane launch of a detached durable-session runner. */
|
|
137
|
+
export interface TaskEnvironmentDetachedSessionRequest
|
|
138
|
+
extends TaskEnvironmentSessionRequest {
|
|
139
|
+
launchTimeoutMs: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface TaskEnvironmentProcessExit {
|
|
143
|
+
exitCode: number | null;
|
|
144
|
+
signal: string | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Streaming process primitive used by durable agent sessions. Providers must
|
|
149
|
+
* keep output buffering within `maxOutputBytes`; AsyncIterable does not imply
|
|
150
|
+
* permission to retain an unbounded unread stream.
|
|
151
|
+
*/
|
|
152
|
+
export interface TaskEnvironmentProcess {
|
|
153
|
+
readonly stdout: AsyncIterable<Uint8Array>;
|
|
154
|
+
readonly stderr: AsyncIterable<Uint8Array>;
|
|
155
|
+
readonly completion: Promise<TaskEnvironmentProcessExit>;
|
|
156
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
157
|
+
closeInput(): Promise<void>;
|
|
158
|
+
terminate(signal?: "SIGTERM" | "SIGKILL"): Promise<void>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Provider-neutral surface handed to agent adapters. `durableIdentity` is an
|
|
163
|
+
* opaque diagnostic/persistence value; callers must never parse it as a
|
|
164
|
+
* container or machine identifier.
|
|
165
|
+
*/
|
|
166
|
+
export interface TaskEnvironmentAgentSessionSurface {
|
|
167
|
+
readonly descriptor: TaskEnvironmentDescriptor;
|
|
168
|
+
readonly durableIdentity: string;
|
|
169
|
+
/** ADR-154 §6: whether the provider starts a request's `anchor`. One that
|
|
170
|
+
* does not never runs a leased engine. */
|
|
171
|
+
readonly anchors?: boolean;
|
|
172
|
+
/** ADR-154 §6: "admission" when a request's environment would reach a
|
|
173
|
+
* command line (a machine's ssh command, a local shell line), so a leased
|
|
174
|
+
* launch's engine credentials travel in its admission instead; absent
|
|
175
|
+
* when the provider passes them by name (`--env NAME`). */
|
|
176
|
+
readonly credentialDelivery?: "admission";
|
|
177
|
+
spawnSession(
|
|
178
|
+
request: TaskEnvironmentSessionRequest,
|
|
179
|
+
): Promise<TaskEnvironmentProcess>;
|
|
180
|
+
launchDetachedSession(
|
|
181
|
+
request: TaskEnvironmentDetachedSessionRequest,
|
|
182
|
+
): Promise<TaskEnvironmentExecResult>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type TaskEnvironmentCopyRequest = (
|
|
186
|
+
| {
|
|
187
|
+
direction: "into";
|
|
188
|
+
/** Absolute host path. */
|
|
189
|
+
source: string;
|
|
190
|
+
/** Absolute environment path. */
|
|
191
|
+
destination: string;
|
|
192
|
+
}
|
|
193
|
+
| {
|
|
194
|
+
direction: "out";
|
|
195
|
+
/** Absolute environment path. */
|
|
196
|
+
source: string;
|
|
197
|
+
/** Absolute host path. */
|
|
198
|
+
destination: string;
|
|
199
|
+
}
|
|
200
|
+
) &
|
|
201
|
+
TaskEnvironmentCallOptions;
|
|
202
|
+
|
|
203
|
+
/** What every call into an environment may carry. */
|
|
204
|
+
export interface TaskEnvironmentCallOptions {
|
|
205
|
+
/** See `TaskEnvironmentExecRequest.admit`: the provider's last word before
|
|
206
|
+
* its process or transport starts, after anything it awaits first. */
|
|
207
|
+
admit?: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** How much of a workspace file a read takes. The task controls its
|
|
211
|
+
* workspace, so every read of it is bounded. */
|
|
212
|
+
export interface TaskEnvironmentReadOptions extends TaskEnvironmentCallOptions {
|
|
213
|
+
/** At most this many bytes (default `WORKSPACE_READ_MAX_BYTES`). */
|
|
214
|
+
maxBytes?: number;
|
|
215
|
+
/** Which end of a longer file is read: its start (the default) or its
|
|
216
|
+
* end. */
|
|
217
|
+
from?: "start" | "end";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The most a workspace read takes, unless it asks for less. */
|
|
221
|
+
export const WORKSPACE_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
222
|
+
/** How long a workspace read may take before it is given up. */
|
|
223
|
+
export const WORKSPACE_READ_TIMEOUT_MS = 60_000;
|
|
224
|
+
|
|
225
|
+
export interface TaskEnvironmentPort {
|
|
226
|
+
name?: string;
|
|
227
|
+
containerPort: number;
|
|
228
|
+
host: "127.0.0.1";
|
|
229
|
+
hostPort: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type TaskEnvironmentStatus =
|
|
233
|
+
| {
|
|
234
|
+
state: "running" | "stopped" | "absent";
|
|
235
|
+
/** Provider-level runtime identities, mapped to the legacy wire's
|
|
236
|
+
* `containers` field while that protocol remains in service. */
|
|
237
|
+
instances: string[];
|
|
238
|
+
workspacePresent: boolean;
|
|
239
|
+
/** Not running, yet its processes still exist (a paused or
|
|
240
|
+
* restarting container): they run again without a new start. */
|
|
241
|
+
frozen?: true;
|
|
242
|
+
}
|
|
243
|
+
| { state: "unknown"; detail: string };
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Recovery is allowed to perform provider-private repair, but its caller owns
|
|
247
|
+
* the surrounding task/channel lifecycle. These callbacks keep that ordering
|
|
248
|
+
* explicit without exposing a Docker container, Compose project, or VM
|
|
249
|
+
* identity to generic orchestration.
|
|
250
|
+
*/
|
|
251
|
+
export interface TaskEnvironmentRecoveryContext {
|
|
252
|
+
/** One-way host maintenance latch (standard image, shared CLI state, etc.). */
|
|
253
|
+
maintenanceReady: Promise<void>;
|
|
254
|
+
/** Provider work must stop before every externally-visible side effect when
|
|
255
|
+
* this generation is no longer current. */
|
|
256
|
+
isCurrent(): boolean;
|
|
257
|
+
/** Drain/tombstone consumers before stopping or repairing a live writer. */
|
|
258
|
+
quarantineConsumers?: () => Promise<void>;
|
|
259
|
+
/** Reassert task-local security/runtime prerequisites before admission. */
|
|
260
|
+
prepareConsumers?: () => Promise<void>;
|
|
261
|
+
/** Re-open admission only after the environment is proven operational. */
|
|
262
|
+
allowConsumers?: () => void;
|
|
263
|
+
/** Recreate durable consumers after admission has reopened. */
|
|
264
|
+
ensureConsumersStarted?: () => Promise<void>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type TaskEnvironmentRecoveryResult =
|
|
268
|
+
| {
|
|
269
|
+
outcome: "recovered";
|
|
270
|
+
/** Optional fresh provider status. Recovery itself is the authoritative
|
|
271
|
+
* proof; callers must not issue a second inventory probe merely to fill
|
|
272
|
+
* this field. */
|
|
273
|
+
status?: TaskEnvironmentStatus;
|
|
274
|
+
descriptor?: TaskEnvironmentDescriptor;
|
|
275
|
+
}
|
|
276
|
+
| {
|
|
277
|
+
outcome: "deferred";
|
|
278
|
+
/** Bounded, operator-safe reason. Deferred recovery is retryable and
|
|
279
|
+
* must never be interpreted as absence. */
|
|
280
|
+
detail: string;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export interface TaskEnvironmentHandle<TTeardownResult = void>
|
|
284
|
+
extends TaskEnvironmentAgentSessionSurface {
|
|
285
|
+
readonly descriptor: TaskEnvironmentDescriptor;
|
|
286
|
+
/** ADR-121 strangler ops: task-scoped file IO through the provider, so
|
|
287
|
+
* call sites stop knowing where the workspace physically lives. Paths are
|
|
288
|
+
* ENVIRONMENT-absolute and must sit inside the descriptor's workspace
|
|
289
|
+
* (assertWorkspaceScopedPath). Providers whose workspace is host-side
|
|
290
|
+
* (docker, apple) implement these as direct filesystem access — zero
|
|
291
|
+
* behavior change; machine providers reach over their transport. Read
|
|
292
|
+
* resolves null for a missing file and rejects for any other failure —
|
|
293
|
+
* absence and error are different facts. A read takes only a regular
|
|
294
|
+
* file, never waits on one it opens (a FIFO or device is refused), takes
|
|
295
|
+
* at most `maxBytes`, and is given up past its bound: what the task puts
|
|
296
|
+
* in its workspace never stalls the runner. */
|
|
297
|
+
readWorkspaceFile(path: string, options?: TaskEnvironmentReadOptions): Promise<Uint8Array | null>;
|
|
298
|
+
writeWorkspaceFile(path: string, bytes: Uint8Array, options?: TaskEnvironmentCallOptions): Promise<void>;
|
|
299
|
+
exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult>;
|
|
300
|
+
spawn(request: TaskEnvironmentSpawnRequest): Promise<TaskEnvironmentProcess>;
|
|
301
|
+
copy(request: TaskEnvironmentCopyRequest): Promise<void>;
|
|
302
|
+
ports(): Promise<TaskEnvironmentPort[]>;
|
|
303
|
+
status(): Promise<TaskEnvironmentStatus>;
|
|
304
|
+
recover(
|
|
305
|
+
context: TaskEnvironmentRecoveryContext,
|
|
306
|
+
): Promise<TaskEnvironmentRecoveryResult>;
|
|
307
|
+
/** Resumable pause: halt the environment's processes while keeping every
|
|
308
|
+
* durable resource (worktree, volumes, container state) so a later
|
|
309
|
+
* provision/recovery can resume it. Idempotent for an already-stopped
|
|
310
|
+
* environment. */
|
|
311
|
+
stop(): Promise<void>;
|
|
312
|
+
/** Idempotent: success means provider-owned runtime resources are absent. */
|
|
313
|
+
teardown(): Promise<TTeardownResult>;
|
|
314
|
+
/**
|
|
315
|
+
* ADR-154 §6, the boot recovery sweep only (task-credential-cleanup.ts):
|
|
316
|
+
* run `work` in this exact runtime while it is stopped, by starting it
|
|
317
|
+
* for `work` alone and stopping it again after, whatever `work` did.
|
|
318
|
+
* Rejects, starting nothing, unless the runtime is provably this task's,
|
|
319
|
+
* stopped (not paused or restarting), idle by its own command, and starts
|
|
320
|
+
* nothing else of the project's either: no enabled healthcheck (its own or
|
|
321
|
+
* its image's), no restart policy, the runner's own init, no auto-remove.
|
|
322
|
+
* `oweStop` is called with the exact container's id before it starts; it
|
|
323
|
+
* must record durably that a stop is owed, and if it throws nothing
|
|
324
|
+
* starts. Rejects after `work` unless the container is proven stopped
|
|
325
|
+
* again. Never creates, recreates or removes anything. Providers that
|
|
326
|
+
* cannot do this omit it, and such a cleanup stays failed.
|
|
327
|
+
*/
|
|
328
|
+
runStoppedForCleanup?<T>(
|
|
329
|
+
work: () => Promise<T>,
|
|
330
|
+
oweStop: (containerId: string) => void | Promise<void>,
|
|
331
|
+
): Promise<T>;
|
|
332
|
+
/**
|
|
333
|
+
* An owed re-stop settled (`runStoppedForCleanup`): true only once the
|
|
334
|
+
* container with exactly this id is proven stopped (stopping it if it
|
|
335
|
+
* runs) or proven gone by a daemon that answered. A failed call is false,
|
|
336
|
+
* never gone.
|
|
337
|
+
*/
|
|
338
|
+
settleCleanupStart?(containerId: string): Promise<boolean>;
|
|
339
|
+
/** Direct-route inspection for backends whose container IPs are
|
|
340
|
+
* host-routable (ADR-106 Apple vmnet). Providers that publish host ports
|
|
341
|
+
* instead simply omit it. The proof must bind the answer to this exact
|
|
342
|
+
* task's container (label check), not merely a name lookup. */
|
|
343
|
+
inspectRoute?(): Promise<
|
|
344
|
+
| { kind: "running"; ipv4Address: string }
|
|
345
|
+
| { kind: "unavailable" }
|
|
346
|
+
| { kind: "unknown"; detail: string }
|
|
347
|
+
>;
|
|
348
|
+
/** ADR-121, machine provider only: full ssh argv for the persistent
|
|
349
|
+
* REVERSE gateway tunnel that makes the host's MCP gateway answer on the
|
|
350
|
+
* environment's own loopback. Container providers omit it — their tasks
|
|
351
|
+
* route to the gateway via container topology instead. */
|
|
352
|
+
gatewayTunnelSshArgs?(
|
|
353
|
+
machinePort: number,
|
|
354
|
+
hostPort: number,
|
|
355
|
+
options?: TaskEnvironmentCallOptions,
|
|
356
|
+
): Promise<string[]>;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface TaskEnvironmentProvisionRequest<TInput, TCredentials> {
|
|
360
|
+
taskId: string;
|
|
361
|
+
/** Omitted only by direct legacy/provider callers; new orchestration passes loader. */
|
|
362
|
+
envMode?: ProjectEnvMode;
|
|
363
|
+
input: TInput;
|
|
364
|
+
credentials: TCredentials;
|
|
365
|
+
/**
|
|
366
|
+
* Awaited after the provider has derived and validated its durable identity,
|
|
367
|
+
* but before it creates any external resources. A crash after this callback
|
|
368
|
+
* can therefore be recovered from the opaque locator alone.
|
|
369
|
+
*/
|
|
370
|
+
onPrepared?: (locator: TaskEnvironmentLocator) => Promise<void>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface TaskEnvironmentProvisioned<TResult, TTeardownResult = void> {
|
|
374
|
+
handle: TaskEnvironmentHandle<TTeardownResult>;
|
|
375
|
+
/** Provider launch result retained for rolling wire compatibility. */
|
|
376
|
+
result: TResult;
|
|
377
|
+
/** The provider reused an environment that already existed — a Resume's
|
|
378
|
+
* stopped machine, whose disk holds the task's work — rather than creating
|
|
379
|
+
* it. A failed start stops such an environment; it never destroys it. */
|
|
380
|
+
reused?: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface TaskEnvironmentProvider<
|
|
384
|
+
TInput,
|
|
385
|
+
TCredentials,
|
|
386
|
+
TResult,
|
|
387
|
+
TTeardownResult = void,
|
|
388
|
+
> {
|
|
389
|
+
readonly kind: string;
|
|
390
|
+
provision(
|
|
391
|
+
request: TaskEnvironmentProvisionRequest<TInput, TCredentials>,
|
|
392
|
+
): Promise<TaskEnvironmentProvisioned<TResult, TTeardownResult>>;
|
|
393
|
+
reconstruct(
|
|
394
|
+
locator: TaskEnvironmentLocator,
|
|
395
|
+
envMode?: ProjectEnvMode,
|
|
396
|
+
): Promise<TaskEnvironmentHandle<TTeardownResult>>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Workspace-scope guard for the strangler file ops: the path must be an
|
|
400
|
+
* absolute, NUL-free, dot-segment-free environment path inside the
|
|
401
|
+
* descriptor's workspace. Escaping the workspace through the file ops would
|
|
402
|
+
* hand callers the environment's whole filesystem. */
|
|
403
|
+
export function assertWorkspaceScopedPath(
|
|
404
|
+
descriptor: TaskEnvironmentDescriptor,
|
|
405
|
+
path: string,
|
|
406
|
+
): void {
|
|
407
|
+
if (!path.startsWith("/") || path.includes("\0")) {
|
|
408
|
+
throw new Error("workspace file path must be an absolute NUL-free path");
|
|
409
|
+
}
|
|
410
|
+
if (path.split("/").some((part) => part === "." || part === "..")) {
|
|
411
|
+
throw new Error("workspace file path must not contain dot segments");
|
|
412
|
+
}
|
|
413
|
+
const root = descriptor.workspacePath.endsWith("/")
|
|
414
|
+
? descriptor.workspacePath
|
|
415
|
+
: `${descriptor.workspacePath}/`;
|
|
416
|
+
if (path !== descriptor.workspacePath && !path.startsWith(root)) {
|
|
417
|
+
throw new Error("workspace file path escapes the task workspace");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A malformed process request, distinct from provider/runtime failures. */
|
|
422
|
+
export class TaskEnvironmentRequestValidationError extends Error {
|
|
423
|
+
override name = "TaskEnvironmentRequestValidationError";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
427
|
+
|
|
428
|
+
/** Shared admission guard for provider implementations and fake providers. */
|
|
429
|
+
/** A one-shot exec: the only request that may be privileged, and then only
|
|
430
|
+
* as root. Providers pass the privilege on explicitly, never by reading the
|
|
431
|
+
* request, so no other surface can carry it. */
|
|
432
|
+
export function assertTaskEnvironmentExecRequest(request: TaskEnvironmentExecRequest): void {
|
|
433
|
+
const { privileged, ...unprivileged } = request;
|
|
434
|
+
if (privileged !== undefined && (privileged !== true || request.user !== "root")) {
|
|
435
|
+
throw new TaskEnvironmentRequestValidationError("a privileged task environment exec must run as root");
|
|
436
|
+
}
|
|
437
|
+
assertTaskEnvironmentProcessRequest(unprivileged);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function assertTaskEnvironmentProcessRequest(
|
|
441
|
+
request: TaskEnvironmentExecRequest | TaskEnvironmentSpawnRequest,
|
|
442
|
+
): void {
|
|
443
|
+
assertTaskEnvironmentSessionRequest(request);
|
|
444
|
+
if (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs <= 0) {
|
|
445
|
+
throw new TaskEnvironmentRequestValidationError("task environment timeout must be a positive integer");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** For providers with no privileged exec: refuse it rather than run the
|
|
450
|
+
* step without the capability it asked for. */
|
|
451
|
+
export function refusePrivilegedExec(request: TaskEnvironmentExecRequest): void {
|
|
452
|
+
if (request.privileged === true) {
|
|
453
|
+
throw new TaskEnvironmentRequestValidationError("this task environment provider has no privileged exec");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function assertTaskEnvironmentSessionRequest(
|
|
458
|
+
request:
|
|
459
|
+
| TaskEnvironmentSessionRequest
|
|
460
|
+
| TaskEnvironmentDetachedSessionRequest
|
|
461
|
+
| TaskEnvironmentExecRequest
|
|
462
|
+
| TaskEnvironmentSpawnRequest,
|
|
463
|
+
): void {
|
|
464
|
+
// Only a one-shot exec may be privileged (assertTaskEnvironmentExecRequest
|
|
465
|
+
// strips it after checking); anywhere else it is refused outright.
|
|
466
|
+
if ("privileged" in request) {
|
|
467
|
+
throw new TaskEnvironmentRequestValidationError("only a one-shot task environment exec may be privileged");
|
|
468
|
+
}
|
|
469
|
+
if (
|
|
470
|
+
request.projectEnvProfile !== "maintenance" &&
|
|
471
|
+
request.projectEnvProfile !== "task"
|
|
472
|
+
) {
|
|
473
|
+
throw new TaskEnvironmentRequestValidationError(
|
|
474
|
+
"task environment project env profile must be maintenance or task",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (
|
|
478
|
+
!Array.isArray(request.argv) ||
|
|
479
|
+
request.argv.length === 0 ||
|
|
480
|
+
request.argv.some(
|
|
481
|
+
(part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
|
|
482
|
+
)
|
|
483
|
+
) {
|
|
484
|
+
throw new TaskEnvironmentRequestValidationError("task environment argv must contain nonempty NUL-free strings");
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
request.cwd !== undefined &&
|
|
488
|
+
(typeof request.cwd !== "string" || !request.cwd.startsWith("/") || request.cwd.includes("\0"))
|
|
489
|
+
) {
|
|
490
|
+
throw new TaskEnvironmentRequestValidationError("task environment cwd must be an absolute NUL-free path");
|
|
491
|
+
}
|
|
492
|
+
if (
|
|
493
|
+
request.user !== undefined &&
|
|
494
|
+
(typeof request.user !== "string" || request.user.length === 0 || request.user.includes("\0"))
|
|
495
|
+
) {
|
|
496
|
+
throw new TaskEnvironmentRequestValidationError("task environment user must be a nonempty NUL-free value");
|
|
497
|
+
}
|
|
498
|
+
if (
|
|
499
|
+
request.anchor !== undefined &&
|
|
500
|
+
(request.user === "root" ||
|
|
501
|
+
!Array.isArray(request.anchor.prefix) ||
|
|
502
|
+
request.anchor.prefix.length === 0 ||
|
|
503
|
+
request.anchor.prefix.some(
|
|
504
|
+
(part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
|
|
505
|
+
))
|
|
506
|
+
) {
|
|
507
|
+
throw new TaskEnvironmentRequestValidationError("a task environment anchor must be a NUL-free argv for a task user's process");
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
request.inheritEnv !== undefined &&
|
|
511
|
+
(!Array.isArray(request.inheritEnv) ||
|
|
512
|
+
request.inheritEnv.length > 128 ||
|
|
513
|
+
new Set(request.inheritEnv).size !== request.inheritEnv.length ||
|
|
514
|
+
request.inheritEnv.some(
|
|
515
|
+
(name) => typeof name !== "string" || !ENV_NAME.test(name),
|
|
516
|
+
))
|
|
517
|
+
) {
|
|
518
|
+
throw new TaskEnvironmentRequestValidationError("task environment inherited env names are invalid");
|
|
519
|
+
}
|
|
520
|
+
for (const [name, value] of Object.entries(request.env ?? {})) {
|
|
521
|
+
if (
|
|
522
|
+
!ENV_NAME.test(name) ||
|
|
523
|
+
typeof value !== "string" ||
|
|
524
|
+
value.includes("\0")
|
|
525
|
+
) {
|
|
526
|
+
throw new TaskEnvironmentRequestValidationError("task environment env must contain valid NUL-free entries");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (
|
|
530
|
+
!Number.isSafeInteger(request.maxOutputBytes) ||
|
|
531
|
+
request.maxOutputBytes <= 0
|
|
532
|
+
) {
|
|
533
|
+
throw new TaskEnvironmentRequestValidationError("task environment output limit must be a positive integer");
|
|
534
|
+
}
|
|
535
|
+
if (
|
|
536
|
+
"launchTimeoutMs" in request &&
|
|
537
|
+
(!Number.isSafeInteger(request.launchTimeoutMs) ||
|
|
538
|
+
request.launchTimeoutMs <= 0)
|
|
539
|
+
) {
|
|
540
|
+
throw new TaskEnvironmentRequestValidationError(
|
|
541
|
+
"task environment detached launch timeout must be a positive integer",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function serializeTaskEnvironmentLocator(
|
|
547
|
+
locator: TaskEnvironmentLocator,
|
|
548
|
+
): string {
|
|
549
|
+
validateTaskEnvironmentLocator(locator);
|
|
550
|
+
return JSON.stringify(locator);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function parseTaskEnvironmentLocator(raw: string): TaskEnvironmentLocator {
|
|
554
|
+
let parsed: unknown;
|
|
555
|
+
try {
|
|
556
|
+
parsed = JSON.parse(raw);
|
|
557
|
+
} catch {
|
|
558
|
+
throw new Error("task environment locator is not valid JSON");
|
|
559
|
+
}
|
|
560
|
+
return validateTaskEnvironmentLocator(parsed);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function validateTaskEnvironmentLocator(
|
|
564
|
+
value: unknown,
|
|
565
|
+
): TaskEnvironmentLocator {
|
|
566
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
567
|
+
throw new Error("task environment locator must be an object");
|
|
568
|
+
}
|
|
569
|
+
const record = value as Record<string, unknown>;
|
|
570
|
+
if (
|
|
571
|
+
Object.keys(record).sort().join("\0") !==
|
|
572
|
+
["provider", "schemaVersion", "value"].sort().join("\0")
|
|
573
|
+
) {
|
|
574
|
+
throw new Error("task environment locator has unexpected fields");
|
|
575
|
+
}
|
|
576
|
+
if (record.schemaVersion !== TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION) {
|
|
577
|
+
throw new Error("unsupported task environment locator schema");
|
|
578
|
+
}
|
|
579
|
+
if (
|
|
580
|
+
typeof record.provider !== "string" ||
|
|
581
|
+
!/^[a-z][a-z0-9-]{0,63}$/.test(record.provider)
|
|
582
|
+
) {
|
|
583
|
+
throw new Error("task environment locator provider is invalid");
|
|
584
|
+
}
|
|
585
|
+
assertJsonValue(record.value, 0);
|
|
586
|
+
return {
|
|
587
|
+
schemaVersion: TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
|
|
588
|
+
provider: record.provider,
|
|
589
|
+
value: record.value,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function assertJsonValue(value: unknown, depth: number): void {
|
|
594
|
+
if (depth > 16) throw new Error("task environment locator is too deeply nested");
|
|
595
|
+
if (
|
|
596
|
+
value === null ||
|
|
597
|
+
typeof value === "string" ||
|
|
598
|
+
typeof value === "boolean"
|
|
599
|
+
) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (typeof value === "number") {
|
|
603
|
+
if (!Number.isFinite(value)) {
|
|
604
|
+
throw new Error("task environment locator contains a non-finite number");
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
if (value.length > 256) {
|
|
610
|
+
throw new Error("task environment locator array is too large");
|
|
611
|
+
}
|
|
612
|
+
for (const item of value) assertJsonValue(item, depth + 1);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (value && typeof value === "object") {
|
|
616
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
617
|
+
if (entries.length > 256) {
|
|
618
|
+
throw new Error("task environment locator object is too large");
|
|
619
|
+
}
|
|
620
|
+
for (const [key, item] of entries) {
|
|
621
|
+
if (key.includes("\0")) {
|
|
622
|
+
throw new Error("task environment locator key contains NUL");
|
|
623
|
+
}
|
|
624
|
+
assertJsonValue(item, depth + 1);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
throw new Error("task environment locator is not JSON-serializable");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Providers whose workspace is reached only through the environment
|
|
633
|
+
* transport — a machine over ssh (ADR-121) or this very machine in sidecar
|
|
634
|
+
* mode (ADR-133) — as opposed to a container the host can `docker exec` into
|
|
635
|
+
* or bind-mount. Every "is this a machine task?" branch in the host means
|
|
636
|
+
* exactly this, so it asks here rather than comparing one provider name.
|
|
637
|
+
*/
|
|
638
|
+
export function isEnvironmentTransportProvider(
|
|
639
|
+
provider: string | null | undefined,
|
|
640
|
+
): boolean {
|
|
641
|
+
return provider === "machine" || provider === "self";
|
|
642
|
+
}
|