@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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154 §13: pre-v1 task runtimes are retired as the runner moves to
|
|
3
|
+
* cloud credentials. Every runtime it has was created on its own
|
|
4
|
+
* credentials (the mode was `legacy` for each one's whole life), and holds
|
|
5
|
+
* them: Compose interpolated the default slot's engine tokens into the
|
|
6
|
+
* container env, and legacy copies of Codex, Grok, OpenCode, `gh` and SSH
|
|
7
|
+
* credentials sit in its home, recorded nowhere.
|
|
8
|
+
*
|
|
9
|
+
* Two halves, because removing a runtime cannot be undone:
|
|
10
|
+
*
|
|
11
|
+
* - `planLegacyTaskRuntimeRetirement` changes nothing. From one snapshot
|
|
12
|
+
* (the task rows, the identity fence on those same rows, the pinned
|
|
13
|
+
* engine) it finds every blocker known in advance, and rejects with all of
|
|
14
|
+
* them, so a runner that cannot finish stays on v0 exactly as it was: the
|
|
15
|
+
* container runtime not operational; a task row of another backend or
|
|
16
|
+
* engine than the one pinned (the persisted identity fence); a machine
|
|
17
|
+
* (ADR-121) or self runtime, which cannot be removed with its worktree
|
|
18
|
+
* kept; a row naming a Compose project that is not its own; a container at
|
|
19
|
+
* a task's name that cannot be proven that task's; a `task-*` Compose
|
|
20
|
+
* project no row with a runtime names, which nothing proves Uai's
|
|
21
|
+
* (`task-*` alone is no ownership, see orchestrator.ts's runtime
|
|
22
|
+
* isolation), and which the operator removes or renames. A row with no
|
|
23
|
+
* runtime (a task that went down keeps its row, its runtime fields
|
|
24
|
+
* cleared) neither blocks nor vouches for anything. The plan records the
|
|
25
|
+
* engine it was made against. Its rejection is a `RetirementBlockedError`:
|
|
26
|
+
* each blocker apart, and every unproven project named, so the cloud can
|
|
27
|
+
* show the operator all of them with the command that clears each
|
|
28
|
+
* (ADR-154 §13 build notes, 2026-10-27). Such a project is never removed
|
|
29
|
+
* here: a Compose label proves neither who made a stack nor that nothing
|
|
30
|
+
* uses it now (another runner may share this Docker and workspace root).
|
|
31
|
+
* - `retireLegacyTaskRuntimes` carries a plan out, only after the runner has
|
|
32
|
+
* committed to the switch. Each task's check, Stop and removal share its
|
|
33
|
+
* lifecycle slot: before its assignment closes and again before anything
|
|
34
|
+
* is removed, the live engine is re-probed and the task's row checked
|
|
35
|
+
* against the plan's engine, and so again before each command of the
|
|
36
|
+
* removal after its first and once more before the task counts as
|
|
37
|
+
* retired, so an absence answered by another engine never counts. A daemon
|
|
38
|
+
* replaced since (even at the same socket), or a row gone or cleared since
|
|
39
|
+
* (its task went down), is a problem, never a removal: the next plan
|
|
40
|
+
* decides. Each task stops, and its
|
|
41
|
+
* container goes with its worktree kept, exactly as task-up's step 2
|
|
42
|
+
* removes a previous container before a Resume (Docker by the Compose
|
|
43
|
+
* project its task id names, proven by an empty label query; Apple by
|
|
44
|
+
* name, its exact identity proven before it is deleted, and its absence
|
|
45
|
+
* by the CLI's not-found answer). An Apple task container no row names
|
|
46
|
+
* goes too when its label names it exactly (apple-uninstall-teardown.ts's
|
|
47
|
+
* proof). It is idempotent: a runtime already gone counts as removed, so
|
|
48
|
+
* a later attempt finishes what one left. Every removal is attempted; what
|
|
49
|
+
* could not be proven is in `problems`, so the caller can still report the
|
|
50
|
+
* tasks that did go.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import type { DockerResult } from "./docker-exec";
|
|
54
|
+
import { SAFE_HOST_TASK_ID, isSafeHostTaskId } from "./task-identity";
|
|
55
|
+
|
|
56
|
+
/** A task row, as far as its retirement goes. */
|
|
57
|
+
export interface RetirementTask {
|
|
58
|
+
taskId: string;
|
|
59
|
+
/** `uai_host_tasks.environment_provider`; null on rows that predate it
|
|
60
|
+
* (Docker Compose). */
|
|
61
|
+
provider: string | null;
|
|
62
|
+
composeProject: string | null;
|
|
63
|
+
/** The task directory (task-up's `$task_dir`). */
|
|
64
|
+
worktreePath: string | null;
|
|
65
|
+
/** It names a runtime (a locator, or a legacy Compose project). */
|
|
66
|
+
hasRuntime: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The container engine the runner has pinned. */
|
|
70
|
+
export interface RetirementMachine {
|
|
71
|
+
backend: string;
|
|
72
|
+
endpoint: string;
|
|
73
|
+
engineId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What one plan reads, all at once. */
|
|
77
|
+
export interface RetirementSnapshot {
|
|
78
|
+
tasks: RetirementTask[];
|
|
79
|
+
/** Why the container runtime cannot be used now; null when it can. */
|
|
80
|
+
runtimeProblem: string | null;
|
|
81
|
+
/** The persisted identity fence over these same rows: every row's runtime
|
|
82
|
+
* is on `machine`. */
|
|
83
|
+
fence: { ok: boolean; detail?: string };
|
|
84
|
+
machine: RetirementMachine | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Cli = (args: string[], opts?: { timeoutMs?: number; maxOutputBytes?: number }) => Promise<DockerResult>;
|
|
88
|
+
|
|
89
|
+
export interface RetirementPlanDeps {
|
|
90
|
+
snapshot: () => RetirementSnapshot;
|
|
91
|
+
/** The pinned container backend. */
|
|
92
|
+
apple: boolean;
|
|
93
|
+
docker: Cli;
|
|
94
|
+
/** The Apple container CLI (only called when `apple`). */
|
|
95
|
+
appleCli: Cli;
|
|
96
|
+
fileExists: (path: string) => boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type PlannedRemoval =
|
|
100
|
+
| { taskId: string; kind: "docker"; project: string; composeFile: string | null }
|
|
101
|
+
| { taskId: string; kind: "apple"; container: string };
|
|
102
|
+
|
|
103
|
+
export interface RetirementPlan {
|
|
104
|
+
/** The engine the plan was made against. */
|
|
105
|
+
machine: RetirementMachine;
|
|
106
|
+
/** Every runtime a task row names, whether or not anything is left of it. */
|
|
107
|
+
removals: PlannedRemoval[];
|
|
108
|
+
/** Apple task containers no row names, each labelled with the task its
|
|
109
|
+
* name names. */
|
|
110
|
+
appleOrphans: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RetirementDeps extends RetirementPlanDeps {
|
|
114
|
+
/** Why the live engine, or this task's row (null: the engine alone), no
|
|
115
|
+
* longer matches the plan; null while it does. Called inside the task's
|
|
116
|
+
* lifecycle slot, right before its Stop and right before its removal. */
|
|
117
|
+
current: (taskId: string | null) => Promise<string | null>;
|
|
118
|
+
/** The task's Stop, for a caller holding its lifecycle slot. */
|
|
119
|
+
stop: (taskId: string) => Promise<{ ok: true } | { ok: false; error: string }>;
|
|
120
|
+
/** `work` in the task's lifecycle slot. */
|
|
121
|
+
inLifecycle: <T>(taskId: string, work: () => Promise<T>) => Promise<T>;
|
|
122
|
+
log: (line: string) => void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The plan's refusal: every blocker apart (`blockers`), and apart from them
|
|
127
|
+
* the `task-*` Compose projects nothing proves Uai's (all of them, for the
|
|
128
|
+
* operator to remove or rename). The message names both, as the log has.
|
|
129
|
+
* `transient`: nothing is wrong yet, the runner only cannot look now (its
|
|
130
|
+
* container runtime is still preparing); the next attempt looks again.
|
|
131
|
+
*/
|
|
132
|
+
export class RetirementBlockedError extends Error {
|
|
133
|
+
readonly blockers: string[];
|
|
134
|
+
readonly unprovenProjects: string[];
|
|
135
|
+
readonly transient: boolean;
|
|
136
|
+
constructor(message: string, options: { blockers?: string[]; unprovenProjects?: string[]; transient?: boolean } = {}) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "RetirementBlockedError";
|
|
139
|
+
this.blockers = options.blockers ?? [];
|
|
140
|
+
this.unprovenProjects = options.unprovenProjects ?? [];
|
|
141
|
+
this.transient = options.transient === true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface RetirementReport {
|
|
146
|
+
/** Tasks whose runtime is gone (or was already). */
|
|
147
|
+
retired: string[];
|
|
148
|
+
/** Apple task containers no row named, removed. */
|
|
149
|
+
orphans: string[];
|
|
150
|
+
/** What could not be proven removed: the retirement is not done. */
|
|
151
|
+
problems: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const DOCKER_PROVIDER = "docker-compose";
|
|
155
|
+
const APPLE_PROVIDER = "apple-container";
|
|
156
|
+
const TASK_ID = SAFE_HOST_TASK_ID.source.slice(1, -1);
|
|
157
|
+
const TASK_PROJECT = new RegExp(`^task-${TASK_ID}$`);
|
|
158
|
+
const APPLE_TASK_CONTAINER = new RegExp(`^moondeck-task-(${TASK_ID})-app$`);
|
|
159
|
+
|
|
160
|
+
export async function planLegacyTaskRuntimeRetirement(deps: RetirementPlanDeps): Promise<RetirementPlan> {
|
|
161
|
+
const blockers: string[] = [];
|
|
162
|
+
const snapshot = deps.snapshot();
|
|
163
|
+
if (snapshot.runtimeProblem !== null || snapshot.machine === null) {
|
|
164
|
+
throw new RetirementBlockedError(
|
|
165
|
+
`task runtimes cannot be retired now: the container runtime is not ready (${snapshot.runtimeProblem ?? "no engine is pinned"})`,
|
|
166
|
+
{ transient: true },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (!snapshot.fence.ok) blockers.push(snapshot.fence.detail ?? "the task rows do not match the pinned container runtime");
|
|
170
|
+
|
|
171
|
+
const removals: PlannedRemoval[] = [];
|
|
172
|
+
// A row with no runtime takes no part: it blocks nothing, and vouches for
|
|
173
|
+
// no Compose project or container of its task's name.
|
|
174
|
+
const rows = snapshot.tasks.filter((task) => task.hasRuntime);
|
|
175
|
+
for (const task of rows) {
|
|
176
|
+
if (!isSafeHostTaskId(task.taskId)) {
|
|
177
|
+
blockers.push(`a task row has an unsafe id ${JSON.stringify(task.taskId)}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const provider = task.provider ?? DOCKER_PROVIDER;
|
|
181
|
+
if (provider !== DOCKER_PROVIDER && provider !== APPLE_PROVIDER) {
|
|
182
|
+
blockers.push(`task ${task.taskId}: its ${provider} runtime cannot be removed with its worktree kept; close the task first`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if ((provider === APPLE_PROVIDER) !== deps.apple) {
|
|
186
|
+
blockers.push(`task ${task.taskId}: its runtime belongs to another container backend`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (provider === DOCKER_PROVIDER) {
|
|
190
|
+
const project = `task-${task.taskId}`;
|
|
191
|
+
if (task.composeProject !== null && task.composeProject !== project) {
|
|
192
|
+
blockers.push(`task ${task.taskId}: its row names Compose project ${JSON.stringify(task.composeProject)}, not its own`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
removals.push({ taskId: task.taskId, kind: "docker", project, composeFile: composeFile(deps, task) });
|
|
196
|
+
} else {
|
|
197
|
+
// appleTaskContainerName, without its assertion: rows are checked above.
|
|
198
|
+
const container = `moondeck-task-${task.taskId}-app`;
|
|
199
|
+
try {
|
|
200
|
+
if ((await appleIdentity(deps.appleCli, container)) === "foreign") {
|
|
201
|
+
blockers.push(`task ${task.taskId}: a container named ${container} exists but cannot be proven this task's; remove or rename it`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
blockers.push(`task ${task.taskId}: ${message(error)}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
removals.push({ taskId: task.taskId, kind: "apple", container });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const appleOrphans: string[] = [];
|
|
213
|
+
const unprovenProjects: string[] = [];
|
|
214
|
+
try {
|
|
215
|
+
if (deps.apple) {
|
|
216
|
+
const named = new Set(rows.map((task) => `moondeck-task-${task.taskId}-app`));
|
|
217
|
+
for (const container of await listOwnedAppleTaskContainers(deps.appleCli)) {
|
|
218
|
+
if (!named.has(container)) appleOrphans.push(container);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
const named = new Set(rows.map((task) => `task-${task.taskId}`));
|
|
222
|
+
for (const project of await listTaskProjects(deps.docker)) {
|
|
223
|
+
if (named.has(project)) continue;
|
|
224
|
+
unprovenProjects.push(project);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
blockers.push(`the task runtime inventory failed: ${message(error)}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (blockers.length > 0 || unprovenProjects.length > 0) {
|
|
232
|
+
const all = [
|
|
233
|
+
...blockers,
|
|
234
|
+
...unprovenProjects.map(
|
|
235
|
+
(project) =>
|
|
236
|
+
`Compose project ${project} has containers but no task on this station: nothing proves it Moondeck's, so it is not removed. ` +
|
|
237
|
+
`If it was a task's, remove it (docker compose -p ${project} down); otherwise rename it`,
|
|
238
|
+
),
|
|
239
|
+
];
|
|
240
|
+
throw new RetirementBlockedError(`task runtimes cannot all be retired: ${all.join("; ").slice(0, 1500)}`, {
|
|
241
|
+
blockers,
|
|
242
|
+
unprovenProjects,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return { machine: snapshot.machine, removals, appleOrphans };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function retireLegacyTaskRuntimes(plan: RetirementPlan, deps: RetirementDeps): Promise<RetirementReport> {
|
|
249
|
+
const problems: string[] = [];
|
|
250
|
+
const retired: string[] = [];
|
|
251
|
+
for (const removal of plan.removals) {
|
|
252
|
+
try {
|
|
253
|
+
await deps.inLifecycle(removal.taskId, async () => {
|
|
254
|
+
const staleBeforeStop = await deps.current(removal.taskId);
|
|
255
|
+
if (staleBeforeStop !== null) throw new Error(`not stopped: ${staleBeforeStop}`);
|
|
256
|
+
const stopped = await deps.stop(removal.taskId);
|
|
257
|
+
if (!stopped.ok && stopped.error !== "task has no running stack") {
|
|
258
|
+
// Its removal below proves what matters: the runtime gone.
|
|
259
|
+
deps.log(`[credentials] task ${removal.taskId}: stop before retirement failed (${stopped.error}); removing its runtime`);
|
|
260
|
+
}
|
|
261
|
+
const fence = async (): Promise<void> => {
|
|
262
|
+
const stale = await deps.current(removal.taskId);
|
|
263
|
+
if (stale !== null) throw new Error(`not removed: ${stale}`);
|
|
264
|
+
};
|
|
265
|
+
await (removal.kind === "apple"
|
|
266
|
+
? removeAppleContainer(deps.appleCli, removal.container, fence)
|
|
267
|
+
: removeComposeProject(deps.docker, removal.project, removal.composeFile, fence));
|
|
268
|
+
});
|
|
269
|
+
retired.push(removal.taskId);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
problems.push(`task ${removal.taskId}: ${message(error)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const orphans: string[] = [];
|
|
275
|
+
for (const container of plan.appleOrphans) {
|
|
276
|
+
try {
|
|
277
|
+
const fence = async (): Promise<void> => {
|
|
278
|
+
const stale = await deps.current(null);
|
|
279
|
+
if (stale !== null) throw new Error(`${container} not removed: ${stale}`);
|
|
280
|
+
};
|
|
281
|
+
await removeAppleContainer(deps.appleCli, container, fence);
|
|
282
|
+
orphans.push(container);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
problems.push(message(error));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { retired, orphans, problems };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function composeFile(deps: RetirementPlanDeps, task: RetirementTask): string | null {
|
|
291
|
+
if (task.worktreePath === null) return null;
|
|
292
|
+
const file = `${task.worktreePath}/.uai/docker-compose.yml`;
|
|
293
|
+
return deps.fileExists(file) ? file : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** task-up's step 2 for Docker, and then whatever Compose left, by label.
|
|
297
|
+
* `fence` re-proves the engine before every command and once more after
|
|
298
|
+
* the last, so each command and the absence proof are the planned
|
|
299
|
+
* engine's. */
|
|
300
|
+
async function removeComposeProject(
|
|
301
|
+
docker: Cli,
|
|
302
|
+
project: string,
|
|
303
|
+
file: string | null,
|
|
304
|
+
fence: () => Promise<void>,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
if (!TASK_PROJECT.test(project)) throw new Error(`refusing to remove Compose project ${JSON.stringify(project)}`);
|
|
307
|
+
await fence();
|
|
308
|
+
await docker(["compose", "-p", project, ...(file === null ? [] : ["-f", file]), "down", "--remove-orphans"], {
|
|
309
|
+
timeoutMs: 180_000,
|
|
310
|
+
maxOutputBytes: 1024 * 1024,
|
|
311
|
+
});
|
|
312
|
+
await fence();
|
|
313
|
+
let left = await projectContainers(docker, project);
|
|
314
|
+
if (left.length > 0) {
|
|
315
|
+
await fence();
|
|
316
|
+
await docker(["rm", "--force", ...left], { timeoutMs: 120_000, maxOutputBytes: 256 * 1024 });
|
|
317
|
+
await fence();
|
|
318
|
+
left = await projectContainers(docker, project);
|
|
319
|
+
}
|
|
320
|
+
await fence();
|
|
321
|
+
if (left.length > 0) throw new Error(`${left.length} container(s) of ${project} are still present`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function projectContainers(docker: Cli, project: string): Promise<string[]> {
|
|
325
|
+
const listed = await docker(["ps", "-aq", "--filter", `label=com.docker.compose.project=${project}`], {
|
|
326
|
+
timeoutMs: 30_000,
|
|
327
|
+
maxOutputBytes: 256 * 1024,
|
|
328
|
+
});
|
|
329
|
+
if (listed.status !== 0) {
|
|
330
|
+
throw new Error(`could not list ${project}'s containers: ${listed.stderr.trim().slice(0, 200) || "docker ps failed"}`);
|
|
331
|
+
}
|
|
332
|
+
return lines(listed.stdout);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Every Compose project of a task-shaped name, whatever its containers' state. */
|
|
336
|
+
async function listTaskProjects(docker: Cli): Promise<string[]> {
|
|
337
|
+
const listed = await docker(
|
|
338
|
+
["ps", "-a", "--filter", "label=com.docker.compose.project", "--format", '{{.Label "com.docker.compose.project"}}'],
|
|
339
|
+
{ timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 },
|
|
340
|
+
);
|
|
341
|
+
if (listed.status !== 0) throw new Error(listed.stderr.trim().slice(0, 200) || "docker ps failed");
|
|
342
|
+
return [...new Set(lines(listed.stdout).filter((project) => TASK_PROJECT.test(project)))];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether the container at a task's name is that task's: absent, or exactly
|
|
347
|
+
* one record whose id is the name, labelled `io.moondeck.task` with the task id
|
|
348
|
+
* the name derives from (task-down.sh's proof). Anything else is foreign.
|
|
349
|
+
* Throws when the CLI gives no answer that settles it.
|
|
350
|
+
*/
|
|
351
|
+
async function appleIdentity(cli: Cli, name: string): Promise<"absent" | "owned" | "foreign"> {
|
|
352
|
+
const taskId = APPLE_TASK_CONTAINER.exec(name)?.[1];
|
|
353
|
+
if (taskId === undefined) return "foreign";
|
|
354
|
+
const inspected = await cli(["inspect", name], { timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 });
|
|
355
|
+
if (inspected.status !== 0) {
|
|
356
|
+
if (
|
|
357
|
+
inspected.stdout.trim() === "" &&
|
|
358
|
+
inspected.stderr.split("\n").some((line) => line.trim() === `Error: container not found: ${name}`)
|
|
359
|
+
) {
|
|
360
|
+
return "absent";
|
|
361
|
+
}
|
|
362
|
+
throw new Error(`could not inspect ${name}: ${inspected.stderr.trim().slice(0, 200) || "inspect failed"}`);
|
|
363
|
+
}
|
|
364
|
+
let records: unknown;
|
|
365
|
+
try {
|
|
366
|
+
records = JSON.parse(inspected.stdout);
|
|
367
|
+
} catch {
|
|
368
|
+
return "foreign";
|
|
369
|
+
}
|
|
370
|
+
if (!Array.isArray(records) || records.length !== 1) return "foreign";
|
|
371
|
+
const record = records[0] as { id?: unknown; configuration?: { labels?: Record<string, unknown> } } | null;
|
|
372
|
+
return record?.id === name && record.configuration?.labels?.["io.moondeck.task"] === taskId ? "owned" : "foreign";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** task-up's step 2 for Apple: its identity proven, then stop, delete, and
|
|
376
|
+
* the not-found answer, `fence` re-proving the engine before every command
|
|
377
|
+
* and once more after the last. */
|
|
378
|
+
async function removeAppleContainer(cli: Cli, name: string, fence: () => Promise<void>): Promise<void> {
|
|
379
|
+
await fence();
|
|
380
|
+
const identity = await appleIdentity(cli, name);
|
|
381
|
+
await fence();
|
|
382
|
+
if (identity === "absent") return;
|
|
383
|
+
if (identity === "foreign") throw new Error(`container ${name} cannot be proven Moondeck's; it was not removed`);
|
|
384
|
+
await cli(["stop", "--time", "10", name], { timeoutMs: 60_000, maxOutputBytes: 256 * 1024 });
|
|
385
|
+
await fence();
|
|
386
|
+
await cli(["delete", "--force", name], { timeoutMs: 60_000, maxOutputBytes: 256 * 1024 });
|
|
387
|
+
await fence();
|
|
388
|
+
const after = await appleIdentity(cli, name).catch(() => "unknown");
|
|
389
|
+
await fence();
|
|
390
|
+
if (after !== "absent") throw new Error(`container ${name} could not be proven removed`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Uai's task containers no matter their state: labelled `io.moondeck.task`
|
|
394
|
+
* with exactly the task their name names. */
|
|
395
|
+
async function listOwnedAppleTaskContainers(cli: Cli): Promise<string[]> {
|
|
396
|
+
const listed = await cli(["list", "--all", "--format", "json"], { timeoutMs: 30_000, maxOutputBytes: 8 * 1024 * 1024 });
|
|
397
|
+
if (listed.status !== 0) throw new Error(listed.stderr.trim().slice(0, 200) || "container list failed");
|
|
398
|
+
let rows: unknown;
|
|
399
|
+
try {
|
|
400
|
+
rows = JSON.parse(listed.stdout);
|
|
401
|
+
} catch {
|
|
402
|
+
throw new Error("the container list is not JSON");
|
|
403
|
+
}
|
|
404
|
+
if (!Array.isArray(rows)) throw new Error("the container list is not an array");
|
|
405
|
+
const names: string[] = [];
|
|
406
|
+
for (const row of rows as Array<{ configuration?: { id?: unknown; labels?: Record<string, unknown> } }>) {
|
|
407
|
+
const id = row?.configuration?.id;
|
|
408
|
+
if (typeof id !== "string") continue;
|
|
409
|
+
const taskId = APPLE_TASK_CONTAINER.exec(id)?.[1];
|
|
410
|
+
if (taskId !== undefined && row.configuration?.labels?.["io.moondeck.task"] === taskId) names.push(id);
|
|
411
|
+
}
|
|
412
|
+
return names;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function lines(text: string): string[] {
|
|
416
|
+
return text
|
|
417
|
+
.split("\n")
|
|
418
|
+
.map((line) => line.trim())
|
|
419
|
+
.filter(Boolean);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function message(error: unknown): string {
|
|
423
|
+
return (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
424
|
+
}
|