Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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/runloopd hosts the runtime; crates/rlp drives routing, KB commands, tracing, and local execution (--local).
  • Agent bundles: canonical bundles live under agents/; companion wasm crates are under crates/agents-wasm/. Manifests, capability policies, and optional tools.json files 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.
  • rlp CLI (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 IDs
  • docs/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

  1. Clone repo and read README.md front-to-back.

  2. Configure ~/.runloop/config.yaml (copy from .env.example guidance when available).

  3. Initialize secrets backend (optional):

    rlp secrets init --backend=secret-service
    
  4. Initialize KB once binaries land:

    rlp kb migrate   # creates events.sqlite, pog.sqlite, vectors/
    rlp kb verify
    
  5. Review trust policy (after Release key published):

    cat ~/.runloop/trust-policy.toml
    
  6. Build the wasm agent bundles and run the smoke test:

    just build-agents-wasm
    just test-agents-wasm
    

    These helpers compile the wasm32-wasip1 binaries under agents/*/bin/ and exercise compose_email end-to-end via rlp --local.

Common operational commands

  • rlp kb migrate|verify|backup|vacuum

Secrets/trust CLIs are still planned: rlp secrets put|get|list|delete and rlp trust update remain interface contracts until their implementations land with the packaging milestone. rlp agent install is available for local bundles (directory or .tar/.tar.gz) and performs digest/tools.json validation; rlp agent remove is still pending. Use rlp agent list to 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

  1. Build the .deb from source (Debian 13/trixie host):

    just deb
    

    Install cargo-deb first (cargo install cargo-deb). Artifacts land under each crate’s target/debian/ directory, e.g. crates/runloopd/target/debian/runloopd_<version>_<arch>.deb.

  2. Install and start the daemon:

    sudo apt install crates/runloopd/target/debian/runloopd_0.1.0_amd64.deb
    sudo systemctl status runloopd
    

    Install 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.deb
    

    The daemon package writes the default config to /etc/runloop/config.yaml, creates /var/lib/runloop, and enables (but does not automatically start) the runloopd systemd unit so you can edit config before running it.

  3. Update /etc/runloop/config.yaml as needed, then run sudo systemctl restart runloopd. Purging the package removes the config, state, and the runloop system 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 checklist
  • ROADMAP.md → milestone-level goals
  • Issues/Discussions (once public) will tag bugs/features/tasks

Contributing flow (preview)

  1. Fork/branch following CONTRIBUTING.md guidance (to be fleshed out).
  2. For spec changes, open an ADR (docs/adr/) and mark affected docs as Draft/Normative.
  3. 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::RunSubmit frames for the daemon.
  • Reads configuration from ~/.runloop/config.yaml//etc/runloop/config.yaml via crates/core and obeys router.fastpath_* policies described in router-shell.md.
  • Talks to runloopd over the Unix socket using CT_CTRL_REQ/RESP and subscribes to rlp/runs/<trace_id>/events for streaming output.
  • Provides shim binaries such as rlp run, rlp replay, and rlp trace plus 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 through crates/executor-local when rlp --local is used.
  • Persists node/run transitions to the KB via crates/kb APIs 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.
  • runloopd now attaches the openings runner to a bus executor: node invocations are serialized into CT_EXECUTOR_AGENT_REQUEST envelopes, published on agent/<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 on rlp/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)

  • rlp offers operational commands (run, trace, replay, kb, config).
  • agtop consumes NDJSON run streams or live bus subscriptions to show plan DAGs, logs, metrics, and trace ladders.
  • executor-local backs rlp --local by 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.json alongside each manifest; see docs/tool-attachments.md for the schema emitted by rlp agent scaffold.

Observability & Ops (crates/agtop, docs/ops.md, docs/perf.md)

  • tracing-based spans propagate trace_id/opening_id; metrics exporters can emit OTLP when configured via observability.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

  1. 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.
  2. Control-plane negotiation: The CLI publishes CT_CTRL_REQ on rlp/ctrl (or runs locally) and waits up to 2000 ms for RunAccepted. The daemon replies with CT_CTRL_RESP including the assigned trace_id and opening metadata.
  3. Run event stream: Once accepted, runloopd emits CT_RUN_EVENT frames to rlp/runs/<trace_id>/events. Consumers (CLI stdout, agtop, tests) render logs, plan status, artifacts, and trace ladder entries from this single stream.
  4. 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.audit events.
  5. Persistence & replay: Ledger events (run.started, run.finished, node.finished, run.trace, artifact.created, etc.) are recorded through crates/kb. Replays sourced from the KB must deterministically regenerate the same outputs (rlp replay).
  6. Observability: Drops, TTL expirations, and dedupe hits publish structured diagnostics on rlp/sys/drops, while tracing spans feed OTLP exporters when enabled.

Runtime Modes & Packaging

  • User mode: cargo run -p runloopd plus rlp uses ~/.runloop for state and discovers sockets via runtime.socket_path or runtime.sockets_dir.
  • System mode: Debian packages install runloopd as runloop: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-build consumes the same binaries and seeds demo scenarios referenced in examples/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 (see docs/policy-caps.md).
  • Secrets always travel via opaque secret_id indirections, never raw blobs in KB events; the CLI documents rollback/disable paths in docs/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

  • agtop panes (Log, Plan, agtop metrics) subscribe to the same run stream and bus metrics.
  • docs/perf.md outlines the throughput harness (≥600 msgs/s target 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

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 (see docs/policy-caps.md).
  • Overrides located at ~/.runloop/caps/overrides only 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_REQ on rlp/ctrl. The daemon responds with CT_CTRL_RESP::RunAccepted and streams CT_RUN_EVENT to rlp/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.decision on the bus; CLI does not prompt when connected to the daemon.

KB Ownership (MVP)

  • For daemon-backed runs, runloopd records run.started, run.finished, per-node node.finished, and the canonical run.trace payload 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 plus entry_wasm digest.
  • 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/agents for bundles and ~/.runloop/examples/openings for openings, with repo paths (./agents, ./examples/openings) and system paths (/var/lib/runloop/agents) searched as fallbacks; use --agents-dir/--openings-dir to point rlp at 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"}'
  • --root and --crates-dir override the default bundle/crate roots.
  • To add the agent to other openings, reference it via use: agent:note_taker and pass parameters in the node’s with: block.
  • Capabilities are enforced at runtime; tighten policy.caps before 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.

  1. 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 stub main.rs
  • examples/openings/system_tra.yaml wired to the new agent
  1. 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.

  1. Implement the agent logic

The bundled implementation already:

  • Parses with.input as JSON.
  • Manages a tmux block bounded by # >>> runloop:system_tra / # <<< … and sets history-limit.
  • Updates HISTSIZE / HISTFILESIZE in ~/.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.

  1. 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/list
    

    If agent is missing, reinstall from the latest .deb.

Steps

  1. Scaffold

    rlp agent scaffold my_agent
    

    This creates:

    • ~/.runloop/agents/my_agent/ bundle with manifest.toml, policy.caps, tools.json, bin/.
    • ~/.runloop/agents-wasm/my_agent/ crate with a stub src/main.rs.
    • Optional starter opening YAML under ~/.runloop/examples/openings/<name>.yaml if requested.
  2. Author

    • Edit ~/.runloop/agents-wasm/my_agent/src/main.rs with your logic.
    • Adjust capabilities in ~/.runloop/agents/my_agent/policy.caps.
    • Add tools in ~/.runloop/agents/my_agent/tools.json (version 1).
  3. Build + install into the bundle

    rlp agent build my_agent
    

    The command compiles the wasm (cargo build --release --target wasm32-wasip1), copies it into ~/.runloop/agents/my_agent/bin/, recomputes BLAKE3 digests for entry_wasm and tools.json, and validates the caps file.

  4. 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>.

Picking an executor (local vs daemon)

  • rlp ... --local uses 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 runloop with home /var/lib/runloop, and its default agent search dirs resolve under that home. Agents you scaffolded in /home/<you>/.runloop/agents will 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

  1. Point the daemon at your bundle/openings and keep the socket reachable (or install the bundle into /var/lib/runloop/agents with rlp 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
  1. 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
  1. Make sure your user can connect to /run/runloop/rmp.sock:

    • Add yourself to the runloop group: 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.
  2. 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.
  3. Reload and restart the service:

sudo systemctl daemon-reload
sudo systemctl restart runloopd
  1. Run your opening (no --local so 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.yaml so the socket lives under your home (either runtime.sockets_dir: $HOME/.runloop/sock or runtime.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 --all shows which config layers are active; unreadable /etc/runloop/config.yaml is 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-dir flags 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:
    1. u32 frame_len prefix that covers header_len + body_len (does not include the prefix itself).
    2. Fixed 64-byte header (see below).
    3. body_len bytes (MsgPack map body).

If the prefix is ever removed in a future transport, header_len + body_len MUST remain the delimiter.

1.1 Fixed header (big-endian, 64 bytes)

OffsetSizeFieldNotes
04magicASCII "RMP0" (0x52 0x4D 0x50 0x30)
42header_version0 for RMP v0; anything else → UnsupportedVersion
62header_len64, compare directly; mismatch → UnsupportedVersion
84flagsMUST be 0 in v0; non-zero → InvalidHeaderFlags
122schema_idu16 primitive family ID (see registry)
142reserved20; otherwise reject
164body_lenu32 body length in bytes
208created_at_msu64 epoch milliseconds
288ttl_msu64 relative TTL; 0InvalidTtl
3616trace_idu128 routed end-to-end
528msg_idu64 monotonic per publisher
604reserved40; 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_len are validated before parsing the rest of the header; a truncated 64-byte header raises TruncatedHeader.
  • schema_id identifies a primitive family (Observation, Intent, Artifact, ToolResult, Critique, StateDelta, ErrorReport, etc.). The registry lives in docs/rmp-registry.md.
  • body_len MUST match the actual MsgPack payload length and participates in the framing equality checks below.
  • trace_id + msg_id are 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_id selects 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_id with the body type; mismatches raise BodyTypeMismatch.
  • meta is optional and carries diagnostics (opening_id, priority, tags, budgeting hints). Unknown keys MUST be ignored. opening_id lives in meta, not the fixed header, in v0.

3. Framing invariants

  • frame_len MUST equal header_len + body_len. Any mismatch yields LengthMismatch and the frame is dropped.
  • header_len is fixed at 64 bytes in v0. Future versions MUST bump header_version and, if necessary, header_len.
  • Implementations MAY cache header_len, but they MUST still compare the actual field to 64 and reject anything else.
  • Ladder bytes (rlp trace): ladder hops render frame_len exactly as transmitted (header_len + body_len) and fall back to 64 + body_len when a pre-serialized frame length is unavailable (e.g., synthetic local runs).

4. TTL & expiry handling

  • Compute expires_at_ms = created_at_ms + ttl_ms using u128 arithmetic.
  • If ttl_ms == 0, raise InvalidTtl before any delivery attempts.
  • If the addition overflows u128 or the resulting value does not fit in u64, raise InvalidExpiry.
  • Receivers MUST drop frames once now_ms >= expires_at_ms, emitting Expired in counters/telemetry. Drops still publish the body to rlp/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:
    1. Increment drops_total{reason=...} metrics.
    2. Publish a structured event on rlp/sys/drops with {reason, topic, trace_id, msg_id, expires_at_ms?}. Emitters MUST rate-limit this topic to avoid storms.

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)

ErrorOne-liner
InvalidMagicmagic bytes were not "RMP0".
UnsupportedVersionheader_version or header_len differed from 0/64.
TruncatedHeaderFewer than 64 header bytes were available.
InvalidHeaderFlagsNon-zero flags, reserved2, or reserved4 encountered in v0.
LengthMismatchframe_len != header_len + body_len.
UnknownSchemaschema_id not present in the registry.
BodyTooLargebody_len exceeded the configured limit.
InvalidTtlttl_ms was zero.
InvalidExpirycreated_at_ms + ttl_ms overflowed u128 or could not fit in u64.
Expirednow_ms >= expires_at_ms at receipt time.
Duplicate(trace_id, msg_id) already seen within the dedupe horizon.
BodyDecodeErrorMsgPack body failed to parse according to the schema.
BodyTypeMismatchBody "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 with meta.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_REQ with ControlRequest::RunSubmit { request_id, opening_yaml }.
    • The CLI applies any --params override and sends the merged YAML as opening_yaml.
    • The frame header.trace_id MUST equal request_id for correlation.
    • ttl_ms = 30000 (30s) for control requests.
  • Response: CT_CTRL_RESP with ControlResponse::{RunAccepted, RunRejected, RunCancelled}.
  • RunAccepted carries { request_id, trace_id, opening_id, opening_name }.
  • Idempotency: the daemon MUST treat duplicate CT_CTRL_REQ with identical trace_id as idempotent.
  • Acceptance timeout: the CLI waits up to 2000 ms for RunAccepted before failing with guidance to start the daemon or use --local.
  • Rejections: RunRejected includes a human reason. Future protocol revisions may add structured code / detail fields 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 unique reply_topic (e.g., rlp/runs/<trace_id>/agents/<agent>/<uuid>), and the agent publishes its response on that topic.

  • Request (CT_EXECUTOR_AGENT_REQUEST, type intent.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 to reply_topic before publishing the request.

  • Response (CT_EXECUTOR_AGENT_RESPONSE, type toolresult.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 rebuilds NodeOutputs from 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 use CT_BUS_DROP_NOTICE on rlp/sys/drops.
  • action.decision (schema CT_ACTION_DECISION) may only be published by ui|tui publisher kinds; the bus rejects other publishers.

12. Socket & transport (MVP)

  • Single Unix domain socket is used for both bus and control.
  • Discovery precedence:
    1. If runtime.socket_path is non-empty, use it (short‑circuit; if unreachable → error immediately).
    2. Else if runtime.sockets_dir is set, ${runtime.sockets_dir}/rmp.sock.
    3. Else ~/.runloop/sock/rmp.sock.
    4. Else /run/runloop/rmp.sock.

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 by crates/rmp::registry); update both locations together.

RangeOwnershipNotes
0x0000ReservedNever used; helps catch default/zero bugs.
0x0001–0x000FCore, stableRunloop-owned; changes require an ADR and backward-compatible plan.
0x0010–0x007FFirst-party extensionsExperimental Runloop schemas that may graduate to Core.
0x0080–0x03FFLocal/privateSafe for development; not guaranteed to interoperate across installs.
0x0400–0x0FFFThird-party provisionalExternal vendors reserve here via PR to this registry.
0x1000–0xFFFFVendor-definedMust embed schema_hash and vendor fields in the payload.

Core assignments (effective)

schema_idNameVersion fieldSummary
0x0001Observationv (u16)Facts gathered by an agent about the environment.
0x0002Intentv (u16)Requested action from router/opening to an agent.
0x0003ToolCallv (u16)Structured tool invocation request.
0x0004ToolResultv (u16)Result of a tool invocation, streaming or final.
0x0005Artifactv (u16)Durable content blob metadata + pointer.
0x0006Critiquev (u16)Quality feedback / review of an artifact or intent.
0x0007StateDeltav (u16)Mutation against agent/opening state machine.
0x0008Run.Eventv (u16)Lifecycle event within an opening run (start/finish/error).
0x0009Controlv (u16)Routing/control plane directive between router and daemon.
0x000AError.Reportv (u16)Structured error diagnostics for operators/UI.
0x0BBFBus.DropNoticev (u16)Delivery failure telemetry published on rlp/sys/drops.
0x000B–0x000FReservedFuture 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

  1. Propose new schema via PR updating this file (and ideally docs/message-protocol.md).
  2. For third-party provisional IDs (0x0400–0x0FFF), include:
    • Contact info / org name
    • Schema summary and stability expectations
    • Sample payload
  3. For vendor-defined IDs (0x1000+), Runloop runtime enforces the presence of schema_hash (e.g., SHA-256 of canonical schema) and vendor string 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_json and provenance_json MUST be JCS canonical JSON strings.
  • Insertions must occur within transactions; never update/delete existing rows.
  • hash_blake3 validation occurs during rlp 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/:

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 KEY
    • dim INTEGER
    • embedding BLOB
    • meta JSON
  • meta(schema_version TEXT, dirty INTEGER, ts DATETIME) (mirrors ledger)
  • snapshots:
    • id INTEGER PRIMARY KEY
    • ts DATETIME
    • events_high_watermark INTEGER
    • comment 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:
    1. Clear vector files.
    2. Stream embeddings via VectorStore::rebuild.
    3. Validate counts and run probe queries.

4. Migration commands

CommandPurpose
rlp kb migrate [--inplace]Backup, migrate schema, replay views, rebuild vectors.
rlp kb verifyHash, referential integrity, schema version checks.
rlp kb backupConsistent backup of both DBs.
rlp kb vacuumVacuum/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.audit events 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_hash hex), decision (allow|deny), severity, and a short reason code. Events are written whenever the runtime’s audit policy enables the corresponding decision stream (see security.caps.audit_on_allow / audit_on_deny in 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 in with, 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.

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

KeyTypeRequiredMeaning
versionintDSL version (this doc = 0).
namestringOpening identifier (DNS‑label recommended).
goals[string]Human intent; logged with trace.
paramsmapParameter bag available to templates.
policy.budget_tokensintAggregate budget hint propagated to nodes/broker.
policy.timeout_msintOpening‑level wall‑clock timeout; cancels remaining nodes.
policy.confirm_externalboolRequire human confirmation for send/delete/spend actions.
nodes[Node]Agent or nested opening invocations.
edges[Edge]Connections between node ports.
successSuccessExprCompletion 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
  • use selects either an agent bundle (WASM under runtime) or a nested opening (composition).
  • with is 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.*}} in with maps. 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 allOf semantics: 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

  1. Parse & validate YAML → IR, producing helpful errors with line/column.
  2. Topological scheduling. Nodes run when all inbound dependencies are ready. Fan‑in edges cause waits; fan‑out clones messages/artifacts.
  3. 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, and ttl_ms; opening_id and other metadata ride in the MsgPack envelope { type, payload, meta? }.
  4. 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 on rlp/sys/drops.
  5. Retries & timeouts. Per‑node retry and timeout_ms govern backoff and cancellation. Opening‑level policy.timeout_ms cancels remaining nodes.
  6. 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.
  7. Audit & KB events. Open/close of a run emits run.started/run.finished; agent actions may emit cap.audit on 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 in crates/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_ms in 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_id propagation; 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 persisted run.trace from 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 in security.confirm_external_actions.
  • 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.
  • rlp run validates every node’s with payload against the signed manifest schemas (plus any schema_hints) before contacting the daemon. Use --errors-format table|json to 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)

  1. Graph soundness: nodes are unique; edges reference existing ports; no cycles.
  2. Port compatibility: serialized payload types must match the consumer input schema_id (static lint + runtime check).
  3. Budgets: non‑negative; per‑node budget must not exceed opening budget (lint).
  4. Timeouts: if absent at node level, the runner inherits opening timeout; long‑running agents must explicitly opt into higher timeouts.
  5. Templating: only {{params.*}} is allowed; unresolved placeholders are errors.
  6. Security: if confirm_external=true, any node with with.require_human_confirm=false is 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 via artifacts.save are materialized and traceable via kb.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 0 semantics: 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 explicit ttl_ms: never sentinel 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: agtop shows 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 & 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.

  1. CLI flags (rlp run --model=…, :budget … inline overrides)
  2. Environment variables (RUNLOOP_*)
  3. User config ~/.runloop/config.yaml
  4. System config /etc/runloop/config.yaml
  5. 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

TypeRule
ScalarsLast writer wins (respecting precedence).
MapsDeep merge; map entries follow precedence per key.
ListsReplace entirely (last writer). Exceptions: models.providers unions entries before applying allow/deny lists.
Capability sets / allowlistsIntersect 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:
    1. If runtime.socket_path is non-empty, use it and error immediately if unreachable (no probing).
    2. Else if runtime.sockets_dir is set, use ${runtime.sockets_dir}/rmp.sock.
    3. Else ~/.runloop/sock/rmp.sock.
    4. Else /run/runloop/rmp.sock.

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.providers lists named backends. kind may be local, http (OpenAI-compatible completions), http_openai_chat (OpenAI chat), http_anthropic (Claude /v1/messages), http_ollama (local Ollama), or http_gemini (Google Gemini generateContent). These HTTP kinds accept base_url, secret_id, and optional static headers.
  • models.broker.route is an ordered array of { pattern, provider, target_model? } entries; the first matching pattern wins. Legacy map syntax like { "*": "local" } (or the legacy key routing) still deserialises into the same shape.
  • models.broker.cache exposes ttl_ms and capacity for the in-memory LRU. Requests may override TTL via cache_ttl_ms; 0 disables caching for that call.
  • models.broker.budgets retains default_tokens, per_request_tokens_cap, and hard_cap_usd. Per-request budgets clamp to the stricter of the request and config-provided values.
  • Provider secret_id values 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 a secret_id such as runloop/models/gemini (the runtime will also look for the environment variable RUNLOOP_MODELS_GEMINI). Agents that invoke Gemini still need model = true in policy.caps; automation agents that shell out (e.g., to manage tmux) also require exec = true plus 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 its mailbox_recv loop (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 in AgentSpec::spawn_ready_timeout_ms; environment variable RUNLOOP_SPAWN_READY_TIMEOUT_MS is the lowest-precedence fallback.
  • When the timeout elapses, callers receive Error::ReadyTimeout, the runtime emits runloop.runtime.spawn.ready_timeouts_total, and it tears down any partially created bus subscriptions/audit state to prevent ghost agents.
  • Treat notify_ready as part of the minimum agent ABI going forward; older agents that cannot be rebuilt should block on mailbox_recv immediately 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)
  • runloopd runs a background materializer that tails the ledger and updates the views. Progress is tracked in the singleton row pog.sqlite.materializer_state with columns:
    • id INTEGER PRIMARY KEY CHECK (id = 1)
    • watermark INTEGER NOT NULL

2.1 Migration workflow

rlp kb migrate orchestrates upgrades across both stores.

  1. Ensure runloopd is stopped (command refuses to run if sockets are open; override with --force).
  2. Create timestamped backups of both DBs.
  3. Apply schema migrations to events.sqlite (rare; append-only).
  4. Rebuild pog.sqlite by replaying events (events.sqlite → views). Use --inplace only for emergency SQL patches.
  5. Rebuild vector index using the VectorStore::rebuild path.
  6. Set meta.dirty = 0, record new schema_version, and create a snapshots entry.
  7. Update materializer_state.watermark with the highest applied ledger id.

Supporting commands:

  • rlp kb verify — referential integrity, hashes, BLAKE3 checks
  • rlp 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=true to allow privileged reads and should set a deployment-specific kb.redaction.salt. Agents must declare kb_read.contacts_raw to 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 KEY
  • ts DATETIME
  • events_high_watermark INTEGER
  • comment TEXT

2.3 Retention

  • Ledger retains all events; corrections produce new StateDelta entries.
  • Operators can archive older events by copying subsets elsewhere; never delete rows in-place.
  • Materialized views compact automatically during rebuild; configure retention by emitting StateDelta events that mark artifacts/contacts inactive.

3. Vector index lifecycle (normative)

  • Implementation milestone 1 uses a pure-Rust HNSW crate (hnsw_rs class). 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.
  • VectorStore trait (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-deb consumes them directly; no in-tree debian/ directory is required.

  • Build requirements: build-essential, cargo, rustc, pkg-config, libssl-dev, libsqlite3-dev, systemd, and cargo-deb (cargo install cargo-deb). The just deb recipe 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 with sudo apt install crates/<crate>/target/debian/<pkg>_<ver>_<arch>.deb.

  • runloopd package duties: install /usr/bin/runloopd, systemd service, tmpfiles definition, /etc/runloop/config.yaml (as a conffile), and docs. The maintainer scripts create the runloop system user, chown /var/lib/runloop / /var/log/runloop, call systemd-tmpfiles --create, run systemctl daemon-reload, and enable but do not start runloopd.service on a first-time install so operators can edit config before launching. They should also create /var/lib/runloop/agents and /var/lib/runloop/openings (owned by runloop:runloop) so rlp agent install --root /var/lib/runloop/agents is immediately usable. Upgrades capture whether the daemon was running prior to dpkg stopping it and automatically restart runloopd.service once 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 on ca-certificates plus transitive Rust runtime libraries.

  • Purging the daemon package (sudo apt purge runloopd) removes /etc/runloop, /var/lib/runloop, /var/log/runloop, and the runloop system user/group; CLI/TUI packages only drop their binaries/docs.

4.2 Additional artifacts

ArtifactLocationStatus
Live ISOpackaging/live-build/Folders exist; scripts TBD after .deb packaging.
Dev containerpackaging/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 install is available for local bundles and validates manifest digests + tools.json schema, but signature verification and rlp trust update are still landing with the packaging milestone. Edit trust policy files manually per the steps below. rlp agent list shows 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.json MUST follow the schema in docs/tool-attachments.md; its digest appears in manifest.toml so 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 update fetches 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 install will refuse bundles until signature verification is implemented.
  • Parameter schemas: agent manifests embed JSON Schemas under [schemas.with] so tooling can validate with payloads 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 is stub (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 list and rlp secrets delete for 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 observability in config.
  • Bus/TUI metrics snapshots: observability.metrics_interval_ms (default 1000, allowed 100–60000) controls how often runloopd publishes CT_METRICS_SNAPSHOT frames to rlp/sys/metrics and rlp/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, and runloop_broker_errors_total{kind=*} counters for dashboards.
  • agtop pane + rlp trace rely on the metrics exported by agents.
  • Capability audit volume is gated by security.caps.audit_on_allow and security.caps.audit_on_deny; the latter defaults to true so denied hostcalls land in the KB as cap.audit events.

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) on rlp/sys/drops whenever TTL expiry or duplicate suppression occurs. Operators should scrape this topic for reliability dashboards.

8.0 Control plane

  • rlp/ctrl carries CT_CTRL_REQ and CT_CTRL_RESP. Submit requests use a 30s TTL; the CLI waits up to 2s for acceptance.
  • After acceptance, the daemon publishes CT_RUN_EVENT to rlp/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:

KeyTypeDefaultNotes
fsarray of absolute paths[]Paths are normalized; globs not allowed in v0.1.
netarray of hostnames[]Hostnames may include port (example.com:443). Wildcards arrive in v0.2.
timeboolfalseGrants access to monotonic/UTC clocks.
kb_readbool or arrayfalsetrue 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_writebool or arrayfalseSame semantics as kb_read; true is discouraged outside trusted agents.
secretsarray of secret IDs[]IDs follow runloop/<scope>/<name>.
modelboolfalseAllows requests to model broker.
execboolfalseCommand 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.caps and 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 false revokes 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.audit entry with cap="caps.empty", op="_start", and reason="caps_empty" so operators understand the agent will be inert until policy is relaxed. Hostcalls will continue to be denied under audit_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_deny events) and, when enabled via security.caps.audit_on_*, persisted to the knowledge base as cap.audit events 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)
    • time
    • kb_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.audit ledger events (and structured logs) when security.caps.audit_on_deny is true (default). Operators may also enable security.caps.audit_on_allow to persist allow decisions for high-scrutiny agents.
  • Implementation status: the runloop-runtime crate embeds Wasmtime, enforces capability checks for every exposed hostcall, and records denials as cap.audit events via the knowledge base (see crates/runtime/tests/capabilities.rs).

Secret Handling (normative)

  • Secret material never lives in the POG; only opaque secret_id references or hashed markers are stored. Hostcalls return raw values by default for backward compatibility. To opt into handle-only mode (rlsec_<...>), set security.testing.expose_raw_secrets = false (RUNLOOP__SECURITY__TESTING__EXPOSE_RAW_SECRETS=0). Default remains true for 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:
    • env
    • secret-service (DBus Secret Service; currently stubbed/best-effort; skipped in auto)
    • pass (pass show runloop/<secret_id>, first line)
    • age (file store under security.secrets.root, default ~/.runloop/secrets; master key at <root>/master.agekey 0o600. Current implementation reads plaintext .age files; encryption TODO.)
    • auto probes pass → age → env+store (secret-service skipped until wired).
  • TTL: security.secrets.default_ttl sets 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 with Error::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=1
RUNLOOP__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; runloopd verifies 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.md overall 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_ms reported 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)

BuildMedianP90Notes
debug (cargo test)31 ms36 msDebian 12, i7‑1185G7, governor performance
release (cargo bench)24 ms28 msSame 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-start CI 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 tagged v1.0.0 per 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 create v0.x.y+1.
  • Annotate every tag (git tag -a v0.x.y -m "Runloop v0.x.y") so git describe remains 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 by feat, 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 in SECURITY.md when capabilities or signing changes are included.
  • Before tagging, confirm CI is green on main and 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, and agtop binaries to the GitHub release along with SHA256 sums.
  • Debian packages: Build via packaging/systemd/ using just deb. Verify install on Debian 13/trixie (x86_64 and arm64), ensure runloopd.service starts, and exercise an apt upgrade from 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

  1. cargo fmt --all, cargo clippy --workspace -- -D warnings, cargo test --workspace
  2. cargo build --workspace --release
  3. Performance harness: cargo test -p runloop-runtime cold_start_p50_under_40ms and cargo bench -p runloop-runtime cold_start on the baseline hardware.
  4. Security review: confirm signed agent bundles install and that capability denials emit KB audit records.
  5. Docs: mdbook build docs and update CHANGELOG.md, docs/index.md, any relevant specs (policy, openings, perf budgets) to reflect the release.
  6. 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_spawn invocation. When exceeded, the child is sent a kill signal and the hostcall returns EXEC_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.json schema used by rlp agent scaffold and 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.json entries).
  • Emits bundle files in agents/<name>/: manifest.toml (placeholder entry_wasm digest), policy.caps (based on responses), tools.json (v1), README, and bin/.gitkeep.
  • Drops a companion crate in crates/agents-wasm/<name>/ with a stub main.rs that signals readiness and prints placeholder JSON.
  • Generates a starter opening YAML (under examples/openings/) wired to the new agent with a single node and exists(node.out) success condition.
  • Digests are computed for tools.json; entry_wasm stays zeroed until just build-agents-wasm rebuilds the .wasm and updates the manifest.

Flags:

  • --root <path> overrides the bundle root (defaults to the first agents.search_dirs entry from config).
  • --crates-dir <path> overrides the wasm crate root (defaults to crates/agents-wasm).
  • --opening-path <path> writes the starter opening to a custom location (defaults to examples/openings/<name>.yaml when generated).
  • --model, --cap-fs, --cap-net, --cap-kb-read, --cap-kb-write seed wizard values or bypass prompts when --non-interactive.
  • --model-secret overrides the provider secret id in non-interactive mode.
  • --force allows scaffolding into existing paths (overwrites files).

Global CLI overrides (local runs only):

  • --agents-dir prepends a search directory for agent bundles (ahead of agents.search_dirs from config). For daemon runs, configure the daemon with the same search dir instead.
  • --openings-dir prepends a search directory for openings (ahead of openings.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>" (or rlp route --stdin) prints a stable JSON payload { "version": 1, "route": "shell|agent", "rule": "...", "blocked": bool }. It exits 10 for shell decisions, 11 for agent decisions, and 12 on a timeout (--timeout-ms or RUNLOOP_ROUTER_TIMEOUT_MS, default 200 ms) so shells can branch on $? without parsing stdout.
  • rlp why "<prompt>" --json|--table shows the matched rule, allow/deny hits, and any features (POSIX tokens, known commands, heuristics).
  • Configuration lives under router.* in config.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-run to preview). The helper canonicalizes snippet and opening paths before writing, so relative inputs like --snippet packaging/shell/runloop.zsh remain valid once you start new shell sessions elsewhere (the block stores an absolute path).

  • --opening <path> injects an export 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.yaml if 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 --opening later.

  • 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-path to override; --shell auto resolves 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, enable replaces it in-place (with a timestamped backup).

  • If ~/.runloop/openings/ is missing, enable creates it (0755) and copies the packaged router-default.yaml unless creation fails, in which case it warns and continues without setting the env.

  • rlp shell disable removes 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:

  1. Guardrails: only runs in interactive shells, skips when $TERM=dumb, when RUNLOOP_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 when rlp is missing from $PATH. Override with RUNLOOP_ROUTER_FORCE=1 or toggle mid-session via runloop_router_on/off.
  2. On Enter it calls rlp route --stdin, ignoring stdout and branching on the exit code:
    • 10 → shell fast-path: call zle .accept-line so the command executes normally.
    • 11 → agent path: convert the buffer into JSON ({"prompt":"..."}), call rlp run <opening.yaml> --params '<json>', clear the buffer, and redraw the prompt. The default opening path resolves from RUNLOOP_ROUTER_OPENING_PATH (injected by the helper) or the fallback ~/.runloop/openings/router-default.yaml if it exists.
    • 12 → router timeout: shows runloop: router timeout; executing in shell and falls back to the shell.
    • other exit codes → warn via zle -M and fall back to the shell.
  3. Helper functions:
    • runloop_router_off / runloop_router_on export or unset RUNLOOP_ROUTER_DISABLE mid-session.
    • _runloop_router_prompt_json handles basic escaping (quotes, backslashes, newlines) to avoid external dependencies.
  4. Key binding: defaults to ^M (Enter). Override with RUNLOOP_ROUTER_BINDKEY='^J' (set before sourcing) to avoid conflicts with oh-my-zsh/Prezto keymaps; the widget binds in the main, viins, and vicmd maps.

Requirements & tips

  • Ensure rlp run can reach runloopd (or pass --local via 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 zsh to start a shell with routing disabled; run runloop_router_on later 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, when TERM=dumb or PS1 is 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 with RUNLOOP_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_MS through to rlp route and treats exit 12 as a timeout (warns, then executes the line via the shell).
  • Execution records history using the same HISTCONTROL / HISTIGNORE semantics 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: $?, &&/||, and set -e behave as if the user executed the command or opening manually.
  • runloop_router_off / runloop_router_on mirror the zsh helpers; key binding defaults to Enter (^M) plus ^J unless RUNLOOP_ROUTER_BINDKEY overrides 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 route exits non-zero (e.g., misconfigured config), the line executes as a shell command after printing the error message.

5. Runtime toggles & troubleshooting

Toggle / CommandEffect
RUNLOOP_ROUTER_DISABLE=1Globally disable routing (shells fall back to normal behavior).
RUNLOOP_ROUTER_FORCE=1Override CI/SSH auto-disable and force routing on.
runloop_router_off/onConvenience helpers provided by both snippets.
RUNLOOP_ROUTER_OPENING_PATHAbsolute path to the YAML opening to invoke for agent routes.
RUNLOOP_ROUTER_OPENING_PATH_DEFAULTOptional fallback path (defaults to ~/.runloop/openings/router-default.yaml).
RUNLOOP_ROUTER_TIMEOUT_MSTimeout for rlp route (default 200); exit code 12 on expiry.
RUNLOOP_ROUTER_BINDKEYKey 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> to rlp shell enable or copy the repo snippets to ~/.runloop/shell/.
  • set RUNLOOP_ROUTER_OPENING_PATH warning – ensure the referenced YAML exists; the helper does not create it for you.
  • Daemon unreachablerlp run will print daemon ... unreachable; start runloopd or rerun with rlp run --local inside 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.md references this document in the CLI/TUI section.
  • docs/architecture.md replaces the “forthcoming router section” with a link here.
  • docs/SUMMARY.md lists 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 disable to 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 (currently 1).
  • 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 as mail.smtp_send. Payload frames reference this name.
  • description (string, required) – short human-readable summary.
  • transport.kind (enum, required) – either exec or http. Additional transport fields depend on the selected kind:
    • exec requires transport.command (absolute path) and optionally transport.args (extra CLI args appended after the JSON blob path).
    • http requires transport.method (GET, POST, etc.), transport.url (absolute URL, templating allowed), and optional transport.headers (static header map; values may include secret placeholders).
  • input_schema (JSON Schema or $ref, required) – describes the CT_TOOL_CALL payload so validation can happen before execution.
  • result_schema (JSON Schema or $ref, required) – shape of the CT_TOOL_RESULT payload 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 example net: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 to false. When set, tool invocations inherit confirm_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 configured SecretResolver and injected into environment variables or HTTP headers depending on the transport. Failures emit cap_denied audit events.
  • Confirmation gate. When requires_confirmation is true, invocations behave like other external actions: the CLI/TUI prompts the operator unless security.confirm_external_actions is disabled.
  • Tracing + replay. Tool calls emit CT_TOOL_CALL / CT_TOOL_RESULT frames with the declared id and schema references. Payloads are persisted in the KB and included in run traces so rlp replay can rehydrate the tool outputs deterministically (or flag missing tools).

4. Wizard & authoring guidance

  • rlp agent scaffold should generate an empty tools.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_schema at existing IDs (see docs/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 id values 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 agtop and 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:

  1. Log – streaming log lines (from unified CT_RUN_EVENT stream).
  2. Plan – DAG state (node status, attempts) derived from RunEvent nodes in the unified stream.
  3. agtop Metrics – per-agent CPU/RSS/tokens and bus health aggregated from rlp/sys/metrics + rlp/agents/<agent_id>/metrics.
  4. Trace – ladder view of crossings (from trace events 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: user vs system (from config).
  • Opening: name + trace_id.
  • Pane: active pane short name.
  • Tokens/Health: summarized token burn, daemon pressure, and agents_running gauge.
  • Confirm Status: shows CONFIRM REQUIRED when a pending action.proposal is 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; Esc rejects (mirrors CLI confirmation semantics).

Data Plane & Topics

  • Run submission: CLI publishes ControlRequest::RunSubmit on rlp/ctrl. The daemon responds with ControlResponse::RunAccepted and begins emitting CT_RUN_EVENT records on rlp/runs/<trace_id>/events.
  • Unified stream: rlp/runs/<trace_id>/events carries {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 surfaces agents_running and bus_queue_depth when present. Subscribe to per-agent metrics with --monitor-agents until auto-discovery is added. Frames conform to CT_METRICS_SNAPSHOT v1: {"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 kind ui|tui may send action.decision. For daemon-backed runs the CLI defers to the TUI (no inline prompt); agtop surfaces dialogs and publishes the decision.
  • Ladder (trace pane): rlp trace <id> and the TUI consume ladder hops emitted on the run stream and persisted in run.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-drops opt 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 --json flag (shared between CLI & TUI for scripts/tests).
  • Error surfacing: desyncs (e.g., missing metrics) render N/A with 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:

  1. Engineering Standards — Technical standards, architectural guidelines, code size limits, and decision-making framework. This is the authoritative reference for how we write code.

  2. CONTRIBUTING.md — Process, workflow, commit style, and DCO requirements.

  3. This guide — Day-to-day expectations and communication norms.

Picking an Issue

  • Start with the GitHub issue tracker and filter by good-first-issue or 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 --all output and cargo clippy --workspace -- -D warnings. CI enforces both gates before merge.
  • Naming: crates/files use snake_case, public types use UpperCamelCase, and constants use SCREAMING_SNAKE_CASE. Avoid unsafe blocks 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.rs with #[cfg(test)]) and cross-crate behavior belongs in tests/ 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 using cargo 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/, and AGENTS.md when you add new capabilities, commands, or bundles.

Review Process

  1. Create a short-lived branch from main (feat/<slug>, fix/<slug>, docs/<slug>, or chore/<slug>). Keep commits focused and use Conventional Commit prefixes; include Signed-off-by trailers for DCO 1.1 compliance.
  2. Run cargo fmt --all, cargo clippy --workspace -- -D warnings, and cargo test --workspace locally. For docs-only changes, run just docs-book or mdbook build docs to catch link issues.
  3. Open a PR that explains the motivation, links the relevant issue/roadmap item, and lists manual verification steps (screenshots for rlp/agtop output are highly encouraged).
  4. Request review from the CODEOWNERS responsible for the touched paths. At least one CODEOWNER approval plus a green CI run are mandatory before merge.
  5. 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.md rather 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 unsafe blocks 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:

  1. What options were considered?
  2. How does chosen option rank on: Reliability > Security > Debuggability > Maintainability > Performance?
  3. 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)

  1. Terminal‑first. The TUI/CLI is the cockpit; everything is inspectable.
  2. Local‑first, cloud‑optional. Works offline; sync/enrich when online.
  3. Small pieces, typed loosely. Agents interoperate over a minimal typed bus (RMP).
  4. Least privilege. Capabilities by default; human confirmation for external side effects.
  5. Deterministic replay. All openings are replayable with provenance and budgets.
  6. 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 in docs/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

  • runloopd skeleton: 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

  • rlp commands: 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 via kb.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_email opening (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

  • agtop pane: 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/install works 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_setuprlp agent build/installrlp run succeeds 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 runloop brings up runloopd.service hardened; 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

MetricTarget (p50 unless noted)Measured at
Agent cold start40 msperf lab harness
Agent RSS8 MBperf lab harness
Bus throughput1000 msgs/sperf lab harness
Replay determinism99% identical outputs on fixed seedstrace replayer
Crash‑free stability24 h soak, zero critical crashesBeta gate
Human‑confirm coverage100% on send/delete/spendsafety 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_id primitive selector, body_len, created_at_ms, ttl_ms, trace_id, msg_id. TTL enforcement computes created_at_ms + ttl_ms in 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 via action.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 at 0005.

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

  1. Draft the ADR in a PR (or convert a design issue into a PR once you have a preferred direction).
  2. Request review from the stakeholders listed in the relevant roadmap phase (runtime, KB, policy, etc.).
  3. Once approved, merge the ADR and immediately link it from the relevant docs (README.md, docs/architecture.md, protocol specs, etc.).
  4. If an ADR becomes obsolete, create a follow-up ADR marked “Supersedes OLD_ADR_ID” and update the old file’s status to Superseded with a backlink.

Storage & Cross-References

  • Keep ADRs under version control; do not edit them retroactively after they are marked Accepted except 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.