@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,633 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# uai agent — shared helpers.
|
|
3
|
+
#
|
|
4
|
+
# Every agent script source's this file at the top. It establishes the
|
|
5
|
+
# strict-mode posture, structured JSON output contract, sqlite access
|
|
6
|
+
# helpers, and a uniform error trap.
|
|
7
|
+
#
|
|
8
|
+
# Output contract (docs/agent.md):
|
|
9
|
+
#
|
|
10
|
+
# success → stdout: {"ok":true,"data":{...}}
|
|
11
|
+
# failure → stderr: {"ok":false,"error":{"code":"...","message":"...","step":"..."}}
|
|
12
|
+
# exit non-zero.
|
|
13
|
+
#
|
|
14
|
+
# Environment:
|
|
15
|
+
# UAI_DB_PATH absolute path to uai.sqlite (required)
|
|
16
|
+
# UAI_OWNER_HOME host $HOME (for ~/.claude, ~/.codex) (default: $HOME)
|
|
17
|
+
# UAI_TTYD_BIND ttyd listen address (default: 127.0.0.1)
|
|
18
|
+
# UAI_LOG_DIR directory for ttyd / docker logs (default: $UAI_DATA_DIR/logs)
|
|
19
|
+
# UAI_DATA_DIR base data dir (default: dirname of UAI_DB_PATH)
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Strict env.
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
: "${UAI_DB_PATH:?UAI_DB_PATH must be set (path to uai.sqlite)}"
|
|
28
|
+
|
|
29
|
+
UAI_DATA_DIR="${UAI_DATA_DIR:-$(dirname "$UAI_DB_PATH")}"
|
|
30
|
+
UAI_OWNER_HOME="${UAI_OWNER_HOME:-$HOME}"
|
|
31
|
+
# The station's storage tree (lib/env.ts: ~/.moondeck unless configured).
|
|
32
|
+
# task-up / task-down derive the per-project mirrors, per-task dirs,
|
|
33
|
+
# browsers and shared files from here. The station passes it explicitly
|
|
34
|
+
# (lib/agent.ts); there is no default, because a wrong one builds a task
|
|
35
|
+
# somewhere the station then refuses (rehearsal #2, 2026-11-02).
|
|
36
|
+
: "${UAI_WORKSPACE_ROOT:?UAI_WORKSPACE_ROOT must be set (the station passes its workspace root)}"
|
|
37
|
+
UAI_TTYD_BIND="${UAI_TTYD_BIND:-127.0.0.1}"
|
|
38
|
+
UAI_LOG_DIR="${UAI_LOG_DIR:-$UAI_DATA_DIR/logs}"
|
|
39
|
+
mkdir -p "$UAI_LOG_DIR"
|
|
40
|
+
|
|
41
|
+
# Optional dependencies — fail fast with a structured error if missing.
|
|
42
|
+
# docker is only a dependency of the docker runtime mode: the Apple runtime
|
|
43
|
+
# path must work on a Docker-free Mac (ADR-106), and its own CLI is validated
|
|
44
|
+
# by validate_task_runtime_selection instead.
|
|
45
|
+
agent_required_bins="sqlite3 jq git"
|
|
46
|
+
if [ "${UAI_TASK_RUNTIME:-docker}" != "apple-container" ]; then
|
|
47
|
+
agent_required_bins="$agent_required_bins docker"
|
|
48
|
+
fi
|
|
49
|
+
# Collect EVERY missing tool before failing: reporting them one at a time
|
|
50
|
+
# cost one full fix-and-Resume round trip per binary on a fresh Amazon
|
|
51
|
+
# Linux box (sqlite3, then jq — live 2026-08-21). Bash 3.2: plain string
|
|
52
|
+
# accumulation, no arrays.
|
|
53
|
+
missing_bins=""
|
|
54
|
+
for bin in $agent_required_bins; do
|
|
55
|
+
if ! command -v "$bin" >/dev/null 2>&1; then
|
|
56
|
+
missing_bins="$missing_bins $bin"
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
if [ -n "$missing_bins" ]; then
|
|
60
|
+
missing_bins="${missing_bins# }"
|
|
61
|
+
printf '{"ok":false,"error":{"code":"MISSING_DEPENDENCY","message":"missing on PATH: %s — install with your package manager (dnf/apt/brew), then Resume"}}\n' "$missing_bins" >&2
|
|
62
|
+
exit 127
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# The docker BINARY existing does not make `docker compose` work: the v2
|
|
67
|
+
# compose plugin ships separately, and a bare-engine install (Amazon Linux
|
|
68
|
+
# 2023's docker package) answers `docker compose` with the top-level help
|
|
69
|
+
# text — the operator saw exactly that as their task error (live
|
|
70
|
+
# 2026-08-21). Called LAZILY by the task scripts right before their first
|
|
71
|
+
# compose use, never at source time: argument validation must precede any
|
|
72
|
+
# docker invocation (the task-down/task-up tests enforce that ordering).
|
|
73
|
+
uai_require_compose_plugin() {
|
|
74
|
+
[ "${UAI_TASK_RUNTIME:-docker}" = "apple-container" ] && return 0
|
|
75
|
+
if ! docker compose version >/dev/null 2>&1; then
|
|
76
|
+
printf '{"ok":false,"error":{"code":"MISSING_DEPENDENCY","message":"missing on PATH: docker-compose-plugin — install the Docker Compose v2 plugin, then Resume"}}\n' >&2
|
|
77
|
+
exit 127
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Logger. All log lines go to stderr so stdout stays reserved for JSON.
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
_uai_ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
86
|
+
|
|
87
|
+
log() { printf '[uai-agent %s] %s\n' "$(_uai_ts)" "$*" >&2; }
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# JSON output helpers.
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
# emit_ok '{"foo":"bar"}'
|
|
94
|
+
#
|
|
95
|
+
# NOTE: do not use `${1:-{}}` here — bash parses `{` inside the default
|
|
96
|
+
# as a literal and the matching `}` closes the substitution prematurely,
|
|
97
|
+
# so a real payload gets a trailing `}` appended. Use an explicit check.
|
|
98
|
+
emit_ok() {
|
|
99
|
+
local data="$1"
|
|
100
|
+
if [ -z "$data" ]; then data='{}'; fi
|
|
101
|
+
printf '{"ok":true,"data":%s}\n' "$data"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# emit_err <code> <message> [step]
|
|
105
|
+
#
|
|
106
|
+
# Persists status='error' on the current task (if UAI_TASK_ID is set),
|
|
107
|
+
# logs an `error` event, prints structured JSON to stderr, and exits 1.
|
|
108
|
+
emit_err() {
|
|
109
|
+
local code="$1" msg="$2" step="${3:-${UAI_CURRENT_STEP:-}}"
|
|
110
|
+
_uai_record_failure "$code" "$step"
|
|
111
|
+
local json
|
|
112
|
+
if [ -n "$step" ]; then
|
|
113
|
+
json=$(jq -nc --arg c "$code" --arg m "$msg" --arg s "$step" \
|
|
114
|
+
'{ok:false,error:{code:$c,message:$m,step:$s}}')
|
|
115
|
+
else
|
|
116
|
+
json=$(jq -nc --arg c "$code" --arg m "$msg" \
|
|
117
|
+
'{ok:false,error:{code:$c,message:$m}}')
|
|
118
|
+
fi
|
|
119
|
+
printf '%s\n' "$json" >&2
|
|
120
|
+
exit 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Docker can disappear after the TypeScript admission check but before a
|
|
124
|
+
# minutes-long compose/init command finishes. These codes describe steps whose
|
|
125
|
+
# failure may be a daemon outage. Keep the command snapshot retryable here; the
|
|
126
|
+
# host process re-probes `docker info` and records a terminal error only when
|
|
127
|
+
# the daemon is positively reachable and the operation itself really failed.
|
|
128
|
+
_uai_is_runtime_sensitive_error() {
|
|
129
|
+
case "$1" in
|
|
130
|
+
STANDARD_IMAGE_MISSING|COMPOSE_UP_FAILED|COMPOSE_DOWN_FAILED|CONTAINER_INIT_FAILED|DOCKER_PS_FAILED)
|
|
131
|
+
return 0
|
|
132
|
+
;;
|
|
133
|
+
*)
|
|
134
|
+
return 1
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_uai_mark_retryable() {
|
|
140
|
+
if [ -n "${UAI_TASK_ID:-}" ] && [ -n "${UAI_DB_PATH:-}" ]; then
|
|
141
|
+
local restore=""
|
|
142
|
+
if [ -n "${UAI_RETRY_STATUS:-}" ]; then
|
|
143
|
+
restore=", status='$(sql_escape "$UAI_RETRY_STATUS")'"
|
|
144
|
+
fi
|
|
145
|
+
sqlite_exec "UPDATE uai_tasks SET locked_at=NULL${restore}, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$UAI_TASK_ID")'" 2>/dev/null || true
|
|
146
|
+
fi
|
|
147
|
+
UAI_LOCK_HELD=""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_uai_record_failure() {
|
|
151
|
+
local code="$1" step="$2"
|
|
152
|
+
if _uai_is_runtime_sensitive_error "$code"; then
|
|
153
|
+
_uai_mark_retryable
|
|
154
|
+
else
|
|
155
|
+
_uai_mark_error "$code" "$step"
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# _uai_mark_error <code> <step> — best-effort persist of status=error.
|
|
160
|
+
# Shared between emit_err (explicit) and the ERR trap (set -e).
|
|
161
|
+
_uai_mark_error() {
|
|
162
|
+
local code="$1" step="$2"
|
|
163
|
+
if [ -n "${UAI_TASK_ID:-}" ] && [ -n "${UAI_DB_PATH:-}" ]; then
|
|
164
|
+
sqlite_exec "UPDATE uai_tasks SET status='error', locked_at=NULL, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$UAI_TASK_ID")'" 2>/dev/null || true
|
|
165
|
+
local payload
|
|
166
|
+
payload=$(jq -nc --arg c "$code" --arg s "$step" '{code:$c,step:$s}')
|
|
167
|
+
sqlite_exec "INSERT INTO uai_task_events (id, task_id, kind, payload) VALUES ('$(sql_escape "$(_uai_event_id)")', '$(sql_escape "$UAI_TASK_ID")', 'error', '$(sql_escape "$payload")')" 2>/dev/null || true
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# SQLite helpers. The DB is local SQLite; concurrency is single-writer per
|
|
173
|
+
# task (enforced via lock_task below + Next.js).
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
# sqlite_exec "<sql>"
|
|
177
|
+
sqlite_exec() {
|
|
178
|
+
sqlite3 "$UAI_DB_PATH" "$1"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# sqlite_json "<sql>" → JSON array
|
|
182
|
+
sqlite_json() {
|
|
183
|
+
sqlite3 -json "$UAI_DB_PATH" "$1"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# sql_escape <value> → single-quote-safe string for inline SQL.
|
|
187
|
+
#
|
|
188
|
+
# We pipe through sed instead of using bash parameter expansion because
|
|
189
|
+
# bash 3.2 (macOS default) keeps backslashes in `${var//\'/\'\'}` —
|
|
190
|
+
# producing `you\'\'re` from `you're` and breaking SQL string parsing.
|
|
191
|
+
# sed handles it identically on bash 3.2 and 5+.
|
|
192
|
+
sql_escape() {
|
|
193
|
+
printf '%s' "$1" | sed "s/'/''/g"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# db_get_task <id> → JSON array (one row) or "[]"
|
|
197
|
+
db_get_task() {
|
|
198
|
+
local out
|
|
199
|
+
out=$(sqlite_json "SELECT * FROM uai_tasks WHERE id='$(sql_escape "$1")'")
|
|
200
|
+
printf '%s' "${out:-[]}"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# db_get_projects_for_task <taskId> → JSON array of the task's selected
|
|
204
|
+
# projects, in position order. Each element carries every uai_projects
|
|
205
|
+
# column plus the `position` from uai_task_projects. Empty array when the
|
|
206
|
+
# task has no selected projects (scratchpad).
|
|
207
|
+
db_get_projects_for_task() {
|
|
208
|
+
local out
|
|
209
|
+
out=$(sqlite_json "SELECT p.*, tp.position AS position FROM uai_projects p JOIN uai_task_projects tp ON tp.project_id = p.id WHERE tp.task_id = '$(sql_escape "$1")' ORDER BY tp.position ASC")
|
|
210
|
+
printf '%s' "${out:-[]}"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# db_update_task <id> "col=value" "col=value" ...
|
|
214
|
+
#
|
|
215
|
+
# Values must be pre-quoted by the caller (e.g. status='running', port=42).
|
|
216
|
+
db_update_task() {
|
|
217
|
+
local id="$1"; shift
|
|
218
|
+
local set_clause
|
|
219
|
+
set_clause=$(IFS=','; echo "$*")
|
|
220
|
+
sqlite_exec "UPDATE uai_tasks SET ${set_clause}, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")'"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# emit_event <task_id> <kind> <payload-json>
|
|
224
|
+
emit_event() {
|
|
225
|
+
local tid="$1" kind="$2" payload="$3"
|
|
226
|
+
if [ -z "$payload" ]; then payload='{}'; fi
|
|
227
|
+
local eid
|
|
228
|
+
eid=$(_uai_event_id)
|
|
229
|
+
sqlite_exec "INSERT INTO uai_task_events (id, task_id, kind, payload) VALUES ('$(sql_escape "$eid")', '$(sql_escape "$tid")', '$(sql_escape "$kind")', '$(sql_escape "$payload")')"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# _uai_event_id — ULID-shaped fallback id generator (Crockford base32 prefix).
|
|
233
|
+
# Real ULIDs come from the Next.js layer; agent-side events use ev_<unix><rand>.
|
|
234
|
+
_uai_event_id() {
|
|
235
|
+
printf 'ev_%s%s' "$(date +%s%N | cut -c1-13)" "$(_uai_rand 10)"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_uai_rand() {
|
|
239
|
+
# Lowercase alphanumeric, length $1. Avoids depending on `uuidgen` etc.
|
|
240
|
+
local n="$1"
|
|
241
|
+
LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c "$n"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Locking. The Task row carries `locked_at`. Next.js also checks this
|
|
246
|
+
# before invoking the agent; the shell-side lock is a safety net for
|
|
247
|
+
# concurrent invocations of the same script.
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
lock_task() {
|
|
251
|
+
local id="$1"
|
|
252
|
+
# Atomic CAS via UPDATE WHERE locked_at IS NULL; success = changes()==1.
|
|
253
|
+
local changes
|
|
254
|
+
changes=$(sqlite_exec "UPDATE uai_tasks SET locked_at=(unixepoch()*1000), updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")' AND locked_at IS NULL; SELECT changes();")
|
|
255
|
+
[ "$changes" = "1" ] || emit_err "TASK_LOCKED" "task $id is already locked" "lock"
|
|
256
|
+
UAI_LOCK_HELD="$id"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
unlock_task() {
|
|
260
|
+
local id="$1"
|
|
261
|
+
sqlite_exec "UPDATE uai_tasks SET locked_at=NULL, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")'"
|
|
262
|
+
UAI_LOCK_HELD=""
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Step tracking + ERR trap.
|
|
267
|
+
#
|
|
268
|
+
# `step <code> <label>` declares the next risky operation. If the script
|
|
269
|
+
# exits non-zero, the trap emits a structured error tagged with the
|
|
270
|
+
# current step's code + label, marks the task `status=error`, and
|
|
271
|
+
# releases the lock.
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
UAI_CURRENT_STEP=""
|
|
275
|
+
UAI_ERROR_CODE="UNKNOWN"
|
|
276
|
+
UAI_TASK_ID=""
|
|
277
|
+
UAI_LOCK_HELD=""
|
|
278
|
+
UAI_RETRY_STATUS=""
|
|
279
|
+
UAI_PROJECT_LOCK_PATH=""
|
|
280
|
+
UAI_PROJECT_LOCK_TOKEN=""
|
|
281
|
+
|
|
282
|
+
# Shared project mirrors are mutated by task-up and task-down across different
|
|
283
|
+
# task processes. A task-row lock cannot serialize those operations, so use a
|
|
284
|
+
# portable atomic directory lock (macOS does not ship `flock`). Never reclaim
|
|
285
|
+
# a stale lock automatically: checking a PID and unlinking a fixed path creates
|
|
286
|
+
# a TOCTOU window where a new owner's lock can be deleted. A stale directory is
|
|
287
|
+
# deliberately surfaced for an operator to inspect and remove.
|
|
288
|
+
acquire_project_lock() {
|
|
289
|
+
local projects_root="$1" project_id="$2"
|
|
290
|
+
local project_parent="$projects_root/$project_id"
|
|
291
|
+
local lock_path="$project_parent/.uai-repo.lock"
|
|
292
|
+
local owner_pid attempts=0
|
|
293
|
+
mkdir -p "$project_parent"
|
|
294
|
+
while ! mkdir "$lock_path" 2>/dev/null; do
|
|
295
|
+
if [ ! -d "$lock_path" ]; then
|
|
296
|
+
emit_err "FETCH_FAILED" \
|
|
297
|
+
"project $project_id has an obsolete or invalid repository lock at $lock_path. Confirm no task-up/task-down process is active, remove that lock manually, then retry." \
|
|
298
|
+
"acquire project cache lock"
|
|
299
|
+
fi
|
|
300
|
+
owner_pid=$(cat "$lock_path/pid" 2>/dev/null || true)
|
|
301
|
+
case "$owner_pid" in
|
|
302
|
+
''|*[!0-9]*) ;;
|
|
303
|
+
*)
|
|
304
|
+
if ! kill -0 "$owner_pid" 2>/dev/null; then
|
|
305
|
+
emit_err "FETCH_FAILED" \
|
|
306
|
+
"project $project_id has a stale repository lock at $lock_path (owner pid $owner_pid). Confirm no task-up/task-down process is active, remove that lock directory manually, then retry." \
|
|
307
|
+
"acquire project cache lock"
|
|
308
|
+
fi
|
|
309
|
+
;;
|
|
310
|
+
esac
|
|
311
|
+
attempts=$((attempts + 1))
|
|
312
|
+
if [ "$attempts" -ge 300 ]; then
|
|
313
|
+
emit_err "FETCH_FAILED" \
|
|
314
|
+
"project $project_id is busy in another host operation, or has an incomplete lock at $lock_path. Confirm no task-up/task-down process is active; if none is, remove that lock directory manually, then retry." \
|
|
315
|
+
"acquire project cache lock"
|
|
316
|
+
fi
|
|
317
|
+
sleep 0.1
|
|
318
|
+
done
|
|
319
|
+
UAI_PROJECT_LOCK_TOKEN="$$:${RANDOM:-0}"
|
|
320
|
+
printf '%s\n' "$$" > "$lock_path/pid"
|
|
321
|
+
printf '%s\n' "$UAI_PROJECT_LOCK_TOKEN" > "$lock_path/owner"
|
|
322
|
+
UAI_PROJECT_LOCK_PATH="$lock_path"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
release_project_lock() {
|
|
326
|
+
[ -n "$UAI_PROJECT_LOCK_PATH" ] || return 0
|
|
327
|
+
local owner
|
|
328
|
+
owner=$(cat "$UAI_PROJECT_LOCK_PATH/owner" 2>/dev/null || true)
|
|
329
|
+
if [ -n "$UAI_PROJECT_LOCK_TOKEN" ] \
|
|
330
|
+
&& [ "$owner" = "$UAI_PROJECT_LOCK_TOKEN" ]; then
|
|
331
|
+
rm -f "$UAI_PROJECT_LOCK_PATH/pid" "$UAI_PROJECT_LOCK_PATH/owner" \
|
|
332
|
+
2>/dev/null || true
|
|
333
|
+
rmdir "$UAI_PROJECT_LOCK_PATH" 2>/dev/null || true
|
|
334
|
+
fi
|
|
335
|
+
UAI_PROJECT_LOCK_PATH=""
|
|
336
|
+
UAI_PROJECT_LOCK_TOKEN=""
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_uai_on_exit() {
|
|
340
|
+
release_project_lock || true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
step() {
|
|
344
|
+
UAI_ERROR_CODE="$1"
|
|
345
|
+
UAI_CURRENT_STEP="$2"
|
|
346
|
+
log "step: $UAI_CURRENT_STEP"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_uai_on_err() {
|
|
350
|
+
local rc=$?
|
|
351
|
+
# Disable further ERR trap recursion.
|
|
352
|
+
trap - ERR
|
|
353
|
+
|
|
354
|
+
local code="$UAI_ERROR_CODE"
|
|
355
|
+
local stepname="$UAI_CURRENT_STEP"
|
|
356
|
+
|
|
357
|
+
_uai_record_failure "$code" "$stepname"
|
|
358
|
+
|
|
359
|
+
local out
|
|
360
|
+
out=$(jq -nc --arg c "$code" --arg m "agent step failed" --arg s "$stepname" --argjson rc "$rc" \
|
|
361
|
+
'{ok:false,error:{code:$c,message:$m,step:$s,exit_code:$rc}}')
|
|
362
|
+
printf '%s\n' "$out" >&2
|
|
363
|
+
exit "$rc"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setup_err_trap() {
|
|
367
|
+
trap _uai_on_err ERR
|
|
368
|
+
trap _uai_on_exit EXIT
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# Compose project name + container name conventions.
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
# Belt-and-braces: docker compose only accepts lowercase project names.
|
|
376
|
+
# We also lowercase ulids at generation (lib/ulid.ts) but defensive
|
|
377
|
+
# lowercasing here protects against any caller passing mixed case.
|
|
378
|
+
_uai_lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
379
|
+
compose_project_for_task() { printf 'task-%s' "$(_uai_lower "$1")"; }
|
|
380
|
+
# Compose v2 names containers as `<project>-<service>-<replica>`. We
|
|
381
|
+
# never scale beyond one replica, so the suffix is always `-1`. If we
|
|
382
|
+
# ever do scale, swap to discovering the container name via the
|
|
383
|
+
# `com.docker.compose.project` + `com.docker.compose.service` labels.
|
|
384
|
+
app_container_for_task() { printf 'task-%s-app-1' "$(_uai_lower "$1")"; }
|
|
385
|
+
# ADR-106: the Apple runtime has no Compose, so its app container drops the
|
|
386
|
+
# replica suffix. Must match appleTaskContainerName in
|
|
387
|
+
# lib/task-environment/apple-container.ts.
|
|
388
|
+
apple_app_container_for_task() { printf 'moondeck-task-%s-app' "$(_uai_lower "$1")"; }
|
|
389
|
+
# ADR-118: per-task block volume carrying the pnpm store. Must match
|
|
390
|
+
# appleTaskStoreVolumeName in lib/task-environment/apple-container.ts.
|
|
391
|
+
apple_store_volume_for_task() { printf 'moondeck-store-%s' "$(_uai_lower "$1")"; }
|
|
392
|
+
|
|
393
|
+
# -----------------------------------------------------------------------------
|
|
394
|
+
# ADR-106 container runtime selection. The host publishes the selected backend
|
|
395
|
+
# in UAI_TASK_RUNTIME (+ UAI_CONTAINER_CLI for the bundled Apple toolchain);
|
|
396
|
+
# `docker` keeps every historical path byte-for-byte.
|
|
397
|
+
# -----------------------------------------------------------------------------
|
|
398
|
+
TASK_RUNTIME_MODE="${UAI_TASK_RUNTIME:-docker}"
|
|
399
|
+
CONTAINER_CLI="${UAI_CONTAINER_CLI:-}"
|
|
400
|
+
|
|
401
|
+
validate_task_runtime_selection() {
|
|
402
|
+
case "$TASK_RUNTIME_MODE" in
|
|
403
|
+
docker|apple-container) ;;
|
|
404
|
+
*)
|
|
405
|
+
emit_err "BAD_ARGS" \
|
|
406
|
+
"unsupported UAI_TASK_RUNTIME '$TASK_RUNTIME_MODE'" \
|
|
407
|
+
"select container runtime"
|
|
408
|
+
;;
|
|
409
|
+
esac
|
|
410
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
411
|
+
case "$CONTAINER_CLI" in
|
|
412
|
+
/*) ;;
|
|
413
|
+
*)
|
|
414
|
+
emit_err "BAD_ARGS" \
|
|
415
|
+
"UAI_CONTAINER_CLI must be an absolute path to the bundled container CLI" \
|
|
416
|
+
"select container runtime"
|
|
417
|
+
;;
|
|
418
|
+
esac
|
|
419
|
+
if [ -L "$CONTAINER_CLI" ] || [ ! -f "$CONTAINER_CLI" ] \
|
|
420
|
+
|| [ ! -x "$CONTAINER_CLI" ]; then
|
|
421
|
+
emit_err "BAD_ARGS" \
|
|
422
|
+
"the bundled container CLI at $CONTAINER_CLI is missing or not executable" \
|
|
423
|
+
"select container runtime"
|
|
424
|
+
fi
|
|
425
|
+
fi
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Single dispatcher for the verbs whose flags are identical across Docker and
|
|
429
|
+
# the Apple CLI (exec/cp/stop/inspect/image inspect/volume create). Verbs that
|
|
430
|
+
# differ (compose, run, build, port) branch explicitly at their call sites.
|
|
431
|
+
crt() {
|
|
432
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
433
|
+
"$CONTAINER_CLI" "$@"
|
|
434
|
+
else
|
|
435
|
+
docker "$@"
|
|
436
|
+
fi
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Absence proof for the Apple runtime. The CLI exits nonzero for BOTH a
|
|
440
|
+
# missing container and a broken apiserver; only the not-found error that
|
|
441
|
+
# names this exact container proves absence. Everything else proves nothing
|
|
442
|
+
# and must never authorize destructive work.
|
|
443
|
+
apple_container_proven_absent() {
|
|
444
|
+
apple_absent_name="$1"
|
|
445
|
+
# One CLI response supplies the whole proof: nonzero exit, empty stdout,
|
|
446
|
+
# and the complete not-found error line naming this exact container (never
|
|
447
|
+
# a substring — a not-found answer for "$name-shadow" must not prove the
|
|
448
|
+
# absence of "$name"). Two separate invocations could each satisfy half
|
|
449
|
+
# while no single response satisfies both.
|
|
450
|
+
apple_absent_dir=$(mktemp -d "${TMPDIR:-/tmp}/uai-absent.XXXXXX") \
|
|
451
|
+
|| return 1
|
|
452
|
+
apple_absent_rc=0
|
|
453
|
+
"$CONTAINER_CLI" inspect "$apple_absent_name" \
|
|
454
|
+
>"$apple_absent_dir/out" 2>"$apple_absent_dir/err" \
|
|
455
|
+
|| apple_absent_rc=$?
|
|
456
|
+
apple_absent_proven=1
|
|
457
|
+
[ "$apple_absent_rc" -ne 0 ] || apple_absent_proven=0
|
|
458
|
+
[ ! -s "$apple_absent_dir/out" ] || apple_absent_proven=0
|
|
459
|
+
grep -Fqx "Error: container not found: $apple_absent_name" \
|
|
460
|
+
"$apple_absent_dir/err" 2>/dev/null || apple_absent_proven=0
|
|
461
|
+
rm -rf "$apple_absent_dir"
|
|
462
|
+
[ "$apple_absent_proven" -eq 1 ]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# ADR-118: same single-response absence discipline for a named volume. Only
|
|
466
|
+
# the exact whole-line not-found grammar naming this volume proves absence;
|
|
467
|
+
# any other failure (broken apiserver, timeout) proves nothing.
|
|
468
|
+
apple_volume_proven_absent() {
|
|
469
|
+
apple_vabsent_name="$1"
|
|
470
|
+
apple_vabsent_dir=$(mktemp -d "${TMPDIR:-/tmp}/uai-vabsent.XXXXXX") \
|
|
471
|
+
|| return 1
|
|
472
|
+
apple_vabsent_rc=0
|
|
473
|
+
"$CONTAINER_CLI" volume inspect "$apple_vabsent_name" \
|
|
474
|
+
>"$apple_vabsent_dir/out" 2>"$apple_vabsent_dir/err" \
|
|
475
|
+
|| apple_vabsent_rc=$?
|
|
476
|
+
apple_vabsent_proven=1
|
|
477
|
+
[ "$apple_vabsent_rc" -ne 0 ] || apple_vabsent_proven=0
|
|
478
|
+
[ ! -s "$apple_vabsent_dir/out" ] || apple_vabsent_proven=0
|
|
479
|
+
grep -Fqx "Error: volume not found: $apple_vabsent_name" \
|
|
480
|
+
"$apple_vabsent_dir/err" 2>/dev/null || apple_vabsent_proven=0
|
|
481
|
+
rm -rf "$apple_vabsent_dir"
|
|
482
|
+
[ "$apple_vabsent_proven" -eq 1 ]
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Canonicalise every GitHub URL shape to credential-free HTTPS. task-up selects
|
|
486
|
+
# the credential per task: the creator's connected GitHub token is primary;
|
|
487
|
+
# SSH is a fallback only when that user has no GitHub connection on this host.
|
|
488
|
+
# Keeping the stored/shared origin transport-neutral avoids binding a project
|
|
489
|
+
# to whichever user's credential happened to touch its mirror first.
|
|
490
|
+
#
|
|
491
|
+
# Mirrors normalizeRepoUrl() in lib/github/repo-url.ts — keep the two in step.
|
|
492
|
+
# Non-GitHub remotes are returned untouched: we only know how to speak for
|
|
493
|
+
# GitHub, and Uai supports any git remote.
|
|
494
|
+
#
|
|
495
|
+
# Bash 3.2 (macOS /bin/bash): no ${var,,}, no associative arrays.
|
|
496
|
+
normalize_github_repo_url() {
|
|
497
|
+
local input="$1" url after_scheme authority host_port host host_lc
|
|
498
|
+
local path owner rest repo repo_lc left
|
|
499
|
+
url=$(printf '%s' "$input" | sed \
|
|
500
|
+
-e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
501
|
+
if [ "${url#*://}" != "$url" ]; then
|
|
502
|
+
after_scheme=${url#*://}
|
|
503
|
+
authority=${after_scheme%%/*}
|
|
504
|
+
[ "$after_scheme" != "$authority" ] || {
|
|
505
|
+
printf '%s' "$url"
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
host_port=${authority##*@}
|
|
509
|
+
host=${host_port%%:*}
|
|
510
|
+
host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
|
|
511
|
+
case "$host_lc" in
|
|
512
|
+
github.com|www.github.com) path=${after_scheme#*/} ;;
|
|
513
|
+
*)
|
|
514
|
+
printf '%s' "$url"
|
|
515
|
+
return
|
|
516
|
+
;;
|
|
517
|
+
esac
|
|
518
|
+
elif [ "${url#*:}" != "$url" ]; then
|
|
519
|
+
left=${url%%:*}
|
|
520
|
+
host=${left##*@}
|
|
521
|
+
host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
|
|
522
|
+
case "$host_lc" in
|
|
523
|
+
github.com|www.github.com) path=${url#*:} ;;
|
|
524
|
+
*)
|
|
525
|
+
printf '%s' "$url"
|
|
526
|
+
return
|
|
527
|
+
;;
|
|
528
|
+
esac
|
|
529
|
+
else
|
|
530
|
+
case "$url" in
|
|
531
|
+
*/*/*|/*)
|
|
532
|
+
printf '%s' "$url"
|
|
533
|
+
return
|
|
534
|
+
;;
|
|
535
|
+
*/*) path=$url ;;
|
|
536
|
+
*)
|
|
537
|
+
printf '%s' "$url"
|
|
538
|
+
return
|
|
539
|
+
;;
|
|
540
|
+
esac
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# Keep only owner/repo. Browser URLs can include /tree/branch/path, and a
|
|
544
|
+
# pasted clone URL can carry both `.git` and a trailing slash.
|
|
545
|
+
path=${path%%\?*}
|
|
546
|
+
path=${path%%\#*}
|
|
547
|
+
path=$(printf '%s' "$path" | sed -e 's#/*$##')
|
|
548
|
+
owner=${path%%/*}
|
|
549
|
+
rest=${path#*/}
|
|
550
|
+
if [ -z "$owner" ] || [ "$rest" = "$path" ]; then
|
|
551
|
+
printf '%s' "$url"
|
|
552
|
+
return
|
|
553
|
+
fi
|
|
554
|
+
repo=${rest%%/*}
|
|
555
|
+
repo=${repo%%\?*}
|
|
556
|
+
repo=${repo%%\#*}
|
|
557
|
+
repo_lc=$(printf '%s' "$repo" | tr '[:upper:]' '[:lower:]')
|
|
558
|
+
case "$repo_lc" in
|
|
559
|
+
*.git) repo=${repo%????} ;;
|
|
560
|
+
esac
|
|
561
|
+
if [ -z "$repo" ]; then
|
|
562
|
+
printf '%s' "$url"
|
|
563
|
+
return
|
|
564
|
+
fi
|
|
565
|
+
printf 'https://github.com/%s/%s.git' "$owner" "$repo"
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# True for a reference whose explicit authority is github.com, even if its
|
|
569
|
+
# path is malformed. Callers use this to fail closed rather than handing a
|
|
570
|
+
# GitHub-looking URL to a generic Git command with ambient credentials.
|
|
571
|
+
is_github_hosted_url() {
|
|
572
|
+
local input="$1" url after_scheme authority host_port host host_lc left
|
|
573
|
+
url=$(printf '%s' "$input" | sed \
|
|
574
|
+
-e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
575
|
+
if [ "${url#*://}" != "$url" ]; then
|
|
576
|
+
after_scheme=${url#*://}
|
|
577
|
+
authority=${after_scheme%%/*}
|
|
578
|
+
host_port=${authority##*@}
|
|
579
|
+
host=${host_port%%:*}
|
|
580
|
+
elif [ "${url#*:}" != "$url" ]; then
|
|
581
|
+
left=${url%%:*}
|
|
582
|
+
host=${left##*@}
|
|
583
|
+
else
|
|
584
|
+
case "$url" in
|
|
585
|
+
*/*/*|/*|*://*) return 1 ;;
|
|
586
|
+
*/*) return 0 ;;
|
|
587
|
+
*) return 1 ;;
|
|
588
|
+
esac
|
|
589
|
+
fi
|
|
590
|
+
host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
|
|
591
|
+
case "$host_lc" in
|
|
592
|
+
github.com|www.github.com) return 0 ;;
|
|
593
|
+
*) return 1 ;;
|
|
594
|
+
esac
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# HTTPS spelling of a github.com repository. Empty for non-GitHub remotes.
|
|
598
|
+
github_https_repo_url() {
|
|
599
|
+
local https_url rest owner repo
|
|
600
|
+
https_url=$(normalize_github_repo_url "$1")
|
|
601
|
+
case "$https_url" in
|
|
602
|
+
https://github.com/*)
|
|
603
|
+
rest=${https_url#https://github.com/}
|
|
604
|
+
owner=${rest%%/*}
|
|
605
|
+
repo=${rest#*/}
|
|
606
|
+
if [ "$repo" = "$rest" ]; then
|
|
607
|
+
printf ''
|
|
608
|
+
else
|
|
609
|
+
case "$owner/$repo" in
|
|
610
|
+
/*|*/|*/*/*) printf '' ;;
|
|
611
|
+
*) printf '%s' "$https_url" ;;
|
|
612
|
+
esac
|
|
613
|
+
fi
|
|
614
|
+
;;
|
|
615
|
+
*)
|
|
616
|
+
printf ''
|
|
617
|
+
;;
|
|
618
|
+
esac
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# SSH spelling used only when the task owner has not connected GitHub.
|
|
622
|
+
github_ssh_repo_url() {
|
|
623
|
+
local https_url
|
|
624
|
+
https_url=$(github_https_repo_url "$1")
|
|
625
|
+
case "$https_url" in
|
|
626
|
+
https://github.com/*)
|
|
627
|
+
printf 'git@github.com:%s' "${https_url#https://github.com/}"
|
|
628
|
+
;;
|
|
629
|
+
*)
|
|
630
|
+
printf ''
|
|
631
|
+
;;
|
|
632
|
+
esac
|
|
633
|
+
}
|