- Python 93.6%
- Shell 6.2%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| scripts | ||
| src/steve_orchestrator | ||
| tests | ||
| .gitignore | ||
| compose.yml | ||
| Dockerfile | ||
| Dockerfile.agent | ||
| Dockerfile.fetcher | ||
| Dockerfile.model-proxy | ||
| Dockerfile.provision-proxy | ||
| Dockerfile.validator | ||
| env.example | ||
| pyproject.toml | ||
| README.md | ||
Steve OS Orchestrator
A deployable v0.1 control plane for semi-autonomous Steve OS development.
The workflow is intentionally split into two human gates:
- A human creates and approves a development phase.
- The orchestrator autonomously clones the target Forgejo repository, runs local OpenHands with DeepSeek V4 Pro, 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 authoritative Forgejo main, 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.
- DeepSeek V4 Pro through OpenHands' OpenAI-compatible LLM configuration.
- Autonomous git branch, commit, push, PR creation, and merge.
- Per-phase acceptance criteria and explicit validation commands.
- Hard stop before deployment.
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
initial sandbox images. They must be built or made available on the dedicated sandbox engine before an
approved phase can execute. Dependency/tool provisioning is intentionally deferred
to a later governed-provisioning phase rather than allowing repository code to
mutate the long-lived control plane.
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; - 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 two repair attempts per
task and is configurable with ORCH_MAX_REPAIR_ATTEMPTS.
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 2). 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.
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 five
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.