No description
  • Python 90.4%
  • JavaScript 7.1%
  • PLpgSQL 0.7%
  • CSS 0.7%
  • Shell 0.6%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-22 19:18:03 +00:00
.github/workflows fix(steward): make planning readback durable 2026-09-17 12:38:15 -07:00
config fix: derive Steward scope from declared objectives 2026-09-20 19:59:09 -07:00
deploy/reverse-proxy Fix interim Web root redirect matcher 2026-09-08 09:17:36 -07:00
docs docs: reconcile Steward intake incident lifecycle 2026-09-20 22:23:24 -07:00
migrations/postgresql Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
scripts feat: supervise and learn bounded module repairs 2026-09-19 15:40:47 -07:00
src/steve_orchestrator Reconcile legacy directory scope snapshots 2026-09-22 12:06:32 -07:00
tests Reconcile legacy directory scope snapshots 2026-09-22 12:06:32 -07:00
tools Fence logout state and prove browser reply recovery with synthetic traffic 2026-09-12 02:18:12 -07:00
.dockerignore Build control plane from locked dependencies 2026-09-07 22:16:10 -07:00
.gitignore fix(steward): make planning readback durable 2026-09-17 12:38:15 -07:00
.node-version Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
compose.yml fix: add capability-aware Steward output budgets 2026-09-20 20:58:26 -07:00
CURRENT_CAPABILITIES.json Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
DEVELOPMENT_STEWARD_OVERNIGHT_REHABILITATION_2026-09-13.md docs: record live Steward planning repair 2026-09-14 05:58:25 -07:00
DEVELOPMENT_STEWARD_V2_AUTHORITATIVE_RECONCILIATION_2026-09-07.md Rehabilitate Development Steward and safe web Markdown 2026-09-13 06:46:27 -07:00
DEVELOPMENT_STEWARD_V2_CONTINUATION_REQUIRED.md Document V2-only cutover gate 2026-09-03 19:20:55 +00:00
Dockerfile Build control plane from locked dependencies 2026-09-07 22:16:10 -07:00
Dockerfile.agent feat: supervise and learn bounded module repairs 2026-09-19 15:40:47 -07:00
Dockerfile.core-chat-fix Rehabilitate Development Steward and safe web Markdown 2026-09-13 06:46:27 -07:00
Dockerfile.core-live Rehabilitate Development Steward and safe web Markdown 2026-09-13 06:46:27 -07:00
Dockerfile.fetcher feat: supervise and learn bounded module repairs 2026-09-19 15:40:47 -07:00
Dockerfile.model-proxy Harden sandbox network and model credential boundary 2026-08-18 14:40:52 +00:00
Dockerfile.provision-proxy Add governed provisioning and bounded self-repair 2026-08-18 15:35:55 +00:00
Dockerfile.uv-lock-bridge phase: Governed uv lock OpenHands integration correction (1/2) 2026-08-20 11:55:47 +00:00
Dockerfile.uv-lock-bridge-authoritative phase: Governed uv lock trust-boundary final correction (4/5) 2026-08-21 02:39:40 +00:00
Dockerfile.uv-resolver phase: Governed uv lock resolution capability (1/4) 2026-08-20 08:12:48 +00:00
Dockerfile.validator fix: include jq in validator image 2026-09-22 02:23:06 -07:00
env.example fix: add capability-aware Steward output budgets 2026-09-20 20:58:26 -07:00
OVERNIGHT_REPORT.md docs: record final main deployment provenance 2026-09-10 06:46:09 -07:00
package-lock.json Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
package.json feat: supervise and learn bounded module repairs 2026-09-19 15:40:47 -07:00
pyproject.toml Make Steward PostgreSQL migrations explicit 2026-09-13 08:10:27 -07:00
README.md Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
REFRACTOR_DELIVERY.md Integrate Development Steward refactor 2026-09-18 21:29:51 -07:00
uv.lock Pin compatible Starlette test client dependency 2026-09-10 02:33:38 -07:00

Refactor candidate: The independent Planner/Coder/Reviewer and recovery changes are described in REFRACTOR_DELIVERY.md. The checked current contract is CURRENT_CAPABILITIES.json. Historical commissioning statements below are not proof that this candidate has been deployed or live-verified.

Steve OS Development Steward

A deployable v0.2 Development Steward control plane for semi-autonomous Steve OS development. This checkout currently retains the legacy Forgejo repository, package, and service names for compatibility: its active repository is jon/steve-os-orchestrator, and the eventual repository target is jon/steve-os-development-steward.

Development Steward is the top-level director. It governs planning and bounded execution through the Orchestrator, which remains subordinate and continues to own coding workers, validators, and the existing portal integration.

The workflow is intentionally split into two human gates:

  1. A human creates and approves a development phase.
  2. The orchestrator autonomously clones the approved Forgejo base branch, runs the selected coding target from the role-specific coding pool through OpenHands, validates the result, commits it, pushes a branch, opens a Forgejo pull request, and merges it. After each merge the orchestrator obtains a fresh copy of the authoritative Forgejo base branch, proves its exact HEAD equals the recorded merge SHA, and re-runs the approved validation commands in the isolated networkless validator before the task is durably complete.
  3. Only after every merged task passes fresh post-merge validation does the phase stop in deployment_pending.
  4. Deployment requires a separate explicit human approval. v0.1 records that approval but does not yet own Steve OS production credentials or execute production deployment.

