@moondeck/station 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/0031_first_turn_receipts.sql +9 -0
- package/db/migrations/meta/_journal.json +230 -0
- package/db/schema.ts +871 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1470 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +937 -0
- package/lib/env.ts +195 -0
- package/lib/first-turn-receipts.ts +62 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/bridge-diagnostics.ts +39 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2585 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/enrollment-next-steps.ts +24 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2907 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6363 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5550 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task-down <taskId> — tear the task stack down. Idempotent (ADR-022).
|
|
3
|
+
#
|
|
4
|
+
# 1. docker compose down -v --rmi local --remove-orphans.
|
|
5
|
+
# 2. rm -rf the whole task dir — workspace + task-private Git repositories.
|
|
6
|
+
# Host caches under projects/<id>/host-cache.git persist.
|
|
7
|
+
# 3. Persist final state: status=killed (unless already error/shipped).
|
|
8
|
+
#
|
|
9
|
+
# Destruction needs explicit terminal intent: the runner sets
|
|
10
|
+
# UAI_TASK_DOWN_TERMINAL=1 only on the cloud taskDown path (a task close or
|
|
11
|
+
# orphan GC). Without it the script refuses before touching Docker or disk —
|
|
12
|
+
# a stray caller (recovery, a credential-cleanup fallback, an operator
|
|
13
|
+
# running the script by hand) must never delete a live task's worktree
|
|
14
|
+
# (2026-10-20: a cleanup fallback did, while Docker was briefly gone).
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
# shellcheck source=_common.sh
|
|
19
|
+
. "$SCRIPT_DIR/_common.sh"
|
|
20
|
+
# The station's standard image (task-up.sh STANDARD_IMAGE): local whenever a
|
|
21
|
+
# Docker task has run here, and only ever run with --pull never.
|
|
22
|
+
TASK_CLEANUP_IMAGE="moondeck-standard:dev"
|
|
23
|
+
|
|
24
|
+
[ "$#" -eq 1 ] || emit_err "BAD_ARGS" "usage: task-down <taskId>" "args"
|
|
25
|
+
|
|
26
|
+
task_id="$1"
|
|
27
|
+
# Make the ASCII security grammar independent of the operator's collation.
|
|
28
|
+
export LC_ALL=C
|
|
29
|
+
if [ "${#task_id}" -gt 128 ] ||
|
|
30
|
+
[[ ! "$task_id" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
|
|
31
|
+
emit_err "BAD_ARGS" "invalid task id" "task identity validation"
|
|
32
|
+
fi
|
|
33
|
+
# Before UAI_TASK_ID is set: a refusal must not mark the live task errored.
|
|
34
|
+
if [ "${UAI_TASK_DOWN_TERMINAL:-}" != "1" ]; then
|
|
35
|
+
emit_err "BAD_ARGS" \
|
|
36
|
+
"task-down requires terminal intent (UAI_TASK_DOWN_TERMINAL=1, set only by the cloud taskDown); refusing to destroy task $task_id" \
|
|
37
|
+
"terminal intent"
|
|
38
|
+
fi
|
|
39
|
+
UAI_TASK_ID="$task_id"
|
|
40
|
+
setup_err_trap
|
|
41
|
+
validate_task_runtime_selection
|
|
42
|
+
|
|
43
|
+
step "TASK_NOT_FOUND" "read task row"
|
|
44
|
+
# Teardown must never depend on bookkeeping: a broken/missing command
|
|
45
|
+
# snapshot (seen live 2026-07-21 cleaning up after a failed task-up —
|
|
46
|
+
# db_get_task errored, the trap reported TASK_NOT_FOUND, and the container
|
|
47
|
+
# survived) still tears the stack down by convention-derived names.
|
|
48
|
+
task_json=$(db_get_task "$task_id" 2>/dev/null) || task_json="[]"
|
|
49
|
+
row_count=$(jq 'length' <<<"$task_json" 2>/dev/null) || row_count=0
|
|
50
|
+
if [ "$row_count" -lt 1 ]; then
|
|
51
|
+
log "task row unavailable — tearing down by derived names"
|
|
52
|
+
else
|
|
53
|
+
UAI_RETRY_STATUS=$(jq -r '.[0].status // ""' <<<"$task_json")
|
|
54
|
+
fi
|
|
55
|
+
# Persisted runtime paths are recovery hints, never deletion authority. A
|
|
56
|
+
# corrupt row must not aim compose/rm outside this exact task identity.
|
|
57
|
+
compose_project=$(compose_project_for_task "$task_id")
|
|
58
|
+
task_dir="$UAI_WORKSPACE_ROOT/tasks/$task_id"
|
|
59
|
+
|
|
60
|
+
# -----------------------------------------------------------------------------
|
|
61
|
+
# 1. Compose stack.
|
|
62
|
+
# -----------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
65
|
+
step "COMPOSE_DOWN_FAILED" "remove task container"
|
|
66
|
+
app_container=$(apple_app_container_for_task "$task_id")
|
|
67
|
+
derived_image="moondeck-task-${task_id}"
|
|
68
|
+
# Ownership proof before destruction (review 2026-08-18 rounds 5+6): every
|
|
69
|
+
# container Uai creates carries io.moondeck.task=<id>. Destruction requires the
|
|
70
|
+
# inspect response to prove EXACT identity — exactly one record, whose
|
|
71
|
+
# top-level id is the requested name, carrying the exact label — the same
|
|
72
|
+
# response-confusion discipline recovery applies. A container merely
|
|
73
|
+
# OCCUPYING the reserved name, or a confused/multi-record answer, is not
|
|
74
|
+
# authority to delete anything.
|
|
75
|
+
if existing_json=$(crt inspect "$app_container" 2>/dev/null); then
|
|
76
|
+
existing_ok=$(printf '%s' "$existing_json" \
|
|
77
|
+
| jq -r --arg name "$app_container" --arg tid "$task_id" '
|
|
78
|
+
if (type == "array" and length == 1
|
|
79
|
+
and (.[0] | type == "object")
|
|
80
|
+
and .[0].id == $name
|
|
81
|
+
and ((.[0].configuration.labels["io.moondeck.task"] // "") == $tid))
|
|
82
|
+
then "ok" else "mismatch" end' 2>/dev/null) \
|
|
83
|
+
|| existing_ok="mismatch"
|
|
84
|
+
if [ "$existing_ok" != "ok" ]; then
|
|
85
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
86
|
+
"A container named $app_container exists but could not be proven as this task's own (exact identity + ownership label); Moondeck will not delete a container it cannot prove it owns. Remove or rename it, then retry." \
|
|
87
|
+
"remove task container"
|
|
88
|
+
fi
|
|
89
|
+
crt stop --time 10 "$app_container" >/dev/null 2>&1 || true
|
|
90
|
+
crt delete --force "$app_container" >/dev/null 2>&1 || true
|
|
91
|
+
fi
|
|
92
|
+
# A derived image is task-private; removing it mirrors `down --rmi local`.
|
|
93
|
+
crt image delete "$derived_image" >/dev/null 2>&1 || true
|
|
94
|
+
# Destruction is not best-effort: prove the container is gone before the
|
|
95
|
+
# writable Git data below is deleted or success is reported. A CLI failure
|
|
96
|
+
# that does not name this container as not-found proves nothing — the
|
|
97
|
+
# workspace and its Git data must survive until absence is confirmed. The
|
|
98
|
+
# Apple runtime has no per-task networks or sidecars; its one per-task
|
|
99
|
+
# volume (the ADR-118 store) is removed under the same ownership proof
|
|
100
|
+
# below.
|
|
101
|
+
if crt inspect "$app_container" >/dev/null 2>&1; then
|
|
102
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
103
|
+
"task runtime resources remain after teardown" \
|
|
104
|
+
"container verification after cleanup"
|
|
105
|
+
fi
|
|
106
|
+
if ! apple_container_proven_absent "$app_container"; then
|
|
107
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
108
|
+
"could not verify task container removal; refusing to delete the task workspace" \
|
|
109
|
+
"container verification after cleanup"
|
|
110
|
+
fi
|
|
111
|
+
# ADR-118: the task's store volume. Deleting only after the container is
|
|
112
|
+
# proven absent releases the attachment; the same exact-identity +
|
|
113
|
+
# ownership-label discipline gates destruction, and absence must be proven
|
|
114
|
+
# before success is reported — a leaked volume defeats the orphan GC that
|
|
115
|
+
# keys on this exact name. A pre-ADR-118 task simply has no such volume
|
|
116
|
+
# and passes straight through the absence proof.
|
|
117
|
+
step "COMPOSE_DOWN_FAILED" "remove task store volume"
|
|
118
|
+
store_volume=$(apple_store_volume_for_task "$task_id")
|
|
119
|
+
if store_volume_json=$(crt volume inspect "$store_volume" 2>/dev/null); then
|
|
120
|
+
store_volume_ok=$(printf '%s' "$store_volume_json" \
|
|
121
|
+
| jq -r --arg name "$store_volume" --arg tid "$task_id" '
|
|
122
|
+
if (type == "array" and length == 1
|
|
123
|
+
and (.[0] | type == "object")
|
|
124
|
+
and .[0].id == $name
|
|
125
|
+
and ((.[0].configuration.labels["io.moondeck.task"] // "") == $tid))
|
|
126
|
+
then "ok" else "mismatch" end' 2>/dev/null) \
|
|
127
|
+
|| store_volume_ok="mismatch"
|
|
128
|
+
if [ "$store_volume_ok" != "ok" ]; then
|
|
129
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
130
|
+
"A volume named $store_volume exists but could not be proven as this task's own (exact identity + ownership label); Moondeck will not delete a volume it cannot prove it owns. Remove or rename it, then retry." \
|
|
131
|
+
"remove task store volume"
|
|
132
|
+
fi
|
|
133
|
+
crt volume delete "$store_volume" >/dev/null 2>&1 || true
|
|
134
|
+
fi
|
|
135
|
+
if ! apple_volume_proven_absent "$store_volume"; then
|
|
136
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
137
|
+
"could not verify task store volume removal; refusing to delete the task workspace" \
|
|
138
|
+
"store volume verification after cleanup"
|
|
139
|
+
fi
|
|
140
|
+
else
|
|
141
|
+
uai_require_compose_plugin
|
|
142
|
+
step "COMPOSE_DOWN_FAILED" "docker compose down -v --rmi local"
|
|
143
|
+
if [ ! -L "$task_dir" ] && [ -f "$task_dir/.uai/docker-compose.yml" ]; then
|
|
144
|
+
docker compose -p "$compose_project" -f "$task_dir/.uai/docker-compose.yml" \
|
|
145
|
+
down -v --rmi local --remove-orphans >/dev/null 2>&1 || true
|
|
146
|
+
else
|
|
147
|
+
# Fallback: tear down by project name only.
|
|
148
|
+
docker compose -p "$compose_project" down -v --rmi local --remove-orphans >/dev/null 2>&1 || true
|
|
149
|
+
fi
|
|
150
|
+
# `compose down` is intentionally best-effort because an already-absent
|
|
151
|
+
# stack can make a config-less fallback fail. Destruction itself is not
|
|
152
|
+
# best-effort: before deleting the worktree or reporting success, prove that
|
|
153
|
+
# no task-owned container/network/volume remains under exact labels.
|
|
154
|
+
preview_containers=""
|
|
155
|
+
if ! preview_containers=$(docker ps -aq \
|
|
156
|
+
--filter "label=io.moondeck.preview-sidecar.task=$task_id"); then
|
|
157
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
158
|
+
"could not verify preview teardown for task $task_id" \
|
|
159
|
+
"docker ps after preview teardown"
|
|
160
|
+
fi
|
|
161
|
+
if [ -n "$preview_containers" ]; then
|
|
162
|
+
# Docker ids contain no whitespace; intentional splitting passes each exact
|
|
163
|
+
# id as its own argv entry.
|
|
164
|
+
# shellcheck disable=SC2086
|
|
165
|
+
docker rm -f $preview_containers >/dev/null 2>&1 || \
|
|
166
|
+
emit_err "COMPOSE_DOWN_FAILED" "could not remove task preview sidecars" "docker rm previews"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
remaining_containers=""
|
|
170
|
+
if ! remaining_containers=$(docker ps -aq \
|
|
171
|
+
--filter "label=com.docker.compose.project=$compose_project"); then
|
|
172
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
173
|
+
"could not verify teardown for compose project $compose_project" \
|
|
174
|
+
"docker ps after compose down"
|
|
175
|
+
fi
|
|
176
|
+
if [ -n "$remaining_containers" ]; then
|
|
177
|
+
# shellcheck disable=SC2086
|
|
178
|
+
docker rm -f $remaining_containers >/dev/null 2>&1 || \
|
|
179
|
+
emit_err "COMPOSE_DOWN_FAILED" "could not remove compose containers" "docker rm after compose down"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
remaining_networks=""
|
|
183
|
+
if ! remaining_networks=$(docker network ls -q \
|
|
184
|
+
--filter "label=com.docker.compose.project=$compose_project"); then
|
|
185
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
186
|
+
"could not verify compose networks for $compose_project" \
|
|
187
|
+
"docker network ls after compose down"
|
|
188
|
+
fi
|
|
189
|
+
if [ -n "$remaining_networks" ]; then
|
|
190
|
+
# shellcheck disable=SC2086
|
|
191
|
+
docker network rm $remaining_networks >/dev/null 2>&1 || \
|
|
192
|
+
emit_err "COMPOSE_DOWN_FAILED" "could not remove compose networks" "docker network rm"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
remaining_volumes=""
|
|
196
|
+
if ! remaining_volumes=$(docker volume ls -q \
|
|
197
|
+
--filter "label=com.docker.compose.project=$compose_project"); then
|
|
198
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
199
|
+
"could not verify compose volumes for $compose_project" \
|
|
200
|
+
"docker volume ls after compose down"
|
|
201
|
+
fi
|
|
202
|
+
if [ -n "$remaining_volumes" ]; then
|
|
203
|
+
# shellcheck disable=SC2086
|
|
204
|
+
docker volume rm $remaining_volumes >/dev/null 2>&1 || \
|
|
205
|
+
emit_err "COMPOSE_DOWN_FAILED" "could not remove compose volumes" "docker volume rm"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Re-query every exact label. A successful remove command is not itself proof,
|
|
209
|
+
# and a failed query is unknown rather than empty.
|
|
210
|
+
verified_previews=""
|
|
211
|
+
verified_containers=""
|
|
212
|
+
verified_networks=""
|
|
213
|
+
verified_volumes=""
|
|
214
|
+
if ! verified_previews=$(docker ps -aq --filter "label=io.moondeck.preview-sidecar.task=$task_id") ||
|
|
215
|
+
! verified_containers=$(docker ps -aq --filter "label=com.docker.compose.project=$compose_project") ||
|
|
216
|
+
! verified_networks=$(docker network ls -q --filter "label=com.docker.compose.project=$compose_project") ||
|
|
217
|
+
! verified_volumes=$(docker volume ls -q --filter "label=com.docker.compose.project=$compose_project"); then
|
|
218
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
219
|
+
"could not verify task runtime cleanup" \
|
|
220
|
+
"docker label verification after cleanup"
|
|
221
|
+
fi
|
|
222
|
+
if [ -n "$verified_previews" ] || [ -n "$verified_containers" ] ||
|
|
223
|
+
[ -n "$verified_networks" ] || [ -n "$verified_volumes" ]; then
|
|
224
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
225
|
+
"task runtime resources remain after teardown" \
|
|
226
|
+
"docker label verification after cleanup"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# -----------------------------------------------------------------------------
|
|
231
|
+
# 2. Remove the entire task directory (workspace + task-private Git data).
|
|
232
|
+
# No host Git command reads a repository that the just-removed container could
|
|
233
|
+
# have modified. Legacy shared-mirror worktree metadata may remain stale, but
|
|
234
|
+
# that retired `repo.git` path is never trusted by task-up again.
|
|
235
|
+
# -----------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
if [ -e "$task_dir" ] || [ -L "$task_dir" ]; then
|
|
238
|
+
step "WORKTREE_REMOVE_FAILED" "rm -rf task dir"
|
|
239
|
+
if ! rm -rf "$task_dir" 2>/dev/null \
|
|
240
|
+
&& [ "$TASK_RUNTIME_MODE" != "apple-container" ] \
|
|
241
|
+
&& [ ! -L "$task_dir" ] && [ -d "$task_dir" ]; then
|
|
242
|
+
# On Linux the task's container writes its files as node (1000) into the
|
|
243
|
+
# bind-mounted workspace, in directories without group write. A host user
|
|
244
|
+
# that is not uid 1000 (an OrbStack VM's 501, say) then can't remove them:
|
|
245
|
+
# "Permission denied", and the task stayed `stopped` (2026-11-01
|
|
246
|
+
# rehearsal). Remove what is left as root in a throwaway container that
|
|
247
|
+
# sees only this task directory: no network, every capability dropped but
|
|
248
|
+
# the two that deleting another user's files needs, and `find -delete`,
|
|
249
|
+
# which never follows a link out of the mount. The image runs by the ID
|
|
250
|
+
# its tag names right now, so the tag can't be swapped between the look
|
|
251
|
+
# and the run; without a well-formed ID nothing runs, and the absence
|
|
252
|
+
# check below reports the leftover.
|
|
253
|
+
step "WORKTREE_REMOVE_FAILED" "remove the task container's files"
|
|
254
|
+
cleanup_image_id="$(docker image inspect --format '{{.Id}}' "$TASK_CLEANUP_IMAGE" 2>/dev/null || true)"
|
|
255
|
+
if printf '%s\n' "$cleanup_image_id" | grep -Eqx 'sha256:[0-9a-f]{64}'; then
|
|
256
|
+
docker run --rm --pull never --network none --user 0:0 \
|
|
257
|
+
--cap-drop ALL --cap-add DAC_OVERRIDE --cap-add FOWNER \
|
|
258
|
+
--security-opt no-new-privileges --entrypoint /usr/bin/find \
|
|
259
|
+
-v "$task_dir:/task" "$cleanup_image_id" /task -mindepth 1 -delete \
|
|
260
|
+
>/dev/null 2>&1 || true
|
|
261
|
+
fi
|
|
262
|
+
rm -rf "$task_dir" 2>/dev/null || true
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
[ ! -e "$task_dir" ] && [ ! -L "$task_dir" ] || \
|
|
266
|
+
emit_err "WORKTREE_REMOVE_FAILED" "task directory remains after teardown" "task dir verification"
|
|
267
|
+
|
|
268
|
+
# -----------------------------------------------------------------------------
|
|
269
|
+
# 3. Persist final state.
|
|
270
|
+
# -----------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
current_status=$(jq -r '.[0].status // ""' <<<"$task_json")
|
|
273
|
+
if [ "$current_status" != "shipped" ] && [ "$current_status" != "error" ]; then
|
|
274
|
+
new_status="killed"
|
|
275
|
+
else
|
|
276
|
+
new_status="$current_status"
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
step "DB_UPDATE_FAILED" "persist final state"
|
|
280
|
+
db_update_task "$task_id" \
|
|
281
|
+
"status='$new_status'" \
|
|
282
|
+
"code_server_port=NULL" \
|
|
283
|
+
"preview_ports='[]'" \
|
|
284
|
+
"compose_project=NULL" \
|
|
285
|
+
"worktree_path=NULL" \
|
|
286
|
+
"locked_at=NULL" \
|
|
287
|
+
"ended_at=(unixepoch()*1000)"
|
|
288
|
+
|
|
289
|
+
emit_event "$task_id" "status_changed" "$(jq -nc --arg f "$current_status" --arg t "$new_status" '{from:$f,to:$t}')"
|
|
290
|
+
|
|
291
|
+
emit_ok "$(jq -nc --arg s "$new_status" '{status:$s}')"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task-status <taskId> — introspect the actual runtime state.
|
|
3
|
+
#
|
|
4
|
+
# Returns the truth from docker / git / process listings rather than
|
|
5
|
+
# trusting the Task row. Used by POST /api/tasks/:id/reconcile to detect
|
|
6
|
+
# drift after a manual `docker compose down` or similar.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
# shellcheck source=_common.sh
|
|
11
|
+
. "$SCRIPT_DIR/_common.sh"
|
|
12
|
+
|
|
13
|
+
[ "$#" -eq 1 ] || emit_err "BAD_ARGS" "usage: task-status <taskId>" "args"
|
|
14
|
+
|
|
15
|
+
task_id="$1"
|
|
16
|
+
UAI_TASK_ID="$task_id"
|
|
17
|
+
setup_err_trap
|
|
18
|
+
|
|
19
|
+
step "TASK_NOT_FOUND" "read task row"
|
|
20
|
+
validate_task_runtime_selection
|
|
21
|
+
|
|
22
|
+
task_json=$(db_get_task "$task_id")
|
|
23
|
+
[ "$(jq 'length' <<<"$task_json")" -ge 1 ] \
|
|
24
|
+
|| emit_err "TASK_NOT_FOUND" "no such task: $task_id" "read task row"
|
|
25
|
+
|
|
26
|
+
compose_project=$(jq -r '.[0].compose_project // ""' <<<"$task_json")
|
|
27
|
+
worktree_path=$(jq -r '.[0].worktree_path // ""' <<<"$task_json")
|
|
28
|
+
|
|
29
|
+
# Containers running under this task, JSON array of names.
|
|
30
|
+
step "DOCKER_PS_FAILED" "docker ps"
|
|
31
|
+
containers_json="[]"
|
|
32
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
33
|
+
# Absence (inspect exit 1) is a definitive "not running" answer, never an
|
|
34
|
+
# unknown-state error; only a live "running" state lists the container.
|
|
35
|
+
apple_app=$(apple_app_container_for_task "$task_id")
|
|
36
|
+
apple_state=$(crt inspect "$apple_app" 2>/dev/null \
|
|
37
|
+
| jq -r '.[0].status.state // empty' 2>/dev/null) || apple_state=""
|
|
38
|
+
if [ "$apple_state" = "running" ]; then
|
|
39
|
+
containers_json=$(jq -nc --arg n "$apple_app" '[$n]')
|
|
40
|
+
fi
|
|
41
|
+
elif [ -n "$compose_project" ]; then
|
|
42
|
+
if ! raw=$(docker ps --filter "label=com.docker.compose.project=$compose_project" --format '{{.Names}}'); then
|
|
43
|
+
jq -nc \
|
|
44
|
+
--arg m "docker ps did not answer; runtime state is unknown" \
|
|
45
|
+
'{ok:false,error:{code:"DOCKER_PS_FAILED",message:$m,step:"docker ps"}}' >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
if [ -n "$raw" ]; then
|
|
49
|
+
containers_json=$(printf '%s\n' "$raw" | jq -R . | jq -sc .)
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Compose running iff at least one container is up.
|
|
54
|
+
if [ "$containers_json" = "[]" ]; then
|
|
55
|
+
compose_running="false"
|
|
56
|
+
else
|
|
57
|
+
compose_running="true"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Worktree present iff the directory exists.
|
|
61
|
+
worktree_present="false"
|
|
62
|
+
if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
|
|
63
|
+
worktree_present="true"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
emit_ok "$(jq -nc \
|
|
67
|
+
--argjson cr "$compose_running" \
|
|
68
|
+
--argjson cs "$containers_json" \
|
|
69
|
+
--argjson wp "$worktree_present" \
|
|
70
|
+
'{composeRunning:$cr,containers:$cs,worktreePresent:$wp}')"
|