- Python 90.4%
- JavaScript 7.1%
- PLpgSQL 0.7%
- CSS 0.7%
- Shell 0.6%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Portal and Orchestrator quality / quality (push) Successful in 11m9s
|
||
| .github/workflows | ||
| config | ||
| deploy/reverse-proxy | ||
| docs | ||
| migrations/postgresql | ||
| scripts | ||
| src/steve_orchestrator | ||
| tests | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| .node-version | ||
| compose.yml | ||
| CURRENT_CAPABILITIES.json | ||
| DEVELOPMENT_STEWARD_OVERNIGHT_REHABILITATION_2026-09-13.md | ||
| DEVELOPMENT_STEWARD_V2_AUTHORITATIVE_RECONCILIATION_2026-09-07.md | ||
| DEVELOPMENT_STEWARD_V2_CONTINUATION_REQUIRED.md | ||
| Dockerfile | ||
| Dockerfile.agent | ||
| Dockerfile.core-chat-fix | ||
| Dockerfile.core-live | ||
| Dockerfile.fetcher | ||
| Dockerfile.model-proxy | ||
| Dockerfile.provision-proxy | ||
| Dockerfile.uv-lock-bridge | ||
| Dockerfile.uv-lock-bridge-authoritative | ||
| Dockerfile.uv-resolver | ||
| Dockerfile.validator | ||
| env.example | ||
| OVERNIGHT_REPORT.md | ||
| package-lock.json | ||
| package.json | ||
| pyproject.toml | ||
| README.md | ||
| REFRACTOR_DELIVERY.md | ||
| uv.lock | ||
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:
- A human creates and approves a development phase.
- 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.
- Only after every merged task passes fresh post-merge validation does the phase stop in
deployment_pending. - 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;
.gitmetadata 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
uvtoolchain 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/mainand the checkoutHEADmust both exactly equal the recorded Forgejo merge SHA before any validation begins; any mismatch fails closed withpost_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 explicitpost_merge.validatedaudit event; - only after that success may a task become
complete, and only after every task is complete does the phase reachdeployment_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_kindofverificationorreview) 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 avalidated_shaequal to the validatedmainhead) and emitted as atask.no_op_validatedaudit event; no commit, branch, or PR is fabricated; - the phase manifest carries
kind,no_op, andno_op_validated_atfor each task, so a phase containing a successful validated no-op task proceeds normally todeployment_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.6copied from Astral's version-tagged image) instead of installing uv dynamically from repository dependencies. The resolver image is selected withORCH_SANDBOX_UV_RESOLVER_IMAGEand defaults tosteve-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.tomland existinguv.lock..gitmetadata, 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.lockartifact. 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 isORCH_UV_LOCK_CAPABILITY_SECRET(empty fails closed). - The broker accepts only a bounded, regular-file
uv.lockresult, 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 workspaceuv.lockuntouched; there is no fallback to hand-editinguv.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 boundedrepairingretry), 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) — noORCHESTRATOR_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 assteve-uv-lock-helper). The helper reads onlySTEVE_UV_LOCK_CAPABILITYandSTEVE_UV_LOCK_BRIDGE_URL, POSTs an empty body with the capability header to the fixed/uv-lock/resolvepath, 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 regenerateduv.lockis present in the worker's/workspacewhen the workspace is exported and therefore survives the subsequent host worktree replacement; a staleuv.lockfrom 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-editinguv.lockas a substitute when governed regeneration is required. - The authoritative bridge is served as a dedicated, capability-only listener on port
8083that mounts onlyPOST /uv-lock-bridge/uv-lock/resolveand the minimumGET /uv-lock-bridge/healthliveness route. The network-exposed process is a minimum-authority forwarder (steve_orchestrator.uv_lock_bridge_server, its ownuv-lock-bridgeCompose service andDockerfile.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_brokercallback) 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 withcap_drop: ALL,no-new-privileges, a read-only root filesystem, no data/workspace/SSH volumes, an explicit least-privilege network attachment (only the dedicated internaluv_lock_bridgenetwork shared with the orchestrator, never the sharedcontrol_planenetwork, 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 on8000. Its exposure is separated from its private bind:ORCHESTRATOR_UV_LOCK_BRIDGE_PUBLISH_HOSTis the concrete host interface the port is published on (loopback-only by default; the actual{host}:8083:8083Compose mapping), andORCHESTRATOR_UV_LOCK_BRIDGE_BIND_HOSTis the private container-network bind (0.0.0.0inside 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 ofGET /uv-lock-bridge/healthagainst the concrete forwarder host (ORCH_UV_LOCK_BRIDGE_READY_HOST, theuv-lock-bridgeservice) 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_SECRETunset the capability cannot be minted, and withORCH_UV_LOCK_BRIDGE_UPSTREAMunset 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_SECONDSplus verification), while the broker's single-flightUvLockJobRegistrymakes 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 everyORCH_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 pastORCH_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 externalORCHESTRATOR_UV_LOCK_RETENTION_SECONDS/ORCHESTRATOR_UV_LOCK_CLEANUP_INTERVAL_SECONDSmap to the application inputsORCH_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 tocompleted_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_attocompleted_atwhen 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/unknownstate 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_ATTEMPTSattempts perORCH_PORTAL_LOGIN_WINDOW_SECONDS) with constant-time password comparison. - Sessions use an HMAC-signed cookie (
ORCH_PORTAL_SECRET), a fixedORCH_PORTAL_SESSION_TTL_SECONDSTTL,HttpOnly,SameSite=Strict, and aSecureflag whenORCH_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_TOKENor 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-planv1): 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 ownPhaseCreateshape 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 animplementationtask) or a structured object with exactlytask_summaryandtask_kind.task_kindmust be one ofimplementation,verification, orreview; 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-updatev1): 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-reportv1): 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_sectionandnotebook.remove_sectioncarryactor_kind,reason, and an optionalexpected_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(default0.0.0.0:8011, bearer-token gated) and thesteve-development-notebook-mcpstdio 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.