What is runnable today

  • Forgejo 16 backed by PostgreSQL.
  • Private local orchestration API with token authentication.
  • Persistent phase and audit-event storage.
  • Local OpenHands CLI in headless mode.
  • Separate Steve executive, Steward planning, and Orchestrator coding model boundaries.
  • A configurable coding-model pool with explicit underlying-identity and degraded-overlap telemetry.
  • Autonomous git branch, commit, push, PR creation, and merge.
  • Per-phase acceptance criteria and explicit validation commands.
  • Hard stop before deployment.

Cognitive model boundaries

Steve, Development Steward, and Orchestrator are separate cognitive roles and do not share one routing policy. Steve uses Steve Core's executive/general router. Steward uses STEWARD_MODEL_* for planning and reasoning. Orchestrator selects a target from CODING_MODEL_POOL_JSON (or the temporary legacy adapter) for repository coding, debugging, tests, and validation.

Production Compose sets ORCHESTRATOR_MODEL_SEPARATION_REQUIRED=true. A live coding attempt records the selected model ID, provider, underlying identity, pool, and full role-separation status in the Steward execution evidence and exposes the credential-free projection at /api/model-status and /v1/model-status. Same-model overlap is rejected unless the explicit ORCHESTRATOR_MODEL_DEGRADED_FAILOVER policy is enabled; it is never silently reported as normal independence.

Droplet installation

Recommended starting size: Ubuntu 24.04, 4 vCPU / 8 GB RAM.

git clone https://forgejo.houseofkrell.com/jon/steve-os-orchestrator.git
cd steve-os-orchestrator
sudo ./scripts/install-docker-ubuntu.sh
cp .env.example .env
./scripts/bootstrap.sh

bootstrap.sh asks for the droplet's public IP/DNS name and the DeepSeek API key. It generates the database password, Forgejo admin password, Forgejo repository token, and orchestrator API token.

The orchestrator API binds to 127.0.0.1:8000 by design. Access it over SSH tunneling until a proper authenticated front end or Steve OS integration is added.

ssh -L 8000:127.0.0.1:8000 root@YOUR_DROPLET

Create and approve a phase

Read the admin token from .env, then create a phase:

export ORCH_TOKEN='...'
curl -sS http://127.0.0.1:8000/api/phases \
  -H "X-Orchestrator-Token: $ORCH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Example phase",
    "repo": "jon/steve-os",
    "objective": "Implement the approved feature completely.",
    "acceptance_criteria": ["Tests pass", "Feature is documented"],
    "validation_commands": ["pytest -q"]
  }'

Approve the returned phase ID:

curl -sS -X POST http://127.0.0.1:8000/api/phases/PHASE_ID/approve \
  -H "X-Orchestrator-Token: $ORCH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"approved_by":"Jon"}'

Follow progress:

curl -sS http://127.0.0.1:8000/api/phases/PHASE_ID \
  -H "X-Orchestrator-Token: $ORCH_TOKEN"
curl -sS http://127.0.0.1:8000/api/phases/PHASE_ID/events \
  -H "X-Orchestrator-Token: $ORCH_TOKEN"

After the PR is merged and fresh post-merge validation of the exact merge SHA passes, the phase becomes deployment_pending. Deployment approval is a separate call:

curl -sS -X POST http://127.0.0.1:8000/api/phases/PHASE_ID/deployment/approve \
  -H "X-Orchestrator-Token: $ORCH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"approved_by":"Jon"}'

Control-plane security boundary

The orchestrator does not receive Steve OS production deployment credentials. Its Forgejo credential is limited to repository operations, and the API is loopback-only by default. Autonomous repository code is executed through the isolated sandbox boundary described below rather than inside the control-plane container.

Do not expose port 8000 directly to the public Internet.

Phase 1 execution boundary

Autonomous repository code no longer executes inside the orchestrator control-plane container. The control plane requires a dedicated sandbox Docker engine configured with SANDBOX_DOCKER_HOST. Local Docker sockets and unauthenticated TCP Docker APIs are rejected deliberately. Phase 1 accepts only an authenticated ssh://... Docker endpoint for the dedicated sandbox engine.

The boundary has these properties:

  • the coding agent and validator are disposable, unprivileged containers;
  • no control-plane filesystem or Docker socket is mounted into a sandbox;
  • .git metadata never crosses into the coding sandbox, so an agent cannot alter hooks, remotes, or Git configuration used later by the control plane;
  • the agent receives a per-job model-proxy token, not the provider API key;
  • the agent network is internal-only and has no direct Internet route;
  • a trusted, disposable model proxy is the only dual-homed component and forwards only the small allowlist of model API endpoints needed by the coding agent;
  • Forgejo credentials, provider API credentials, and the orchestrator administrator token never enter the agent;
  • validation runs in a separate container with no network and no environment secrets;
  • sandbox root filesystems are read-only, capabilities are dropped, and no-new-privileges, PID, CPU, memory, and workspace-export limits are enforced;
  • the orchestrator fails closed if the dedicated sandbox engine is unavailable.

