preloop reimplements the GitHub Actions control plane and official actions/runner in Rust. The unmodified runner registers, polls, executes, and reports against it. Also contains runner-watch for protocol conformance testing.
| Crate | Role |
|---|---|
preloop-runner-server |
HTTP control plane: /_apis/… (runner protocol) + /api/v1/… (native REST) + /broker/… |
preloop-gha-parser |
Workflow YAML → typed model → job DAG/matrix expansion |
preloop-gha-expressions |
${{ }} parser/evaluator |
preloop-gha-protocol |
Wire DTOs, session crypto, secret wrappers, NDJSON events |
preloop-runner |
Rust runner: Listener + Worker (faithful to actions/runner v2.335.1 wire protocol; official runner pin 2.336.0 in versions.toml) |
preloop-runner-client |
CLI for submitting workflows |
preloop-cache / preloop-artifacts |
File-backed protocol storage |
preloop-dap |
Debug Adapter Protocol bridge |
preloop-conformance / runner-watch |
Conformance harnesses and protocol-diff tooling |
preloop-cli |
The preloop binary (run/serve/debug/push CLI) |
preloop-orchestrator |
Golden/env provisioning and the runner pool |
preloop-vm |
VmProvider backends (SmolVM/libkrun, AgentENV) |
preloop-observability |
Logging/metrics/OTel export |
preloop-socket-activation |
systemd socket activation |
just test-ci # fmt-check + clippy + zizmor + test + conform replay (the full gate)
just serve # cargo run --release -p preloop-runner-server -- serve --listen 127.0.0.1:9090
just dogfood # E2E with real runner- Toolchain: Rust 1.97,
cargo fmt,cargo clippy --workspace --all-targets. - Error handling:
anyhowat top-level;ApiErrorin HTTP handlers;thiserrorenums in libraries. - State: in-memory behind
Arc<Mutex<…>>+Notify/broadcast. Secrets useSecretString— callexpose()only at protocol boundaries. - Wire compatibility:
/_apis/…is the source of truth. Validate protocol changes against the official runner, not only unit tests. - Broker path only: all work targets the modern broker + Twirp results-service protocol (v2.329.0+).
- VM substrates: two backends behind
preloop_vm::VmProvider— SmolVM (libkrun; the default everywhere) and AgentENV (Firecracker over/dev/kvm; opt-in viaPRELOOP_VM_BACKEND=agentenv). Selected byPRELOOP_VM_BACKEND; capability differences are declared byVmProvider::capabilities()and the orchestrator branches on them. Seedocs/vm-substrates.mdbefore touching pool or provider code. - Store backends: the
Storetrait (store.rs, async, object-safe) is the only surface the server sees; backends are SQLite (store.rs, default,<state_dir>/preloop.db) and Postgres (store_pg.rs), selected viaPRELOOP_STORE_URL(sqlite://<path>/ bare path /postgres://…). Both are single-writer: one connection behind a mutex. Two servers on the same SQLite file (or same PG database) still diverge in-memory — the DB is a restart source, not a shared bus. - Store is best-effort: in-memory state is the source of truth, the database is a restart source. Store failures are logged; the affected event is still broadcast (see
state.rs::emit). Per-backendMIGRATIONSis the schema source of truth (SQLite:PRAGMA user_version; PG:schema_migrationsversion table). - Encryption-at-rest is obfuscation, not security:
<state_dir>/hmac-key.binandpreloop.dbsit in the same directory; the store key is HKDF-derived from the JWT HMAC key with domain separation. It stops a stolen DB file, not a compromised state dir. Key loss = unbootable state. The envelope is backend-independent (sealed blobs), so it applies to Postgres rows too — for remote PG, rely on TLS + DB auth instead.
docs/architecture.md— crate map + module mapdocs/fidelity-gap.md— protocol gaps and conformance statusCONTRIBUTING.md— dev workflow and compatibility checklistfixtures/workflows/dogfood.yml— local self-hosted validation workflow.runner-watch/golden/v2.335.1/— protocol golden captures (prior baseline)versions.toml— pinned official runner (2.336.0)- Official runner binary cache:
~/.cache/actions-runner/current(osx-arm64) - Official runner source checkout:
/tmp/runner-v2.336.0(commit98aabcd)
- Be critical. Push back with evidence when a plan hides risk or a claim is wrong.
- Composability is the goal. Any runner should work with any server. Never introduce protocol divergences.
- Local CI is mandatory. After every large chunk of work or task, run
just test-cito validate the changes and dogfood the workflow. - Drop-in workflows. Users should be able to run their workflows in local CI unmodified.
- Supply-chain policy isolation. A PR must not mix policy files
(
Cargo.lock,deny.toml,.cargo/audit.toml,supply-chain/,.github/workflows/supply-chain.yml) with non-policy changes — the supply-chain audit job fails the PR. The one sanctioned mixed shape is a dedicated dependency PR: exactlyCargo.lockplus the root/crateCargo.tomlmanifests that declare the dependency; the guard accepts that shape and rejects anything else riding along (source, tests, docs, other policy files). Adding a dependency (even a dev-dependency) touchesCargo.lock; use that dedicated PR or vendor the need away.
When investigating CI behavior — queue stalls, job failures, check-run state,
runner churn — default to preloop's own debug features before log-diving.
Dogfood them; any divergence from docs/debug-sessions.md is a bug to report.
- Sessions: failed jobs pause into debug sessions (state machine in
docs/debug-sessions.md§2). List:preloop debugorGET /api/v1/debug/sessions(native bearer). Attach:preloop debug [<ref>]— in-VM verbs:retry,:retry --sync,:retry --from <step>,:sync,:export. Non-interactive:preloop debug --verdict retry|continue|abort. - Agent API (native bearer): lease
POST /api/v1/agent/debug/sessions/<id>/lease, streamGET .../events, drivePOST .../operations, auditGET .../audit. - DAP: attach over WS
GET /api/v1/runs/<run_id>/debug(native bearer);preloop-dapbridges DAP to the session state machine. - Hold VMs:
preloop run --preserve-on-failurekeeps the VM for a laterpreloop shell/attach when nothing can answer interactively (piped/detached/CI). - Leaked sessions (terminal states
abandoned,aborted, or sessions that never close) count as findings, not noise.