Runloop OS Documentation
Runloop OS combines agent runtimes, tooling, and policy controls to help operators deploy dependable autonomous services. This landing page gathers the most relevant guides and specs from this workspace.
Start Here
- Project overview
- Getting started guide
- Architecture overview
- Agent bundles & scaffolding
- Example openings
Key Specifications
Operational Playbooks
More Resources
Check back after each release—updates to this directory automatically publish to GitHub Pages once the site is enabled under repository settings.
Project Overview
Runloop OS is an agent-native operating layer that sits above your existing Linux system. It routes prompts either to the POSIX shell or to openings: declarative DAGs of WASM agents running with least-privilege capabilities over a typed local message bus.
What ships in this repo
- Daemon and CLI:
crates/runloopdhosts the runtime;crates/rlpdrives routing, KB commands, tracing, and local execution (--local). - Agent bundles: canonical bundles live under
agents/; companion wasm crates are undercrates/agents-wasm/. Manifests, capability policies, and optionaltools.jsonfiles sit next to each bundle. - Openings and scenarios: runnable DAGs live in
examples/openings/and double as fixtures for the planner and executor. - Shared libraries: the message bus, protocol codecs, KB APIs, and model
broker are in
crates/(rmp,bus,kb,model-broker, etc.). - Docs: mdBook sources live in
docs/; the roadmap and ADRs sit alongside specifications for openings, KB schemas, and security policy.
For deeper design context, see DESCRIPTION.md in the repository root or the
architecture overview in docs/architecture.md.
Getting Started (Draft)
Doc status: Informative — links to normative specs. Last updated: 2025-11-02.
Prerequisites
- Debian/Ubuntu host (or container/VM) with Rust toolchain (
rustup,cargo,clippy,rustfmt). - Optional tooling:
just,sqlite3,cargo-deb,live-build,qemu-system-x86_64. rlpCLI (will be built from source once crates exist).
Repository layout refresher
See the tree in README.md for directories. Key docs:
docs/message-protocol.md— wire format spec (normative)docs/rmp-registry.md— schema IDsdocs/kb-schemas.md— ledger & materialized views (normative)docs/ops.md— operations, config precedence, trust policy (normative sections marked)docs/security-model.md— sandbox, secret store, threat model
First-run checklist
-
Clone repo and read
README.mdfront-to-back. -
Configure
~/.runloop/config.yaml(copy from.env.exampleguidance when available). -
Initialize secrets backend (optional):
rlp secrets init --backend=secret-service -
Initialize KB once binaries land:
rlp kb migrate # creates events.sqlite, pog.sqlite, vectors/ rlp kb verify -
Review trust policy (after Release key published):
cat ~/.runloop/trust-policy.toml -
Build the wasm agent bundles and run the smoke test:
just build-agents-wasm just test-agents-wasmThese helpers compile the wasm32-wasip1 binaries under
agents/*/bin/and exercisecompose_emailend-to-end viarlp --local.
Common operational commands
rlp kb migrate|verify|backup|vacuum
Secrets/trust CLIs are still planned:
rlp secrets put|get|list|deleteandrlp trust updateremain interface contracts until their implementations land with the packaging milestone.rlp agent installis available for local bundles (directory or .tar/.tar.gz) and performs digest/tools.json validation;rlp agent removeis still pending. Userlp agent listto inspect bundles already present in your search dirs.
Finer details appear in docs/ops.md. As implementation lands, these commands
gain real outputs; until then they serve as interface contracts.
Installing from the Debian package
-
Build the
.debfrom source (Debian 13/trixie host):just debInstall
cargo-debfirst (cargo install cargo-deb). Artifacts land under each crate’starget/debian/directory, e.g.crates/runloopd/target/debian/runloopd_<version>_<arch>.deb. -
Install and start the daemon:
sudo apt install crates/runloopd/target/debian/runloopd_0.1.0_amd64.deb sudo systemctl status runloopdInstall the CLI/TUI helpers separately if desired:
sudo apt install crates/rlp/target/debian/rlp_0.1.0_amd64.deb sudo apt install crates/agtop/target/debian/agtop_0.1.0_amd64.debThe daemon package writes the default config to
/etc/runloop/config.yaml, creates/var/lib/runloop, and enables (but does not automatically start) therunloopdsystemd unit so you can edit config before running it. -
Update
/etc/runloop/config.yamlas needed, then runsudo systemctl restart runloopd. Purging the package removes the config, state, and therunloopsystem user.
Authoring an agent on a Debian install
If you install Runloop from the packages and want to add your own agent without
cloning the source tree, follow docs/authoring-on-deb.md for the scaffold →
edit → build → run loop (Rust + wasm32-wasip1 target required).
Finding work items
TODO.md→ repo scaffolding checklistROADMAP.md→ milestone-level goals- Issues/Discussions (once public) will tag bugs/features/tasks
Contributing flow (preview)
- Fork/branch following
CONTRIBUTING.mdguidance (to be fleshed out). - For spec changes, open an ADR (
docs/adr/) and mark affected docs as Draft/Normative. - Ensure docs and fixtures stay in sync (e.g., schema registry, KB migrations).
Questions? Open an issue or reach out via channels in SUPPORT.md.
Architecture Overview
Doc status: Informative overview that mirrors the current workspace layout and references normative specs. Last updated: 2025-11-16.
Goals
- Route user prompts between POSIX shell execution and AI agent “openings”.
- Provide deterministic provenance (POG) and audit trails.
- Enforce least-privilege capability security for agents.
- Remain terminal-first and local-first (offline-friendly).
Non-goals (v1)
- Shipping a kernel or Linux distribution.
- Multi-host scheduling or distributed openings.
- Non-Rust agent SDKs (beyond WASM interop).
- GUI/window manager integration.
Components & Boundaries (current)
Each entry lists the owning crate/binary and the primary interfaces it exposes
or depends on. Paths refer to workspace members declared in Cargo.toml.
Router & CLI (crates/router, crates/rlp)
- Classifies interactive prompts (shell fast-path vs. agent opening) and
prepares
ControlRequest::RunSubmitframes for the daemon. - Reads configuration from
~/.runloop/config.yaml//etc/runloop/config.yamlviacrates/coreand obeysrouter.fastpath_*policies described in router-shell.md. - Talks to
runloopdover the Unix socket usingCT_CTRL_REQ/RESPand subscribes torlp/runs/<trace_id>/eventsfor streaming output. - Provides shim binaries such as
rlp run,rlp replay, andrlp traceplus router-aware shell integration snippets (see TODO epic O).
Openings Engine (crates/openings, crates/runloopd)
- Compiles YAML openings into DAGs with node budgets, retries, and deterministic
replay metadata (e.g.,
trace_id,opening_id). - Exposes planners/executors consumed by
runloopd; local/offline execution is surfaced throughcrates/executor-localwhenrlp --localis used. - Persists node/run transitions to the KB via
crates/kbAPIs so replay and audit trails share the same provenance data.
Runtime & Agent Host (crates/runloopd, crates/runtime)
- Owns the Wasmtime engine, spawns agent WASM bundles with capability gating, and enforces lifecycle (timeouts, retry orchestration, resource budgeting).
- Hosts the message bus listener and publishes audit events (
run.*,node.*,cap.audit) into the KB. runloopdnow attaches the openings runner to a bus executor: node invocations are serialized intoCT_EXECUTOR_AGENT_REQUESTenvelopes, published onagent/<agent>, and responses (CT_EXECUTOR_AGENT_RESPONSE) arrive on the per-request reply topic encoded in the payload. A dispatcher keeps per-agent workers alive, so the runner no longer calls the LocalExecutor directly.- Exposes the daemon service entrypoint packaged for both user and system mode.
Message Bus & RMP (crates/rmp, crates/bus)
- Implements the 64-byte header/frame codec plus MsgPack body helpers described in docs/message-protocol.md.
- Provides subscription/publish APIs layered on Unix domain sockets with TTL
enforcement, dedupe caches, ACLs for
action.decision, and drop telemetry onrlp/sys/drops. - Shared by the daemon, CLI, TUI, and agents; all components depend on the same
schema registry (
docs/rmp-registry.md).
Knowledge Base / POG (crates/kb, docs/kb-schemas.md)
- Maintains the append-only ledger (
events.sqlite) plus materialized views (pog.sqlite) and optional embedding stores. - Validates event payloads against schemas checked into
crates/kb/schemas/and exposes migration/verify commands consumed by the CLI (rlp kb ...). - Provides APIs to query runs, artifacts, contacts, and deterministic replay fixtures.
Model Broker (crates/model-broker)
- Centralizes provider credentials, rate limits, and caching for model/tool calls made by agents.
- Enforced budgets flow through
runloopd(per-opening limits) and are exposed to TUI panes for operator visibility.
CLI Tooling & TUI (crates/rlp, crates/agtop, crates/executor-local)
rlpoffers operational commands (run,trace,replay,kb,config).agtopconsumes NDJSON run streams or live bus subscriptions to show plan DAGs, logs, metrics, and trace ladders.executor-localbacksrlp --localby instantiating agents within the CLI process for offline validation while still emitting the unified run-event schema.
Agent Registry & Bundles (crates/agent-registry, crates/agents/*, agents/)
- Source of truth for canonical agent bundles, manifests, capabilities, and DCO signatures referenced by the runtime.
- Includes shared SDK layers (
crates/sdk/runloop-sdk,crates/sdk/agent-shim) so native and WASM agents speak RMP consistently until the pure WASM path is complete. - Optional host tool attachments live in
tools.jsonalongside each manifest; seedocs/tool-attachments.mdfor the schema emitted byrlp agent scaffold.
Observability & Ops (crates/agtop, docs/ops.md, docs/perf.md)
tracing-based spans propagatetrace_id/opening_id; metrics exporters can emit OTLP when configured viaobservability.traces.- ASCII ladder traces (
rlp trace) and KB-backed audits provide operator-level visibility; packaging assets (packaging/) install service units and shell hooks on Debian.
Interaction Model
- Prompt ingress: An interactive shell prompt is handed to the router via
shell hooks or directly via
rlp run. Policies decide between POSIX execution and submitting an opening request. - Control-plane negotiation: The CLI publishes
CT_CTRL_REQonrlp/ctrl(or runs locally) and waits up to 2000 ms forRunAccepted. The daemon replies withCT_CTRL_RESPincluding the assignedtrace_idand opening metadata. - Run event stream: Once accepted,
runloopdemitsCT_RUN_EVENTframes torlp/runs/<trace_id>/events. Consumers (CLI stdout,agtop, tests) render logs, plan status, artifacts, and trace ladder entries from this single stream. - Agent execution: Opening nodes orchestrated by the engine exchange RMP
messages over the bus; capability checks fire before any hostcall (FS/NET/
TIME/Kb/Model) and denied calls generate
cap.auditevents. - Persistence & replay: Ledger events (
run.started,run.finished,node.finished,run.trace,artifact.created, etc.) are recorded throughcrates/kb. Replays sourced from the KB must deterministically regenerate the same outputs (rlp replay). - Observability: Drops, TTL expirations, and dedupe hits publish structured
diagnostics on
rlp/sys/drops, whiletracingspans feed OTLP exporters when enabled.
Runtime Modes & Packaging
- User mode:
cargo run -p runloopdplusrlpuses~/.runloopfor state and discovers sockets viaruntime.socket_pathorruntime.sockets_dir. - System mode: Debian packages install
runloopdasrunloop:runloop, store KB data under/var/lib/runloop, drop sockets into/run/runloop, and install shell integration snippets (opt-in) under/etc/profile.d. - Live-build ISO tooling in
packaging/live-buildconsumes the same binaries and seeds demo scenarios referenced inexamples/openings/.
Security & Capabilities
- Capability manifests reside alongside agents (
policy.caps) and are parsed by the runtime before any hostcall is granted; overrides can only further limit access (seedocs/policy-caps.md). - Secrets always travel via opaque
secret_idindirections, never raw blobs in KB events; the CLI documents rollback/disable paths indocs/ops.md. - The router enforces shell fast-path guards to avoid routing non-interactive
scripts; ACLs prevent untrusted publishers from emitting
action.decision.
Observability Surfaces
agtoppanes (Log, Plan, agtop metrics) subscribe to the same run stream and bus metrics.docs/perf.mdoutlines the throughput harness (≥600 msgs/starget for loopback testing) and should be referenced when adjusting bus internals.- Counters (
msgs_sent,drops_total,cap_denied, etc.) and gauges (agents_running,bus_queue_depth) are emitted via the runtime’s metrics facade for scraping or interactive display.
Reference Map
- Protocol: docs/message-protocol.md
- Knowledge base schemas: docs/kb-schemas.md
- Openings grammar: docs/openings-dsl.md
- Router & shell integration: see
docs/router-shell.mdfor routing heuristics, snippets, and opt-in shell wiring. - Operations: docs/ops.md
- Observability tooling: docs/perf.md,
docs/tui.md(screenshots).
Router configuration (MVP)
router.fastpath_shell: enable or disable the shell fast-path.router.default_opening: opening name used when routing to agents.router.allowlist/router.denylist: prompt patterns that force or block shell execution.router.known_commands: extra command names layered onto the builtin + PATH discovery set.
Trust & Capabilities Model
- Agents declare capabilities via
policy.caps(seedocs/policy-caps.md). - Overrides located at
~/.runloop/caps/overridesonly revoke privileges. - Trust policy controls which signatures can install/run agents (
docs/ops.md,docs/security-model.md).
Configuration precedence (summary)
- CLI flags → environment (
RUNLOOP_*) → user config (~/.runloop/config.yaml) → system config (/etc/runloop/config.yaml) → defaults. - Policy keys defined by system config act as hard limits; lower layers can only tighten them.
- Merge semantics detailed in
docs/ops.md.
Portability Plan
- Primary target: Debian 12 (x86_64, arm64) host OS.
- Future exploration: Redox and container-portable builds once v1.0 baseline achieved.
For deeper details, consult README.md, docs/message-protocol.md,
docs/kb-schemas.md, and docs/ops.md.
Control & Transport (MVP)
- A single Unix domain socket serves both the message bus and control plane.
- The CLI submits openings by publishing
CT_CTRL_REQonrlp/ctrl. The daemon responds withCT_CTRL_RESP::RunAcceptedand streamsCT_RUN_EVENTtorlp/runs/<trace_id>/events. - Socket discovery precedence:
runtime.socket_path(short‑circuit), then${runtime.sockets_dir}/rmp.sock, then~/.runloop/sock/rmp.sock, then/run/runloop/rmp.sock. - Only UI/TUI publishers may emit
action.decisionon the bus; CLI does not prompt when connected to the daemon.
KB Ownership (MVP)
- For daemon-backed runs,
runloopdrecordsrun.started,run.finished, per-nodenode.finished, and the canonicalrun.tracepayload so replay and audit can run without re-contacting agents. Local CLI runs (--local) follow the same path and flush the materializer before returning.
Agents
Agent bundles are the deployable units the runtime executes. Each bundle declares its capabilities, optional host tool attachments, and a wasm binary built from the companion crate.
Bundle layout
agents/<name>/manifest.toml— metadata plusentry_wasmdigest.agents/<name>/policy.caps— capability allowlist (fs/net/kb/model, etc.).agents/<name>/tools.json— optional host tools exposed to the agent.agents/<name>/bin/*.wasm— compiled wasm binaries.crates/agents-wasm/<name>/— source crate that produces the wasm binary.- Discovery defaults (user mode):
~/.runloop/agentsfor bundles and~/.runloop/examples/openingsfor openings, with repo paths (./agents,./examples/openings) and system paths (/var/lib/runloop/agents) searched as fallbacks; use--agents-dir/--openings-dirto pointrlpat custom locations.
Use just build-agents-wasm to rebuild the canonical bundles and refresh their
digests, or just test-agents-wasm for an end-to-end smoke test of the
compose_email opening.
Install prebuilt bundles
If you have a packaged bundle (directory or .tar/.tar.gz), install it into a
search dir with:
rlp agent install /path/to/agent.bundle.tar
Use --root to target a specific registry (e.g. /var/lib/runloop/agents for
the system daemon). rlp agent list now reports bundle status and the source
search dir so you can confirm discoverability.
Scaffold → build → run (example)
The CLI can generate a new agent plus a starter opening wired to it. This flow assumes a source checkout; see authoring-on-deb.md for the packaged install variant.
# 1) Scaffold bundle + crate + optional starter opening
rlp agent scaffold note_taker --opening
# 2) Implement your agent in crates/agents-wasm/note_taker/src/main.rs
# (the stub prints placeholder JSON so the next step already succeeds)
# 3) Build and copy the wasm into the bundle, updating digests
rlp agent build note_taker
# 4) Run the generated opening locally to verify the wiring
rlp run examples/openings/note_taker.yaml --local \
--params '{"prompt":"draft a standup note"}'
--rootand--crates-diroverride the default bundle/crate roots.- To add the agent to other openings, reference it via
use: agent:note_takerand pass parameters in the node’swith:block. - Capabilities are enforced at runtime; tighten
policy.capsbefore shipping.
Walkthrough: system settings agent (tmux/history)
Example flow for an agent that improves your tmux setup or extends shell history by editing dotfiles and (optionally) running tmux to reload the config.
- Scaffold (interactive wizard)
rlp agent scaffold system_tra --opening
Answer the prompts as you did (model = google:gemini-2.5-flash, blank network,
KB disabled). This creates:
agents/system_tra/bundle (manifest,policy.caps,tools.json, bin/)crates/agents-wasm/system_tra/crate with a stubmain.rsexamples/openings/system_tra.yamlwired to the new agent
- Grant the right capabilities
Edit agents/system_tra/policy.caps to allow the files you want to touch and,
if needed, host command execution. Use absolute paths (replace /home/ivan with
your home):
[capabilities]
model = true # keep model access if the agent uses LLMs
fs = [
"/home/ivan/.tmux.conf",
"/home/ivan/.config/tmux",
"/home/ivan/.bashrc", # for HISTSIZE/HISTFILESIZE tweaks
]
exec = true # required only if you will run tmux to reload
time = true # optional: timestamps for logs/decisions
net = [] # stay offline for system tweaks
kb_read = false
kb_write = false
Drop exec = true if the agent will only edit files. Keep fs as narrow as
possible; add other paths (e.g., /home/ivan/.zshrc) as needed.
- Implement the agent logic
The bundled implementation already:
- Parses
with.inputas JSON. - Manages a tmux block bounded by
# >>> runloop:system_tra/# <<< …and setshistory-limit. - Updates
HISTSIZE/HISTFILESIZEin~/.bashrc. - Emits a JSON result with file paths and whether each file was updated.
Tweak crates/agents-wasm/system_tra/src/main.rs if you want different defaults
(e.g., disable shell history edits) and widen policy.caps only when needed.
- Build and run
rlp agent build system_tra
rlp run examples/openings/system_tra.yaml --local \
--params '{"prompt":"raise tmux history limit to 50000"}'
The generated opening already passes a prompt param; adjust it or add more
structured fields under with: if your agent expects specific inputs.
Authoring an agent on a Debian install
This is the minimal flow for creating and running a custom agent when Runloop is
installed from the .deb packages (no source checkout required).
Prerequisites
-
Rust toolchain installed via
rustup. -
WebAssembly target installed once:
rustup target add wasm32-wasip1. -
Runloop packages (
runloopd,rlp,agtop) installed from the.deb. -
Verify the CLI you installed exposes agent commands:
rlp --version # expects rlp >= 0.1.2 rlp agent --help # should list scaffold/build/listIf
agentis missing, reinstall from the latest.deb.
Steps
-
Scaffold
rlp agent scaffold my_agentThis creates:
~/.runloop/agents/my_agent/bundle withmanifest.toml,policy.caps,tools.json,bin/.~/.runloop/agents-wasm/my_agent/crate with a stubsrc/main.rs.- Optional starter opening YAML under
~/.runloop/examples/openings/<name>.yamlif requested.
-
Author
- Edit
~/.runloop/agents-wasm/my_agent/src/main.rswith your logic. - Adjust capabilities in
~/.runloop/agents/my_agent/policy.caps. - Add tools in
~/.runloop/agents/my_agent/tools.json(version 1).
- Edit
-
Build + install into the bundle
rlp agent build my_agentThe command compiles the wasm (
cargo build --release --target wasm32-wasip1), copies it into~/.runloop/agents/my_agent/bin/, recomputes BLAKE3 digests forentry_wasmandtools.json, and validates the caps file. -
Run
- If you generated a starter opening:
rlp run ~/.runloop/examples/openings/my_agent.yaml --params '{"prompt":"..."}' - Otherwise wire the agent into an opening and run it via
rlp run <opening.yaml>.
- If you generated a starter opening:
Picking an executor (local vs daemon)
rlp ... --localuses the same registry search dirs as the daemon. If your bundle lives under~/.runloop/agents(default) or you pass--agents-dir, it is discoverable in local runs.- The packaged daemon runs as user
runloopwith home/var/lib/runloop, and its default agent search dirs resolve under that home. Agents you scaffolded in/home/<you>/.runloop/agentswill not be visible to the daemon until you point the daemon at them or install into a daemon-owned search dir.
Running your agent with the packaged systemd service
- Point the daemon at your bundle/openings and keep the socket reachable (or
install the bundle into
/var/lib/runloop/agentswithrlp agent install --root /var/lib/runloop/agents <bundle.tar>):
mkdir -p ~/.runloop
cat > ~/.runloop/config.yaml <<EOF
agents:
search_dirs:
- $HOME/.runloop/agents
openings:
search_dirs:
- $HOME/.runloop/examples/openings # adjust to where you keep openings
runtime:
sockets_dir: /run/runloop # matches the packaged service default
EOF
- Add a systemd drop-in so the service uses your config and creates a group-writable socket:
sudo mkdir -p /etc/systemd/system/runloopd.service.d
cat <<EOF | sudo tee /etc/systemd/system/runloopd.service.d/override.conf
[Service]
Environment=RUNLOOP_CONFIG=$HOME/.runloop/config.yaml
UMask=0002
RuntimeDirectoryMode=0775
EOF
-
Make sure your user can connect to
/run/runloop/rmp.sock:- Add yourself to the
runloopgroup:sudo usermod -a -G runloop "$USER"(open a new shell so the group is applied). - Keep the socket group-writable via the drop-in above.
- Add yourself to the
-
Ensure the daemon user can read your bundle:
- Simplest: make your home traversable (
chmod 711 "$HOME") so$HOME/.runloop/agents/...is readable, or copy the bundle to a daemon-owned path such as/usr/lib/runloop/agents/my_agent.
- Simplest: make your home traversable (
-
Reload and restart the service:
sudo systemctl daemon-reload
sudo systemctl restart runloopd
- Run your opening (no
--localso it goes through the daemon):
rlp run "$HOME/examples/openings/my_agent.yaml" --params '{"prompt":"..."}'
If you prefer not to touch the system service, point both daemon and CLI at a home-local socket instead:
- Update
~/.runloop/config.yamlso the socket lives under your home (eitherruntime.sockets_dir: $HOME/.runloop/sockorruntime.socket_path: $HOME/.runloop/sock/rmp.sock). The CLI resolves the socket in this order:runtime.socket_path, then${runtime.sockets_dir}/rmp.sock, then~/.runloop/sock/rmp.sock, then/run/runloop/rmp.sock. - Run your own daemon with that config:
runloopd --config ~/.runloop/config.yaml &
rlp run "$HOME/examples/openings/my_agent.yaml" --params '{"prompt":"..."}'
Use an env override only for a temporary socket change:
RUNLOOP__RUNTIME__SOCKET_PATH=$HOME/.runloop/sock/rmp.sock rlp run ...
Notes
rlp config path --allshows which config layers are active; unreadable/etc/runloop/config.yamlis skipped with a warning.- To rebuild after edits, rerun
rlp agent build <name>; it will refresh the wasm and manifest digests. - Bundle/crate roots can be overridden with
--root/--crates-dirflags if you keep agents outside~/.runloop.
Examples & Openings
Sample openings live in examples/openings/ and double as fixtures for the
planner/executor. They run against the canonical agents stored in agents/ once
their wasm bundles are built.
compose_email.yaml— multi-agent crew: contacts → context → writer → critic → mailer (human confirm). This is the canonical smoke test.system_helper.yaml— single-node helper that can invoke a host command and optionally call Gemini.tmux_layout.yaml— apply tmux presets and optionally reload/apply a layout.system_tra.yaml— raise tmux/shell history limits with a managed config block.
Running an example opening
# ensure wasm bundles are present
just build-agents-wasm
# or: cargo build --release --target wasm32-wasip1 \
# --manifest-path crates/agents-wasm/Cargo.toml
# run compose_email locally (no daemon)
cargo run -p rlp -- run examples/openings/compose_email.yaml --local \
--params '{"recipient":"john"}'
Monitor a run by piping the NDJSON stream into the TUI:
cargo run -p rlp -- run examples/openings/compose_email.yaml --local \
--params '{"recipient":"john"}' > run.ndjson
cargo run -p agtop -- --input run.ndjson
Starter openings generated by rlp agent scaffold --opening are also written to
examples/openings/<name>.yaml; run them the same way to validate new agents.
Protocols
Runloop’s message transports and schema guarantees live here so new agent bundles can interoperate without surprises.
- Message Protocol describes the wire format between agents and the daemon.
- RMP Registry captures reserved operation codes and versioning rules for the protocol layer.
- KB Schemas documents shared knowledge-base structures exchanged over the bus.
Runloop Message Protocol (RMP v0)
Doc status: Normative for framing, headers, TTL/duplicate handling, and error taxonomy. Last updated: 2025-11-13.
RMP v0 defines a binary envelope for agent messages on the local Runloop bus. The protocol is frozen for v0 under the rules below.
1. Wire framing (normative)
- Byte order: every multi-byte integer is encoded in big-endian (network order). Parsers MUST reject host-endian variants.
- Transport: streaming transports (Unix domain sockets by default). Frames may not be reordered or partially replayed.
- Frame format:
u32 frame_lenprefix that coversheader_len + body_len(does not include the prefix itself).- Fixed 64-byte header (see below).
body_lenbytes (MsgPack map body).
If the prefix is ever removed in a future transport,
header_len + body_lenMUST remain the delimiter.
1.1 Fixed header (big-endian, 64 bytes)
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 4 | magic | ASCII "RMP0" (0x52 0x4D 0x50 0x30) |
| 4 | 2 | header_version | 0 for RMP v0; anything else → UnsupportedVersion |
| 6 | 2 | header_len | 64, compare directly; mismatch → UnsupportedVersion |
| 8 | 4 | flags | MUST be 0 in v0; non-zero → InvalidHeaderFlags |
| 12 | 2 | schema_id | u16 primitive family ID (see registry) |
| 14 | 2 | reserved2 | 0; otherwise reject |
| 16 | 4 | body_len | u32 body length in bytes |
| 20 | 8 | created_at_ms | u64 epoch milliseconds |
| 28 | 8 | ttl_ms | u64 relative TTL; 0 → InvalidTtl |
| 36 | 16 | trace_id | u128 routed end-to-end |
| 52 | 8 | msg_id | u64 monotonic per publisher |
| 60 | 4 | reserved4 | 0; non-zero reject |
Reserved fields (reserved2, reserved4) and all flag bits MUST be
zero until RMP v1 negotiates new semantics.
1.2 Field requirements
magic/header_version/header_lenare validated before parsing the rest of the header; a truncated 64-byte header raises TruncatedHeader.schema_ididentifies a primitive family (Observation, Intent, Artifact, ToolResult, Critique, StateDelta, ErrorReport, etc.). The registry lives indocs/rmp-registry.md.body_lenMUST match the actual MsgPack payload length and participates in the framing equality checks below.trace_id+msg_idare used for dedupe and telemetry.
2. Body envelope (MsgPack map)
Body bytes are encoded as a MsgPack map of the form:
{ "type": "<family.kind.vN>", "payload": <object|bytes>, "meta"?: <map> }
- The header
schema_idselects the primitive family (Observation, Intent, Artifact, ToolResult, Critique, StateDelta, ErrorReport, etc.). - The
"type"string is the canonical registry entry (e.g.,"artifact.created.v1","error.report.v1") and MUST parse as<family>.<kind>.<version>where the family prefix matches the primitive. - Receivers MUST cross-check
schema_idwith the body type; mismatches raise BodyTypeMismatch. metais optional and carries diagnostics (opening_id,priority, tags, budgeting hints). Unknown keys MUST be ignored.opening_idlives inmeta, not the fixed header, in v0.
3. Framing invariants
frame_lenMUST equalheader_len + body_len. Any mismatch yields LengthMismatch and the frame is dropped.header_lenis fixed at 64 bytes in v0. Future versions MUST bumpheader_versionand, if necessary,header_len.- Implementations MAY cache
header_len, but they MUST still compare the actual field to64and reject anything else. - Ladder bytes (rlp trace): ladder hops render
frame_lenexactly as transmitted (header_len + body_len) and fall back to64 + body_lenwhen a pre-serialized frame length is unavailable (e.g., synthetic local runs).
4. TTL & expiry handling
- Compute
expires_at_ms = created_at_ms + ttl_msusing u128 arithmetic. - If
ttl_ms == 0, raise InvalidTtl before any delivery attempts. - If the addition overflows
u128or the resulting value does not fit inu64, raise InvalidExpiry. - Receivers MUST drop frames once
now_ms >= expires_at_ms, emitting Expired in counters/telemetry. Drops still publish the body torlp/sys/drops(see below) so operators can inspect failure causes.
5. Duplicate suppression & drop telemetry
- Dedupe scope: per (topic + subscriber instance).
- Key:
(trace_id, msg_id)tracked in a bounded LRU whose max-age is at least the configured max TTL. - Dropping a frame for TTL, Duplicate, or back-pressure MUST:
- Increment
drops_total{reason=...}metrics. - Publish a structured event on
rlp/sys/dropswith{reason, topic, trace_id, msg_id, expires_at_ms?}. Emitters MUST rate-limit this topic to avoid storms.
- Increment
6. Limits & safety
- Max
body_len: configurable, default 8 MiB. Larger frames are rejected with BodyTooLarge before touching MsgPack state. - Unknown
schema_id: reject with UnknownSchema. - Unsupported
header_version: reject with UnsupportedVersion. - Reserved fields / flags: reject non-zero values with InvalidHeaderFlags.
- Body decoding: MsgPack parse failures raise BodyDecodeError.
7. Error taxonomy (test oracle)
| Error | One-liner |
|---|---|
InvalidMagic | magic bytes were not "RMP0". |
UnsupportedVersion | header_version or header_len differed from 0/64. |
TruncatedHeader | Fewer than 64 header bytes were available. |
InvalidHeaderFlags | Non-zero flags, reserved2, or reserved4 encountered in v0. |
LengthMismatch | frame_len != header_len + body_len. |
UnknownSchema | schema_id not present in the registry. |
BodyTooLarge | body_len exceeded the configured limit. |
InvalidTtl | ttl_ms was zero. |
InvalidExpiry | created_at_ms + ttl_ms overflowed u128 or could not fit in u64. |
Expired | now_ms >= expires_at_ms at receipt time. |
Duplicate | (trace_id, msg_id) already seen within the dedupe horizon. |
BodyDecodeError | MsgPack body failed to parse according to the schema. |
BodyTypeMismatch | Body "type" string did not match the family implied by schema_id. |
Test suites MUST pin at least one fixture for each entry above.
8. Example: frame hex dump
A minimal ErrorReport frame with schema_id = 0x000A and body:
{
"type": "error.report.v1",
"payload": {
"code": "tool.unavailable",
"message": "mailer offline"
},
"meta": {
"opening_id": 1234
}
}
0000: 00 00 00 a0 52 4d 50 30 00 00 00 40 00 00 00 00
0010: 00 0a 00 00 00 00 00 60 00 00 01 93 23 64 5c 7b
0020: 00 00 00 00 00 00 ea 60 11 22 33 44 55 66 77 88
0030: 99 aa bb cc dd ee ff 00 00 00 00 00 00 00 00 2a
0040: 00 00 00 00 83 a4 74 79 70 65 af 65 72 72 6f 72
0050: 2e 72 65 70 6f 72 74 2e 76 31 a7 70 61 79 6c 6f
0060: 61 64 82 a4 63 6f 64 65 b0 74 6f 6f 6c 2e 75 6e
0070: 61 76 61 69 6c 61 62 6c 65 a7 6d 65 73 73 61 67
0080: 65 ae 6d 61 69 6c 65 72 20 6f 66 66 6c 69 6e 65
0090: a4 6d 65 74 61 81 aa 6f 70 65 6e 69 6e 67 5f 69
00a0: 64 cd 04 d2
- Bytes
0000–0003:frame_len = 0x000000a0 = 160(64 + 96). - Bytes
0004–003f: fixed header (magic, version, len, flags, schema, body length, timestamps, TTL, IDs, reserved zeros). - Bytes
0040–00a3: MsgPack body (three-entry map withmeta.opening_id).
Use this vector as a golden test for codecs; decoding should produce the header fields above and the stated body map exactly.
9. Control plane (MVP)
The control plane uses the same bus and framing:
- Topic:
rlp/ctrl. - Submit:
CT_CTRL_REQwithControlRequest::RunSubmit { request_id, opening_yaml }.- The CLI applies any
--paramsoverride and sends the merged YAML asopening_yaml. - The frame
header.trace_idMUST equalrequest_idfor correlation. ttl_ms = 30000(30s) for control requests.
- The CLI applies any
- Response:
CT_CTRL_RESPwithControlResponse::{RunAccepted, RunRejected, RunCancelled}. RunAcceptedcarries{ request_id, trace_id, opening_id, opening_name }.- Idempotency: the daemon MUST treat duplicate
CT_CTRL_REQwith identicaltrace_idas idempotent. - Acceptance timeout: the CLI waits up to 2000 ms for
RunAcceptedbefore failing with guidance to start the daemon or use--local. - Rejections:
RunRejectedincludes a humanreason. Future protocol revisions may add structuredcode/detailfields once the daemon emits them; the CLI surfaces the textual reason today.
9.1 Executor ↔ agent envelopes
-
Topic namespace:
agent/<agent_ref>(one topic per logical agent). The executor publishes requests here. Each request includes a uniquereply_topic(e.g.,rlp/runs/<trace_id>/agents/<agent>/<uuid>), and the agent publishes its response on that topic. -
Request (
CT_EXECUTOR_AGENT_REQUEST, typeintent.executor.agent.request.v1) body:{ "type": "intent.executor.agent.request.v1", "payload": { "node": { /* serialized Node from the DSL */ }, "inputs": { /* NodeInputs */ }, "trace_id": "trace:...", "opening_id": "opening:...", "attempt": 1, "reply_topic": "rlp/runs/<trace>/agents/<agent>/<uuid>" }, "meta": null }RMP headers carry
trace_id,msg_id, and TTL (default 30s). The executor subscribes toreply_topicbefore publishing the request. -
Response (
CT_EXECUTOR_AGENT_RESPONSE, typetoolresult.executor.agent.response.v1) body:{ "type": "toolresult.executor.agent.response.v1", "payload": { "Completed": { "ports": { "out": [ { "schema_id": 0x00D1, "type": "artifact.draft.email.v1", "value": { ... } } ] } } } // meta is currently omitted }or
{ "type": "toolresult.executor.agent.response.v1", "payload": { "Failed": { "retryable": false, "reason": "timeout" } } } -
Agents (WASM or shim) consume the request body, execute their work, and publish the response on the per-request
reply_topic. The executor rebuildsNodeOutputsfrom the typed port data.
10. Run event stream (MVP)
After RunAccepted, the daemon publishes a single unified stream of run events:
- Topic:
rlp/runs/<trace_id>/events. - Schema:
CT_RUN_EVENT. - Payload shape:
{
"kind": "log|status|plan|trace|artifact|progress",
"level": "info|warn|error"?,
"message": "...",
"meta": {
"ts_ms": <u64>,
"run_id": "...",
"node_id": "..."?,
"span_id": "..."?
}
}
- Ordering: FIFO per publisher per topic; no sequence number in MVP.
- Live-only: no backfill on subscribe; the daemon persists durable history in the KB.
11. Topic namespaces & ACLs
rlp/ctrl— control-plane submit/ack.rlp/runs/<trace_id>/events— unified per-run event stream.rlp/sys/*— reserved for system/admin (drops, metrics, stats). Drop notices useCT_BUS_DROP_NOTICEonrlp/sys/drops.action.decision(schemaCT_ACTION_DECISION) may only be published byui|tuipublisher kinds; the bus rejects other publishers.
12. Socket & transport (MVP)
- Single Unix domain socket is used for both bus and control.
- Discovery precedence:
- If
runtime.socket_pathis non-empty, use it (short‑circuit; if unreachable → error immediately). - Else if
runtime.sockets_diris set,${runtime.sockets_dir}/rmp.sock. - Else
~/.runloop/sock/rmp.sock. - Else
/run/runloop/rmp.sock.
- If
RMP Schema Registry (Draft)
Doc status: Draft — updates require review. Last updated: 2025-11-03.
Schema identifiers (schema_id) are 16-bit unsigned integers carried in every
RMP header. The ranges below reserve space for core, extensions, and
vendor-defined payloads.
Runtime constants live in
crates/core::content(re-exported bycrates/rmp::registry); update both locations together.
| Range | Ownership | Notes |
|---|---|---|
0x0000 | Reserved | Never used; helps catch default/zero bugs. |
0x0001–0x000F | Core, stable | Runloop-owned; changes require an ADR and backward-compatible plan. |
0x0010–0x007F | First-party extensions | Experimental Runloop schemas that may graduate to Core. |
0x0080–0x03FF | Local/private | Safe for development; not guaranteed to interoperate across installs. |
0x0400–0x0FFF | Third-party provisional | External vendors reserve here via PR to this registry. |
0x1000–0xFFFF | Vendor-defined | Must embed schema_hash and vendor fields in the payload. |
Core assignments (effective)
schema_id | Name | Version field | Summary |
|---|---|---|---|
0x0001 | Observation | v (u16) | Facts gathered by an agent about the environment. |
0x0002 | Intent | v (u16) | Requested action from router/opening to an agent. |
0x0003 | ToolCall | v (u16) | Structured tool invocation request. |
0x0004 | ToolResult | v (u16) | Result of a tool invocation, streaming or final. |
0x0005 | Artifact | v (u16) | Durable content blob metadata + pointer. |
0x0006 | Critique | v (u16) | Quality feedback / review of an artifact or intent. |
0x0007 | StateDelta | v (u16) | Mutation against agent/opening state machine. |
0x0008 | Run.Event | v (u16) | Lifecycle event within an opening run (start/finish/error). |
0x0009 | Control | v (u16) | Routing/control plane directive between router and daemon. |
0x000A | Error.Report | v (u16) | Structured error diagnostics for operators/UI. |
0x0BBF | Bus.DropNotice | v (u16) | Delivery failure telemetry published on rlp/sys/drops. |
0x000B–0x000F | Reserved | — | Future core payloads. |
Each payload MUST include a version field named v (u16). Additive changes stay
within the same schema_id; breaking changes require a new schema_id or a
version bump accompanied by backward-compat logic.
Registration process
- Propose new schema via PR updating this file (and ideally
docs/message-protocol.md). - For third-party provisional IDs (
0x0400–0x0FFF), include:- Contact info / org name
- Schema summary and stability expectations
- Sample payload
- For vendor-defined IDs (
0x1000+), Runloop runtime enforces the presence ofschema_hash(e.g., SHA-256 of canonical schema) andvendorstring inside the payload body.
Test vectors
Add fixtures under tests/fixtures/rmp/ (not yet in repo) using the schema IDs
above. Each vector should contain:
- RMP frame (hex/base64)
- Parsed JSON for header/body
- Expected signature status (if any)
These fixtures drive interoperability tests for SDKs.
Knowledge Base Schemas (Draft)
Doc status: Draft — schema tables and migration rules are normative for v0.1. Last updated: 2025-11-02.
The Personal Operations Graph (POG) comprises two SQLite databases plus derived vector artifacts.
1. Ledger (events.sqlite)
- Journal mode:
WAL - Synchronous:
FULL - Immutable append-only; corrections are emitted as new events.
1.1 Tables
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts_ms INTEGER NOT NULL, -- unix time in milliseconds
actor TEXT NOT NULL, -- agent:<id> or user
kind TEXT NOT NULL, -- e.g. contact.upserted
scope TEXT NOT NULL, -- user|system|agent:<id>
payload_json TEXT NOT NULL, -- JCS canonical JSON blob
provenance_json TEXT NOT NULL, -- JCS canonical JSON
-- (model inputs, hash, etc.)
hash_blake3 BLOB NOT NULL UNIQUE -- BLAKE3 digest of canonical envelope
);
CREATE INDEX IF NOT EXISTS idx_events_kind_ts
ON events(kind, ts_ms);
CREATE INDEX IF NOT EXISTS idx_events_actor_ts
ON events(actor, ts_ms);
meta(schema_version TEXT, dirty INTEGER, ts DATETIME) tracks migrations.
schema_version uses SemVer.
1.2 Constraints
payload_jsonandprovenance_jsonMUST be JCS canonical JSON strings.- Insertions must occur within transactions; never update/delete existing rows.
hash_blake3validation occurs duringrlp kb verify; the digest is stored as raw BLOB(32) and rendered in hex only for logs/UIs.
1.3 Seed JSON schemas
Normative JSON Schemas for the initial event kinds live under
crates/kb/schemas/:
| Kind | Schema |
|---|---|
contact.upserted | contact.upserted.schema.json |
artifact.created | artifact.created.schema.json |
email.sent | email.sent.schema.json |
run.started | run.started.schema.json |
run.finished | run.finished.schema.json |
node.finished | node.finished.schema.json |
run.trace | run.trace.schema.json |
cap.audit | cap.audit.schema.json |
Validators MUST embed these schemas and enforce them at StateDelta ingestion
time. $id versions bump on breaking changes; implementations SHOULD accept the
latest compatible revision.
2. Materialized views (pog.sqlite)
- Journal mode:
WAL - Synchronous:
NORMAL
2.1 Tables
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY,
external_id TEXT,
name TEXT,
email TEXT,
org TEXT,
trust REAL DEFAULT 0.5,
source_event INTEGER NOT NULL REFERENCES events(id)
);
CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY,
kind TEXT,
handle TEXT,
auth_ref TEXT,
scopes TEXT,
verified INTEGER DEFAULT 0,
source_event INTEGER NOT NULL REFERENCES events(id)
);
CREATE TABLE IF NOT EXISTS artifacts (
id INTEGER PRIMARY KEY,
kind TEXT,
path TEXT,
sha256 TEXT,
summary TEXT,
source_event INTEGER NOT NULL REFERENCES events(id)
);
CREATE INDEX IF NOT EXISTS idx_artifacts_kind_ts
ON artifacts(kind, source_event);
CREATE TABLE IF NOT EXISTS edges (
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
kind TEXT NOT NULL,
source_event INTEGER NOT NULL REFERENCES events(id)
);
CREATE INDEX IF NOT EXISTS idx_edges_from_kind
ON edges(from_id, kind, to_id);
Supplemental tables:
embeddings:artifact_id INTEGER PRIMARY KEYdim INTEGERembedding BLOBmeta JSON
meta(schema_version TEXT, dirty INTEGER, ts DATETIME)(mirrors ledger)snapshots:id INTEGER PRIMARY KEYts DATETIMEevents_high_watermark INTEGERcomment TEXT
2.2 Rebuild process
Rebuilds iterate over the ledger and repopulate tables; embeddings drive the vector store rebuild. Existing rows are truncated before replay.
3. Vector index artifacts
- Primary store lives in
pog.sqlite.embeddings. - Derived HNSW files reside under
~/.runloop/pog/vectors/. - Rebuild steps:
- Clear vector files.
- Stream embeddings via
VectorStore::rebuild. - Validate counts and run probe queries.
4. Migration commands
| Command | Purpose |
|---|---|
rlp kb migrate [--inplace] | Backup, migrate schema, replay views, rebuild vectors. |
rlp kb verify | Hash, referential integrity, schema version checks. |
rlp kb backup | Consistent backup of both DBs. |
rlp kb vacuum | Vacuum/ANALYZE databases (requires exclusive access). |
Migration sets meta.dirty = 1 during execution; resets to 0 after a
successful run.
5. Retention & archival
- Default policy retains ledger indefinitely; configure archival via automation (copy rows older than N days to external storage).
- Materialized views follow logical retention (inactive flags) rather than deletions.
- Vector index rebuilds drop entries whose source events are archived.
6. Future extensions (informative)
- Additional views (
policies,runs,cost_usage) after the model broker lands. - Alternate backends (e.g.,
redb) behind feature flags without changing the logical schema.cap.auditevents capture per-hostcall capability decisions. Each payload stores the agent label, capability family (e.g.,time.now), operation name, target string, the BLAKE3 hash of the serialized arguments (args_hashhex), decision (allow|deny), severity, and a shortreasoncode. Events are written whenever the runtime’s audit policy enables the corresponding decision stream (seesecurity.caps.audit_on_allow/audit_on_denyin config).
Openings
Guidance for defining, distributing, and validating Runloop openings lives in this section.
- Openings DSL explains the declarative format for describing desired agent competencies.
Openings DSL
Status: v0 (draft). Defines the declarative language for composing agents into runnable DAGs (“Openings”) with replay and budgets. Normative behaviors (execution, bus, TTL, audit) align with the Protocol, Runtime, and Roadmap docs.
1) What is an Opening?
An Opening is a typed, declarative DAG of agents (“nodes”) and their connections (“edges”). It is the plan the runtime executes: fan‑out/fan‑in, retries, timeouts, budgets, and deterministic replay are first‑class. The bus carries schema‑tagged messages (RMP), and every crossing is traced and auditable.
MVP reference opening (compose_email):
contact_resolver → context_gatherer → writer → critic → mailer (human-confirm);
used throughout examples. The canonical YAML lives at
examples/openings/compose_email.yaml.
2) Goals & non‑goals
Goals
- Human‑readable spec for orchestrating agents.
- Deterministic IR (intermediate representation) suitable for validation, execution, and replay.
- Minimal templating; no Turing‑complete logic.
Non‑goals (v0)
- Distributed execution (single machine only).
- Rich conditionals/loops; stick to edge predicates and retries.
3) Two surfaces: YAML (canonical) and a “text sugar”
- Canonical: YAML → IR parser in
crates/openings/. All validation and runtime semantics reference the YAML form. - Sugar: an optional brace‑style snippet mirrors README examples and compiles to the same IR; it is syntactic sugar only (non‑normative).
Consistency note. README shows a brace DSL, while the implementation plan standardizes on YAML → IR. This spec makes YAML normative and documents the brace form as optional sugar to keep examples approachable.
4) Quick start
4.1 Minimal YAML opening
version: 0
name: compose_email
goals:
- "send a concise email to john about Q4 plan"
params:
recipient: "john"
topic: "Q4 plan"
policy:
budget_tokens: 8000 # overall model budget hint
timeout_ms: 30000 # opening-level wall time
confirm_external: true # force human confirm on external actions
nodes:
- id: contacts
use: agent:contact_resolver
with: { query: "{{params.recipient}}" }
retry: { max_attempts: 2, backoff_ms: 2500 }
timeout_ms: 5000
- id: context
use: agent:context_gatherer
with: { topic: "{{params.topic}}" }
- id: draft
use: agent:writer
with:
model: "mixtral-8x7b"
topic: "{{params.topic}}"
tone: "neutral-friendly"
timeout_ms: 15000
budget_tokens: 4000
- id: review
use: agent:critic
- id: send
use: agent:mailer
with:
require_human_confirm: true
topic: "{{params.topic}}"
edges:
- from: contacts.out # port on 'contacts'
to: draft.recipients
- from: contacts.out
to: context.contact
- from: context.out
to: draft.context
- from: draft.out
to: review.in
- from: draft.out
to: send.draft
- from: review.review
to: send.review
- from: contacts.out
to: send.contact
- from: review.ok==true # simple predicate on a boolean output
to: send.in
success:
any_of:
- review.ok == true
artifacts:
save:
- draft.out
- Templating: only
{{params.*}}substitution; no loops/expressions in templates. Use simple edge predicates for control.- Agents that expect JSON configs (e.g.,
system_tra) should take a structured object inwith, letting the executor serialize it to JSON. Avoid embedding multiple{{…}}tokens inside a single string; those are rejected by the parser. - If you keep a raw string, it must be exactly one
{{params.*}}token or a plain string with no braces; mixed content like"foo {{params.bar}}"fails validation.
- Agents that expect JSON configs (e.g.,
4.2 Equivalent “text sugar” (non‑normative)
opening "compose_email" {
goals: ["email to john about q4 plan"]
nodes:
contacts := agent("contact_resolver", query="{{params.recipient}}")
context := agent("context_gatherer", topic="{{params.topic}}")
draft := agent("writer", model="mixtral-8x7b",
topic="{{params.topic}}", tone="neutral-friendly")
review := agent("critic")
send := agent("mailer", require_human_confirm=true,
topic="{{params.topic}}")
edges:
contacts.out -> draft.recipients
contacts.out -> context.contact
context.out -> draft.context
draft.out -> review.in
draft.out -> send.draft
review.review -> send.review
contacts.out -> send.contact
review.ok==true -> send.in
}
This compiles to the same IR as the YAML.
5) Language reference (YAML)
5.1 Top‑level keys
| Key | Type | Required | Meaning |
|---|---|---|---|
version | int | ✓ | DSL version (this doc = 0). |
name | string | ✓ | Opening identifier (DNS‑label recommended). |
goals | [string] | – | Human intent; logged with trace. |
params | map | – | Parameter bag available to templates. |
policy.budget_tokens | int | – | Aggregate budget hint propagated to nodes/broker. |
policy.timeout_ms | int | – | Opening‑level wall‑clock timeout; cancels remaining nodes. |
policy.confirm_external | bool | – | Require human confirmation for send/delete/spend actions. |
nodes | [Node] | ✓ | Agent or nested opening invocations. |
edges | [Edge] | ✓ | Connections between node ports. |
success | SuccessExpr | – | Completion condition. |
artifacts.save | [PortRef] | – | Ports whose outputs should be persisted in KB. |
5.2 Node
- id: string # unique within opening
use: "agent:<name>" | "opening:<name>"
with: { ... } # agent config / args
retry: { max_attempts: 0..N, backoff_ms: 0.. } # default: 0
timeout_ms: 0.. # default: none (but see §8 safety)
budget_tokens: 0.. # overrides opening budget for this node
tags: [string] # arbitrary labels for metrics/filtering
useselects either an agent bundle (WASM under runtime) or a nested opening (composition).withis passed as the payload of the node’s initial Intent message; the node decides how to interpret it.
5.3 Ports & edges
- from: "<node>.<port>[?predicate]"
to: "<node>.<port>"
- Every node exposes named output ports and input ports. The SDK
recommends defaults (
in,out,ok,err) but agents may define richer port schemas. Fan‑out is N edges from one output; fan‑in blocks until all upstreams are satisfied. - Predicates are simple comparisons on boolean or enum outputs (e.g.,
review.ok==true). Complex control belongs in the agent, not the DSL.
5.4 Success expressions
success:
all_of: ["review.ok == true", "exists(draft.out)"] # OR
# success:
# any_of: ["review.ok == true"]
On timeout/failure with no satisfied success condition, the opening fails.
5.5 Parameters & templating
- Only string replacement of
{{params.*}}inwithmaps. No arithmetic, no conditionals. This avoids making the DSL a programming language.
5.6 Schema hints
Agent manifests are the signed source of truth for capability claims and
parameter schemas. When a manifest has not published a field yet, openings may
add a temporary schema_hints section:
nodes:
- id: draft
use: agent:writer
with:
topic: "{{params.topic}}"
schema_hints:
with:
topic:
required: true # shorthand for “this key must be present”
tone:
enum: ["neutral", "neutral-friendly"]
- Hints are merged via JSON Schema
allOfsemantics: they can tighten the manifest schema but never relax it. - Hints may introduce new fields only until the manifest grows a real schema, at which point the hint should be removed.
- The CLI logs whenever it relies on a hint so maintainers know to update the manifest. Hints are scoped per node.
6) Execution semantics
- Parse & validate YAML → IR, producing helpful errors with line/column.
- Topological scheduling. Nodes run when all inbound dependencies are ready. Fan‑in edges cause waits; fan‑out clones messages/artifacts.
- Runtime & bus. Node work is enacted by sending/receiving RMP messages
over the local UDS bus. The fixed header carries
trace_id,msg_id,schema_id,created_at_ms, andttl_ms;opening_idand other metadata ride in the MsgPack envelope{ type, payload, meta? }. - TTL & duplicates. Receivers enforce TTL using
created_at_ms + ttl_ms; duplicates are ignored using an LRU dedupe cache keyed by(trace_id,msg_id). Drops increment counters and may be broadcast onrlp/sys/drops. - Retries & timeouts. Per‑node
retryandtimeout_msgovern backoff and cancellation. Opening‑levelpolicy.timeout_mscancels remaining nodes. - Replay. The engine records input/output message IDs per node;
rlp replay <trace_id>re‑feeds inputs to reproduce identical outputs (given deterministic providers). Diff tooling highlights mismatches. - Audit & KB events. Open/close of a run emits
run.started/run.finished; agent actions may emitcap.auditon denied ops per policy.
7) Types & schemas on the wire
RMP schema_id is a u16 referencing a small registry of content types
(e.g., CT_OBSERVATION=1, CT_INTENT=2, CT_ARTIFACT=5, CT_STATE_DELTA=7).
Message bodies are MsgPack with JSON‑Schema definitions for validation and
evolution.
Envelope. The
{ type, payload }MsgPack map is the default body shape; higher‑level helpers exist incrates/rmp/to encode/decode and stamp TTL.
8) Safety, capabilities, and confirmation
- Agents run as WASM/WASI sandboxes under the Runtime with least‑privilege capabilities. File system, network, time, KB, model, secrets, and exec are checked by hostcalls; deny‑by‑default.
- External actions (send/delete/spend) must be human‑confirmed unless policy explicitly allows. The mailer example keeps “dry‑run + confirm” for MVP.
- TTL defaults. To avoid “immortal” frames, DSL v0 requires explicit
ttl_msin RMP headers at encode time; the engine rejects zero TTL during publication. (Runtime’s bus enforces TTL regardless.)
9) Observability & metrics
- Tracing. Each crossing creates a span with
trace_idpropagation;runloop trace <id>renders a ladder diagram. - Metrics. Counters for sent/received/dropped, cap_denied, broker_calls,
cache_hits; gauges for agents_running, rss_total, bus_queue_depth; surfaced in
agtop. - Drops. Receivers may publish structured drop notifications to
rlp/sys/drops; failure totals are also readable via metrics.
10) CLI integration
-
Run:
rlp run examples/openings/compose_email.yaml \ --params '{"recipient":"john","topic":"Q4 plan"}' \ --trace-out trace.json- Executes the YAML plan via the openings engine, drives the canonical crew
(contact resolver → context gatherer → writer → critic → mailer), prints
node status, and writes an optional JSON trace for later replay—even when
the run is submitted to
runloopd(the CLI fetches the persistedrun.tracefrom the KB once available). You need a writable KB path plus a configured model broker; if no provider is reachable the writer falls back to the heuristic template. The CLI secret resolver reads broker credentials from the environment, and mail send still requires human confirmation unless you opt out insecurity.confirm_external_actions.
- Executes the YAML plan via the openings engine, drives the canonical crew
(contact resolver → context gatherer → writer → critic → mailer), prints
node status, and writes an optional JSON trace for later replay—even when
the run is submitted to
-
Explain routing:
rlp why "<prompt>" -
Replay:
rlp replay trace:<uuid> --opening examples/openings/compose_email.yaml(pass a JSON file path to replay ad-hoc captures).- KB:
rlp kb query ...,rlp kb why <id>All commands produce structured output with sensible exit codes.
- KB:
-
rlp runvalidates every node’swithpayload against the signed manifest schemas (plus anyschema_hints) before contacting the daemon. Use--errors-format table|jsonto choose how aggregated validation errors are rendered; failures exit with code 2 so automation can distinguish input issues from runtime problems.
11) Validation rules (normative)
- Graph soundness: nodes are unique; edges reference existing ports; no cycles.
- Port compatibility: serialized payload types must match the consumer input schema_id (static lint + runtime check).
- Budgets: non‑negative; per‑node budget must not exceed opening budget (lint).
- Timeouts: if absent at node level, the runner inherits opening timeout; long‑running agents must explicitly opt into higher timeouts.
- Templating: only
{{params.*}}is allowed; unresolved placeholders are errors. - Security: if
confirm_external=true, any node withwith.require_human_confirm=falseis rejected (cannot downgrade global policy).
12) Examples
12.1 Compose email (canonical)
examples/openings/compose_email.yaml (identical to §4.1) is the reference
artifact used by MVP acceptance.
12.2 Summarize logs (fan‑out/fan‑in)
version: 0
name: summarize_logs
nodes:
- id: gather1
use: agent:context_gatherer
with: { topic: "service:A" }
- id: gather2
use: agent:context_gatherer
with: { topic: "service:B" }
- id: merge
use: agent:writer
edges:
- { from: gather1.out, to: merge.in }
- { from: gather2.out, to: merge.in }
success:
any_of: ["exists(merge.out)"]
13) File locations & packaging
- Openings live under
examples/openings/in‑repo; installed bundles may ship curated openings with their agents. - OS packages place state under
/var/lib/runloop(system mode) and the Runloop Message Protocol socket under/run/runloop/rmp.sock; user mode uses~/.runloop/sock/rmp.sock.
14) Alignment with Protocol, Runtime, KB
- Protocol (RMP). Fixed‑size header (incl.
created_at_ms,ttl_ms, IDs) + MsgPack envelope; TTL and duplicate rules apply to all frames emitted by openings. - Runtime. Agents are wasm32‑wasi; capability gates around FS/NET/TIME/KB/MODEL/SECRETS/EXEC; audit events for denied ops.
- KB (POG). Runs emit
run.started/finished; artifacts saved viaartifacts.saveare materialized and traceable viakb.why.
15) EBNF (YAML schema projection)
YAML is the source of truth; this EBNF describes the IR structure after parsing.
Opening := { version: Int, name: Ident, goals?: [Str], params?: Map,
policy?: Policy, nodes: [Node], edges: [Edge],
success?: Success, artifacts?: Artifacts }
Policy := { budget_tokens?: Int, timeout_ms?: Int,
confirm_external?: Bool }
Node := { id: Ident, use: Use, with?: Map, retry?: Retry,
timeout_ms?: Int, budget_tokens?: Int,
tags?: [Ident] }
Use := "agent:" Ident | "opening:" Ident
Retry := { max_attempts: Int, backoff_ms: Int }
Edge := { from: PortRef, to: PortRef }
PortRef := Ident "." Ident [ "==" (Bool | Int | Str) ]
# simple predicate suffix
Success := { any_of?: [Expr] } | { all_of?: [Expr] }
Expr := Ident "." Ident "==" (Bool | Int | Str)
| "exists(" PortRef ")"
Artifacts := { save?: [PortRef] }
Ident := /[a-z][a-z0-9_]{0,63}/
16) Known mismatches & decisions
- YAML vs brace DSL: YAML is normative; brace DSL is sugar compiled to IR. (README examples remain valid as sugar.)
- RMP header fields: This doc assumes the header includes
created_at_ms,ttl_ms, fixed header version/length, and IDs per Protocol/TODO. If the codec stub diverges, Protocol drives; update the runner accordingly. - TTL
0semantics: The DSL validator rejects 0 to avoid immortal frames. Bus/runtime still enforce TTL per Protocol. If a “no‑expire” policy is required later, we will add an explicitttl_ms: neversentinel with clear safeguards. - Drop notifications: Receivers may auto‑publish to
rlp/sys/drops, but counters are always available; the daemon controls emission volume.
17) Tooling
- CLI:
rlp run,rlp why,rlp replay,rlp kb.* - TUI:
agtopshows Plan, Log, agtop metrics, Trace ladder. - Repo hygiene: place openings in
examples/openings/, keep examples compiling in tests, and document agent caps alongside updated openings.
18) Appendix — Reference IDs & content types
Core IDs (TraceId, OpeningId, MsgId) and the content type registry
(CT_INTENT, CT_ARTIFACT, etc., u16) are defined in crates/core/. Use
these when authoring schemas and mapping ports.
Change log
- v0: Initial spec (YAML normative, brace sugar optional), edges with simple predicates, retries/timeouts/budgets, KB artifacts, TTL rules, drop notifications, and replay semantics.
References
- README (concepts, sample opening), Roadmap (acceptance criteria), TODO (epics
for RMP/Bus/Openings), Protocol/Runtime design notes (RMP headers, capability
enforcement), KB design (events, artifacts,
kb.why).
See also: docs/message-protocol.md and docs/kb-schemas.md for schema IDs
and KB event kinds referenced here.
Operations
Operational practices for running Runloop in production are grouped under this chapter.
- Operations Runbook covers day-to-day procedures, alert responses, and replay tooling.
- Policy Capabilities lists the privileged capabilities granted to agent bundles.
- Security Model explains isolation boundaries and signing requirements.
- Performance Harness summarizes the benchmarking setup for regressions.
- Release Process outlines promotion criteria for tagged builds.
Operations & Packaging Guide (Draft)
Doc status: Draft — normative for migration, trust policy, and config precedence. Last updated: 2025-11-02.
This guide covers operational tasks: configuration layering, KB migrations, vector index maintenance, packaging targets, and trust management.
1. Configuration precedence (normative)
Runloop merges configuration from several layers. Highest precedence wins unless a system policy forbids the change.
- CLI flags (
rlp run --model=…,:budget …inline overrides) - Environment variables (
RUNLOOP_*) - User config
~/.runloop/config.yaml - System config
/etc/runloop/config.yaml - Built-in defaults
1.1 Policy overlays
System config may define policy.* keys that represent hard limits (e.g.,
policy.max_tokens, policy.providers.allowlist,
policy.confirm_external_actions = true). Lower layers may only tighten these
values. Attempts to exceed policy MUST cause the command to fail with a
descriptive error.
1.2 Merge semantics
| Type | Rule |
|---|---|
| Scalars | Last writer wins (respecting precedence). |
| Maps | Deep merge; map entries follow precedence per key. |
| Lists | Replace entirely (last writer). Exceptions: models.providers unions entries before applying allow/deny lists. |
| Capability sets / allowlists | Intersect with policy first, then apply precedence. |
Environment variables mirror YAML paths (upper case, underscores). Examples:
RUNLOOP_MODELS_DEFAULT=local:llama3.1-8b
RUNLOOP_MODELS_BUDGETS_SYSTEM_TOKENS_HARD=750000
RUNLOOP_SECURITY_CONFIRM_EXTERNAL_ACTIONS=true
RUNLOOP_CONFIG=/custom/path/config.yaml
1.5 Runtime socket & discovery (MVP, normative)
- Runloop uses a single Unix domain socket for both the bus and control plane.
- Default naming and discovery precedence:
- If
runtime.socket_pathis non-empty, use it and error immediately if unreachable (no probing). - Else if
runtime.sockets_diris set, use${runtime.sockets_dir}/rmp.sock. - Else
~/.runloop/sock/rmp.sock. - Else
/run/runloop/rmp.sock.
- If
Examples:
runtime:
socket_path: "/run/runloop/rmp.sock" # overrides discovery; short-circuits probing
sockets_dir: "/var/run/runloop" # used only when socket_path is empty
The CLI refuses to silently fall back to local execution when the daemon is
unavailable. It fails fast with guidance to start the daemon (or re-run with
--local).
1.3 Model broker configuration (MVP)
models.broker.providerslists named backends.kindmay belocal,http(OpenAI-compatible completions),http_openai_chat(OpenAI chat),http_anthropic(Claude/v1/messages),http_ollama(local Ollama), orhttp_gemini(Google GeminigenerateContent). These HTTP kinds acceptbase_url,secret_id, and optional static headers.models.broker.routeis an ordered array of{ pattern, provider, target_model? }entries; the first matching pattern wins. Legacy map syntax like{ "*": "local" }(or the legacy keyrouting) still deserialises into the same shape.models.broker.cacheexposesttl_msandcapacityfor the in-memory LRU. Requests may override TTL viacache_ttl_ms;0disables caching for that call.models.broker.budgetsretainsdefault_tokens,per_request_tokens_cap, andhard_cap_usd. Per-request budgets clamp to the stricter of the request and config-provided values.- Provider
secret_idvalues resolve at runtime via the configured secret store; raw API keys should never be stored in YAML. - To use Gemini, add a provider entry with
kind: http_gemini,base_url: https://generativelanguage.googleapis.com, and asecret_idsuch asrunloop/models/gemini(the runtime will also look for the environment variableRUNLOOP_MODELS_GEMINI). Agents that invoke Gemini still needmodel = trueinpolicy.caps; automation agents that shell out (e.g., to manage tmux) also requireexec = trueplus explicit filesystem whitelists for any touched configs.
1.4 Runtime readiness gate (normative)
- Agents only become visible to supervisors after a two-sided readiness
handshake: Wasmtime instantiates, the bus mailbox subscribes, tracing
context is seeded, and the guest either calls the hostcall
runloop::notify_ready()or enters itsmailbox_recvloop (fallback for pre-ready binaries). runtime.spawn_ready_timeout_ms(default 5000 ms) controls how long the runtime waits for that handshake. Per-agent overrides live inAgentSpec::spawn_ready_timeout_ms; environment variableRUNLOOP_SPAWN_READY_TIMEOUT_MSis the lowest-precedence fallback.- When the timeout elapses, callers receive
Error::ReadyTimeout, the runtime emitsrunloop.runtime.spawn.ready_timeouts_total, and it tears down any partially created bus subscriptions/audit state to prevent ghost agents. - Treat
notify_readyas part of the minimum agent ABI going forward; older agents that cannot be rebuilt should block onmailbox_recvimmediately so the fallback signal still fires.
2. Knowledge Base (POG) operations (normative)
The POG consists of two SQLite files and a derived vector index.
~/.runloop/pog/events.sqlite— append-only ledger (WAL, synchronous=FULL)~/.runloop/pog/pog.sqlite— materialized views (WAL, synchronous=NORMAL)~/.runloop/pog/vectors/— HNSW index files (derived; safe to rebuild)runloopdruns a background materializer that tails the ledger and updates the views. Progress is tracked in the singleton rowpog.sqlite.materializer_statewith columns:id INTEGER PRIMARY KEY CHECK (id = 1)watermark INTEGER NOT NULL
2.1 Migration workflow
rlp kb migrate orchestrates upgrades across both stores.
- Ensure
runloopdis stopped (command refuses to run if sockets are open; override with--force). - Create timestamped backups of both DBs.
- Apply schema migrations to
events.sqlite(rare; append-only). - Rebuild
pog.sqliteby replaying events (events.sqlite→ views). Use--inplaceonly for emergency SQL patches. - Rebuild vector index using the
VectorStore::rebuildpath. - Set
meta.dirty = 0, record newschema_version, and create asnapshotsentry. - Update
materializer_state.watermarkwith the highest applied ledger id.
Supporting commands:
rlp kb verify— referential integrity, hashes, BLAKE3 checksrlp kb backup— consistent hot backup (uses SQLite backup APIs)rlp kb vacuum— optional compaction (requires exclusive lock)rlp kb why <entity>— print ordered source events for a materialized entity key.- Redaction: by default, emails are masked at read time for all interfaces (CLI,
hostcalls, backups). Operators can set
kb.redaction.allow_unredacted_admin=trueto allow privileged reads and should set a deployment-specifickb.redaction.salt. Agents must declarekb_read.contacts_rawto bypass masking; such reads should be audited.
2.2 Metadata tables
Both databases include meta(schema_version TEXT, dirty INTEGER, ts DATETIME).
pog.sqlite also tracks the snapshots table with columns:
id INTEGER PRIMARY KEYts DATETIMEevents_high_watermark INTEGERcomment TEXT
2.3 Retention
- Ledger retains all events; corrections produce new
StateDeltaentries. - Operators can archive older events by copying subsets elsewhere; never delete rows in-place.
- Materialized views compact automatically during rebuild; configure retention
by emitting
StateDeltaevents that mark artifacts/contacts inactive.
3. Vector index lifecycle (normative)
- Implementation milestone 1 uses a pure-Rust HNSW crate (
hnsw_rsclass). Keyword search uses SQLite FTS5; results fuse via Reciprocal Rank Fusion (RRF). - Embeddings are stored in
pog.sqlite(blob column) with metadata. The vector index is derived and can be discarded/rebuilt. VectorStoretrait (conceptual):
#![allow(unused)]
fn main() {
trait VectorStore {
fn upsert(&self, id: ItemId, embedding: &[f32], meta: &Meta) -> Result<()>;
fn delete(&self, id: ItemId) -> Result<()>;
fn search(&self, q: &[f32], k: usize, filter: &MetaFilter) -> Result<Vec<Hit>>;
fn rebuild(&self, iter: impl Iterator<Item = (ItemId, Embedding, Meta)>) -> Result<()>;
}
}
- Provenance filters (
confirmed_only,agent_allowlist) run before final scoring. - Future milestone may integrate Tantivy; implementations must conform to the same trait.
4. Packaging targets (informative)
4.1 Debian 13 (trixie) packages
-
Assets live under
packaging/systemd/(systemd unit, tmpfiles definition, default config, README, maintainer scripts).cargo-debconsumes them directly; no in-treedebian/directory is required. -
Build requirements:
build-essential,cargo,rustc,pkg-config,libssl-dev,libsqlite3-dev,systemd, andcargo-deb(cargo install cargo-deb). Thejust debrecipe runs the three builds in sequence:just deb # equivalent to: cargo deb -p runloopd cargo deb -p rlp cargo deb -p agtop -
Artifacts land under each crate’s
target/debian/directory (e.g.,crates/runloopd/target/debian/runloopd_0.1.0_amd64.deb). Install withsudo apt install crates/<crate>/target/debian/<pkg>_<ver>_<arch>.deb. -
runloopdpackage duties: install/usr/bin/runloopd, systemd service, tmpfiles definition,/etc/runloop/config.yaml(as a conffile), and docs. The maintainer scripts create therunloopsystem user, chown/var/lib/runloop//var/log/runloop, callsystemd-tmpfiles --create, runsystemctl daemon-reload, and enable but do not startrunloopd.serviceon a first-time install so operators can edit config before launching. They should also create/var/lib/runloop/agentsand/var/lib/runloop/openings(owned byrunloop:runloop) sorlp agent install --root /var/lib/runloop/agentsis immediately usable. Upgrades capture whether the daemon was running prior todpkgstopping it and automatically restartrunloopd.serviceonce the new bits are configured, keeping CLI/agent traffic flowing with zero downtime. -
The CLI (
rlp) and monitor (agtop) ship as independent packages so they can be updated without restarting the daemon; they just depend onca-certificatesplus transitive Rust runtime libraries. -
Purging the daemon package (
sudo apt purge runloopd) removes/etc/runloop,/var/lib/runloop,/var/log/runloop, and therunloopsystem user/group; CLI/TUI packages only drop their binaries/docs.
4.2 Additional artifacts
| Artifact | Location | Status |
|---|---|---|
| Live ISO | packaging/live-build/ | Folders exist; scripts TBD after .deb packaging. |
| Dev container | packaging/container/ | README tracks mounts, base image expectations. |
5. Trust policy & agent signatures (normative)
Runloop enforces signatures on agent bundles before install/launch.
Status:
rlp agent installis available for local bundles and validates manifest digests +tools.jsonschema, but signature verification andrlp trust updateare still landing with the packaging milestone. Edit trust policy files manually per the steps below.rlp agent listshows discovered bundles plus digest status.
- Algorithm: Ed25519 detached signature over
manifest.toml(canonicalized) and referenced files. - Bundle layout:
agent.bundle/
├─ manifest.toml # includes digests of contents
├─ policy.caps
├─ tools.json # optional host tool attachments
├─ agent.wasm
├─ schemas/… (optional)
├─ SBOM/spdx.json (optional)
└─ SIGNATURES/manifest.sig
-
Tool attachments: When present,
tools.jsonMUST follow the schema indocs/tool-attachments.md; its digest appears inmanifest.tomlso the signature covers attachment metadata alongside binaries. -
Trust policy file:
~/.runloop/trust-policy.toml
[anchors]
runloop_release = "ed25519:ABCD..."
dev = {
key = "ed25519:DEAD...",
allow_dev = true
}
[rules]
runloop_release = {
allow_caps = "any",
allow_net = "any",
allow_exec = false
}
dev = {
allow_caps = ["kb_read", "kb_write"],
allow_net = [],
allow_exec = false
}
-
Lifecycle:
- First-party releases signed with Runloop Release key (private material stored outside repo).
- Third-party vendors sign with their key; operators add the corresponding anchor.
rlp trust updatefetches keysets/CRLs.- Install flow:
rlp agent install bundle.tar→ verify digests → (signature verification pending) → enforce trust policy (pending) → stage bundle. - Launch flow re-verifies manifest + signature as defense in depth.
- If
security.allow_unsigned_agents=false,rlp agent installwill refuse bundles until signature verification is implemented.
-
Parameter schemas: agent manifests embed JSON Schemas under
[schemas.with]so tooling can validatewithpayloads before execution. Each schema becomes part of the signed manifest; CLI and daemon consumers load them via the shared agent registry. -
Revocation: increment keyset version or publish revocation list; runtime refuses to start bundles signed by revoked keys.
6. Secrets backends (summary)
See docs/security-model.md for secret-store details. Ops tasks:
Status:
rlp secrets ...tooling is being wired up; use your platform’s secret store CLI until the native commands ship. Default provider isstub(in-memory, for dev) but it will consult environment variables first to preserve existing env-only setups. Prefer a real backend for anything sensitive.
- Planned:
rlp secrets init --backend=secret-service|pass|age - Planned:
rlp secrets put runloop/mail/smtp_api_key(reads from stdin) - Planned:
rlp secrets listandrlp secrets deletefor maintenance
7. Observability (summary)
- Default logging: JSON (ndjson) with keys
ts,level,service.name,trace_id,opening_id,agent_id. - Tracing & metrics via OpenTelemetry OTLP. Configure endpoint, protocol, and
sampling under
observabilityin config. - Bus/TUI metrics snapshots:
observability.metrics_interval_ms(default 1000, allowed 100–60000) controls how oftenrunloopdpublishesCT_METRICS_SNAPSHOTframes torlp/sys/metricsandrlp/agents/<agent_id>/metrics(TTL = 2× interval, minimum interval+250 ms). System frames include queue depth/capacity, drop counters, and broker/hostcall totals; agent frames include RSS/CPU and mailbox depth, with a final zeroed snapshot on teardown. - Model broker exports
runloop_broker_calls_total,runloop_broker_cache_hits_total, andrunloop_broker_errors_total{kind=*}counters for dashboards. agtoppane +rlp tracerely on the metrics exported by agents.- Capability audit volume is gated by
security.caps.audit_on_allowandsecurity.caps.audit_on_deny; the latter defaults totrueso denied hostcalls land in the KB ascap.auditevents.
8. Message bus topics (normative)
- Only UI/TUI processes may publish
action.decision; the bus rejects other publishers and emits an audit event. - The runtime publishes drop notices (
DropNotice) onrlp/sys/dropswhenever TTL expiry or duplicate suppression occurs. Operators should scrape this topic for reliability dashboards.
8.0 Control plane
rlp/ctrlcarriesCT_CTRL_REQandCT_CTRL_RESP. Submit requests use a 30s TTL; the CLI waits up to 2s for acceptance.- After acceptance, the daemon publishes
CT_RUN_EVENTtorlp/runs/<trace_id>/events. This is a live-only stream; historical events are persisted in the KB.
8.1 Bus publisher ACL (configuration)
Configure publisher kinds allowed to emit specific schemas:
bus:
auth:
publishers:
action_decision:
allowed_kinds: ["ui", "tui"]
Defaults permit only ui and tui. Publishers establish identity at connect
time (connect_as). runloopd validates the list at startup; unknown strings
or empty entries cause the daemon to fail fast so operators notice
misconfigurations.
Appendix A. Repo admin checklist
Branch protection (owner: @release-eng)
- Protect
main: require PRs, 1+ code owner review, dismiss stale reviews on changes. - Require status checks: build, test, clippy, fmt, docs-check, commitlint.
- Require branch to be up to date before merging.
- Disallow force-push to
main.
Security features (owner: @release-eng)
- Enable Dependabot alerts & updates.
- Enable secret scanning & push protection.
- Enable code scanning (CodeQL or equivalent).
Labels (owner: @pm)
- Create: bug, feature, task, docs, infra, security, design, good-first-issue, epic, phase:g.
CI secrets (owner: @release-eng)
CRATES_IO_TOKEN(future), signing keys, release GPG key (optional).
Release gates (owner: @pm, @release-eng)
- Tag pattern
v0.x.y. - Required checks green.
- CHANGELOG updated.
- SBOM/signatures attached (when implemented).
Further reading:
Capability Policy Format (Draft)
Doc status: Draft — schema and override semantics are normative for v0.1. Last updated: 2025-11-02.
Agents declare their required capabilities in policy.caps files bundled with
the agent crate. Operators may override these policies locally, but only to
revoke privileges.
1. Locations (normative)
- Authoring (source):
agents/<agent_name>/policy.caps - Bundle (packaged):
agent.bundle/policy.caps - Operator overrides:
~/.runloop/caps/overrides/<agent_id>.toml
Overrides are applied before launch with intersection semantics (see §3).
2. TOML schema (normative)
[capabilities]
fs = ["/home/user/work/notes"]
net = ["api.mailprovider.com"]
time = true
kb_read = true
kb_write = ["contacts", "artifacts"]
secrets = ["runloop/mail/smtp_api_key"]
model = true
exec = false
Supported keys:
| Key | Type | Default | Notes |
|---|---|---|---|
fs | array of absolute paths | [] | Paths are normalized; globs not allowed in v0.1. |
net | array of hostnames | [] | Hostnames may include port (example.com:443). Wildcards arrive in v0.2. |
time | bool | false | Grants access to monotonic/UTC clocks. |
kb_read | bool or array | false | true grants read-only access to all domains; array limits to subset (["contacts"]). The kb_read.contacts_raw grant lifts KB redaction for PII (emails). |
kb_write | bool or array | false | Same semantics as kb_read; true is discouraged outside trusted agents. |
secrets | array of secret IDs | [] | IDs follow runloop/<scope>/<name>. |
model | bool | false | Allows requests to model broker. |
exec | bool | false | Command execution on host; off by default and should be granted sparingly. |
Shorthand booleans (kb_read = true) expand to the full domain set at load
time. Internally the runtime converts the configuration into bitsets per
capability family.
3. Override semantics (normative)
- Base policy: union of bundle
policy.capsand agent manifest requirements. - Operator override is loaded (if present). Effective capabilities =
intersection(
base,override). - Missing override keys imply no additional restriction. Setting a key to an
empty list or
falserevokes the capability entirely. - Overrides cannot grant capabilities absent from base; attempting to do so logs a warning and the extra entries are ignored.
- When the effective capabilities resolve to empty (no fs/net/time/kb/model/
secrets/exec grants), the runtime still launches the agent but records a
cap.auditentry withcap="caps.empty",op="_start", andreason="caps_empty"so operators understand the agent will be inert until policy is relaxed. Hostcalls will continue to be denied underaudit_on_deny=true.
Example override (~/.runloop/caps/overrides/writer.toml):
[capabilities]
net = [] # revoke network access entirely
kb_write = ["drafts"] # restrict writes to the drafts domain
4. Validation & tooling (informative)
rlp caps lint(planned) will validate TOML against the schema and show diff vs. base policy.- During agent launch, Runloop logs both base and effective capability sets for auditing.
- Capability decisions are surfaced in structured logs (
cap_allow/cap_denyevents) and, when enabled viasecurity.caps.audit_on_*, persisted to the knowledge base ascap.auditevents for replay.
5. Future additions
- Wildcard hostnames with verification (
*.example.com). - Rate limits (
model.tokens_per_minute). - Capability templates shared across agents.
- Optional hardening flag (
security.caps.fail_on_empty) to fail-fast when effective capabilities are empty instead of launching an inert agent.
Security Model (Draft)
Doc status: Draft — normative for v0.1 where explicitly labeled. Last updated: 2025‑11‑02.
Sandbox & Capabilities (normative)
- Agents execute inside WASM/WASI sandboxes hosted by Wasmtime with capability-based host calls.
- Capability manifests (
policy.caps) are deny-by-default. Operators may only remove grants via overrides. - Capability families (normative set):
fs(scoped path lists)net(hostname allowlists; off by default)timekb_read/kb_write(domain lists, e.g.,contacts,artifacts)secrets(SecretStore IDs)model(broker usage)exec(disabled in v0.1)
- The runtime enforces capability checks at hostcall boundaries and records
denials as
cap.auditledger events (and structured logs) whensecurity.caps.audit_on_denyistrue(default). Operators may also enablesecurity.caps.audit_on_allowto persist allow decisions for high-scrutiny agents. - Implementation status: the
runloop-runtimecrate embeds Wasmtime, enforces capability checks for every exposed hostcall, and records denials ascap.auditevents via the knowledge base (seecrates/runtime/tests/capabilities.rs).
Secret Handling (normative)
- Secret material never lives in the POG; only opaque
secret_idreferences or hashed markers are stored. Hostcalls return raw values by default for backward compatibility. To opt into handle-only mode (rlsec_<...>), setsecurity.testing.expose_raw_secrets = false(RUNLOOP__SECURITY__TESTING__EXPOSE_RAW_SECRETS=0). Default remainstruefor now and will flip to handle-only in a future breaking release. - Default provider:
security.secrets.provider = "stub"= env-first + in-memory (EnvThenStore). Other providers:envsecret-service(DBus Secret Service; currently stubbed/best-effort; skipped inauto)pass(pass show runloop/<secret_id>, first line)age(file store undersecurity.secrets.root, default~/.runloop/secrets; master key at<root>/master.agekey0o600. Current implementation reads plaintext.agefiles; encryption TODO.)autoprobes pass → age → env+store (secret-service skipped until wired).
- TTL:
security.secrets.default_ttlsets cache max-age; expired entries are discarded and refreshed. Stale secrets are never served. - CLI (
rlp secrets put/get/list/delete) is planned; until then, provision secrets through the chosen backend directly. - Overrides may only reference existing secret IDs; agents cannot read arbitrary secrets without explicit capability grants.
Fail-fast secret validation
Agent launch validates that all declared secrets (in policy.caps) can be
resolved before loading the WASM module. If any secret cannot be resolved:
- Default behavior (
security.testing.allow_missing_secrets = false): agent launch fails immediately withError::SecretsMissing, listing the missing secret IDs. - Development override (
security.testing.allow_missing_secrets = true): agent launch proceeds with a warning. Use only for local development/testing.
Environment variable overrides:RUNLOOP__SECURITY__TESTING__ALLOW_MISSING_SECRETS=1RUNLOOP__SECURITY__TESTING__EXPOSE_RAW_SECRETS=1
This validation runs after secret IDs are pre-registered with the provider
(allow()), so stub/fixture providers that rely on pre-registration work
correctly.
Provenance & Audit (normative)
- Every agent message carries RMP provenance metadata (
model,provider,parameters,tooling). - All writes into the POG ledger (
events.sqlite) include BLAKE3 content hashes and source identifiers. - Structured JSON logs include
trace_id,opening_id,agent_id; redaction filters scrub secrets or PII patterns before sink. - Knowledge Base redacts PII (emails) at read time unless the caller holds
kb_read.contacts_raw. Masking uses a salted deterministic token so joins still work; privileged unmasking is audited and gated by config (kb.redaction.allow_unredacted_admin). - Optional Ed25519 signatures protect message integrity when crossing trust
boundaries or when
security.require_signed_messages = true.
Threat Model (v0)
Assumptions
- Host OS (Debian 12) is trusted and kept patched.
- Agents are untrusted code but must pass the WASM validator.
- Operators can inspect and reset the environment; hardware physical security is out of scope.
In-scope threats
- Malicious or compromised agent attempting to exfiltrate data outside granted capabilities.
- Supply-chain tampering of agent bundles (mitigated via manifest signatures + SBOM).
- Secrets disclosure through logs or unredacted artifacts.
- Privilege escalation via hostcall misuse.
Mitigations
- WASM sandbox with constrained syscalls; hostcalls check capability bitsets each invocation.
- Agent bundles are signed;
runloopdverifies signatures before install/launch. - Runloopd enforces outbound confirmation (
confirm_external_actions) and tripwires (network/FS volume thresholds). - Structured logging + telemetry scrubbing; security tests include fuzzing denied syscalls.
Out-of-scope (v0)
- Kernel-level exploits or side-channel attacks in Wasmtime/CPU.
- Multi-tenant isolation across different Unix users or hosts.
- Compromise of external LLM providers or user-supplied secrets outside Runloop’s control.
Package trust & signatures (normative)
- Agent bundles must ship an Ed25519 signature over
manifest.toml(canonical form) and referenced digests. - Trust anchors live in
~/.runloop/trust-policy.toml; install/launch flows refuse bundles without a matching, non-revoked key. - Capabilities permitted are constrained by trust rules (see
docs/ops.mdoverall policy). - First-party release keys rotate via signed keyset files; runtime caches latest keyset hash to detect downgrade attacks.
Telemetry & Privacy (informative)
- Telemetry is opt-in; default is local-only.
- When enabled, OTLP exporters remove
secret_id, raw prompt text is hashed, and cost metrics are aggregated per opening.
Performance Harness
This note captures the cold-start methodology, hardware envelope, and commands that back Epic C’s acceptance criteria. The doc is normative for the current workspace; CI perf jobs reference it when validating thresholds.
Hardware Profile
All threshold numbers assume the baseline lab node below. When running on different hardware, record the delta alongside the measurements.
- Debian 12 (kernel 6.x)
- x86_64 with 8 logical cores (11th-gen Intel i7 class) or Apple Silicon M-class equivalent
- 16 GB RAM, NVMe SSD
- CPU governor set to
performance; background services minimal - Wasmtime 38.0.4 (pinned via
crates/runtime/Cargo.toml) - Build profile:
cargo test --workspace(debug),cargo bench(release)
Cold-start Integration Gate
- Command:
cargo test -p runloop-runtime cold_start_p50_under_40ms - Scenario: spawn 50 idle WASM agents, capture wall-clock spawn time, assert median < 40 ms.
- Assertions: non-zero RSS (all platforms) and
cpu_total_msreported on Linux builds with default features. - Output: test failure message includes the observed median when the threshold is exceeded.
Criterion Benchmark (Informational)
- Command:
cargo bench -p runloop-runtime cold_start - Purpose: capture distribution statistics (p50/p90) for cold-start latency; results are uploaded as CI artifacts but not committed.
- Consumption: engineers reviewing perf regressions compare the latest artifact against the historical trend.
Representative Result (Illustrative)
| Build | Median | P90 | Notes |
|---|---|---|---|
debug (cargo test) | 31 ms | 36 ms | Debian 12, i7‑1185G7, governor performance |
release (cargo bench) | 24 ms | 28 ms | Same host |
Numbers above are examples taken from the baseline lab node; they are not updated automatically. When recording new measurements, include hardware + commit hash in the accompanying doc or ticket.
CI Expectations
- The integration test above runs in the
perf-cold-startCI job on hardware matching the baseline profile. Build failures gate merges. - Criterion benchmarks run in the same job; results are attached as artifacts for manual inspection.
- Regressions >10% versus the baseline require investigation before landing.
Updating This Document
- When hardware changes, update the profile table and note the effective date.
- If thresholds move (e.g., Phase 6 budgets tighten), amend both the test assertions and the documentation here.
- Keep examples concise; avoid copying full benchmark output into the repo.
Release Process
This document replaces ad-hoc release notes with a repeatable checklist. It covers semantic versioning, approvals, and the artifacts we ship for each tag.
Versioning & Tags
- We follow SemVer. While the project is pre-1.0, use
v0.<minor>.<patch>. Increment<minor>for roadmap milestones (Phase-level features) and<patch>for fixes. The first stable GA will be taggedv1.0.0per the roadmap. - Tags live on
main. Cut a release branch only when preparing a beta/RC that needs additional soak; otherwise cherry-pick critical fixes directly onto the tag and createv0.x.y+1. - Annotate every tag (
git tag -a v0.x.y -m "Runloop v0.x.y") sogit describeremains meaningful for debugging.
Changelog Rules
- CHANGELOG entries are generated from Conventional Commits. Before tagging,
aggregate commits since the previous release (e.g.,
git log --format='%s' <prev-tag>..HEAD) and group them byfeat,fix,perf,docs,chore, etc. - Manually summarize any noteworthy highlights (perf wins, security fixes) at the top of the release section. Include links to key PRs.
- If a change requires operator action (schema migration, new config flag), add an Action Required bullet with the exact commands/config snippets.
Release Approvals
- Every release requires sign-off from: (1) the release manager for the phase,
(2) a runtime or platform engineer covering
runloopd/runtime, and (3) the security reviewer listed inSECURITY.mdwhen capabilities or signing changes are included. - Before tagging, confirm CI is green on
mainand the following commands pass locally in release mode:cargo build --workspace --release,cargo test --workspace,cargo clippy --workspace -- -D warnings, and the perf harness documented in docs/perf.md. - Capture the manual verification steps (smoke tests, upgrade/downgrade, sample opening replay) in the release PR description; attach terminal output or TUI screenshots for traceability.
Artifacts
- Crates & Binaries: Publish the
runloopd,rlp, andagtopbinaries to the GitHub release along with SHA256 sums. - Debian packages: Build via
packaging/systemd/usingjust deb. Verify install on Debian 13/trixie (x86_64 and arm64), ensurerunloopd.servicestarts, and exercise anapt upgradefrom the prior release to confirm a running daemon is restarted automatically. - Containers/dev images: Build from
packaging/container/for CI and developer environments. Tag images with the exact git SHA. - Live ISO: Optional for minor releases; mandatory for public betas. The
scripts live under
packaging/live-build/. - Document where each artifact is uploaded (GitHub Releases, package repo, internal mirror) and include download instructions in the release notes.
Verification Checklist
cargo fmt --all,cargo clippy --workspace -- -D warnings,cargo test --workspacecargo build --workspace --release- Performance harness:
cargo test -p runloop-runtime cold_start_p50_under_40msandcargo bench -p runloop-runtime cold_starton the baseline hardware. - Security review: confirm signed agent bundles install and that capability denials emit KB audit records.
- Docs:
mdbook build docsand updateCHANGELOG.md,docs/index.md, any relevant specs (policy, openings, perf budgets) to reflect the release. - Tag and push:
git tag -a v0.x.y -m "Runloop v0.x.y" && git push origin v0.x.y.
Record the completion timestamp and signer in the release issue for auditing.
Exec hostcall timeouts
The runtime can optionally impose a wall-clock timeout on exec_spawn
hostcalls. By default no timeout is applied; commands run until completion. To
enforce an upper bound, set the environment variable RUNLOOP_EXEC_TIMEOUT_SECS
to a positive integer number of seconds.
Special values:
RUNLOOP_EXEC_TIMEOUT_SECS=0(or unset) disables any timeout explicitly.
Notes:
- The timeout applies per
exec_spawninvocation. When exceeded, the child is sent akillsignal and the hostcall returnsEXEC_ESIGNAL. - This setting is process-wide for the daemon; there is currently no per-call or per-capability override.
Tools
Reference material for the developer tooling that ships with Runloop.
- Tool Attachments describes the
tools.jsonschema used byrlp agent scaffoldand bundle packaging. - TUI documents the interactive terminal UI for inspecting agents and workloads.
Agent scaffold
rlp agent scaffold <name> runs an interactive wizard (unless
--non-interactive) to bootstrap a wasm agent skeleton:
- Prompts for model/provider route + secret hints, capability grants
(fs/net/kb), and optional tool attachments (
tools.jsonentries). - Emits bundle files in
agents/<name>/:manifest.toml(placeholderentry_wasmdigest),policy.caps(based on responses),tools.json(v1), README, andbin/.gitkeep. - Drops a companion crate in
crates/agents-wasm/<name>/with a stubmain.rsthat signals readiness and prints placeholder JSON. - Generates a starter opening YAML (under
examples/openings/) wired to the new agent with a single node andexists(node.out)success condition. - Digests are computed for
tools.json;entry_wasmstays zeroed untiljust build-agents-wasmrebuilds the.wasmand updates the manifest.
Flags:
--root <path>overrides the bundle root (defaults to the firstagents.search_dirsentry from config).--crates-dir <path>overrides the wasm crate root (defaults tocrates/agents-wasm).--opening-path <path>writes the starter opening to a custom location (defaults toexamples/openings/<name>.yamlwhen generated).--model,--cap-fs,--cap-net,--cap-kb-read,--cap-kb-writeseed wizard values or bypass prompts when--non-interactive.--model-secretoverrides the provider secret id in non-interactive mode.--forceallows scaffolding into existing paths (overwrites files).
Global CLI overrides (local runs only):
--agents-dirprepends a search directory for agent bundles (ahead ofagents.search_dirsfrom config). For daemon runs, configure the daemon with the same search dir instead.--openings-dirprepends a search directory for openings (ahead ofopenings.search_dirs); requires--local.
Prompt Routing & Shell Integration
Doc status: MVP implementation guide for Epic O. Last updated: 2025-11-25.
This guide explains how Runloop classifies interactive prompts (rlp route),
why/when they are sent to agents, and how to wire the provided zsh/bash snippets
so that your terminal consults the router before executing a command.
1. Router quick reference
rlp route "<prompt>"(orrlp route --stdin) prints a stable JSON payload{ "version": 1, "route": "shell|agent", "rule": "...", "blocked": bool }. It exits10for shell decisions,11for agent decisions, and12on a timeout (--timeout-msorRUNLOOP_ROUTER_TIMEOUT_MS, default 200 ms) so shells can branch on$?without parsing stdout.rlp why "<prompt>" --json|--tableshows the matched rule, allow/deny hits, and any features (POSIX tokens, known commands, heuristics).- Configuration lives under
router.*inconfig.yaml:router.fastpath_shell: disable to always route to the default opening.router.default_opening: semantic name used in traces/telemetry.router.allowlist/router.denylist: substring matches that force shell routing or block prompts entirely.router.known_commands: extends the builtin command dictionary feeding the heuristics.
- All routing is local: there is no KB/model work in
rlp route, so it is safe to call on every prompt.
2. Shell helper (rlp shell)
The CLI manages shell snippets via rlp shell:
# inspect without changing files
rlp shell enable --shell zsh --dry-run --snippet packaging/shell/runloop.zsh \
--opening "$HOME/.runloop/openings/router-default.yaml"
# write the block into ~/.zshrc (default rc path)
rlp shell enable --shell zsh --snippet packaging/shell/runloop.zsh \
--opening "$HOME/.runloop/openings/router-default.yaml"
# remove it later
rlp shell disable --shell zsh
Key behaviors:
-
The helper looks for snippets under
/usr/share/runloop/shell(packaged installs),/usr/local/share/runloop/shell,/opt/homebrew/share/runloop/shell,~/.runloop/shell, and—as a convenience when working inside the repo—packaging/shell/. -
--snippet <path>overrides discovery; the command errors if the file does not exist (use--dry-runto preview). The helper canonicalizes snippet and opening paths before writing, so relative inputs like--snippet packaging/shell/runloop.zshremain valid once you start new shell sessions elsewhere (the block stores an absolute path). -
--opening <path>injects anexport RUNLOOP_ROUTER_OPENING_PATH=...line so the snippet knows which YAML opening to use when routing to agents. If you omit--opening, the helper copies a packaged default (from/usr/share/runloop/openings/router-default.yaml,/usr/local/share/runloop/openings/router-default.yaml, or/opt/homebrew/share/runloop/openings/router-default.yaml) into~/.runloop/openings/router-default.yamlif that file is absent, and points the env var at the user copy. Existing user copies are never overwritten; if neither path exists you can still pass--openinglater. -
All edits are wrapped by a marker block, e.g.
# >>> runloop shell (zsh) >>> export RUNLOOP_ROUTER_OPENING_PATH='/home/me/.runloop/openings/router-default.yaml' source '/usr/share/runloop/shell/runloop.zsh' # <<< runloop shell (zsh) <<< -
Default rc targets: zsh →
~/.zshrc, bash →~/.bashrc. Use--rc-pathto override;--shell autoresolves from$SHELL. -
Dry-run prints the resolved rc path, snippet/opening paths, and the exact block without writing files.
-
If the block already exists but points to a different snippet/opening,
enablereplaces it in-place (with a timestamped backup). -
If
~/.runloop/openings/is missing,enablecreates it (0755) and copies the packagedrouter-default.yamlunless creation fails, in which case it warns and continues without setting the env. -
rlp shell disableremoves the block (idempotent) and keeps timestamped backups (.zshrc.runloop.bak.<epoch>).
3. Zsh integration (packaging/shell/runloop.zsh)
The zsh snippet installs a ZLE widget runloop-accept-line that wraps the
builtin accept-line and behaves as follows:
- Guardrails: only runs in interactive shells, skips when
$TERM=dumb, whenRUNLOOP_ROUTER_DISABLE=1, when common CI/SSH envs are present (CI|GITHUB_ACTIONS|BUILDKITE|TEAMCITY_VERSION|JENKINS_URL|GITLAB_CI|CIRCLECI|SSH_CONNECTION|SSH_TTY), or whenrlpis missing from$PATH. Override withRUNLOOP_ROUTER_FORCE=1or toggle mid-session viarunloop_router_on/off. - On Enter it calls
rlp route --stdin, ignoring stdout and branching on the exit code:10→ shell fast-path: callzle .accept-lineso the command executes normally.11→ agent path: convert the buffer into JSON ({"prompt":"..."}), callrlp run <opening.yaml> --params '<json>', clear the buffer, and redraw the prompt. The default opening path resolves fromRUNLOOP_ROUTER_OPENING_PATH(injected by the helper) or the fallback~/.runloop/openings/router-default.yamlif it exists.12→ router timeout: showsrunloop: router timeout; executing in shelland falls back to the shell.- other exit codes → warn via
zle -Mand fall back to the shell.
- Helper functions:
runloop_router_off/runloop_router_onexport or unsetRUNLOOP_ROUTER_DISABLEmid-session._runloop_router_prompt_jsonhandles basic escaping (quotes, backslashes, newlines) to avoid external dependencies.
- Key binding: defaults to
^M(Enter). Override withRUNLOOP_ROUTER_BINDKEY='^J'(set before sourcing) to avoid conflicts with oh-my-zsh/Prezto keymaps; the widget binds in the main,viins, andvicmdmaps.
Requirements & tips
- Ensure
rlp runcan reachrunloopd(or pass--localvia your opening) so agent runs succeed; failures are echoed inline and the original command is not executed. - When copying additional openings for shell routing, keep them under a writable
directory (
~/.runloop/openings/or/usr/share/runloop/openings/). - Use
RUNLOOP_ROUTER_DISABLE=1 zshto start a shell with routing disabled; runrunloop_router_onlater to re-enable.
4. Bash integration (packaging/shell/runloop.bash)
The bash snippet wires a bind -x handler for Ctrl-M / Ctrl-J (Enter). The
handler inspects $READLINE_LINE, calls rlp route, and either evaluates the
line via eval (shell route) or invokes the opening (agent route). Behavior
mirrors the zsh widget with bash-specific nuances:
- Guardrails: interactive shells only; skips when
RUNLOOP_ROUTER_DISABLE=1, whenTERM=dumborPS1is empty, or when common CI/SSH envs are present (CI|GITHUB_ACTIONS|BUILDKITE|TEAMCITY_VERSION|JENKINS_URL|GITLAB_CI|CIRCLECI|SSH_CONNECTION|SSH_TTY). Override withRUNLOOP_ROUTER_FORCE=1. - Version gate: requires Bash ≥5.1. Older shells (e.g., macOS system Bash 3.2) emit a warning and skip to avoid readline/bind incompatibilities.
- Routing path: passes
RUNLOOP_ROUTER_TIMEOUT_MSthrough torlp routeand treats exit12as a timeout (warns, then executes the line via the shell). - Execution records history using the same
HISTCONTROL/HISTIGNOREsemantics as readline (leading-space secrets and ignored patterns remain out of history). - Agent runs print a newline, run
rlp run <opening.yaml> --params ..., and return to the prompt without executing the buffer. - Exit codes propagate:
$?,&&/||, andset -ebehave as if the user executed the command or opening manually. runloop_router_off/runloop_router_onmirror the zsh helpers; key binding defaults to Enter (^M) plus^JunlessRUNLOOP_ROUTER_BINDKEYoverrides with a single binding (invalid bindings log a warning and fall back to^M/^J).- Because bash lacks a native status bar, errors are printed to stderr.
- The snippet avoids non-interactive shells by examining
$-; sourcing it in scripts is a no-op.
Limitations (MVP):
- Multi-line PS2 prompts are not currently intercepted; the router only handles single-line READLINE buffers.
- If
rlp routeexits non-zero (e.g., misconfigured config), the line executes as a shell command after printing the error message.
5. Runtime toggles & troubleshooting
| Toggle / Command | Effect |
|---|---|
RUNLOOP_ROUTER_DISABLE=1 | Globally disable routing (shells fall back to normal behavior). |
RUNLOOP_ROUTER_FORCE=1 | Override CI/SSH auto-disable and force routing on. |
runloop_router_off/on | Convenience helpers provided by both snippets. |
RUNLOOP_ROUTER_OPENING_PATH | Absolute path to the YAML opening to invoke for agent routes. |
RUNLOOP_ROUTER_OPENING_PATH_DEFAULT | Optional fallback path (defaults to ~/.runloop/openings/router-default.yaml). |
RUNLOOP_ROUTER_TIMEOUT_MS | Timeout for rlp route (default 200); exit code 12 on expiry. |
RUNLOOP_ROUTER_BINDKEY | Key sequence to bind the widget (default ^M; bash also binds ^J unless set). |
RUNLOOP_ROUTER_DEBUG=1 (future) | Reserved for verbose logging (not yet implemented). |
Common issues:
snippet not found– pass--snippet <path>torlp shell enableor copy the repo snippets to~/.runloop/shell/.set RUNLOOP_ROUTER_OPENING_PATHwarning – ensure the referenced YAML exists; the helper does not create it for you.- Daemon unreachable –
rlp runwill printdaemon ... unreachable; startrunloopdor rerun withrlp run --localinside your opening. - Inspecting decisions – run
rlp why "<prompt>"after the fact to see the rule/feature breakdown. - Rolling back – run
rlp shell disable --shell <zsh|bash>to remove the block or manually delete the marker section.
6. Linking from other docs
README.mdreferences this document in the CLI/TUI section.docs/architecture.mdreplaces the “forthcoming router section” with a link here.docs/SUMMARY.mdlists it under “Tools” for mdBook navigation.- Debian postinst (rlp package) prompts the invoking user once to enable the
login shell; it skips CI/SSH/noninteractive/TERM=dumb and never edits rc files
on uninstall (use
rlp shell disableto remove the block).
With the helper and snippets in place, interactive prompts in supported shells
are classified before execution, shell-routed commands behave normally, and
agent-routed prompts flow through rlp run without manual wiring.
Tool Attachments (tools.json)
Status: Draft (normative for bundle layout once tooling lands). Defines how agent bundles describe optional host-provided tools so the runtime, router, and CLI wizard share the same contract.
Runloop agents may delegate specific actions to host tools (CLI binaries, HTTP
APIs, etc.). These attachments are declared in a tools.json file that lives
next to manifest.toml and policy.caps inside each bundle
(agents/<name>/tools.json in source, agent.bundle/tools.json after
packaging). Bundles are signed via manifest.toml, so the manifest must list
the BLAKE3 digest of tools.json just like other artifacts (see docs/ops.md).
Consumers treat the JSON document as the single source of truth for tool
metadata; no new [tools] tables are added to the manifest.
1. Top-level structure
{
"version": 1,
"tools": [{ "...": "see §2" }]
}
version— schema version of this document (currently1).tools— ordered list of attachments available to the agent. Missing or empty means the agent has no attachments.
Each bundle’s manifest.toml must include a digest for the attachment:
[artifacts.tools]
path = "tools.json"
blake3 = "<64-hex-digest>"
version = 1
2. Tool entry schema
id(string, required) – stable identifier such asmail.smtp_send. Payload frames reference this name.description(string, required) – short human-readable summary.transport.kind(enum, required) – eitherexecorhttp. Additional transport fields depend on the selected kind:execrequirestransport.command(absolute path) and optionallytransport.args(extra CLI args appended after the JSON blob path).httprequirestransport.method(GET,POST, etc.),transport.url(absolute URL, templating allowed), and optionaltransport.headers(static header map; values may include secret placeholders).
input_schema(JSON Schema or$ref, required) – describes theCT_TOOL_CALLpayload so validation can happen before execution.result_schema(JSON Schema or$ref, required) – shape of theCT_TOOL_RESULTpayload stored in the KB.schema_refs(object, optional) – helper map for additional schema IDs used inside this entry.capabilities(array of strings, optional) – capability families required before exposing the tool (for examplenet:api.mail.io).secrets(array of strings, optional) – secret IDs resolved prior to invocation.budget.tokens/budget.usd(optional) – per-tool budget hints enforced by the runtime.requires_confirmation(bool, optional) – defaults tofalse. When set, tool invocations inheritconfirm_external_actions.observability.tags(array of strings, optional) – tags appended to trace or audit records for filtering.
JSON example
{
"version": 1,
"tools": [
{
"id": "mail.smtp_send",
"description": "Send rendered drafts via the configured SMTP relay.",
"transport": {
"kind": "http",
"method": "POST",
"url": "https://api.mail.example/send"
},
"input_schema": {
"$ref": "schema:mail.send.request.v1"
},
"result_schema": {
"$ref": "schema:mail.send.result.v1"
},
"capabilities": ["net:api.mail.example"],
"secrets": ["runloop/mail/smtp_api_key"],
"requires_confirmation": true,
"observability": {
"tags": ["external", "mail"]
}
}
]
}
3. Runtime behavior
- Capability enforcement. Before exposing a tool, the runtime intersects the
tool’s required capability families with the agent’s effective
policy.caps. Any missing grant denies the attachment outright. - Secrets. Secret identifiers listed in
secrets[]are resolved via the configuredSecretResolverand injected into environment variables or HTTP headers depending on the transport. Failures emitcap_deniedaudit events. - Confirmation gate. When
requires_confirmationistrue, invocations behave like other external actions: the CLI/TUI prompts the operator unlesssecurity.confirm_external_actionsis disabled. - Tracing + replay. Tool calls emit
CT_TOOL_CALL/CT_TOOL_RESULTframes with the declaredidand schema references. Payloads are persisted in the KB and included in run traces sorlp replaycan rehydrate the tool outputs deterministically (or flag missing tools).
4. Wizard & authoring guidance
rlp agent scaffoldshould generate an emptytools.json(version 1) and offer to add entries interactively. Documented defaults make the wizard predictable and lintable.- Authors can share schema references by pointing
input_schema/result_schemaat existing IDs (seedocs/rmp-registry.md). Inline schemas are also allowed but should stay concise; large definitions belong in shared files that the wizard references via$ref. - Keep
idvalues stable. They become part of the KB provenance and any change should be treated as a breaking update.
Future revisions can introduce new transport.kind variants (e.g., unix
socket) via a new version number. Older runtimes must reject unsupported
versions so the CLI can guide authors to downgrade or install newer binaries.
Terminal UI
Status: In implementation for Epic I (CLI/TUI). This document captures the canonical UX/telemetry contract so
agtopand the daemon evolve in lock-step.
Overview
agtop is a ratatui-based monitor that subscribes to the Runloop bus for live
openings (or replays NDJSON from stdin/files). It presents four panes plus a
status bar:
- Log – streaming log lines (from unified
CT_RUN_EVENTstream). - Plan – DAG state (node status, attempts) derived from
RunEventnodes in the unified stream. - agtop Metrics – per-agent CPU/RSS/tokens and bus health aggregated from
rlp/sys/metrics+rlp/agents/<agent_id>/metrics. - Trace – ladder view of crossings (from
traceevents in the unified stream).
Artifacts remain out of scope for Epic I and will follow in the Observability epic.
Layout snapshots (ASCII)
Live run (streaming)
┌─────────────────────────────────────────────────────────────────────────────┐
│ agtop • mode:user • opening:compose_email (trace 63f1…) • pane:Log • tkns │
│ agents_running:3 • bus_queue_depth:1 • CONFIRM STATUS: clear │
├──────────────────────────────┬──────────────────────────────────────────────┤
│ LOG (rlp/runs/<id>/events) │ PLAN (DAG from run events) │
│ [12:01:03] writer ▸ draft ok │ contacts ✔ context ✔ draft ✱ review … │
│ [12:01:05] critic warn retry │ edges animate attempts / retries │
│ … │ │
├──────────────────────────────┼──────────────────────────────────────────────┤
│ agtop metrics (rlp/sys/*) │ TRACE (rlp/runs/<id>/events kind=trace) │
│ agents_running:3 rss:420MB │ [12:01:02] CLI → router → contact_resolver │
│ msgs_sent:134 drops:0 │ [12:01:04] writer → critic │
│ bus_queue_depth:1 cache:4 │ … ladder scroll │
└──────────────────────────────┴──────────────────────────────────────────────┘
Keyed areas:
- Status bar mirrors the “Status Bar” contract and surfaces confirm state.
- Log/Plan panes share the unified stream; left = textual events, right = DAG view.
- Metrics pane aggregates global/per-agent gauges; Trace pane renders the ladder.
Confirmation prompt visible
┌──────────────────────────────────────────────────────────────────────────────┐
│ … CONFIRM STATUS: PENDING (action.proposal 27ac…) │
├───────────────────────────────┬──────────────────────────────────────────────┤
│ LOG … │ PLAN … │
├───────────────────────────────┴──────────────────────────────────────────────┤
│ CONFIRMATION REQUIRED │
│ Agent mailer proposed sending draft v3 to `john@example.com` │
│ Enter=approve • Esc=reject • (.) pause stream • [?] help │
│ meta{opening_id:draft.send, ttl_ms:15000, trace_id:63f1…} │
├───────────────────────────────┬──────────────────────────────────────────────┤
│ agtop metrics … │ TRACE … │
└───────────────────────────────┴──────────────────────────────────────────────┘
When a confirmation dialog is focused the log/plan panes keep buffering events,
but input focus moves to the overlay; decisions publish action.decision via
the UI/TUI capability described in “Confirmations”.
Status Bar
Always-on, single line:
- Mode:
uservssystem(from config). - Opening: name +
trace_id. - Pane: active pane short name.
- Tokens/Health: summarized token burn, daemon pressure, and
agents_runninggauge. - Confirm Status: shows
CONFIRM REQUIREDwhen a pendingaction.proposalis awaiting a decision.
Keybinds & Controls
agtop is keyboard-only. Defaults:
Tab/Shift+Tab– cycle panes forward/backward.q– quit.?– help overlay (lists panes, keys, topic names)./– filter/search within the active pane (e.g., log substring, node id)..– pause/resume live updates (events buffer while paused and apply on resume).!– clear active pane buffer.Enter– when a confirmation prompt is focused, approve;Escrejects (mirrors CLI confirmation semantics).
Data Plane & Topics
- Run submission: CLI publishes
ControlRequest::RunSubmitonrlp/ctrl. The daemon responds withControlResponse::RunAcceptedand begins emittingCT_RUN_EVENTrecords onrlp/runs/<trace_id>/events. - Unified stream:
rlp/runs/<trace_id>/eventscarries{kind, level?, message, meta{ts_ms, run_id, node_id?, span_id?}}events. - Metrics:
rlp/sys/metrics(global) +rlp/agents/<agent_id>/metrics(per agent). Minimum gauges/counters (Epic J prerequisites):agents_running,rss_total,bus_queue_depth_max,bus_queue_capacity_max,msgs_sent,msgs_dropped,cap_denied,broker_calls,cache_hits. Status bar surfacesagents_runningandbus_queue_depthwhen present. Subscribe to per-agent metrics with--monitor-agentsuntil auto-discovery is added. Frames conform toCT_METRICS_SNAPSHOTv1:{"v":1,"scope":"system|agent","ts_ms":...,"interval_ms":...,"labels":{executor_id,node_id,agent_id?},"gauges":{...},"counters":{...}}. When token usage is unknown, token counters are omitted (rendered as N/A). - Confirmations: Agents emit
action.proposal. Only publishers with kindui|tuimay sendaction.decision. For daemon-backed runs the CLI defers to the TUI (no inline prompt);agtopsurfaces dialogs and publishes the decision. - Ladder (trace pane):
rlp trace <id>and the TUI consume ladder hops emitted on the run stream and persisted inrun.trace.ladder. Each hop captures{ts_ms, from, to, schema_id, frame_len, body_len, msg_id, kind}. Live streams are merged with the persisted ladder;--include-control/--include-dropsopt into control-plane and drop notices when debugging.
UX Principles
- Non-blocking panes: pane switches (
Tab) never pause other streams; pausing is explicit via.. - Structured layout: each pane uses consistent column widths; tables fall
back to JSON view with
--jsonflag (shared between CLI & TUI for scripts/tests). - Error surfacing: desyncs (e.g., missing metrics) render
N/Awith muted styling rather than stale values. Bus disconnects surface in the status bar with reconnect attempts. - Deterministic replay: trace pane can load historical runs via
rlp replay <trace_id>without contacting live agents (side-effect-free replayer).
Accessibility & Theme
- Default theme
mono(Config v1) uses high-contrast colors; no critical signal relies on color alone. - All panes expose textual labels (e.g.,
[WARN] critic timed out) for screen readers. - Keyboard-only interactions; focus indicators are double-underlined entries.
- Future enhancements (post-Epic I): configurable palettes and screen-reader mode.
Contributor Guide
This guide captures the day-to-day expectations that supplement
CONTRIBUTING.md and the workflows documented in AGENTS.md. Treat it as the
single source of truth for onboarding new collaborators.
Before Your First PR
Read these documents in order:
-
Engineering Standards — Technical standards, architectural guidelines, code size limits, and decision-making framework. This is the authoritative reference for how we write code.
-
CONTRIBUTING.md — Process, workflow, commit style, and DCO requirements.
-
This guide — Day-to-day expectations and communication norms.
Picking an Issue
- Start with the GitHub issue tracker and filter by
good-first-issueor roadmap labels (epic,phase:*) to find scoped work. Larger epics reference the milestones in ROADMAP. - Please comment
/assign(or leave a note) before beginning work so we avoid duplicate efforts. If an item touches multiple crates, outline the plan in the issue or create an Architecture Decision Record (ADR) stub. - For work that spans more than a few days, draft a short proposal describing problem, approach, and acceptance criteria. Link it from the issue for review before coding.
- When you open new issues, include reproduction steps, expected vs. actual behavior, and whether the bug blocks a roadmap milestone.
Style Conventions
- Rust code follows
cargo fmt --alloutput andcargo clippy --workspace -- -D warnings. CI enforces both gates before merge. - Naming: crates/files use
snake_case, public types useUpperCamelCase, and constants useSCREAMING_SNAKE_CASE. Avoidunsafeblocks unless the maintainers have signed off;deny(unsafe_code)is enabled in the workspace. - Markdown files use 2-space indentation per
.editorconfig; keep lines under ~100 characters when practical. - Tests live next to the modules they cover (
src/**/mod.rswith#[cfg(test)]) and cross-crate behavior belongs intests/harnesses. When a change alters capabilities or policies, add regression tests around the failure paths. - Golden Tests: For end-to-end regression testing of openings, we maintain a
golden corpus in
tests/golden/. Run these tests usingcargo test --package runloop-executor-local --test golden -- --ignored. These tests verify that openings produce expected outcomes (like correct recipient resolution or draft properties) across various input scenarios. - Update documentation and examples alongside code changes. At minimum, touch
docs/specs,examples/openings/, andAGENTS.mdwhen you add new capabilities, commands, or bundles.
Review Process
- Create a short-lived branch from
main(feat/<slug>,fix/<slug>,docs/<slug>, orchore/<slug>). Keep commits focused and use Conventional Commit prefixes; includeSigned-off-bytrailers for DCO 1.1 compliance. - Run
cargo fmt --all,cargo clippy --workspace -- -D warnings, andcargo test --workspacelocally. For docs-only changes, runjust docs-bookormdbook build docsto catch link issues. - Open a PR that explains the motivation, links the relevant issue/roadmap
item, and lists manual verification steps (screenshots for
rlp/agtopoutput are highly encouraged). - Request review from the CODEOWNERS responsible for the touched paths. At least one CODEOWNER approval plus a green CI run are mandatory before merge.
- Squash-or-merge via GitHub once approvals land; avoid force-pushing onto
main. Release managers will cherry-pick fixes as needed for stabilization branches.
Communication Norms
- Use GitHub Issues or Discussions for asynchronous design questions. Maintainer response-time goal is two US business days; if you need a faster answer, note the urgency in the issue title.
- Status updates: when you are assigned to an issue, post a short progress note at least every other day. If you are blocked, explain what is needed so a maintainer can help.
- Synchronous syncs (pairing, design walkthroughs) are opt-in. Ask for one in the issue/PR and propose time slots; maintainers will provide a video/voice link if necessary.
- Security-sensitive reports (e.g., capability bypasses) should go to the
contact listed in
SECURITY.mdrather than a public issue.
Technical Decision Checklist
Before submitting code for review, verify your changes against the Engineering Standards. Quick checklist:
Code Quality
- Functions under 100 lines, modules under 800 lines
- No
.unwrap()or.expect()in library code - All
unsafeblocks have// SAFETY:comments - Public items documented with
///doc comments -
#[non_exhaustive]on new public enums
Architecture
- Dependencies flow downward (see crate diagram in engineering standards)
- New crates justified (not just “might need it later”)
- Traits have multiple implementations or clear mocking need
Testing
- New functionality has tests
- Error paths exercised
- Property tests for parsers or security-critical code
Decision Framework
For significant choices, document in PR description:
- What options were considered?
- How does chosen option rank on: Reliability > Security > Debuggability > Maintainability > Performance?
- What trade-offs were accepted and why?
Roadmap
Runloop — Roadmap (12 months to v1.0)
Doc status: Living spec (acceptance metrics are normative).
Last updated: 2025‑11‑03
This roadmap is milestone‑driven with clear deliverables, exit criteria, and acceptance metrics. Month counts are relative to project start (M0 = kickoff). Tracks can run in parallel where dependencies allow.
0) Scope & principles (used for fast trade‑offs)
- Terminal‑first. The TUI/CLI is the cockpit; everything is inspectable.
- Local‑first, cloud‑optional. Works offline; sync/enrich when online.
- Small pieces, typed loosely. Agents interoperate over a minimal typed bus (RMP).
- Least privilege. Capabilities by default; human confirmation for external side effects.
- Deterministic replay. All openings are replayable with provenance and budgets.
- Ship > perfect. Debian first; keep clean seams for portability.
1) Staffing assumptions (minimal viable team)
- Core: 1 runtime lead, 1 infra/ops, 2 systems engineers (Rust), 1 ML/LLM engineer, 1 TUI/UX engineer, 1 PM/TPM, 1 security/priv reviewer (part‑time), 1 QA/automation.
- Optional: 1 tech writer, 1 developer relations.
Governance & repo hygiene run in parallel (see Phase G below).
2) Artifact map (what “done” looks like at v1.0)
- Daemons & CLIs:
runloopd,rlp,agtop. - Agent SDK (Rust/wasm32‑wasi) with examples & docs.
- RMP (Runloop Message Protocol) v0 (frozen) with header table, framing example, and message registry (foundation for future v1 negotiations).
- Openings DSL with grammar (EBNF/ABNF), replay semantics, and examples.
- POG (Personal Ops Graph): SQLite event log + materialized views + vector
index;
kb.query,kb.why,kb.write_event. - TUI panes: Plan, Log, Artifacts, agtop, Trace.
- Packaging: Signed agent bundles; OS packages (
.deb), ISO image for demo; upgrade/migration path. - Docs: Quickstart, SDK guide, Opening cookbook, Security whitepaper, man pages.
3) Phased plan (months → milestones → exit gates)
Phase 0 — Preflight & foundation (M0–M1)
M0.1 — Charter & constraints
Deliverables
- Product brief v1, non‑goals, supported platforms (Debian 12; x86_64 + arm64).
- License decision (e.g., Apache‑2.0) and third‑party notice template.
Exit criteria - Sign‑off on scope, principles, and release targets.
M0.2 — Architecture & interfaces 0.1
Deliverables
- RMP v0 freeze: header/body schemas finalized (
magic="RMP0",header_version=0,header_len=64, reserved flags/words zeroed, TTL & dedupe rules, schema ↔ body kind cross-checks, MsgPack envelope). - Agent packaging 0.1:
manifest.toml,policy.caps,tools.json(documented indocs/tool-attachments.md), signing model. - Openings DSL 0.1: grammar (EBNF) + replay semantics + examples.
- POG data model 0.1: entities (
Identity,Account,Contact,Artifact,Event,Policy), JCS JSON for payload/provenance, BLAKE3 BLOB(32) hashes.
Exit criteria - RFCs merged; skeletal crates compile.
M1.0 — Repo, build, CI
Deliverables
- Monorepo layout (
/runloopd,/cli,/tui,/sdk/agent-rust,/pog,/broker,/examples). - CI: fmt, clippy, unit tests, cross‑compile, nightly artifacts.
Exit criteria - One‑command dev setup; CI green on two arches.
Phase 1 — Minimal runnable system (M2)
M2.0 — Runtime & sandbox MVP
Deliverables
runloopdskeleton: process manager, config loader (schema v1), capability registry.- Wasmtime (WASI) integration; launch
hello-agent.wasm. - Capability gates: FS (scoped), Net (off by default), Time, POG read.
Exit criteria - Run a sample agent via CLI; controlled failure/exit codes observable.
M2.1 — CLI & TUI skeleton
Deliverables
rlpcommands:run,plan,trace <id>,cap grant.- TUI status bar + panes (Plan, Log); non‑blocking rendering.
Exit criteria - Route a prompt to a static agent; watch execution in TUI.
M2.2 — POG storage 0.1
Deliverables
- Append‑only event log (SQLite) + materialized view service.
- APIs:
kb.query,kb.why,kb.write_event.
Exit criteria - Insert
contact.upserted, retrieve with provenance viakb.why.
Phase 2 — Router & model broker (M3)
M3.0 — Router v0 (shell‑first)
Deliverables
- Shell fast‑path; policy file with allow/deny rules.
rlp why "<prompt>"explains routing.
Exit criteria- Interactive “why this route?” shown for three prompts (shell, agent, opening).
M3.1 — Model broker v0
Deliverables
- Local+remote providers; per‑request budgets; deterministic mode toggle for
replay.
Exit criteria - Budget/latency visible per request; deterministic replay mode usable in tests.
M3.2 — Canonical agents & opening
Deliverables
contact_resolver,context_gatherer,writer,critic,mailer(send requires human confirm).compose_emailopening (DAG + budgets + success criteria).
Exit criteria- End‑to‑end “draft email to John” with confirm before send.
Phase 3 — Openings engine & SDK (M4)
M4.0 — Opening engine v1
Deliverables
- Declarative DAG (YAML/DSL): retries, timeouts, budgets, success predicates.
- Deterministic replay over captured traces.
Exit criteria - Replay produces identical artifacts/messages for fixed seeds.
M4.1 — Agent SDK (Rust, wasm32‑wasi)
Deliverables
handle(Message) -> Result<Message)traits, cap helpers, test harness.
Exit criteria- Third‑party example agent compiles to Wasm and runs under
runloopd.
Phase 4 — Observability & performance harness (M5–M6)
M5.0 — Observability v1
Deliverables
agtoppane: per‑agent CPU/RSS, tokens in/out, error rate, cache hits.- Tracing ladder diagrams in
rlp trace. - Per‑opening/agent/provider cost accounting.
Exit criteria - Kill/restore tests present; trace spans visible for crossings.
M6.0 — Performance harness & budgets
Deliverables
- Lab capturing cold/warm startup, message latency, RSS, throughput; dashboards
against budgets.
Exit criteria - Measured: cold start p50 ≤ 40 ms, agent RSS p50 ≤ 8 MB, bus throughput ≥ 1000 msgs/s (hardware profile documented).
Phase 5 — Safety, scheduling, and self‑improvement (M8–M9)
M8.0 — Scheduler & pressure controls
Deliverables
- Fair‑share per opening; cgroups for CPU/mem/io; soft throttles; quarantine on
sandbox crash; circuit breakers for flapping agents.
Exit criteria - Stress test maintains interactivity while isolating “bad” agents.
M8.1 — Safety 0.1
Deliverables
- Capability tokens with expirations; human confirmation for external side effects (send/delete/spend).
- Tripwires for outbound spikes and exfil heuristics.
Exit criteria - Disallowed actions blocked with human‑readable reasons.
M9.0 — Self‑improvement harness 0.1
Deliverables
- Trace capture → clustering → patch proposals (prompts/policies) → sandbox A/B
→ adoption rules; golden task suite + regressions dashboard.
Exit criteria - System proposes & validates ≥1 improvement without manual prompt engineering.
Phase 6 — Beta hardening (M10)
M10.0 — Public Beta (0.9)
Deliverables
- Installers for Debian/Ubuntu (
.deb), code‑signed artifacts. - Docs: Quickstart, SDK, Opening cookbook, Security whitepaper.
- Telemetry opt‑in (anonymous) for stability metrics.
- Reliability/performance dashboard tracking acceptance metrics.
- Performance harness methodology captured in docs/perf.md.
Exit criteria (beta gate) - 24‑hour soak with zero critical crashes; 3 reference openings reproduce cleanly; upgrade/downgrade works.
Phase 7 — Release candidate & 1.0 (M11–M12)
M11.0 — RC (0.99)
Deliverables
- Backward‑compatible RMP/DSL finalized; migration scripts.
- API freeze; fault‑injection integration tests.
Exit criteria - No known P0/P1 defects; perf & memory targets met.
M12.0 — v1.0 GA
Deliverables
- Stable 1.0 release notes, long‑term support & deprecation policy.
Exit criteria - All acceptance metrics green; docs complete; supply‑chain attestation published.
4) Ecosystem, packaging & portability (runs alongside Phases 4–7)
Plugin packaging (bundles)
- Layout:
/agent/
manifest.toml # name, version, entrypoint, schemas, caps
policy.caps
tools.json # external tool contracts (see docs/tool-attachments.md)
agent.wasm
LICENSE
README.md
-
Authoring UX (no repo required):
rlp agent scaffold/build/installworks on a clean Debian install, scaffolded crates do not assume a workspace root, and digest tooling ships with the package. -
rlp agent install <path|uri>validates signature/caps and registers the bundle; upgrade path handles versioning. -
Signed bundles (Ed25519), deterministic Wasm builds; trust roots in config.
Exit criteria (Bundles)
- Tampered bundle rejected; repro builds verified on two machines; install/upgrade rollback tested.
- On a clean Debian host (no source tree),
rlp agent scaffold system_setup→rlp agent build/install→rlp runsucceeds without manual manifest edits or workspace setup.
OS packaging / images
- System user
runloop; state in/var/lib/runloop; per‑user config in~/.runloop. - Packages for Debian; demo ISO for fast trial; container image for dev only.
Exit criteria (OS packaging)
apt install runloopbrings uprunloopd.servicehardened; ISO boots to working TUI with sample openings.
Portability
- Abstract platform shims; Redox PoC (3 canonical agents) to inform future port.
5) Acceptance metrics & performance budgets
| Metric | Target (p50 unless noted) | Measured at |
|---|---|---|
| Agent cold start | ≤ 40 ms | perf lab harness |
| Agent RSS | ≤ 8 MB | perf lab harness |
| Bus throughput | ≥ 1000 msgs/s | perf lab harness |
| Replay determinism | ≥ 99% identical outputs on fixed seeds | trace replayer |
| Crash‑free stability | 24 h soak, zero critical crashes | Beta gate |
| Human‑confirm coverage | 100% on send/delete/spend | safety harness |
Notes: hardware profile, OS, and model/provider mix recorded with every run; thresholds are enforced in CI perf jobs.
6) Interfaces & invariants (normative notes)
- RMP v0 header (M0.2): 64-byte, big-endian header with magic
"RMP0",header_version=0,header_len=64, zeroed flags/reserved words,schema_idprimitive selector,body_len,created_at_ms,ttl_ms,trace_id,msg_id. TTL enforcement computescreated_at_ms + ttl_msin u128 and rejects zero/overflow before delivery. - KB storage: canonicalize payload/provenance to JCS JSON, hash as BLAKE3 BLOB(32); hex form is for logs/UI only.
- Action confirmation ACL: only UI may publish
action.decision; agents request viaaction.request. - Routing policy: shell fast‑path toggleable; “why this route?” always producible.
7) Docs, ADRs, governance (Phase G, parallel)
- ADRs: 0001 Debian + WASM/WASI + SQLite; 0002 RMP v0; 0003 KB event sourcing; 0004 Capabilities/security model.
- Docs: architecture (C4), protocol, openings DSL, KB schemas, TUI.
- Repo hygiene: CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, CODEOWNERS; branch protections; labels; issue templates.
Exit criteria (Docs & Governance)
- ADRs exist & linked; docs lint clean; contributors can build an agent in < 1 hour using the docs.
8) Risks & mitigations (live)
- LLM provider variance → deterministic broker mode for tests; hybrid local/remote with budgets.
- Perf regressions → perf gates in CI; nightly dashboards; rolling baseline.
- Security drift → capability audit logs; policy tests; signed bundles required.
- Protocol churn → header versioning; message registry; migration scripts.
9) How to read this document
- Deliverables are artifacts we will ship.
- Exit criteria are binary gates that must turn green before the milestone closes.
- Acceptance metrics are enforced in CI perf jobs and in Beta soak gates.
Architecture Decision Records (ADRs)
ADRs capture irreversible technical choices so future contributors understand why Runloop’s architecture looks the way it does. Keep every ADR in this directory and reference them from specs, code comments, and PRs.
Numbering & Naming
- Use zero-padded identifiers (
0001,0002, …). The file name should match the identifier and a short slug, e.g.,0001-debian-wasm-wasi-sqlite.md. - Reserve the following IDs per the roadmap:
0001— Debian base image, WASM/WASI runtime, SQLite storage.0002— Runloop Message Protocol (RMP) v0 framing & registry policy.0003— Personal Ops Graph (POG) event-sourcing model.0004— Capabilities & security enforcement model. Create new ADRs starting at0005.
Template
Include the sections below (feel free to copy/paste when creating a new file):
---
adr: 0005
title: Short Title
status: Proposed | Accepted | Superseded
date: 2025-11-13
deciders: names/handles
tags: [runtime, kb]
---
## Context
Problem being solved, constraints, prior art.
## Decision
The option we picked and why.
## Consequences
Positive and negative fallout, follow-up tasks.
## Alternatives Considered
Bulleted list with 2–3 sentences on why each was rejected.
Workflow
- Draft the ADR in a PR (or convert a design issue into a PR once you have a preferred direction).
- Request review from the stakeholders listed in the relevant roadmap phase (runtime, KB, policy, etc.).
- Once approved, merge the ADR and immediately link it from the relevant docs
(
README.md,docs/architecture.md, protocol specs, etc.). - If an ADR becomes obsolete, create a follow-up ADR marked “Supersedes
OLD_ADR_ID” and update the old file’s status to
Supersededwith a backlink.
Storage & Cross-References
- Keep ADRs under version control; do not edit them retroactively after they are
marked
Acceptedexcept to fix broken links or typos. - Reference ADR IDs in code comments (
// ADR-0002: reason), docs, and commit messages to provide traceability. - When adding new components or policies, check existing ADRs first to avoid conflicting decisions. If a conflict is unavoidable, call it out explicitly in the new ADR.