@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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort task.status frames have no acknowledgement, but they must at
|
|
3
|
+
* least survive bridge disconnects within this host process. The local task
|
|
4
|
+
* mirror remains the authority at flush time so a later Resume wins over an
|
|
5
|
+
* older queued stop.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Why the host stopped the task, when the host knows.
|
|
9
|
+
*
|
|
10
|
+
* `"manual"` is ADR-028's local-UI Stop — a person at the machine pressed it.
|
|
11
|
+
* Everything else the host reports through this frame is automatic (writable
|
|
12
|
+
* -container quarantine during a runtime activation or a shared-volume
|
|
13
|
+
* repair), and carries no reason. The cloud needs the difference because
|
|
14
|
+
* ADR-144 lets a collaborator resume a task that stopped on its own but not
|
|
15
|
+
* one a human deliberately paused, and until now both arrived as the same
|
|
16
|
+
* three fields.
|
|
17
|
+
*/
|
|
18
|
+
export type StoppedTaskReason = "manual";
|
|
19
|
+
|
|
20
|
+
interface StoppedTaskState {
|
|
21
|
+
status: string;
|
|
22
|
+
runEpoch: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PendingStoppedTaskStatus {
|
|
26
|
+
reason: StoppedTaskReason | undefined;
|
|
27
|
+
runEpoch: number | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class StoppedTaskStatusQueue {
|
|
31
|
+
private readonly pending = new Map<string, PendingStoppedTaskStatus>();
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly stateForTask: (
|
|
35
|
+
taskId: string,
|
|
36
|
+
) => StoppedTaskState | null | undefined,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
enqueue(
|
|
40
|
+
taskId: string,
|
|
41
|
+
reason?: StoppedTaskReason,
|
|
42
|
+
runEpoch?: number,
|
|
43
|
+
): void {
|
|
44
|
+
const previous = this.pending.get(taskId);
|
|
45
|
+
const previousEpoch = previous?.runEpoch ?? 0;
|
|
46
|
+
const nextEpoch = runEpoch ?? 0;
|
|
47
|
+
// A command result can arrive after the next assignment has already
|
|
48
|
+
// queued its own status. Run epochs are monotonic, so an older result must
|
|
49
|
+
// not replace the newer pending truth (or donate its reason to it).
|
|
50
|
+
if (previous !== undefined && nextEpoch < previousEpoch) return;
|
|
51
|
+
// A reason, once known, STICKS across a re-enqueue. The same task can be
|
|
52
|
+
// queued twice before a flush — an operator Stop, then a quarantine sweep
|
|
53
|
+
// that finds its container already down — and the automatic pass arriving
|
|
54
|
+
// second does not make the human's decision retroactively automatic.
|
|
55
|
+
// Dropping it would silently hand the task back to every collaborator.
|
|
56
|
+
this.pending.set(taskId, {
|
|
57
|
+
reason:
|
|
58
|
+
previous !== undefined && previousEpoch === nextEpoch
|
|
59
|
+
? reason ?? previous.reason
|
|
60
|
+
: reason,
|
|
61
|
+
runEpoch,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A failed taskUp may have stopped a legacy runtime before recreation lost
|
|
66
|
+
* Docker. Queue the physical truth after the command result is published. */
|
|
67
|
+
enqueueStoppedTaskUp(command: string, args: readonly unknown[]): boolean {
|
|
68
|
+
if (command !== "taskUp") return false;
|
|
69
|
+
const input = args[0];
|
|
70
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const task = (input as { task?: unknown }).task;
|
|
74
|
+
if (!task || typeof task !== "object" || Array.isArray(task)) return false;
|
|
75
|
+
const taskId = (task as { id?: unknown }).id;
|
|
76
|
+
const state = typeof taskId === "string" ? this.stateForTask(taskId) : null;
|
|
77
|
+
if (typeof taskId !== "string" || state?.status !== "stopped") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const hasRunEpoch = Object.hasOwn(task, "runEpoch");
|
|
81
|
+
const inputRunEpoch = (task as { runEpoch?: unknown }).runEpoch;
|
|
82
|
+
if (hasRunEpoch) {
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(inputRunEpoch) ||
|
|
85
|
+
(inputRunEpoch as number) < 1 ||
|
|
86
|
+
inputRunEpoch !== state.runEpoch
|
|
87
|
+
) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
} else if (state.runEpoch !== 0) {
|
|
91
|
+
// Never relabel a delayed v0 taskUp result as the current v1 run.
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const runEpoch = hasRunEpoch ? (inputRunEpoch as number) : undefined;
|
|
95
|
+
this.enqueue(taskId, undefined, runEpoch);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
flush(
|
|
100
|
+
publish: (
|
|
101
|
+
taskId: string,
|
|
102
|
+
reason?: StoppedTaskReason,
|
|
103
|
+
runEpoch?: number,
|
|
104
|
+
) => void,
|
|
105
|
+
): void {
|
|
106
|
+
for (const [taskId, pending] of [...this.pending]) {
|
|
107
|
+
const state = this.stateForTask(taskId);
|
|
108
|
+
if (
|
|
109
|
+
state?.status === "stopped" &&
|
|
110
|
+
(pending.runEpoch === undefined
|
|
111
|
+
? state.runEpoch === 0
|
|
112
|
+
: state.runEpoch === pending.runEpoch)
|
|
113
|
+
) {
|
|
114
|
+
if (pending.runEpoch === undefined) publish(taskId, pending.reason);
|
|
115
|
+
else publish(taskId, pending.reason, pending.runEpoch);
|
|
116
|
+
}
|
|
117
|
+
this.pending.delete(taskId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-aware CLI access to TASK app containers for the auxiliary
|
|
3
|
+
* subsystems that predate the TaskEnvironment provider (GitHub credential
|
|
4
|
+
* injection, git identity, Codex reinjection, skills). Found live 2026-08-16:
|
|
5
|
+
* these spawned the literal `docker` binary with Compose replica names, so on
|
|
6
|
+
* a Docker-free Apple host every agent session start died at the credential
|
|
7
|
+
* step with spawn ENOENT.
|
|
8
|
+
*
|
|
9
|
+
* The exec/cp verbs are flag-compatible across Docker and the bundled Apple
|
|
10
|
+
* CLI, so most callers only need the command + container-name translation
|
|
11
|
+
* here. Verbs that differ (ps/rm/inventory) get explicit helpers.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
appleContainerRuntimeBinding,
|
|
18
|
+
pinnedContainerRuntimeProvider,
|
|
19
|
+
} from "./container-runtime";
|
|
20
|
+
import { dockerCli, type DockerResult } from "./docker-exec";
|
|
21
|
+
import { assertSafeHostTaskId } from "./task-identity";
|
|
22
|
+
|
|
23
|
+
export interface TaskContainerBackend {
|
|
24
|
+
command: string;
|
|
25
|
+
apple: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The pin, not the readiness-gated identity: session ensure and recovery
|
|
29
|
+
* both run while the public state can still be `checking`. */
|
|
30
|
+
export function taskContainerBackend(): TaskContainerBackend {
|
|
31
|
+
if (pinnedContainerRuntimeProvider() === "apple-container") {
|
|
32
|
+
const binding = appleContainerRuntimeBinding();
|
|
33
|
+
if (binding !== null) {
|
|
34
|
+
return { command: binding.containerCliPath, apple: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { command: "docker", apple: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The app container's name under the selected backend: Compose replica
|
|
41
|
+
* naming for Docker, the provider's fixed name for Apple (must match
|
|
42
|
+
* appleTaskContainerName in task-environment/apple-container.ts). */
|
|
43
|
+
export function taskAppContainerName(taskId: string): string {
|
|
44
|
+
assertSafeHostTaskId(taskId);
|
|
45
|
+
const lowered = taskId.toLowerCase();
|
|
46
|
+
return taskContainerBackend().apple
|
|
47
|
+
? `moondeck-task-${lowered}-app`
|
|
48
|
+
: `task-${lowered}-app-1`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** dockerCli-compatible runner that targets the selected backend. Every kill
|
|
52
|
+
* (deadline or overflow) forces settlement within a bounded grace. */
|
|
53
|
+
export function taskContainerCli(
|
|
54
|
+
args: string[],
|
|
55
|
+
opts: {
|
|
56
|
+
input?: string;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
maxOutputBytes?: number;
|
|
59
|
+
} = {},
|
|
60
|
+
): Promise<DockerResult> {
|
|
61
|
+
const backend = taskContainerBackend();
|
|
62
|
+
if (!backend.apple) return dockerCli(args, opts);
|
|
63
|
+
return new Promise<DockerResult>((resolve) => {
|
|
64
|
+
const child = nodeSpawn(backend.command, args, {
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let stderr = "";
|
|
69
|
+
let settled = false;
|
|
70
|
+
const finish = (status: number | null): void => {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
resolve({ status, stdout, stderr });
|
|
75
|
+
};
|
|
76
|
+
const killAndSettle = (): void => {
|
|
77
|
+
child.kill("SIGKILL");
|
|
78
|
+
const settle = setTimeout(() => finish(null), 5_000);
|
|
79
|
+
settle.unref?.();
|
|
80
|
+
};
|
|
81
|
+
const timer = setTimeout(killAndSettle, opts.timeoutMs ?? 30_000);
|
|
82
|
+
timer.unref?.();
|
|
83
|
+
child.stdout.setEncoding("utf8");
|
|
84
|
+
child.stderr.setEncoding("utf8");
|
|
85
|
+
child.stdout.on("data", (chunk: string) => {
|
|
86
|
+
stdout += chunk;
|
|
87
|
+
if (
|
|
88
|
+
opts.maxOutputBytes !== undefined &&
|
|
89
|
+
stdout.length + stderr.length > opts.maxOutputBytes
|
|
90
|
+
) {
|
|
91
|
+
killAndSettle();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.stderr.on("data", (chunk: string) => {
|
|
95
|
+
stderr += chunk;
|
|
96
|
+
if (
|
|
97
|
+
opts.maxOutputBytes !== undefined &&
|
|
98
|
+
stdout.length + stderr.length > opts.maxOutputBytes
|
|
99
|
+
) {
|
|
100
|
+
killAndSettle();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
child.stdin.on("error", () => {});
|
|
104
|
+
if (opts.input !== undefined) child.stdin.write(opts.input);
|
|
105
|
+
child.stdin.end();
|
|
106
|
+
child.on("error", () => finish(null));
|
|
107
|
+
child.on("close", (code) => finish(code));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type TaskContainerState =
|
|
112
|
+
| { kind: "running" }
|
|
113
|
+
| { kind: "stopped"; state: string }
|
|
114
|
+
| { kind: "absent" }
|
|
115
|
+
| { kind: "unknown"; detail: string };
|
|
116
|
+
|
|
117
|
+
/** Backend-aware state probe for one exact task app container. Absence is
|
|
118
|
+
* proven only by the complete not-found error naming the container (Apple)
|
|
119
|
+
* or an empty exact-name filter answer (Docker); anything else is unknown. */
|
|
120
|
+
export async function taskAppContainerState(
|
|
121
|
+
containerName: string,
|
|
122
|
+
run: typeof taskContainerCli = taskContainerCli,
|
|
123
|
+
): Promise<TaskContainerState> {
|
|
124
|
+
const backend = taskContainerBackend();
|
|
125
|
+
if (backend.apple) {
|
|
126
|
+
const inspected = await run(["inspect", containerName], {
|
|
127
|
+
timeoutMs: 30_000,
|
|
128
|
+
maxOutputBytes: 1024 * 1024,
|
|
129
|
+
});
|
|
130
|
+
if (inspected.status !== 0) {
|
|
131
|
+
if (
|
|
132
|
+
inspected.stdout.trim() === "" &&
|
|
133
|
+
inspected.stderr
|
|
134
|
+
.split("\n")
|
|
135
|
+
.some(
|
|
136
|
+
(line) =>
|
|
137
|
+
line.trim() === `Error: container not found: ${containerName}`,
|
|
138
|
+
)
|
|
139
|
+
) {
|
|
140
|
+
return { kind: "absent" };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
kind: "unknown",
|
|
144
|
+
detail: inspected.stderr.trim().slice(0, 300) || "inspect failed",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(inspected.stdout) as Array<{
|
|
149
|
+
id?: unknown;
|
|
150
|
+
status?: { state?: unknown };
|
|
151
|
+
}>;
|
|
152
|
+
if (
|
|
153
|
+
!Array.isArray(parsed) ||
|
|
154
|
+
parsed.length !== 1 ||
|
|
155
|
+
parsed[0]?.id !== containerName
|
|
156
|
+
) {
|
|
157
|
+
return { kind: "unknown", detail: "inspect shape is invalid" };
|
|
158
|
+
}
|
|
159
|
+
const state = parsed[0]?.status?.state;
|
|
160
|
+
if (typeof state !== "string") {
|
|
161
|
+
return { kind: "unknown", detail: "inspect state is missing" };
|
|
162
|
+
}
|
|
163
|
+
return state === "running"
|
|
164
|
+
? { kind: "running" }
|
|
165
|
+
: { kind: "stopped", state };
|
|
166
|
+
} catch {
|
|
167
|
+
return { kind: "unknown", detail: "inspect output is not JSON" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const listed = await run(
|
|
171
|
+
[
|
|
172
|
+
"ps",
|
|
173
|
+
"--all",
|
|
174
|
+
"--filter",
|
|
175
|
+
`name=^/${containerName}$`,
|
|
176
|
+
"--format",
|
|
177
|
+
"{{.State}}",
|
|
178
|
+
],
|
|
179
|
+
{ timeoutMs: 30_000, maxOutputBytes: 64 * 1024 },
|
|
180
|
+
);
|
|
181
|
+
if (listed.status !== 0) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "unknown",
|
|
184
|
+
detail: listed.stderr.trim().slice(0, 300) || "docker ps failed",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const states = listed.stdout
|
|
188
|
+
.split(/\r?\n/)
|
|
189
|
+
.map((line) => line.trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
if (states.length === 0) return { kind: "absent" };
|
|
192
|
+
if (states.length !== 1) {
|
|
193
|
+
return { kind: "unknown", detail: `${states.length} containers matched` };
|
|
194
|
+
}
|
|
195
|
+
return states[0] === "running"
|
|
196
|
+
? { kind: "running" }
|
|
197
|
+
: { kind: "stopped", state: states[0]! };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Force-remove one exact task app container and prove its absence. */
|
|
201
|
+
export async function removeTaskAppContainer(
|
|
202
|
+
containerName: string,
|
|
203
|
+
run: typeof taskContainerCli = taskContainerCli,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
const backend = taskContainerBackend();
|
|
206
|
+
const removed = await run(
|
|
207
|
+
backend.apple
|
|
208
|
+
? ["delete", "--force", containerName]
|
|
209
|
+
: ["rm", "--force", containerName],
|
|
210
|
+
{ timeoutMs: 60_000, maxOutputBytes: 64 * 1024 },
|
|
211
|
+
);
|
|
212
|
+
if (removed.status !== 0 && backend.apple) {
|
|
213
|
+
// Deleting an already-absent container is success; anything else is not.
|
|
214
|
+
const state = await taskAppContainerState(containerName, run);
|
|
215
|
+
if (state.kind === "absent") return;
|
|
216
|
+
throw new Error(
|
|
217
|
+
`remove ${containerName} failed: ${removed.stderr.trim().slice(0, 300)}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const after = await taskAppContainerState(containerName, run);
|
|
221
|
+
if (after.kind !== "absent") {
|
|
222
|
+
throw new Error(`remove ${containerName} was not verified`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Names of RUNNING task app containers under the selected backend. */
|
|
227
|
+
export async function listRunningTaskAppContainerNames(
|
|
228
|
+
run: typeof taskContainerCli = taskContainerCli,
|
|
229
|
+
): Promise<string[] | null> {
|
|
230
|
+
const backend = taskContainerBackend();
|
|
231
|
+
if (backend.apple) {
|
|
232
|
+
const listed = await run(["list", "--format", "json"], {
|
|
233
|
+
timeoutMs: 30_000,
|
|
234
|
+
maxOutputBytes: 8 * 1024 * 1024,
|
|
235
|
+
});
|
|
236
|
+
if (listed.status !== 0) return null;
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(listed.stdout) as Array<{
|
|
239
|
+
configuration?: { id?: unknown };
|
|
240
|
+
status?: { state?: unknown };
|
|
241
|
+
}>;
|
|
242
|
+
if (!Array.isArray(parsed)) return null;
|
|
243
|
+
// `container list` without --all already returns running containers,
|
|
244
|
+
// but the state check keeps this honest against CLI behavior drift.
|
|
245
|
+
return parsed
|
|
246
|
+
.filter(
|
|
247
|
+
(item) =>
|
|
248
|
+
item?.status?.state === undefined ||
|
|
249
|
+
item?.status?.state === "running",
|
|
250
|
+
)
|
|
251
|
+
.map((item) => item?.configuration?.id)
|
|
252
|
+
.filter(
|
|
253
|
+
(id): id is string =>
|
|
254
|
+
typeof id === "string" && /^moondeck-task-[a-z0-9][a-z0-9_-]*-app$/.test(id),
|
|
255
|
+
);
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const listed = await run(
|
|
261
|
+
["ps", "--filter", "status=running", "--format", "{{.Names}}"],
|
|
262
|
+
{ timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 },
|
|
263
|
+
);
|
|
264
|
+
if (listed.status !== 0) return null;
|
|
265
|
+
return listed.stdout
|
|
266
|
+
.split(/\r?\n/)
|
|
267
|
+
.map((line) => line.trim())
|
|
268
|
+
.filter(Boolean);
|
|
269
|
+
}
|