Dockerfile.agent, Dockerfile.validator, and Dockerfile.model-proxy define the sandbox images. They must be built or made available on the dedicated sandbox engine before an approved phase can execute. The validator image additionally carries a fixed, exactly version-pinned uv toolchain (ARG UV_VERSION=0.7.6 in Dockerfile.validator, captured as a named uv build stage from Astral's version-tagged image and copied onto /usr/local/bin). uv is therefore a validator toolchain prerequisite, not a repository-derived dependency: it is present before any repository source crosses the boundary and never travels through the governed per-repository wheelhouse path described in Phase 3.

Editing Dockerfile.validator or any other sandbox image source does not automatically rebuild, restart, or deploy the live orchestrator or validator image. A new image must be rebuilt separately on the sandbox engine and then deployed through the explicit human deployment gate.

This phase does not change the deployment boundary: the orchestrator still stops at deployment_pending, and production deployment remains a separate explicit human gate.

Phase 2 durable reconciliation

Development approval is now a durable, one-time phase authorization. The orchestrator records an approval ID and timestamp when a phase leaves draft; process restarts do not replace that approval or require a second approval.

Each PR-sized task has its own durable checkpoint containing its stable branch name, base and validated commit SHAs, Forgejo pull-request identity, and merge SHA. Task branch names do not contain an attempt number, so a restart can reconcile the same remote object instead of creating a duplicate branch or PR.

On startup, phases in approved, reconciling, developing, validating, or pr_open are scheduled for autonomous reconciliation. The control plane checks Forgejo before taking another action and can recover a previously pushed branch, open PR, or completed merge. A PR whose head no longer matches the validated SHA fails closed rather than being merged.

If the process was interrupted before any durable remote state existed, partial local agent output is not trusted. That task is deliberately re-executed from the current durable main state. A caught execution failure still enters failed; the /resume endpoint is now an operator retry request rather than a second development approval, and the original approval provenance remains unchanged. Bounded autonomous repair of execution/validation failures is introduced in the next phase.

Phase 2 does not broaden merge policy and does not perform post-merge validation yet. Those remain separate reviewable phases. The orchestrator still stops at deployment_pending; deployment is a separate explicit human gate.

Phase 3 governed provisioning and bounded self-repair

Validation no longer assumes that the long-lived validator image already contains every repository dependency. Before each validation attempt, the orchestrator derives a conservative provisioning plan from static Python dependency metadata. Phase 3 currently supports pyproject.toml (including PEP 735 top-level [dependency-groups]), requirements-dev.txt, and requirements.txt; known unsupported ecosystems fail closed instead of receiving a general-purpose network path. Declared dependency groups are expanded statically with deterministic, dependency-first include-group resolution; unknown or cyclic include references fail closed.

Dependency fetching and repository execution remain separate trust domains:

  • a disposable dependency-fetcher container receives dependency specifiers but never repository source or control-plane secrets;
  • the fetcher has no direct Internet route and reaches only explicitly allowlisted TLS package hosts through a disposable CONNECT proxy;
  • the proxy rejects non-443 destinations, non-allowlisted hosts, and DNS results that resolve to private, loopback, link-local, reserved, or other non-public addresses;
  • dependency fetching uses pip download --only-binary=:all: so source distributions and their build hooks are not executed in the egress-capable fetcher;
  • downloaded wheels cross into the validator as an artifact tar stream;
  • the validator is created with --network none, installs dependencies only from that local wheelhouse, and never receives model, Forgejo, or orchestrator credentials;
  • the validator's fixed Python, pip, and version-pinned uv toolchain is built into the validator image and is never fetched through the governed wheelhouse; the wheelhouse supplies only per-repository project dependencies;
  • direct URLs, VCS references, local paths, pip index overrides, recursive requirement options, and dynamic Python dependency metadata are rejected by policy.

A failed governed provisioning or validation command may trigger autonomous repair inside the existing isolated coding-agent boundary. Repair receives only the approved task, acceptance criteria, externally owned validation commands, and the bounded failure tail. Repair attempts and validation attempts are stored durably in the task checkpoint and survive restart/reconciliation; restarting a task does not refill the repair budget. The default budget is three repair attempts per task and is configurable with ORCH_MAX_REPAIR_ATTEMPTS. Repair stops before exhausting the budget when the same failure repeats without substantive progress, when two failures oscillate between equivalent states, or when the next repair would materially broaden scope.

Phase 3 intentionally does not broaden PR review or merge policy, and it does not perform post-merge validation. Those remain later, separately reviewable phases. Deployment still stops at deployment_pending and remains an explicit human gate.

Phase 4 fresh post-merge validation and governed repair

A merged task is no longer considered complete immediately after its Forgejo pull request is merged. The orchestrator must prove, from a fresh clone of authoritative Forgejo main, that the recorded merge SHA is real and passes validation:

  • the orchestrator clones a disposable checkout of the authoritative Forgejo repository, never reusing the pre-merge development workspace as proof;
  • origin/main and the checkout HEAD must both exactly equal the recorded Forgejo merge SHA before any validation begins; any mismatch fails closed with post_merge.sha_mismatch;
  • validation runs through the existing governed provisioning path and the existing isolated, networkless validator, using the externally owned validation commands;
  • a successful fresh post-merge validation is durably recorded on the task checkpoint (post_merge_validated_sha, post_merge_validated_at, post_merge_validation_attempts) and as an explicit post_merge.validated audit event;
  • only after that success may a task become complete, and only after every task is complete does the phase reach deployment_pending.

If fresh post-merge validation fails, the orchestrator fails closed. It may use only the configured bounded repair budget (ORCH_MAX_REPAIR_ATTEMPTS, default 3) and stops early when repair failures repeat without substantive progress, oscillate, or would broaden scope. Repair of already-merged code never modifies, rewrites, or force-pushes main. Instead the orchestrator:

  • checks out the recorded merge SHA in a fresh, disposable worktree;
  • runs the isolated coding agent to fix the failure inside the approved task scope;
  • re-validates the repair in the networkless validator;
  • commits to a new durable branch (orch/<phase>-t<task>-fix<attempt>), pushes it, and opens a normal Forgejo pull request;
  • merges that repair PR normally, records the resulting repair merge SHA, and then runs another fresh post-merge validation of the new authoritative main.

A repair PR merge is therefore itself subject to the same fresh post-merge validation gate as the original merge. Restart reconciliation safely resumes an in-progress post-merge validation or repair from durable checkpoint provenance without requiring a second development approval: it reconciles the recorded repair branch/PR against Forgejo and never re-consumes the repair budget for a slot that was already started. The repair branch name is attempt-scoped so a restart reconciles the same remote object instead of creating a duplicate branch or PR.

This phase preserves Forgejo repository allowlisting, PR identity, validated-SHA, and merge-SHA checks, all of which remain fail-closed. deployment_pending remains a separate, explicit human deployment gate; no production deployment or orchestrator restart is performed by this phase.

Phase 5 validated no-op completion and durable progress heartbeats

A task may legitimately be verification/review-only: the approved work is to confirm that the current repository already satisfies the acceptance criteria and validation commands, so OpenHands correctly makes no repository changes. Such a task now completes successfully without a commit, branch merge, or pull request. The result is recorded as an explicit governed no-op rather than an ordinary changed-code PR:

  • a task must be explicitly classified (task_kind of verification or review) in the phase task specification to be eligible; an ordinary implementation task that produces no changes still fails closed;
  • no-op completion is allowed only when the initial OpenHands execution made zero repository changes and the normal governed validation run passed for the current authoritative main;
  • the outcome is durably recorded on the task checkpoint (no_op, no_op_validated_at, no_op_kind, and a validated_sha equal to the validated main head) and emitted as a task.no_op_validated audit event; no commit, branch, or PR is fabricated;
  • the phase manifest carries kind, no_op, and no_op_validated_at for each task, so a phase containing a successful validated no-op task proceeds normally to deployment_pending.

Progress heartbeats keep a long OpenHands execution visibly alive before any task checkpoint is written. While an agent run is active the orchestrator emits openhands.heartbeat, repair.heartbeat, and post_merge.repair.heartbeat events at a configurable interval (ORCHESTRATOR_HEARTBEAT_INTERVAL_SECONDS, default 30). Heartbeats are content-minimized: they carry only the task index and elapsed wall-clock seconds. They never expose model prompts, repository source, user data, credentials, or control-plane secrets. Heartbeat emission stops as soon as the agent run returns, fails, or is interrupted.

Single-host rootless sandbox mode

When a separate sandbox VM is not available, the supported bootstrap topology is a dedicated steve-sandbox Unix account and rootless Docker daemon on the orchestrator host. The control plane still never receives the rootful host Docker socket. It reaches the rootless daemon through an SSH key whose authorized_keys entry is restricted to the single forced command docker system dial-stdio.

scripts/setup-single-host-sandbox.sh creates the account, rootless daemon, pinned host-key material, and dedicated service SSH identity. SANDBOX_SSH_DIR is the directory mounted directly as /root/.ssh in the control-plane container, so config, known_hosts, and id_ed25519 live directly inside that directory rather than in a nested .ssh directory. scripts/build-sandbox-images.sh builds the six sandbox images into that rootless engine and scripts/verify-single-host-sandbox.sh verifies the engine and image inventory.

The Compose service mounts only the dedicated sandbox SSH directory at /root/.ssh read-only and maps host.docker.internal to the host gateway. Use SANDBOX_DOCKER_HOST=ssh://steve-sandbox-local and keep ORCHESTRATOR_MAX_CONCURRENT_PHASES=1 while the control plane and sandbox share a host. This is a transitional isolation boundary: the rootless daemon, Unix account, credentials, and Docker endpoint are already separated so a future dedicated sandbox machine requires changing the SSH destination rather than changing orchestration code. A separate machine remains stronger because same-host mode necessarily shares the host kernel.

Infrastructure execution fabric

The server-side infrastructure execution fabric is separate from the coding sandbox and is disabled unless ORCHESTRATOR_INFRASTRUCTURE_INVENTORY_FILE names a root-owned inventory. The inventory uses logical resource identities, a fixed remote user, a dedicated identity-file reference, and a pinned known_hosts fingerprint. It contains no password, API token, or private key material. See config/infrastructure-inventory.example.json.

Every operation must first be bound to a durable deployment authorization envelope containing the campaign, repository, immutable candidate SHA, target environment, resource scope, permitted operation classes, approval evidence, and expiry. The authenticated infrastructure endpoints only accept a typed argument vector against that inventory; they do not accept arbitrary host names or interactive credentials. SSH always uses batch mode, password authentication disabled, StrictHostKeyChecking=yes, a single bounded connection attempt, bounded command timeout, and redacted/bounded evidence. A crash while an operation is running becomes reconciliation_required; recovery never repeats the uncertain remote command automatically.

Destructive operation classes are deliberately rejected by this V1 fabric. They remain a protected boundary even if an envelope was incorrectly broadened. A future workload identity, short-lived SSH certificate, or target-specific credential provider can replace the local identity-file resolver without changing envelope or campaign semantics.

Phase 6 governed uv lock resolution

The orchestrator gains a narrowly scoped, separately governed capability to regenerate uv.lock through trusted package-resolution infrastructure. This capability never grants the OpenHands coding agent, the validator, or repository code general Internet access.

  • A dedicated resolver sandbox role runs the exactly version-pinned uv toolchain (Dockerfile.uv-resolver, ARG UV_VERSION=0.7.6 copied from Astral's version-tagged image) instead of installing uv dynamically from repository dependencies. The resolver image is selected with ORCH_SANDBOX_UV_RESOLVER_IMAGE and defaults to steve-os-orchestrator-uv-resolver:0.1.0.
  • The resolver runs as uid/gid 10001 with a read-only root filesystem, dropped capabilities, no-new-privileges, bounded CPU, memory, and PID limits, no Docker socket, and no privileged host mounts.
  • The resolver receives no model API credentials, Forgejo credentials, orchestrator administrator credentials, SSH private keys, Vault credentials, or unrelated control-plane secrets. Its only environment is the disposable provision-proxy CONNECT endpoint and the fixed PyPI index.
  • Package resolution reaches only the existing explicitly allowlisted package hosts through the same governed provisioning-egress boundary used by dependency fetching; the resolver has no unrestricted outbound networking.
  • Resolver input is restricted to the current workspace pyproject.toml and existing uv.lock. .git metadata, environment files, private keys, credentials, and unrelated repository data never cross into the resolver. Existing policy restrictions on direct URLs, VCS dependencies, local-path escapes, package-index overrides, recursive requirement options, and dynamic dependency metadata remain enforced rather than bypassed.
  • Resolver output is restricted to a single uv.lock artifact. Unexpected files, symlinks, hard links, path traversal, oversized output, malformed TOML, or additional artifacts fail closed. The resolver verifies the generated lock internally (uv lock --check) before returning it, and the single artifact is hashed and validated before it leaves the sandbox.
  • Resolution is brokered through a task-scoped capability credential (src/steve_orchestrator/uv_lock_broker.py). The orchestrator issues a single-use, HMAC-SHA256-signed token bound to the exact phase, task, repository, and workspace; the broker verifies the signature, the phase/task implementation-window authorization, and the scope against durable phase/task state, rejects replay, and re-enforces the dependency-source policy before any package egress. The signing key is ORCH_UV_LOCK_CAPABILITY_SECRET (empty fails closed).
  • The broker accepts only a bounded, regular-file uv.lock result, verifies its hash, records a safe audit trail (task, artifact hash, size — never credentials or resolver output), and applies the artifact atomically. Any capability, scope, policy, resolver, transfer, validation, or application failure leaves the previous workspace uv.lock untouched; there is no fallback to hand-editing uv.lock, direct package Internet access, an alternate index, or relaxed policy.

When the capability is used (worker-to-resolver bridge)

The governed resolver is reached only through a narrow, disposable bridge integrated into the normal isolated OpenHands implementation execution path:

  • While a phase is inside an active implementation window (developing, or a bounded repairing retry), trusted orchestrator code automatically mints a single-use capability (try_issue_uv_lock_capability) and hands it to the implementation worker before the agent starts. The worker receives only the capability token and a bridge URL (STEVE_UV_LOCK_CAPABILITY, STEVE_UV_LOCK_BRIDGE_URL) — no ORCHESTRATOR_ADMIN_TOKEN, Forgejo credentials, SSH keys, Vault credentials, deployment credentials, model-proxy credentials, general orchestrator API access, or arbitrary command/network capability. The worker remains without general Internet access and without direct package-network access.
  • The bridge URL points at a disposable, credential-less proxy sidecar (Dockerfile.uv-lock-bridge) dual-homed onto the agent's internal job network and the sandbox egress network. The sidecar forwards only the capability header to the single configured upstream authoritative bridge (ORCH_UV_LOCK_BRIDGE_UPSTREAM) and cannot be used as an arbitrary egress hop.
  • The worker invokes the bridge through a deterministic, argument-free helper (src/steve_orchestrator/uv_lock_helper.py, copied into the agent image as steve-uv-lock-helper). The helper reads only STEVE_UV_LOCK_CAPABILITY and STEVE_UV_LOCK_BRIDGE_URL, POSTs an empty body with the capability header to the fixed /uv-lock/resolve path, and atomically applies the single validated result to the worker's own current workspace directory before the normal run_agent export. This worker-side application ensures the regenerated uv.lock is present in the worker's /workspace when the workspace is exported and therefore survives the subsequent host worktree replacement; a stale uv.lock from the original clone cannot overwrite the successfully regenerated lock. The helper re-checks the artifact hash, size, and fixed path before writing and never writes a partial file. It accepts no destination URL, command, alternate index, extra header, or general networking argument and never prints the capability token. The implementation and repair prompts include deterministic governed-regeneration instructions that tell the worker when to run the helper and explicitly prohibit hand-editing uv.lock as a substitute when governed regeneration is required.
  • The authoritative bridge is served as a dedicated, capability-only listener on port 8083 that mounts only POST /uv-lock-bridge/uv-lock/resolve and the minimum GET /uv-lock-bridge/health liveness route. The network-exposed process is a minimum-authority forwarder (steve_orchestrator.uv_lock_bridge_server, its own uv-lock-bridge Compose service and Dockerfile.uv-lock-bridge-authoritative): it is a forward-only hop that relays the capability header to the trusted internal broker (POST /api/uv-lock/resolve's trusted _resolve_uv_lock_via_broker callback) and therefore holds no capability secret, no database access, no workspace access, no sandbox SSH key, and no admin/Forgejo/model/portal/deployment/Steve/ Vault/database credential. The forwarder runs as an unprivileged non-shell user with cap_drop: ALL, no-new-privileges, a read-only root filesystem, no data/workspace/SSH volumes, an explicit least-privilege network attachment (only the dedicated internal uv_lock_bridge network shared with the orchestrator, never the shared control_plane network, so it has no network adjacency to Forgejo, the Forgejo database, or any other peer and no external egress), and an entry point that re-execs itself with a strictly allowlisted environment so the running process cannot inherit unrelated control-plane secrets; startup fails closed if any required value is missing, a wildcard (0.0.0.0/::) publication is configured, or the internal upstream is off-contract. The forwarder never mounts the general orchestrator API, so publishing its port to the separate rootless sandbox does not expose phase/approval/deployment/Forgejo/administrative surfaces; the general API stays loopback-bound on 8000. Its exposure is separated from its private bind: ORCHESTRATOR_UV_LOCK_BRIDGE_PUBLISH_HOST is the concrete host interface the port is published on (loopback-only by default; the actual {host}:8083:8083 Compose mapping), and ORCHESTRATOR_UV_LOCK_BRIDGE_BIND_HOST is the private container-network bind (0.0.0.0 inside the container only so Docker's published-port forwarding can reach it); both are validated by the entry point before the listener binds. Before any worker is launched with governed uv.lock capability, the trusted orchestration boundary performs a bounded readiness probe of GET /uv-lock-bridge/health against the concrete forwarder host (ORCH_UV_LOCK_BRIDGE_READY_HOST, the uv-lock-bridge service) and fails closed if the bridge is unavailable or unhealthy.
  • The capability is single-use (a primary-key nonce claim) and bound to the exact phase, task, repository, and workspace. Its authorization lifetime is the phase/task implementation window itself, re-checked against durable orchestrator state at first use, not a fixed wall-clock TTL. An expired capability cannot start a new operation, but an exact reconnect to an operation that was validly started while the window was open may rejoin or re-observe that same in-progress or completed result; the reconnect cannot start a second operation or broaden phase/task/repository/ workspace/input/package-source scope. It cannot be replayed, reused for a different phase/task/repository/workspace, or presented as an administrator credential on any other API surface. A missing, malformed, expired, replayed, or scope-mismatched token fails closed before any resolver container is created. The capability cannot authorize resolver operations if the model-proxy credential is used in its place.
  • The capability material is single-use and scoped to the active implementation window; it is injected as a per-job environment file, never persisted to the repository, never logged, and removed during sandbox cleanup. With ORCH_UV_LOCK_CAPABILITY_SECRET unset the capability cannot be minted, and with ORCH_UV_LOCK_BRIDGE_UPSTREAM unset the worker bridge is not established.
  • Long-running resolution is handled without a substantially shorter helper/proxy timeout killing a legitimate operation. The helper and proxy use the same bounded resolution budget as the broker (up to the governed UV_LOCK_RESOLVE_TIMEOUT_SECONDS plus verification), while the broker's single-flight UvLockJobRegistry makes retries idempotent: a concurrent or later retry for the same capability nonce joins the already-running operation and re-observes its result instead of starting a second one. A transient proxy timeout or disconnect therefore does not leave a successfully started operation unrecoverable after its single-use nonce was consumed. If the in-flight job has already been evicted and the nonce was consumed, the durable primary-key nonce claim still fails closed (replay rejected), so replay protection never depends on cache residency.
  • A successful resolution is recorded durably, not just in the in-memory registry: the trusted artifact and a nonce-keyed completion record are persisted in a single transaction before the worker path returns. A supervised bridge-process restart therefore clears only the in-memory single-flight state; an exact retry of an already-consumed capability re-observes the recorded result (exact bytes and hash) without re-running the resolver, while nonce reuse, replay, scope mismatch, capability mismatch, and result substitution fail closed against the durable record. The durable footprint is the minimum bounded state needed for this invariant: the completion record stores the bound scope and artifact hash (never the artifact bytes, which already live in the trusted store keyed by phase/task), so it is one small row per consumed capability and one record per resolved phase/task; the in-memory single-flight registry additionally evicts completed jobs as soon as a new nonce is registered, keeping it bounded by concurrently in-flight operations.
  • The durable capability/rejoin footprint is operationally bounded by a deterministic lifecycle cleanup (cleanup_uv_lock_durable_state): one transactional pass at startup plus a periodic background pass every ORCH_UV_LOCK_CLEANUP_INTERVAL_SECONDS (default one hour) during long-running operation, so finalized records are pruned without waiting for a restart. A record is eligible for deletion only when (a) its phase has reached a final deployment gate (deployment_pending/deployment_approved/deployment_rejected), which can never re-enter the active implementation window, and (b) it has aged past ORCH_UV_LOCK_RETENTION_SECONDS (default one day) since it was consumed/resolved, leaving in-flight worker reconnects a grace period after the phase closes. Pending-application records and their trusted bytes for active (developing/repairing) or retriable (failed) phases are always retained so a crash between durable trusted-result persistence and authoritative application can still recover deterministically without re-running the resolver, and so the single-use nonce gate stays closed across an operator retry. The prune is transactional and fails closed: a missing, malformed, hash-mismatched, or unknown-lifecycle record aborts the entire prune rather than being silently discarded, and a deleted nonce is never re-inserted, so cleanup cannot weaken replay protection or convert missing state into fail-open behaviour. In production Compose/service configuration the documented external ORCHESTRATOR_UV_LOCK_RETENTION_SECONDS/ORCHESTRATOR_UV_LOCK_CLEANUP_INTERVAL_SECONDS map to the application inputs ORCH_UV_LOCK_RETENTION_SECONDS/ORCH_UV_LOCK_CLEANUP_INTERVAL_SECONDS, keeping one coherent configuration path authoritative.

This bridge narrows rather than widens the existing boundaries. It reuses the governed provisioning-egress package-host allowlist and dependency-source policy; it introduces no new credential store, no new sandbox, and no new host trust; and it does not change the networkless coding agent or validator, the Forgejo provenance chain, or the human deployment gate.

This phase adds the resolver primitive, the governed resolution broker, the task-scoped capability credential, the worker-to-resolver bridge above, and atomic workspace artifact application. No live resolver or bridge image is built or activated, no running orchestrator is restarted, and no existing sandbox image or deployment configuration is switched during this phase; the orchestrator still stops at deployment_pending.

Mobile-control read model and timing/status APIs

The orchestrator exposes a bounded, secret-free status projection for a future mobile-control portal. It is a read-only view of durable orchestration state and never carries credentials or unbounded raw logs.

Timing semantics

  • Total phase wall-clock elapsed time is measured from the phase's durable started_at (first approval for legacy rows) to the current time while the phase is actively running, and to completed_at/last update once it is no longer active (draft, terminal, or paused). Non-active phases freeze their elapsed value; active phases advance until the next authoritative refresh.
  • Per-task wall-clock elapsed time is measured from the task's started_at to completed_at when complete, otherwise to the current time. An unstarted task has no elapsed value, and a completed task's duration freezes and never changes.
  • Active-execution time is recorded separately from wall-clock elapsed time. It is the integral number of wall-clock seconds spent inside orchestrator-owned execution blocks (OpenHands agent runs and isolated validation/provisioning sessions), accumulated durably on both the task and the phase as active_seconds. It excludes waiting for phase capacity, git reconciliation, PR polling, and merge latency. Durations shorter than one second are recorded as zero.
  • Elapsed timers may advance locally between polls, but they resynchronize from the authoritative durable timestamps on every refresh.

Heartbeat freshness

While an agent run is active the orchestrator emits content-minimized heartbeats. /api/phases/{id}/status reports heartbeat freshness for the current task as one of inactive, fresh, stale, or unknown. A heartbeat (or a heartbeat-free active window) is stale once it is older than three heartbeat intervals, with a 90-second floor (ORCH_HEARTBEAT_STALE_AFTER_SECONDS overrides the derived value). stale and unknown are truthful uncertainty signals: they never imply that a task has failed.

Status endpoint

GET /api/phases/{id}/status returns a bounded read model containing phase state, next_action, current task, total tasks, current task title, repair/validation attempt counts (current and phase totals), phase timing, active-execution seconds, heartbeat freshness, the single latest meaningful event, per-task provenance, and the latest event summary. Full activity streams are served separately by the bounded, cursor-based GET /api/phases/{id}/events?since=<id>&limit=<n> delta endpoint, which returns at most 500 events per poll.

Mobile operator portal (installable PWA)

The portal is an operator interface to the orchestrator. It is deliberately independent of Steve OS, Core Steve, Telegram, and the Steve production runtime: it needs none of those services to be available and only reads/writes durable orchestration state through its own narrow API surface.

  • Installable iPhone-first PWA. A web app manifest and an offline-capable service worker let an operator add the portal to the iOS Home Screen. The service worker is a caching pass-through only; it never performs background polling and never performs a privileged or state-changing action.
  • Manual refresh first. Every phase view has a prominent manual Refresh control. Optional foreground auto-refresh polls at a conservative interval (default 30s, minimum 15s) and only while the tab is visible; it can be disabled by the operator.
  • Bounded polling. The dashboard list and phase detail read the bounded status projection; incremental activity uses the bounded event-delta endpoint rather than returning complete logs.
  • Timers and heartbeats. Active phases and tasks show wall-clock elapsed time that ticks locally between polls and resynchronizes from authoritative server timestamps on every refresh. Active-execution time is shown separately. Heartbeat age is displayed clearly, and missing/overdue heartbeats produce a truthful stale/unknown state rather than being mislabeled as task failure.
  • Provenance and activity. Phase detail renders task checkpoints, validation and repair activity, Forgejo branch/PR/merge provenance, and errors as readable mobile cards, plus the latest meaningful event and an incremental event stream.

Authentication and session boundary

The portal has its own one-user operator session, independent of the orchestrator's ORCHESTRATOR_ADMIN_TOKEN. The browser never receives that token or any Forgejo, database, model-provider, SSH, deployment, or infrastructure credential.

  • Login is rate-limited (ORCH_PORTAL_LOGIN_MAX_ATTEMPTS attempts per ORCH_PORTAL_LOGIN_WINDOW_SECONDS) with constant-time password comparison.
  • Sessions use an HMAC-signed cookie (ORCH_PORTAL_SECRET), a fixed ORCH_PORTAL_SESSION_TTL_SECONDS TTL, HttpOnly, SameSite=Strict, and a Secure flag when ORCH_PORTAL_SECURE_COOKIES=true. The session cookie is never readable by browser JavaScript.
  • All state-changing routes require a per-session CSRF token (single-use for login, reusable for the other actions) via the double-submit cookie/header pattern.
  • Privileged human actions are server-mediated: the browser never receives the ORCHESTRATOR_ADMIN_TOKEN or any Forgejo, database, model-provider, SSH, deployment, or infrastructure credential. The portal exposes four distinct, CSRF-protected, rate-limited actions after operator authentication — development approval (draft → approved, and pauses a deployed phase), failed-phase resume (reuses the existing approval, never fabricates a second one), deployment approval, and deployment rejection. Deployment approval only advances governance state; it never triggers an external deployment from this MVP.

Portal configuration

Variable Purpose
ORCH_PORTAL_USERNAME Operator username (default operator).
ORCH_PORTAL_PASSWORD Local-test compatibility value; production Compose leaves it empty.
ORCH_PORTAL_PASSWORD_HASH Vault-resolved pbkdf2_sha256$iterations$salt_b64$hash_b64 value.
ORCH_PORTAL_SECRET Vault-resolved stable HMAC key for signed session cookies.
ORCH_PORTAL_SECURE_COOKIES true when served over HTTPS.
ORCH_PORTAL_SESSION_TTL_SECONDS Session lifetime (default 43200).
ORCH_PORTAL_LOGIN_MAX_ATTEMPTS / ORCH_PORTAL_LOGIN_WINDOW_SECONDS Login rate limit.
ORCH_PORTAL_ACTION_MAX_ATTEMPTS / ORCH_PORTAL_ACTION_WINDOW_SECONDS Rate limit for state-changing portal actions.

Public deployment configuration fails closed: production resolves protected portal credentials through Vault references and never falls back to plaintext environment values; secure cookies and SameSite=Strict are enforced when secure cookies are requested, and no reverse-proxy example publishes the existing privileged /api/* surface. This development phase ends at deployment_pending; it does not deploy the portal or replace the running orchestrator.

Use python3 scripts/provision_portal_credentials.py from the approved deployment checkout to enter the portal password without echo and provision the hash/session secret through the private Vault admin path. The helper prints only generic status; never paste its input or protected output into chat, Git, Notebook, or reports.

See docs/operator-portal.md for the operator workflow, docs/deployment.md for deployment/fail-closed configuration, and deploy/reverse-proxy/ for the portal-only Caddy/nginx examples and the security rules they enforce.

ChatGPT handoff envelopes

The portal uses versioned, transport-neutral JSON envelopes to move work between an operator and ChatGPT. Each envelope is a plain JSON object with an envelope type and a version; the same contract is reusable by a future direct ChatGPT/MCP integration.

  • Paste ChatGPT Plan (steve-orch-plan v1): pasted text is parsed tolerantly (plain JSON or a fenced code block) into { title, repo, objective, acceptance_criteria, validation_commands, tasks[] }, validated against the orchestrator's own PhaseCreate shape and repo allowlist, and rendered as a human-readable preview before the operator submits it. Submission only creates a draft phase — it never approves or runs anything.

    Each tasks[] entry is either a legacy string (interpreted as an implementation task) or a structured object with exactly task_summary and task_kind. task_kind must be one of implementation, verification, or review; any other value, a missing summary/kind, or an unexpected field fails validation before a draft phase can be created:

    {
      "envelope": "steve-orch-plan",
      "version": 1,
      "title": "Mobile portal phase",
      "repo": "jon/steve-os",
      "objective": "Build the mobile control portal",
      "acceptance_criteria": ["Portal is installable"],
      "validation_commands": ["python -m pytest -q"],
      "tasks": [
        "Build the dashboard",
        {"task_summary": "Verify installability", "task_kind": "verification"},
        {"task_summary": "Review the diff", "task_kind": "review"}
      ]
    }
    

    The plan preview and the phase detail screen both show each task's kind, so the operator can verify classification before approving development.

  • Copy Update for ChatGPT (steve-orch-update v1): a compact, sanitized summary of phase state, task, status, heartbeat freshness, wall-clock and active-execution timing, PR provenance, latest error, and the single required next human action. No secrets, credentials, or raw logs are included.

  • Copy Failure Report for ChatGPT (steve-orch-failure-report v1): bounded diagnostic content — latest error, current task, repair/validation counts, and a small truncated event tail — sufficient for troubleshooting without secrets or giant raw logs.

All envelope content is subject to the same content-minimization rules as portal status, heartbeat, and handoff output (bounded strings and truncated event tails).

Development Notebook (Forgejo-backed current truth)

The Development Notebook is the canonical, continuously maintained, secret-free description of current Steve OS development reality. It renders a single STEVE-DEVELOPMENT-NOTEBOOK.md from small typed current-state source sections stored in Forgejo, and is the single source of current truth that new ChatGPT conversations, Codex tasks, Steward sessions, and operator handoffs read instead of rediscovering or reteaching operating knowledge. It is not an append-only journal: superseded current state is replaced or removed, while historical evidence remains in Forgejo history and campaign/release/incident records.

  • Service. src/steve_orchestrator/notebook/ exposes a token-authenticated REST API plus a tool-only MCP JSON-RPC 2.0 surface. The Forgejo token (NOTEBOOK_FORGEJO_TOKEN) stays inside the service; clients authenticate only with the Notebook API token and supply an explicit actor identity on writes.
  • Read/search/fetch. notebook.read, notebook.list_sections, notebook.fetch_section, notebook.search, notebook.render, notebook.validate.
  • Bounded revision-aware writes. notebook.update_section and notebook.remove_section carry actor_kind, reason, and an optional expected_revision; a stale revision fails closed instead of overwriting newer truth, and every mutation records actor/reason/previous/resulting revision provenance. Writes reject passwords, tokens, private keys, app passwords, and secret-bearing DSNs at the boundary and can never mint operational, approval, or deployment authority.
  • Steward integration. Development Steward reads relevant Notebook context before planning and reconciles affected Notebook sections as part of campaign completion.
  • MCP transports. Streamable HTTP POST /mcp (default 0.0.0.0:8011, bearer-token gated) and the steve-development-notebook-mcp stdio console script. Neither hands Forgejo credentials to ChatGPT or Codex.

See docs/notebook-mcp.md for the stable tool contract, environment variables, and the exact ChatGPT Developer Mode custom-app and Codex registration procedures. The fresh-context acceptance test (a brand-new ChatGPT conversation or Codex task recovering current development context from the Notebook alone and producing a valid legacy steve-orch-plan v1 envelope without operator reteaching) is defined in docs/notebook-commissioning.md and is executed only after Notebook deployment/connectivity is explicitly authorized.