## Context Task 05 made lifecycle/config/file jobs real and Task 06 made logs, artifacts, metrics, backups, and remote adapters durable. Distribution APIs already create `dependencies.check`, `dependencies.install`, and `run.self-update` jobs, but Run currently returns immediate synthetic success. Platform also accepts any available same-owner-visible artifact for an update and has no Run-only artifact read route, immutable dependency-plan approval, terminal dependency projection, or update activation journal. The implementation spans the main repository and the independent `run/` repository. Platform remains authoritative for user ownership/admin scope, installed plugin/version, server/runtime binding, selected endpoint, Run session/signature, attempt/lease/cancel fencing, artifact ownership, immutable plan digest, and audit. Run owns machine-local resolution, typed adapter execution, staging, replacement, health confirmation, and rollback. Plugins declare safe logical plans; platform_web receives only safe projections. ## Goals / Non-Goals **Goals:** - Execute declared dependency probes and install plans through fixed, testable adapters with bounded time, output, retries, and cancellation. - Make the exact platform/architecture-specific plan reviewable and bind installation approval to its digest. - Persist dependency status and update phases across Platform and Run restarts. - Download only an approved same-server Run distribution through a signed, active-attempt-only, resumable contract. - Verify archive and binary bounds/checksums, preserve the current package configuration, stage durably, activate only after the terminal result is accepted, confirm startup health, and roll back on failure. - Keep heartbeat, job ack/result/cancel polling, logs, and artifact upload independent from slow dependency/update work. **Non-Goals:** - Arbitrary shell/script execution, user-supplied command vectors, generic root package management, unverified HTTP downloads, or undeclared host/credential access. - Production code-signing/KMS, release rings, fleet rollout, centralized binary mirrors, dependency solving, Run service-manager installation, client-manager lifecycle, plugin lifecycle, production scaling/alerts, or real AI-provider integration. ## Decisions ### Decision 1: Platform snapshots declared inputs and approves an immutable digest Platform derives a safe dependency catalog from the installed plugin version, selected runtime profile, endpoint OS/architecture, and declared probes/plans. A canonical digest covers the declaration and non-secret logical binding generation. An install request must include the digest returned by the catalog. Dispatch re-resolves the declaration and rejects changed plugin versions, plan steps, target platform, bindings, endpoint, or digest before creating the job. Alternative considered: accept only a plan key and resolve it at execution time. Rejected because an operator could approve one plan and execute a later plugin revision. ### Decision 2: Private execution inputs use active fenced Run routes Dependency declarations/resolved target values and Run update manifests are not placed in browser-visible job DTOs. Run retrieves them through signed `/api/v1/run/jobs/dependency-input`, `/api/v1/run/jobs/update-input`, and `/api/v1/run/jobs/update-chunk` routes carrying endpoint, session, job, attempt, and lease. Platform uses the existing fenced-job check and requires an active matching capability. Update chunks are bounded and range-addressed; no browser download token or raw storage path is returned. Alternative considered: embed all data in `Job.ExecutionInput.Content`. Rejected because it weakens type separation and increases the chance of private binding or package data entering general job projections. ### Decision 3: Dependency work uses a closed adapter registry Run maps probe kinds and install step types to fixed implementations. Package steps map a whitelisted manager to fixed argument builders and validate package/version tokens. Verified downloads require HTTPS, a declared SHA-256 checksum, a size limit, and a scoped destination. SteamCMD uses a fixed executable/argument shape. Manual steps return a safe blocked result and never claim installation. No adapter invokes a shell, evaluates manifest text, accepts environment overrides, or returns command output/paths. The executor has injected command/download/filesystem interfaces for deterministic tests; production implementations use `exec.CommandContext`, bounded HTTPS, owner-only workspaces, and atomic files. Alternative considered: translate declarations into shell scripts. Rejected because shell parsing defeats the declared capability boundary. ### Decision 4: Dependency state is projected from terminal evidence Run returns a typed execution result containing only declaration key, present/missing/installed/failed classification, bounded version evidence, completed step count, and plan digest. Platform verifies that evidence against the job snapshot before updating `DependencyStatus`. Attempts and local journals are idempotent; retry/cancel/stale results remain governed by the existing scheduler. Audit summaries never include resolved paths, commands, package-manager output, or credentials. ### Decision 5: Self-update is a durable two-process transaction Run streams the approved distribution archive into an owner-only transaction directory, persists offset/hash metadata, verifies the final artifact checksum, safely extracts exactly one expected Run binary, and records a staged manifest. Archive traversal, links, devices, duplicate executables, excess entries, oversized bodies, target mismatch, and bundled configuration replacement are rejected. After Platform accepts the successful staged job result and the Run journal has persisted the acknowledgement, the worker launches the staged binary in helper mode and exits. The helper waits for the old PID, backs up the current executable, copies the staged binary through an atomic temporary target, starts the new executable with helper-only environment removed, and waits for a startup-health marker. The new worker writes that marker and sends a signed update-health report only after registration and job reconciliation succeed; Platform keeps the safe phase at `restart-requested/activating` until that report matches the terminal update job, endpoint, attempt, lease proof, target release, and current session. Failure restores the backup and restarts the previous binary. The package's existing `config.json` remains untouched. Alternative considered: replace the executable before reporting the job. Rejected because Platform could retain a running lease with no terminal result. Alternative considered: report success after merely staging. Rejected because the update record would misrepresent activation; the safe projection distinguishes `staged/restart-requested`, `activating`, `succeeded`, `rolled-back`, and `failed` phases. ### Decision 6: Recovery is driven by journals, not process memory Dependency executions store completed step indexes and immutable digests under the scoped workspace. Update transactions store artifact offset, expected checksum, staged binary checksum, current/backup logical locations, phase, attempt, and timestamps. Startup recovery removes invalid partial data, resumes eligible downloads, confirms a healthy activated transaction, or rolls back an interrupted activation. Attempt/lease values are used for fencing but never exposed in safe status or logs. ### Decision 7: Endpoint target identity and channel priority remain explicit Run endpoint records persist OS/architecture from hello so Platform can reject cross-target distributions and plans. Downloading dependencies or update chunks occurs inside the claimed job goroutine; heartbeat and durable log/artifact upload loops remain separate. Progress/cancel polling uses bounded contexts. Tests block download/adapters while asserting heartbeat, ack/result, and log upload deadlines. ## Risks / Trade-offs - [Package managers vary across distributions and may require privilege] → Validate the endpoint OS, use manager-specific fixed arguments, surface a safe permission failure, and never auto-escalate through sudo/shell. - [A process can crash between staged result and helper activation] → Persist the post-ack activation request and recover it at startup; Platform distinguishes staging from confirmed version/health. - [Windows executable replacement differs from Unix rename behavior] → Helper copies from the staged executable after the parent exits and uses backup/temporary targets instead of renaming a running binary. - [A newly started binary can launch but fail registration] → New Run writes health only after successful registration/reconciliation; helper times out and restores the previous executable. - [Old records lack endpoint OS/architecture or plan digests] → Existing endpoints re-register before real actions become available; legacy queued placeholder jobs are not retroactively executed. - [Large update archives can consume disk/network] → Enforce artifact/archive/binary limits, bounded chunks, resumable offsets, owner-only roots, and cleanup after terminal retention. ## Migration Plan 1. Add backward-compatible endpoint target fields, dependency/update records, DTOs, protocols, repositories, and private routes. 2. Require endpoint re-registration to advertise a supported OS/architecture before enabling dependency install or self-update. 3. Publish safe dependency catalog/status and update phase projections; existing generic job views remain compatible. 4. Enable real Run capabilities only when the typed executors and journals initialize successfully. 5. On rollback, stop advertising the real capabilities and leave private journals/artifacts for a compatible binary to recover; do not delete or reinterpret prior Platform records. ## Open Questions - Production signing policy and rollout rings remain a future change; this task enforces artifact ownership, target match, content checksum, and optional signature metadata without claiming a production PKI.