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

Truss Documentation

Truss is a Rust-native, provider-agnostic agentic harness for durable, verifiable runs. The docs site collects the architecture docs, configuration surface, worked examples, and decision records into one searchable book.

Start with:

The generated site is built from the Markdown already in this repository. Design docs and ADRs remain the source of truth; example pages include the live files from examples/ so code snippets do not drift.

Truss engineering tour

This is a source-oriented map of workspace 0.58.0, reviewed against commit 5e26aacb9123b9e25b06a111ad781f413e846dec on 2026-10-12. Unlike the historical roadmap, the links below point to implemented code and its test coverage.

1. Contract acceptance is separate from execution

contract.rs models intent, acceptance clauses, budget, and constraints. delegation.rs coordinates execution, harness-run shell checks, and the model-review panel.

Read contract_fails_when_a_check_fails in durable_test.rs. A fake executor writes the wrong content and completes; the check still yields a failed verdict. The majority-review tests separately exercise panel aggregation. These are useful regressions, not a demonstration that an LLM reviewer reliably catches every bug.

2. The journal is a recovery mechanism and an audit record

Explore durable.rs and the suspend/resume, recorded-step replay, tamper, and signature tests in durable_test.rs.

The architectural choice is to reconstruct from persisted outcomes, rather than ask the model to infer what happened before a restart. Reusing a recorded outcome is distinct from guaranteeing exactly-once effects in an external service. A crash between an effect and its journal commit still requires idempotency or reconciliation.

3. Cross-cutting controls use one context

ctx.rs carries controls through model and tool calls. agent.rs, policy.rs, and budget.rs show how the loop consumes them. This keeps new controls from requiring an unrelated change to every integration.

4. The isolation boundary is explicit

sandbox.rs defaults to NoSandbox and also provides a working-copy mode. Neither is a claim of kernel or network isolation. sandbox_test.rs tests the implemented boundary. A fresh working copy can make validation more repeatable without making arbitrary shell commands safe to run on a sensitive host.

5. Integrations stay inspectable

Reproduce the short tour

cargo test --locked -p truss-core --test durable_test

This uses fake providers and temporary state, not a model subscription or API key. It checks runtime behavior, not live-provider quality. See the root README for build steps and the broader CI commands.

Verification recorded on 2026-10-12

The command above completed locally on Linux: 18 passed, 0 failed, 0 ignored. It used controlled providers and temporary state. No paid model call was made. This is a dated result for the referenced source revision, not a continuously updated CI badge or a statement that the entire workspace was requalified.

Truss — Architecture & How It Differs

Thesis: generation isn’t the bottleneck anymore — trust is. Truss is a durable, verifiable agent runtime. You delegate work as a contract and review a signed verdict backed by independent evidence — not a transcript.

This document is the map. Each diagram describes how a part of the harness is intended to work; the implementation is held to match it.


1. The big picture — contract in, signed verdict out

Everything else in Truss exists to make this one path trustworthy.

flowchart TD
    H[Human] -->|"contract.toml<br/>(intent + definition-of-done<br/>+ budget + constraints)"| RC[run_contract]

    subgraph EXEC["1 · Execute (durable, signed)"]
        RC --> EX[Executor agent<br/>does the work]
        EX <-->|tool calls| TOOLS[Tools<br/>read/write/bash]
        EX -->|every step| J[(Journal<br/>hash-chained + ed25519)]
    end

    subgraph VERIFY["2 · Verify (independent evidence)"]
        J --> CK["check clauses<br/>harness runs shell cmds<br/>executor never controlled"]
        J --> RV["review clauses<br/>N-reviewer adversarial panel"]
    end

    CK --> VD{{Verdict<br/>PASS / FAIL / UNVERIFIABLE}}
    RV --> VD
    VD -->|signed journal entry| J
    J -->|head hash = portable<br/>evidence bundle| ANCHOR[(Transparency log<br/>only hashes leave the box)]

    VD -->|"proof, not transcript"| H

    style VD fill:#1f6feb,color:#fff
    style J fill:#2d2d2d,color:#fff
    style ANCHOR fill:#2d2d2d,color:#fff

The invariant: the executor never grades its own homework. Checks run in an environment it didn’t control; reviewers are separate model/contexts prompted to refute. The human sees a verdict + evidence, not a wall of tool calls.


2. The harness loop — one Ctx threads every concern

A provider API is stateless: one request in, one response out. The harness is the loop around it. Truss threads a single Ctx handle into every model and tool call, so cancellation, budget, policy, and audit all flow through one place — adding a cross-cutting concern later puts a field on Ctx, it never re-breaks signatures.

flowchart LR
    P[Prompt / contract] --> L{{Agent loop}}
    L -->|"history + request"| PROV[Provider.complete<br/>or complete_stream]
    PROV -->|text + tool calls| L
    L -->|"tool call?"| T[ToolRegistry.call]
    T -->|result appended| L
    L -->|"done?"| OUT[Final text]

    CTX[["Ctx (one handle)"]]
    CTX -.cancel.-> PROV
    CTX -.budget meter.-> PROV
    CTX -.policy / scopes.-> T
    CTX -.audit events.-> L

    style CTX fill:#1f6feb,color:#fff
    style L fill:#2d2d2d,color:#fff
  • Cancellation — Ctrl-C aborts the in-flight model call and any running child process; the partial event log is flushed.
  • Budget — hard ceilings on USD / tokens / steps, checked before each call.
  • Policy — write-scope, read-scope, network mode, tool allowlist, forbidden substrings — enforced at the tool boundary, not by trusting the model.

3. Durable execution — suspend, crash, resume, replay

A contract run is event-sourced. Each step is a journal entry; the agent state is rebuilt by replaying entries (positional replay). This is what lets a run survive a crash, suspend on an external await (timer / signal), and resume exactly where it left off — without re-doing or double-charging completed work.

stateDiagram-v2
    [*] --> Running
    Running --> Running: step → append journal entry<br/>(replayable)
    Running --> Suspended: await timer / signal
    Suspended --> Running: signal delivered /<br/>timer elapsed (daemon resumes)
    Running --> Crashed: process dies
    Crashed --> Running: restart → replay journal<br/>to last good position
    Running --> Completed: final text
    Completed --> [*]

    note right of Suspended
        State lives in SQLite, not RAM.
        Resume is positional replay:
        completed steps return cached
        results instantly; only the
        first unfinished step re-runs.
    end note

Why it matters: most harnesses keep the loop in memory — kill the process and the run is gone. Truss treats a run like a database transaction log. The same mechanism powers human-in-the-loop pauses and long-horizon (hours/days) work.


4. The verifiable ledger — hash chain → sign → anchor

Every journal entry is content-addressed and chained to its predecessor, then the head is signed. Tampering with any past entry breaks the chain. Periodically the head hash is anchored to an external append-only transparency log — so even the machine that produced the run can’t silently rewrite history.

flowchart LR
    E0["entry₀<br/>prev: ∅"] --> E1["entry₁<br/>prev: H₀"]
    E1 --> E2["entry₂<br/>prev: H₁"]
    E2 --> EH["head<br/>prev: H₂"]
    EH -->|ed25519 sign| SIG["signature<br/>(identity key)"]
    EH -->|salted hash only| ANCH[(Transparency log<br/>append-only, chained)]

    subgraph BOX["inside the box (private — NDA-safe)"]
        E0
        E1
        E2
        EH
        SIG
    end

    style EH fill:#1f6feb,color:#fff
    style ANCH fill:#2d2d2d,color:#fff
    style BOX fill:#0d1117,color:#fff,stroke:#30363d

Privacy invariant: only salted hashes leave the box. Evidence content (which may be NDA / client work) is never put on-chain or in the anchor log. Anchoring proves when a head existed, not what it contained. --verify re-walks the chain and checks the signature; --verify-anchor-log checks the anchor log’s own chain.


5. N-reviewer adversarial verification

A single reviewer is a single point of failure — a lenient model waves bad work through. Truss convenes a panel of N independent reviewers for each review clause. They run in parallel, each assigned a different lens, each prompted to refute. A strict majority must say “met” or the clause fails. Unverifiable votes count as not-met (default-to-fail). All-unverifiable → the clause is Unverifiable.

flowchart TD
    WORK[Executor summary<br/>+ reviewer_context files] --> PANEL{{Reviewer panel<br/>run in parallel}}

    PANEL --> R1["Reviewer 1 · lens:<br/>does it satisfy intent?"]
    PANEL --> R2["Reviewer 2 · lens:<br/>completeness — anything missing?"]
    PANEL --> R3["Reviewer 3 · lens:<br/>correctness — subtle errors?"]
    PANEL --> RN["Reviewer N · lens:<br/>shortcuts — corners cut?"]

    R1 --> V{Strict majority<br/>met · 2 &gt; N?}
    R2 --> V
    R3 --> V
    RN --> V

    V -->|yes| PASS[✓ Pass]
    V -->|no| FAIL[✗ Fail]
    V -->|all unverifiable| UNV[? Unverifiable]

    PASS --> EV["evidence:<br/>'panel: K/N reviewers say met'<br/>+ each seat's reasoning"]
    FAIL --> EV
    UNV --> EV

    style PANEL fill:#1f6feb,color:#fff
    style V fill:#2d2d2d,color:#fff

Lenses are assigned round-robin, so even a panel of one model gets perspective diversity. Set the size with reviewers = N in the contract or --reviewers N. (Diverse models per seat is a planned enhancement — the panel is already a Vec<Arc<dyn Provider>>.)


6. How Truss differs from other harnesses

Most harnesses optimize generation — get the model to produce the artifact. Truss optimizes trust — let you accept the artifact without re-reading the work.

flowchart LR
    subgraph TYP["Typical harness (Claude CLI / OpenClaw / Hermes)"]
        direction TB
        TP[Prompt] --> TL[Agent loop<br/>in memory] --> TT[Transcript]
        TT --> TH[Human reads<br/>the whole transcript<br/>to decide trust]
    end

    subgraph TRUSS["Truss"]
        direction TB
        CP[Contract] --> CL[Durable, signed loop] --> CE[Independent evidence<br/>+ N-reviewer panel]
        CE --> CV[Signed verdict<br/>+ evidence bundle]
        CV --> CH[Human reviews<br/>the proof]
    end

    style TH fill:#6e2d2d,color:#fff
    style CH fill:#1f6feb,color:#fff
    style CV fill:#1f6feb,color:#fff
DimensionTypical harnessTruss
Unit of workpromptcontract (intent + checkable done + budget + constraints)
What you reviewtranscriptsigned verdict + independent evidence
Who verifiesthe executor (self-reported “done”)harness-run checks + N-reviewer adversarial panel
Run statein-memory; dies with the processdurable journal (SQLite); suspend / crash / resume
Auditlogs you trusthash-chained, ed25519-signed ledger; externally anchored
Privacyn/aonly salted hashes leave the box (NDA-safe)
Provider lock-inusually one vendorProvider trait; core is vendor-free, providers are plugins

7. Crate layout — small, vendor-free core

The moat is in the core; vendors are swappable plugins. The core has no vendor code and no reqwest — it depends only on the Provider trait.

flowchart TD
    CLI[truss-cli<br/>flags, config, render] --> CORE
    PROV[truss-providers<br/>anthropic · openai · local<br/>SSE streaming, auth] -->|implements<br/>Provider trait| CORE

    subgraph CORE["truss-core (vendor-free)"]
        AG[agent loop]
        DUR[durable execution]
        DEL[delegation / contracts]
        LED[ledger: store · identity · anchor]
        CTX2[Ctx · policy · budget]
        PT[[Provider trait]]
    end

    style CORE fill:#0d1117,color:#fff,stroke:#1f6feb
    style PT fill:#1f6feb,color:#fff

See adr/0001-plugin-architecture.md for why the core stays small and how providers plug in, and auth-strategy.md for the provider-agnostic auth model (API key / OAuth subscription / CLI subprocess).

Truss Build Plan

Status: historical implementation plan. Created 2026-06-20 from the parity-via-plugins discussion.

This is the build sequence for turning Truss from a strong verified-agent core into a tool a Claude Code / Codex / OpenClaw user can actually switch to without losing daily-driver capability.

This file records the sequencing rationale: what to build, why that order, and how each feature expands or deliberately does not expand the verification/crypto and pause/resume substrates.

1. Product Thesis

Truss wins by letting users review proofs, not transcripts. That means the core must stay small and load-bearing:

  • contracts with definition-of-done, budget, and constraints,
  • independently produced evidence,
  • N-reviewer adversarial verification,
  • diff-aware review,
  • hash-chained ed25519 signed ledger,
  • durable journal-authoritative replay,
  • governed memory and verified self-improvement,
  • local-first execution with routing/escalation.

Parity with other tools comes through plugins and extension seams, but those plugins must inherit the moat where it belongs. A plugin that mutates state, calls a tool, fetches data, produces an artifact, spends budget, or affects a verdict should route through policy, budget, provenance, and durable replay. A plugin that only renders a screen or watches an event stream should stay light.

2. Design Gate For Every Feature

Every feature, milestone, and plugin must answer these three questions in its spec or PR notes:

  1. Verification / crypto: does this produce a claim, artifact, state change, tool result, or external observation that should be signed into the ledger, hashed, anchored, or reviewed by the panel?
  2. Pause / resume: does this do I/O, spend money, spawn work, call a model, run a tool, or create a side effect that must execute once and replay from the journal on resume?
  3. Honest no: if the answer is no, why is that correct? Presentation-only token streaming, transient UI layout, and local read-only diagnostics are legitimate no’s.

This gate prevents the bad outcome: a differentiated verified core surrounded by a pile of unverified, unresumable parity plugins.

3. Current State

Shipped foundation:

  • durable runtime, signals, daemon, replay,
  • signed ledger, anchoring, tamper/property tests,
  • contracts and N-reviewer verification,
  • diff-aware review,
  • child-run, spawn, fan-out, panel unification,
  • governed memory, verified lessons, paired eval,
  • compaction controller and effective-context gauge,
  • sandbox substrate SB-1,
  • ledger read tools LR-1 through LR-3,
  • mdBook docs site,
  • MCP architecture spec.

Still missing for daily-driver parity:

  • MCP client tool tier,
  • plugin authoring docs and examples,
  • Verified Playbooks as the on-moat skills replacement,
  • verdict-review surface / interactive TUI,
  • web/search/fetch and native git/GitHub integration,
  • packaging/install,
  • stronger sandbox isolation,
  • apples-to-apples benchmark number.

4. Build Sequence

Phase 1 - MCP Client Tier

Why first: MCP unlocks the largest parity gap in one move. Claude Code and OpenClaw feel powerful because their tool ecosystem is broad. Truss should inherit that ecosystem without losing policy, budget, provenance, and durable replay.

Scope:

  • [[mcp.servers]] in layered config and contract/agent config.
  • Stdio transport first; HTTP transport next.
  • Connect/list/probe.
  • Mount advertised MCP tools into the existing tool registry with namespaced IDs.
  • Enforce contract-scoped allowlists, policy, budget, deadline, and cancellation.
  • Record signed mcp_tool_call ledger entries with arg/result hashes and redaction.
  • Memoize each call as a durable activity; resume replays recorded result.
  • Async/long tools suspend and resume via a handle/signal pattern.

Lens:

  • Verification / crypto: yes. Third-party tool calls become signed provenance.
  • Pause / resume: yes. Tool calls are durable activities, never re-run on resume.
  • Honest no: streaming partial tool output is presentation-only; record the completed result.

Milestones:

  • MCP-1: stdio fake-server tests, list/mount/invoke, signed ledger entry.
  • MCP-2: durable memoized replay.
  • MCP-3: HTTP transport and auth redaction.
  • MCP-4: async suspend/resume. Implemented via structured MCP pending results that suspend durable runs on a signal and memoize the completed signal payload.
  • MCP-5: CLI surface (truss mcp list|probe) and docs examples. Implemented with human and JSON diagnostics for configured MCP servers.

Done when: a contract can use a scoped external MCP tool, the ledger proves what was called and returned, and resume does not call the external server twice.

Phase 2 - Plugin Authoring Docs And Examples

Why second: once MCP is real, Truss needs a clean story for extension authors. The docs should make the default path the safe path: plugins opt into the moat because the extension seams make that easier than bypassing them.

Scope:

  • Website/mdBook “Write a plugin” section.
  • Runnable examples for:
    • Provider plugin,
    • in-process Tool,
    • MCP server tool,
    • ContextProvider,
    • Sandbox,
    • Anchor backend,
    • event/ledger stream consumer.
  • A per-example “verification + durability hook” checklist.
  • A “when not to journal” section for presentation-only plugins.

Lens:

  • Verification / crypto: docs must teach the hook points. Examples that affect work outcomes should emit events and ledger entries.
  • Pause / resume: docs must distinguish durable activities from observers.
  • Honest no: UI widgets and passive event consumers should not be forced into the durable journal.

Done when: a developer can add a new tool/provider/context plugin from docs without reading Truss internals, and the examples demonstrate provenance rather than just function calls.

Phase 3 - Verified Playbooks

Why third: this is the on-moat replacement for skills/slash commands. Copying Anthropic/OpenClaw skills as markdown blobs would be table stakes. Truss’s version is governed, verified, reversible competence.

Scope:

  • Playbook memory entry kind with schema: purpose, trigger, prerequisites, procedure, checks, examples, known failures.
  • Import path from markdown/SKILL.md into unverified playbook drafts.
  • Promotion path through the N-reviewer panel and paired eval where possible.
  • Recall into L2 context by task match and trust tier.
  • Revocation/demotion on regression.
  • CLI: truss playbook import|list|show|promote|revoke.

Lens:

  • Verification / crypto: yes. Playbook promotion is a signed trust-tier transition with reviewer evidence and optional eval evidence.
  • Pause / resume: mostly no for authoring; yes for promotion/eval. Importing a file is a local transform. Promotion and eval are durable contract-like work.
  • Honest no: recall is context projection over existing memory; don’t invent a parallel journal path.

Done when: a successful run can become a verified playbook, a future run can recall it, and regression can demote it with provenance.

Phase 4 - Verdict-Review Surface / Interactive TUI

Why fourth: Truss’s moat is only useful if humans can review evidence quickly. The surface should optimize for verdicts, proofs, diffs, ledger heads, and approval actions, not chat-first transcript reading.

Scope:

  • Event/ledger read service or CLI-backed stream for ClawTerm/Warp.
  • Screens for active contracts, verdict bundle, evidence clauses, reviewer panel, diff summary, ledger chain/head hash, budget, and resume/signal controls.
  • Approval gates for high-risk tools or spending thresholds. Implemented in VR-4 / ADR 0037 as opt-in global→agent→contract policy that suspends durable runs and releases only on signed approved human_review.
  • Transcript available as drill-down, not the default.

Lens:

  • Verification / crypto: partial. The UI consumes signed evidence and should display chain/head status, but the UI itself is not the provenance substrate.
  • Pause / resume: underlying runs yes; UI state no. The run is durable. Window layout, selected tab, and scroll position are presentation.
  • Honest no: don’t over-engineer UI state into the ledger.

Done when: Matt can approve or reject a real Truss task from the verdict view without reading the full transcript.

Phase 5 - Native Tool Parity Pack

Why fifth: after MCP and verdict review, fill the obvious daily-driver holes that users expect from Claude Code/Codex/OpenClaw.

Scope:

  • Web search/fetch tool.
  • Native git and GitHub tools for status, diff, branch, commit, PR, issue, checks.
  • Image/file artifact input.
  • Hook/lifecycle observers.
  • Optional delegated adapters for Slack/cron/OpenClaw surfaces where they support verified work rather than replacing OpenClaw.

Lens:

  • Verification / crypto: yes for fetched content, git artifacts, PRs, and claims. Hash fetched content and diffs; ledger tool outputs used by verdicts.
  • Pause / resume: yes for network and git operations that produce side effects. Replay recorded results or require explicit revalidation.
  • Honest no: multi-channel chat delivery is mostly orchestration around Truss, not the verified-work core. Keep it as adapters/plugins or leave it to OpenClaw.

Done when: a normal coding/research task can run in Truss without reaching for bash as the only integration layer.

Phase 6 - Packaging And Install

Why sixth: table stakes, but a real adoption blocker. A user cannot evaluate Truss if the install story is “clone and build the private repo.”

Scope:

  • cargo install path for public/open crate names that don’t collide.
  • Release workflow for Linux/macOS binaries.
  • Versioned docs matching release tags.
  • First-run sample config and example contracts.
  • Optional shell completions.

Lens:

  • Verification / crypto: no. Distribution does not affect work evidence.
  • Pause / resume: no. Install is outside the runtime.
  • Honest no: keep this simple; don’t pretend packaging is moat.

Done when: a new user can install truss, run a sample verified contract, and inspect the ledger without building from source.

Phase 7 - MCP Server Tier

Why after client: useful but secondary. Truss can become a verification backend for other harnesses, but the headline product should remain standalone verified work, not “remote shell over MCP.”

Scope:

  • truss serve-mcp.
  • truss_run_contract tool returning verdict/head hash/bundle reference.
  • truss_contract_status for async handles.
  • Ledger read tools exposed over MCP.
  • Per-caller Principal and auth for HTTP mode.

Lens:

  • Verification / crypto: yes. The server exposes the signed-verdict substrate.
  • Pause / resume: yes. Long jobs return handles and reconnect to durable sessions.
  • Honest no: do not expose raw internal read/write/bash tools; the unit of work is a contract, not an unverified shell.

Done when: Claude Code/Codex/OpenClaw can delegate a bounded contract to Truss and receive a signed verdict without Truss becoming a generic tool server.

Phase 8 - Sandbox Hardening

Why here: SB-1 makes self-validation real, but buyer diligence and multi-tenant operation need actual isolation.

Scope:

  • SB-2 namespace/bwrap sandbox.
  • SB-3 Docker/Podman sandbox.
  • SB-4 per-tenant isolation, seccomp, egress control.
  • Attestation fields in sandbox evidence.

Lens:

  • Verification / crypto: yes. Stronger sandboxing makes evidence more credible.
  • Pause / resume: yes where sandbox lifecycle is part of a run.
  • Honest no: local one-shot diagnostics can remain outside heavy isolation if they don’t affect a verdict.

Done when: acceptance/self-check evidence can say not only “this passed” but “this passed inside a confined, reproducible environment.”

Phase 9 - Benchmarks And Buyer Proof

Why: buyers need honest numbers. Truss should not claim “better than Claude Code” without matched tasks and baselines.

Scope:

  • Stratified SWE-bench style run.
  • Matched baseline: native Claude Code/Codex vs Truss in-loop with same model where possible.
  • Report pass rate, cost, wall-clock, verification catches, and failure taxonomy.
  • Keep failed/ugly results; do not only publish wins.

Lens:

  • Verification / crypto: yes. Benchmark reports should include signed eval provenance and official grader outputs.
  • Pause / resume: yes. Long evals are durable jobs with resumable state.
  • Honest no: marketing summaries are downstream projections; the signed eval report is the source of truth.

Done when: the website can make a narrow, defensible claim with receipts.

5. Parallel Work Policy

These can run in parallel if we keep ownership clean:

  • MCP implementation and plugin docs can overlap after MCP-1 shape is stable.
  • Verified Playbooks can start once the memory/playbook schema is agreed.
  • UI can start against ledger/event read APIs before every backend feature lands.
  • Packaging can proceed any time after public crate naming is resolved.
  • Sandbox hardening should not block MCP unless MCP tool execution needs stronger isolation for a specific demo.

Avoid parallelizing two branches that both mutate the core event vocabulary, ledger schema, or config layering without an explicit coordination note.

6. Website Documentation Plan

The website/docs should expose three tracks:

  • Use Truss: install, configure, run a contract, verify a ledger, resume a run.
  • Extend Truss: providers, tools, MCP, context providers, sandboxes, anchors, event consumers.
  • Trust Truss: verification model, ledger model, replay model, threat model, benchmark receipts.

The plugin authoring section needs to be example-heavy. Each page should show:

  • what interface to implement,
  • what config block enables it,
  • what events/ledger entries it emits,
  • what gets replayed on resume,
  • what secrets are redacted,
  • what intentionally stays off-journal.

7. Release Gates

Every milestone should ship with:

  • spec or ADR updated,
  • docs page or example updated,
  • CLI/config reachability if the feature is user-facing,
  • tests for the core invariant,
  • at least one negative test for trust boundaries,
  • cargo fmt --all --check,
  • cargo clippy --all-targets --all-features -- -D warnings,
  • cargo test,
  • cargo test --all-features,
  • mdbook build docs when docs change.

For docs-only changes, git diff --check and mdbook build docs are enough.

8. Immediate Next Moves

  1. Open MCP-1 implementation branch.
  2. Decide truss-mcp crate vs provider module and SDK vs hand-rolled JSON-RPC.
  3. Implement fake stdio MCP server tests first.
  4. Draft the plugin-authoring docs skeleton in parallel after the MCP API surface stops moving.
  5. Track remaining release work in GitHub issues.

9. What Not To Build Yet

  • A broad multi-channel OpenClaw replacement inside Truss.
  • Raw MCP server access to internal shell/file tools.
  • Dynamic Rust .so plugins.
  • A polished SDK before the primitive/event/provenance vocabulary settles.
  • Public docs hosting if it leaks private ADR/GTM material before the website/domain decision.

Durable Execution + Verifiable Evidence Ledger — design

Status: DRAFT for review · 2026-06-14 · target: harness v0.3+

This is the architecture spec for making the harness a durable, verifiable agent runtime — runs that survive process death and multi-day pauses and resume with exact context, on a journal that is simultaneously the execution source-of-truth, the audit trail, and a tamper-evident evidence ledger.

Decisions locked with Matt (2026-06-14):

  • C-grows-into-A staging (hybrid first, full event-sourced replay as the destination — see §3).
  • Embedded SQLite as the durable store (local-first, zero-ops, scales up).
  • Verifiable ledger is native, not a separate layer: the journal is hash-chained and signed per entry (§9). Real blockchain is reserved for the future economy/settlement layer only (§12).

1. Goal & non-goals

Goals

  • A run can crash (or be Ctrl-C’d, or the box can reboot) and resume exactly.
  • A run can deliberately suspend for minutes-to-days awaiting an external event (human approval, long job) with the process fully exited, and resume exactly.
  • “Never lose context” is achieved by replaying recorded truth, not by re-prompting from a reconstructed snapshot (the failure mode of the ADK-blog approach, which re-injects state and lets the model hallucinate the gap).
  • Side effects are not duplicated on resume (no double-charge / double-send).
  • The execution record is tamper-evident and signed — provenance for the trust layer comes from the same substrate that powers durability.

Non-goals (now)

  • No external workflow server (Temporal/cluster) — it would be the opposite of “native.” We implement a minimal embedded durable runtime.
  • No on-chain anything in the core. Anchoring is an optional trait (§11).
  • No infinite context. Durable ≠ unlimited; the model is fed a projection of the journal (§10).

2. The core inversion

Today the event stream (src/events.rs → src/state.rs) is observational: the loop runs and emits events as a side effect. Durable execution inverts this: the journal becomes the source of truth for execution, and the agent loop becomes a deterministic reducer over it.

Every non-deterministic / side-effecting step — a model call, a tool call, a clock read, a timer, a random draw — is an activity: it is executed once, its outcome is appended to the journal (durably, fsync’d), and on any later replay the recorded outcome is returned without re-execution. The orchestration between activities is deterministic, so replay reproduces the identical sequence of activity requests and matches them to recorded outcomes. (This is the Temporal/DBOS workflow-vs- activity split, applied to an agent loop and embedded rather than served.)

The model being nondeterministic is fine: we never re-run a model call on replay, we replay its recorded output. Determinism is required only of our orchestration code, not the model or the tools.

3. C-grows-into-A staging

Both stages read the same SQLite schema (§4). The difference is only how the loop rehydrates. No migration between them.

Stage C (hybrid — ships first):

  • Context continuity by snapshot. After each step, persist the full message list
    • structured state. Resume = load the latest snapshot and re-enter the loop. No determinism constraint on orchestration yet.
  • Idempotency by side-effect ledger. Every activity (tool/model call) is recorded with its result. Before executing, check the ledger; if the result exists, return it. This gives no-double-execute even though we’re snapshot-restoring, not replaying.

Stage A (full event-sourced replay — destination):

  • The loop replays the journal positionally from seq 0, returning recorded activity outcomes until the frontier, then runs live. Snapshots demote to replay checkpoints (a performance optimization: replay from the nearest checkpoint instead of seq 0). The side-effect ledger is the journal.

Why C grows into A with no rewrite: the journal is a superset. In C we already write every activity result to it (for idempotency); we simply also keep snapshots and restore from them. To become A, we start replaying the journal we were already writing, and treat snapshots as optional. Same tables, same writes — we flip the read path. The one discipline we adopt from day one to make this free: keep orchestration deterministic (route clock/rand/IO through the durable context even in C — see §8), so the C loop is already replay-faithful when we turn A on.

4. SQLite schema

Embedded via rusqlite (bundled SQLite, no system dependency) behind a small blocking DB actor task (spawn_blocking), or sqlx if we want async-native — to decide at implementation (lean rusqlite for simplicity + zero async coupling).

-- One row per durable run.
CREATE TABLE sessions (
  id            TEXT PRIMARY KEY,          -- session/run id
  status        TEXT NOT NULL,             -- running|suspended|completed|failed
  code_version  TEXT NOT NULL,             -- for replay compatibility (§13)
  model         TEXT,
  tenant        TEXT NOT NULL DEFAULT 'local',
  user          TEXT NOT NULL DEFAULT 'local',
  head_hash     BLOB,                      -- tip of the hash chain (§9)
  pubkey_id     TEXT,                      -- signing identity (§9)
  created_at    INTEGER NOT NULL,
  updated_at    INTEGER NOT NULL
);

-- The authoritative, append-only, hash-chained, signed log.
CREATE TABLE journal (
  session_id    TEXT NOT NULL,
  seq           INTEGER NOT NULL,          -- 0-based, contiguous
  kind          TEXT NOT NULL,             -- see §5
  payload       TEXT NOT NULL,             -- canonical JSON (or blob ref)
  prev_hash     BLOB NOT NULL,             -- entry_hash of seq-1 (or zero)
  entry_hash    BLOB NOT NULL,             -- H(prev_hash || kind || canon(payload) || seq)
  signature     BLOB NOT NULL,            -- Sign(sk, entry_hash)
  created_at    INTEGER NOT NULL,
  PRIMARY KEY (session_id, seq)
);

-- Optional restore/replay checkpoints (C: restore; A: replay shortcut).
CREATE TABLE snapshots (
  session_id    TEXT NOT NULL,
  at_seq        INTEGER NOT NULL,          -- journal seq this snapshot reflects
  messages      TEXT NOT NULL,             -- serialized Vec<Message> (or blob ref)
  state         TEXT NOT NULL,             -- structured key/value state
  PRIMARY KEY (session_id, at_seq)
);

-- Content-addressed blobs: large tool outputs, large payloads, redactable secrets.
CREATE TABLE blobs (
  hash          BLOB PRIMARY KEY,          -- sha256 of bytes
  bytes         BLOB,                      -- NULL after crypto-shred (§11 redaction)
  size          INTEGER NOT NULL,
  created_at    INTEGER NOT NULL
);

-- External wake sources.
CREATE TABLE signals (
  session_id    TEXT NOT NULL,
  name          TEXT NOT NULL,
  payload       TEXT,
  status        TEXT NOT NULL,             -- pending|delivered
  created_at    INTEGER NOT NULL,
  PRIMARY KEY (session_id, name, created_at)
);

CREATE TABLE timers (
  session_id    TEXT NOT NULL,
  seq           INTEGER NOT NULL,          -- journal seq that set the timer
  fire_at       INTEGER NOT NULL,
  status        TEXT NOT NULL,             -- armed|fired|cancelled
  PRIMARY KEY (session_id, seq)
);

The durability boundary is the single SQLite transaction that appends a journal entry (+ updates snapshots/sessions.head_hash). It is atomic and fsync’d. A crash before commit means the activity “didn’t officially happen” and is safely re-executed on resume; a crash after means it’s memoized. External side effects are therefore at-least-once unless the tool passes an idempotency key to the downstream system — documented as the irreducible boundary (§8).

5. Journal entry kinds

Each entry is one completed activity or one control event:

  • run_started — { prompt, system, model }
  • model_result — { request_digest, content[], stop_reason, usage } (the recorded output of one model call)
  • tool_result — { call_id, name, input, content|blob_ref, is_error } (recorded output of one tool call; memoized on replay)
  • state_set — { key, value } (structured state, decoupled from transcript)
  • timer_set — { fire_at } / timer_fired — { }
  • suspended — { reason, await: { signal: name } | { timer: seq } }
  • signal_received — { name, payload }
  • run_completed — { final_text, usage }
  • run_failed — { error }

6. Execution model

resume(session):
  load session row; if absent -> new run
  rehydrate:
     C: load latest snapshot -> messages, state ; cursor = snapshot.at_seq
     A: replay journal from nearest checkpoint -> messages, state ; cursor = frontier
  loop:
     deterministic orchestration decides next activity
     for each activity (model call / tool call / now / sleep / random):
        if journal has a recorded outcome at this logical position -> return it
        else execute -> append (txn: journal entry + snapshot + head_hash) -> return
     on tool/loop signalling SUSPEND:
        write `suspended` entry; set session.status=suspended; return (process may exit)
     on terminal stop -> write run_completed; status=completed

In C, “recorded outcome at this logical position” is keyed by a per-session monotonic activity counter persisted in state (so a re-entered loop that re-issues the same call finds the prior result). In A it’s keyed by journal seq (positional, exact).

7. Suspend / resume / signal

  • Suspend: a tool returns a Suspend { await } outcome (new tool result variant), or the loop hits a durable sleep_until. We append suspended, mark the session, and return cleanly. The process can exit / scale to zero.
  • Resume triggers (harness owns these natively — no OpenClaw):
    • truss resume <session> (CLI) — cold resume on demand.
    • truss signal <session> <name> [payload] — deliver an external signal; appends signal_received, then resumes.
    • Durable timers: an optional long-lived truss daemon holds armed timers and fires them (resuming the owning session). Without the daemon, timers are checked on the next resume. Either way the timer is in SQLite, so nothing is lost if the daemon is down.
  • A local HTTP endpoint (signal/resume) can wrap the same calls for webhook- style delivery — our own surface, not a dependency.

8. Idempotency, side effects & determinism

  • Internal memoization (model/tool/clock/rand): exact, via the journal.
  • External effects (a tool that emails, pays, mutates a remote system): the journal makes them at-least-once. Tools that touch the outside world SHOULD accept and forward an idempotency key (we derive a stable one per activity position) so the downstream system dedupes. Tools declare whether they’re pure / idempotent / effectful in their def; effectful tools without a key get a loud warning in the ledger.
  • Determinism rules for A (adopt now in C so growth is free): orchestration must not read wall-clock, randomness, env, or do ad-hoc IO. Use durable context accessors instead — ctx.now(), ctx.sleep_until(), ctx.random() — each a journaled activity. Model + tools are the only other nondeterminism, and both are journaled.

9. Verifiable evidence ledger (native)

Every journal entry is hash-chained and signed:

entry_hash[0] = H( ZERO || kind || canon(payload) || 0 )
entry_hash[n] = H( entry_hash[n-1] || kind || canon(payload) || n )
signature[n]  = Sign(sk, entry_hash[n])
session.head_hash = entry_hash[last]
  • Hashing: SHA-256 over a canonical (stable-key-ordered) encoding.
  • Signing: ed25519. A harness identity keypair (per install); optionally a per-agent key so multi-agent runs are attributable. pubkey_id recorded on the session.
  • Verify: truss verify <session> recomputes the chain and checks every signature against pubkey_id. Any edit, deletion, or reordering breaks the chain from that point — tamper-evident.
  • What this buys, without a blockchain: tamper-evidence + non-repudiation + provenance. This is the Certificate-Transparency / git model: cryptographic verifiability is a data-structure + signatures property; consensus and tokens are a separate concern we don’t need for single-operator trust.

10. Context projection (durable ≠ infinite)

The journal/messages grow without bound; the model’s context does not. What we feed the model each step is a derived projection over the journal, recomputed as a pure function:

  • Head: system prompt + (later) contract + pinned structured state.
  • Tail: the most recent N turns verbatim.
  • Middle: compacted/summarized older turns (summaries are themselves journaled activities, so they’re durable and replay-faithful).

The full journal stays authoritative; the projection is disposable and recomputable. This is exactly where the Context-Management Engine plugs in — as a projection function, not a bolt-on. ADK-style structured-state injection ({current_step} etc.) is the Head portion done right.

11. External anchoring & redaction

  • Anchoring trait (optional, default off):
    trait Anchor { async fn anchor(&self, session_id: &str, head_hash: &[u8]) -> Result<AnchorReceipt>; }
    • NoopAnchor (default) — local signatures only.
    • TransparencyLogAnchor — publish the head/Merkle root to Sigstore/Rekor (public, checkable timestamp; no private data leaves the box).
    • ChainAnchor (future) — publish the root to an L2 only if a counterparty demands permissionless proof. Reserved; not core. Rule: only ever publish a hash/root, never evidence data. Work may be confidential; anchoring a salted root proves existence-at-time without disclosure.
  • Redaction / crypto-shred: because A replays the journal, entries can’t be deleted. Sensitive payloads are stored by reference in blobs; the journal holds only the blob hash. Deleting the blob bytes (set bytes = NULL) crypto-shreds the data while leaving the hash chain intact (replay degrades gracefully for that one value; chain still verifies). This is how we satisfy “delete the secret” without breaking durability or the ledger.

12. How the trust & economy layers plug in later

  • Verified delegation (the north-star): a contract is the first journal entry after run_started; evidence (acceptance checks, no-stake review) are journaled activities; the verdict is a signed entry. The head_hash over a completed contract IS the portable, tamper-evident evidence bundle. Durable execution and verified delegation share one substrate — no second system.
  • Agent economy / settlement (where real blockchain earns its place): an escrow smart contract can use a session’s anchored head_hash + an accepted verdict as its release condition — an oracle attests the verdict, funds release. The ledger is the bridge; we build none of it now, but the design leaves the door open.

13. Versioning & replay compatibility

  • sessions.code_version stamps the orchestration version that wrote the journal.
  • On resume, if the current binary’s version differs, we either (a) replay under a compatibility shim, or (b) in C, simply restore from snapshot (snapshot restore is version-tolerant because it doesn’t re-derive — another reason C ships first).
  • Activity payloads are versioned JSON; add fields, never repurpose them.
  • A “patching” mechanism (à la Temporal getVersion) is the long-term answer for changing orchestration mid-flight; out of scope for v0.3.

14. Mapping to current code

  • src/ctx.rs — add session_id; add durable accessors now()/sleep_until()/ random() (journaled in A; thin in C). Principal already present.
  • src/state.rs — evolve RunLog (JSONL) into a SQLite-backed Journal + Ledger
    • SnapshotStore. JSONL becomes an export of the journal, not the store.
  • src/agent.rs — wrap model/tool calls through a Durable layer (memoize via journal; per-step transactional append of result + snapshot); recognize a Suspend outcome; rehydrate on resume.
  • src/provider.rs / src/tools — tools gain an effect class (pure|idempotent| effectful) and may return Suspend { await }.
  • New module src/journal.rs (store + hash-chain + signing) and src/anchor.rs (trait + Noop). New deps: rusqlite (bundled), ed25519-dalek, sha2.
  • CLI: resume, signal, verify, and an optional daemon.

15. Milestones

  1. M1 — durable spike (Stage C): SQLite journal + snapshot/restore; a long-running tool that suspends on a timer; process exits; resume <session> continues with exact context; tool memoization proven (no double-exec). Kills or confirms the design cheaply.
  2. M2 — ledger: hash-chain + ed25519 signing per entry; verify <session>.
  3. M3 — signals & timers: external signal suspend/resume; optional daemon for durable timers.
  4. M4 — anchoring + context projection: Anchor trait + transparency-log anchor; Head/Tail/compacted-middle projection hook.
  5. M5 — Stage A: positional journal replay; durable now/sleep/random; snapshots demote to checkpoints.

Then the verified-delegation/contract layer (§12) lands on top of this substrate.

Truss Extensibility & Product Architecture

Status: design / roadmap. Captured 2026-06-17 from the #truss product discussion. Decisions here drive the milestone sequence after v0.16.0.

Product framing

Truss is not an application — it is an embeddable verified-agent engine. The deliverable an enterprise integrates is a library + a streaming service, not a UI. Almost everything a company wants to “plug in” reduces to two things:

  1. Stable extension traits (backends, tools, context sources, routing).
  2. A typed event/provenance stream they can observe and drive a UI from.

The Ctx keystone (cancellation, deadline, shared budget meter, Principal, enforced Policy, events sink) is the spine that lets new cross-cutting concerns attach without changing complete/call signatures. New capabilities ride the Ctx and emit through the existing events + durable ledger.

The three “customs” an enterprise asks for → which seam

  • Custom UI → the events stream + ledger is the UI contract. Today CLI-only. Gap: expose the same typed events (step / tool-call / verdict / ledger-head) over a long-running service (WS/gRPC). Build the stream, not the screen. ClawTerm/Warp is just the first consumer.
  • Custom tools → the tool registry exists but is in-process Rust only. Gap: an out-of-process tool tier. Adopt MCP as the standard for it so enterprises reuse existing MCP servers and add tools without forking/recompiling. In-process Rust stays for hot/trusted tools. Both register into the same loop, both captured in the ledger.
  • Custom content injection → generalize the L0–L4 layered injection (M4). Today it pulls from our SQLite memory store + persona. Gap: a pluggable ContextProvider chain so a company injects from their vector DB / docs / policy engine into specific layers, routed through the same trust-tier + provenance machinery. The principled replacement for OpenClaw’s agent.md blob-dump injection.

Sequencing decision: primitives before the SDK

Decision (Matt, 2026-06-17): build the core primitives first; cut the SDK last. Rationale: the SDK is a surface. Freezing a stable surface and then widening it when spawn/fan-out land would churn the very thing we promised was stable. Draw the boundary around the finished shape.

There is a hidden third layer that makes this work:

  • Primitives — internal Rust (child-run, spawn, fan-out, ContextProvider, routing). Compose with the verified loop + ledger.
  • Event / provenance vocabulary — the new event types these primitives emit, plus child-ledger anchoring into the parent hash chain.
  • SDK — the thin external projection of that vocabulary + a handle to invoke the primitives.

The discipline that makes the SDK cheap: every primitive must route its events/provenance through the existing events + ledger spine as it is built — never bolt events on afterward. Then the SDK is a projection, not a redesign.

Keystone primitive: child-run

spawn and fan-out are the same underlying primitive. Build one internal child-run abstraction first:

Run a sub-task with its own scoped Ctx (budget slice, deadline, scope), returning a verified result (verdict + artifacts) — not a transcript — with its child ledger anchored into the parent’s hash chain.

That one primitive unlocks three features and de-duplicates existing code:

  • subagent spawn = expose child-run as a model-facing tool.
  • best-of-N fan-out = launch K child-runs of the same task + a reducer (vote / judge / merge).
  • the existing N-reviewer panel is already a fan-out instance — refactor it onto the same substrate instead of keeping it as a one-off.

This composability with verification is what a simple harness (pi-class) can’t do — it’s the differentiator, not a checkbox.

Build order (post v0.16.0)

  1. child-run substrate — scoped Ctx + budget slicing + anchored child ledger + its event/provenance vocabulary. The keystone; real design weight. Leave the spawn/fan-out tool surface as a follow-up so the substrate shape can be reviewed before building on it.
  2. spawn tool + best-of-N fan-out on top of #1; refactor the N-reviewer panel onto the same substrate.
  3. ContextProvider trait + MCP tool tier — independent seams; can run in parallel with #2.
  4. truss-sdk — projects the now-coherent event stream and exposes the primitives. Cut last.

Other primitives flagged (don’t forget)

  • Tree-wide budget/concurrency governance — one ceiling for a spawned fleet (the Ctx shared meter extends tree-wide).
  • Human-in-the-loop approval gates / pausable-resumable contracts — “pause before this tool, or before spend > $X.” Enterprise governance table stakes.
  • Smart routing as a Provider decorator — the local-first pillar; route task → cheapest-capable model; the router itself a small local model later.
  • Out-of-process tool sandboxing + per-tenant isolation — the multi-tenant, API-keys-only path.

Discipline rule (so core doesn’t bloat)

If provenance must capture it, it’s core (child-run, spawn, fan-out, context injection, routing, budget). If it only consumes the stream, it’s a plugin (the UI, concrete tools). Keeps faith with the “get out of the model’s way” thesis.

For author-facing recipes, see the Plugin Authoring Overview.

MCP architecture — client tier (headline) + server tier (secondary)

Status: spec + MCP-1 implementation. 2026-06-20. Repo target v0.42.0.

MCP (Model Context Protocol) is the keystone parity move: one protocol and Truss inherits the entire external-tool ecosystem that Claude Code and OpenClaw rely on. This spec covers two directions, run through the verification + durability lens (described in the build plan):

  • Client (headline): Truss consumes external MCP servers. The path to real parity — and parity that’s still on-moat, because every external tool call is policy-gated, budget-metered, signed into the ledger, and replayed durably.
  • Server (secondary, not highlighted): Truss exposes itself as an MCP server so another harness (Claude Code, Codex, an IDE) can delegate a verified contract to Truss and receive a signed verdict. Real benefit (Truss becomes a verification backend others plug into), but not a marquee feature — keep it lean.

Aligns with docs/extensibility-architecture.md (“custom tools → MCP out-of-process tool tier”) and ADR 0001 (plugin tiers).


Part 1 — MCP client tier (headline)

1.1 The lens (why this is on-moat, not just parity)

  • Verification: YES. Every MCP tool invocation becomes a signed mcp_tool_call ledger entry — server id, tool name, argument hash, result hash, provenance LedgerRef. Results that feed a claim are available to the N-reviewer panel and to truss ledger. So third-party tools inherit the audit trail by construction. No competitor offers audited MCP execution. This is the differentiator, not a checkbox.
  • Durability: YES. Each call is a memoized durable activity (same machinery as the existing in-proc tool memo). On resume, recorded results replay — Truss does not re-invoke the external server (principle #1: replay recorded truth). Long / async tools map to a suspend point (reuse await_signal): suspend the run, let the external job finish, resume on signal with the recorded result.
  • Honest non-goals: MCP resources and prompts (vs tools) are lower value for our loop initially; tools first. Streaming partial tool output to a screen is presentation (off-journal); the completed result is what’s journaled.

1.2 Where servers are declared — contract/agent config, never global

Scoping is per-agent / per-contract by construction (this is the thing OpenClaw’s global MCP registry got wrong for multi-tenant). MCP servers are declared in the contract or agent config, so a contract’s tool surface is part of its signed definition:

# in a contract or agent config
[[mcp.servers]]
name      = "github"
transport = "stdio"                 # stdio | http
command   = "gh-mcp-server"         # for stdio
args      = ["--repo", "ControlStackAI/truss"]
# url     = "https://..."           # for http transport
allow_tools = ["list_issues", "create_pr"]   # allowlist; empty = all advertised
budget_calls = 50                   # per-run cap, counts against Ctx budget

The set of declared servers + allowlists is part of the contract → it’s covered by the contract hash → the verdict attests which external tools were even available.

1.3 Lifecycle & wiring

  1. Connect declared servers at run start (stdio child process with kill_on_drop, or HTTP client). Handshake, list tools.
  2. Mount advertised tools (post-allowlist) into the loop’s tool registry alongside in-proc tools (ReadFile/WriteFile/ListDir/Bash/memory/spawn). Namespace as mcp:<server>:<tool> to avoid collisions.
  3. Present via provider-typed schema where it maps (precedent: provider_alias.rs, ADR 0019); otherwise as a generic custom tool. Execution stays local to Truss (we own provenance), only the tool call crosses to the server.
  4. Invoke inside the agent loop: enforce Policy (network/forbidden) + Ctx budget/deadline/cancel → call server → record signed mcp_tool_call ledger entry + memo → return result to the model.
  5. Resume: memoized result replays; no re-call. Async tool → suspend/await_signal.
  6. Verify: truss ledger shows MCP calls with arg/result hashes; the panel can cite them.

1.4 New surface

  • Core: truss_core::mcp module (client) — McpServerConfig, stdio JSON-RPC framing, and a tool adapter onto the existing Tool trait. Ledger entry kind mcp_tool_call.
  • CLI: truss mcp list (declared servers + advertised tools), --mcp-config overlay, truss mcp probe <server> (connectivity/diagnostics).
  • Config: [[mcp.servers]] block, layered global→agent→contract.

1.5 Milestones (client)

  • MCP-1 stdio transport + connect/list/mount + invoke wrapped in policy/budget + signed mcp_tool_call ledger. Hermetic fake-server tests. Implemented as the first PR.
  • MCP-2 durable memoized replay (same server/tool/input = recorded result, no re-call) backed by the signed mcp_tool_call provenance hash and existing tool_memo substrate. Implemented as the second PR.
  • MCP-3 HTTP transport + auth (env-injected, never journaled — see crypto note). Implemented behind the off-by-default mcp-http feature so truss-core remains vendor-free by default.
  • MCP-4 async tool suspend via await_signal. Implemented with structured MCP pending content that records mcp_tool_pending, suspends the durable run, and records the delivered signal payload as the final signed mcp_tool_call.
  • MCP-5 CLI diagnostics surface. Implemented as truss mcp list and truss mcp probe <server> with human and JSON output. Provider-typed presentation for mapped tools can ride the broader native-tool parity pack.

Part 2 — MCP server tier (secondary, not highlighted)

2.1 The pitch (and why it’s secondary)

Expose Truss over MCP so another harness can call it as a tool. The headline tool:

  • truss_run_contract(intent, done, budget, constraints, …) -> { verdict, head_hash, anchor, bundle_ref } — the caller delegates work and gets back a signed verdict + anchored evidence head, not a transcript. Truss becomes the verification backend inside someone else’s loop.
  • Read tools: truss_ledger_verdict(session), truss_ledger_export(session) — let an external UI/agent read proofs.

Why secondary: it’s a genuine wedge (a Claude Code user could route high-stakes work through Truss for the audit trail without leaving Claude Code), but it inverts the “Truss owns the loop” thesis and the headline story is the standalone product. Build it lean; don’t let it pull design weight from the client tier.

2.2 The lens

  • Verification: YES — this is the substrate exposed. The server’s whole value is returning a signed verdict + anchor. Map directly onto delegation::run_contract + truss ledger reads. Nothing new on the trust path.
  • Durability: YES, and it’s a feature. A delegated contract is a normal durable Truss session → the caller gets a session/head_hash handle and can reconnect to a long-running delegated job (suspend/resume across the MCP boundary). Honest edge: MCP request/response is typically synchronous; long contracts need an async pattern (return a handle immediately + a truss_contract_status(session) poll/await tool) rather than blocking the MCP call.
  • Honest non-goal: don’t expose Truss’s internal tools (read/write/bash) over the server — that’s just remoting a shell and drops the verification point. The server’s unit of work is a contract, never a raw tool.

2.3 Surface & milestones (server)

  • Core/bin: truss serve-mcp, a thin MCP server exposing truss_run_contract + truss_contract_status + ledger read tools, backed by existing delegation + LedgerReader.
  • MCP-S1 stdio server + truss_run_contract (sync, bounded contracts) + verdict return. Implemented in the first server slice.
  • MCP-S2 async handle + truss_contract_status (durable reconnect). The stdio server can return a handle immediately, then poll in-process job state or the durable session by id. Resume semantics continue to ride the existing journal: recorded results are replayed, not re-executed.
  • MCP-S3 http transport + auth + per-caller Principal isolation. HTTP mode is currently an honest CLI error rather than a partial unauthenticated server.
  • Sequencing: ships after the client tier (MCP-1..4). Spec’d now so the client design doesn’t paint the server into a corner (shared truss-mcp transport code, shared tool-schema types).

2.4 Implemented stdio server slice

truss serve-mcp --transport stdio advertises exactly these tools:

  • truss_run_contract
  • truss_contract_status
  • truss_ledger_verdict
  • truss_ledger_export

truss_run_contract takes a contract TOML path and optional session_id. A synchronous call returns the contract verdict, the signed ledger verdict bundle, the session head_hash, and a bundle_ref pointing at truss_ledger_export. With async = true, it returns a handle immediately and truss_contract_status polls that handle.

Stdio mode carries one process-level principal:

truss --db harness.db --key harness-identity.key serve-mcp \
  --principal-tenant local \
  --principal-user alice

The principal is threaded into the contract Ctx and store session. HTTP auth and request-scoped principals are deliberately left for the HTTP slice.

The server trust boundary is negative as well as positive: raw internal tools such as bash, read_file, write_file, and list_dir are not advertised and return an unknown-tool JSON-RPC error if called. Contracts may still use allowed tools internally, but those calls remain inside Truss policy, budget, sandbox, provenance, and durable replay.


Cross-cutting

  • Crypto/secrets: MCP server auth (tokens, API keys) is injected from env/config, never written to the journal (same discipline as provider auth — codex_auth.rs has no Debug). Argument hashes in the ledger must redact declared-secret fields.
  • Shared code: one truss-mcp module owns transport (stdio/http) + JSON-RPC framing
    • tool-schema types, used by both client and server. Adding MCP must not add a vendor dep to truss-core’s vendor-free guarantee — put the client/server behind features in an appropriate crate (likely truss-providers or a new truss-mcp crate; decide at MCP-1).
  • Plugin-authoring docs (paired deliverable): the website’s “write a plugin” guide documents MCP as the primary out-of-process extension path, with the verification + durability hooks called out so third-party tools opt into the moat by default.

Open questions (resolve at MCP-1)

  1. New truss-mcp crate vs module in truss-providers? (Vendor-dep boundary.)
  2. MCP Rust SDK (rmcp/official) vs hand-rolled JSON-RPC? (Dep weight vs control — mirrors the SSE hand-roll decision.)
  3. Async-tool suspend: reuse await_signal as-is, or a dedicated mcp_pending await kind?
  4. Server async pattern: handle+poll vs MCP progress notifications.

Native Git and GitHub Tools

Phase 5 P5-1 adds first-class git and GitHub tools so coding contracts do not need to treat bash as the only integration layer.

Enable them with flags:

truss --durable --tools --git-tools --github-tools --workdir /path/to/repo \
  --config harness.toml \
  "inspect the diff, commit the fix, open a PR, and read checks"

Or enable them in config:

[tools]
git = true
github = true

[policy]
write_scope = ["/path/to/repo"]
read_scope = ["/path/to/repo"]
network = "allowlist"
allowlist = ["github.com", "api.github.com"]
allow_tools = [
  "read_file",
  "write_file",
  "list_dir",
  "git_status",
  "git_diff",
  "git_commit",
  "github_pr_create",
  "github_pr_checks",
]

[tools] controls registration. [policy].allow_tools still controls which registered tools the model may call.

Git Tools

All git tools accept repo, defaulting to the current directory. The repo path must be allowed by the active policy. Read-only tools require read scope; side-effecting tools require write scope.

ToolPurposeSide effect
git_statusStructured git status --porcelain=v1 --branch output.No
git_diffDiff patch plus patch_sha256. Supports staged, base, head, and paths.No
git_branchcurrent, list, create, checkout, or create_checkout.Only create/checkout actions
git_commitOptionally stage all or paths, then commit with message.Yes

git_commit returns the commit SHA, short SHA, previous head, and a hash of the raw commit output. In a durable run, the result is memoized with signed git_tool_call provenance so resume returns the recorded commit result instead of creating a second commit.

GitHub Tools

GitHub tools use the gh CLI for auth and transport, but the model-facing surface is typed. Network policy must allow github.com or api.github.com.

ToolPurposeSide effect
github_pr_viewView PR metadata, commits, files, and review state.No
github_pr_createCreate a PR with title, body, optional base, head, and draft.Yes
github_pr_diffFetch PR diff plus diff_sha256.No
github_pr_checksRead PR checks through gh pr checks --json.No
github_issue_viewView issue metadata, labels, comments, and body.No
github_issue_listList issues by state and limit.No
github_check_statusFetch combined commit status and check runs for a ref.No

Read-only GitHub outputs are signed into the durable ledger as github_tool_call entries with input and result hashes. github_pr_create also writes a durable memo so resume replays the recorded PR result rather than creating a duplicate pull request.

Ledger Semantics

Native git/GitHub provenance follows the same journal discipline as MCP:

  • each durable tool call records a signed ledger entry;
  • ledger payloads store the tool name, input hash, result hash, error state, side-effect flag, optional producer ledger ref, and memo key;
  • read-only calls are ledgered but not memoized;
  • side-effecting calls are ledgered and memoized atomically;
  • a memo is replayed only when a matching signed provenance entry exists.

This means a verdict can cite a git diff, PR diff, issue, or check result by ledger reference and hash. It also means resume cannot silently repeat a commit or PR creation after the side effect has already been recorded.

Tool Architecture: Harness Tools ↔ Provider Tools (design, pre-build)

Status: DESIGN for review (Matt, 2026-06-18). No implementation yet. Resolves the question: should Truss expose its tools through each provider’s trained typed-tool schemas, and how do harness tools and provider tools work together? Principle to protect: verification + provenance (local execution, signed ledger).

Two kinds of tools

  1. Harness / client tools — Truss defines the schema; the model emits a tool call; Truss executes locally and returns the result. Execution stays in our control → sandboxing + signed-ledger provenance preserved. (Truss’s read_file, write_file, list_dir, bash, spawn are these.)
  2. Provider tools — schemas the model was post-trained on:
    • (a) typed, client-executed — e.g. Anthropic text_editor / bash (text_editor_20250728, bash_20250124); the model knows the exact interface, but the client still executes and returns results. Best of both: native model fluency + local execution/provenance.
    • (b) server-executed — e.g. web_search, code_execution; the provider runs them. These bypass our local execution and provenance, so use selectively and never for the trust-critical path.

Current state (verified in code)

  • Generic custom function tools for read_file/write_file/list_dir/bash.
  • Precedent already exists: the memory tool is wired to Anthropic’s typed memory_20250818 schema (anthropic.rs). So the pattern is proven — just not applied to the file/shell tools.
  • Observed cost: the in-loop SWE-bench run leaned on bash, used the structured file tools little, and validated poorly — consistent with handing the model ad-hoc tools instead of the editor/bash interface it was trained on.

Principle

Adopt the provider’s canonical typed-tool SCHEMA when one exists; keep EXECUTION local. One internal capability → mapped to the best available provider surface. Never trade provenance for a server-executed tool on the verified path.

Design

  • A provider-aware tool mapping layer: each harness capability (edit file, read file, run shell) declares a canonical generic schema AND an optional per-provider typed-schema binding.
    • Anthropic → text_editor for file edits, bash for shell (client-executed).
    • OpenAI / codex → their equivalent typed tool surface (confirm exact schemas via the claude-api / provider guides at build time).
    • Unknown/local providers (Ollama etc.) → fall back to the generic custom tools.
  • Execution path is unchanged and local regardless of which schema was used to present the tool — so the ledger/verification story is identical.
  • Tool results normalize back into one internal representation before the loop continues, so the agent loop and provenance don’t care which surface was used.

Explicitly NOT doing

  • No SWE-bench-specific tools. This is a general capability, not benchmark scaffolding. We measure SWE-bench after, to verify the general improvement.

Separate, larger question (defer + design on its own)

The in-loop run also showed Truss’s agent has no reliable sandboxed environment to validate its own work before the verification boundary (it hit host Python/dep friction and never ran the task’s real tests). That’s a general “execution sandbox for self-validation” capability — valuable for any coding task — but it’s a bigger design (sandbox model, per-task env, security) and should get its own spec, NOT a quick SWE-bench bolt-on.

Sequence

  1. (this) agree the principle: typed schema + local execution.
  2. implement provider-aware mapping for file/shell tools (reuse the memory-tool pattern).
  3. re-run the in-loop benchmark → measure improvement vs the current generic-tool baseline (the ~50-task run starting now is that baseline).
  4. separately spec the self-validation sandbox.

Auth strategy — subscription vs API key (research capture, 2026-06-15)

Findings from investigating how to authenticate to model providers, especially via a subscription (Claude Pro/Max, ChatGPT Plus/Pro) instead of pay-as-you-go API keys. Informs ADR 0001 (plugins) and the auth abstraction.

The three mechanisms

  • (a) Drive the vendor CLI/SDK as a subprocess. Compliant; the official client owns OAuth login/refresh. Necessarily per-vendor. (OpenClaw’s claude-cli and Codex app-server runtimes; our PR #10 prototype.)
  • (b) Replay the vendor’s OAuth token against its HTTP endpoint (“subscription like an API key”). Works, but not provider-agnostic and per-vendor fragile.
  • (c) API keys only. Clean, multi-vendor, always-supported. The safe baseline.

Bottom line: only OAuth login/refresh generalizes. The inference call is irreducibly per-vendor (different host, wire API, mandatory first-party headers). There is no universal “swap base URL + bearer” shim.

OpenAI / ChatGPT (Codex) — mechanism (b) is viable, tolerated

  • Token: ~/.codex/auth.json → tokens.{access_token, refresh_token, id_token, account_id}; auth_mode. Access-token JWT: iss=https://auth.openai.com, aud=https://api.openai.com/v1, claims namespaced https://api.openai.com/auth.
  • Inference: POST https://chatgpt.com/backend-api/codex/responses (Responses API, SSE, store:false). Headers: Authorization: Bearer <access_token>, ChatGPT-Account-ID: <account_id>, originator: codex_cli_rs, OpenAI-Beta: responses_websockets=2026-02-06, session-id/thread-id.
  • Refresh: POST https://auth.openai.com/oauth/token, client_id=app_EMoamEEZ73f0CkXaXp7hrann, grant_type=refresh_token.
  • Status: reverse-engineered, tolerated for personal use (semi-endorsed by OpenAI). No replay block to date. Rate-limited to the ChatGPT plan window.

Anthropic / Claude Pro/Max — mechanism (b) is BANNED

  • Token: ~/.claude/.credentials.json → claudeAiOauth.{accessToken, refreshToken, expiresAt, scopes, subscriptionType, rateLimitTier} (Keychain on macOS).
  • Inference: POST https://api.anthropic.com/v1/messages with Authorization: Bearer <accessToken>, anthropic-beta: claude-code-20250219,oauth-2025-04-20, anthropic-version: 2023-06-01, no x-api-key, and a required first system block "You are Claude Code, Anthropic's official CLI for Claude."
  • Refresh: https://platform.claude.com/v1/oauth/token, client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e, 8h expiry.
  • Status: prohibited. ToS amended 2026-02-19 to ban Pro/Max OAuth credentials in any non-Claude-Code tool (incl. the Agent SDK); blocked server-side; accounts banned. OpenCode and Crush were forced to remove it. → For Anthropic subscriptions, use mechanism (a) (official CLI) or (c).

What others do

  • pi (earendil-works/pi, the minimal harness we resemble): mechanism (b) — its own OAuth flow + token replay. Its tracker shows the Anthropic path breaking / being per-token billed — i.e. the approach now under pressure.
  • OpenClaw: hybrid mechanism (a) — claude-cli runtime + Codex app-server runtime, plus a ChatGPT-responses token transport. All per-vendor.
  • OpenCode / Crush: had (b); Claude path removed at Anthropic’s request.
  • aider: (c) only.

Product implications

Shipping a tool that replays Anthropic subscription tokens violates ToS and can get users banned (since 2026-02-19). Safe matrix for a distributable product:

  • API keys — always.
  • OpenAI subscription — via token replay, with a personal-use caveat.
  • Anthropic subscription — official CLI only, or omit.

Design consequence

Auth is an Auth strategy behind the Provider, and every strategy is a plugin (ADR 0001): auth-apikey (default), auth-oauth (generic login/refresh + tiny per-vendor descriptor; use for OpenAI), runtime-cli (compliant Anthropic path). Per-vendor, ToS-sensitive logic stays out of the core.

Sources

github.com/openai/codex (codex-rs), code.claude.com/docs/.../authentication, github.com/earendil-works/pi, github.com/sst/opencode (#417), charmbracelet/crush (PR #1783), theregister.com 2026-02-20, simonw/llm-openai-via-codex.


Deep dive (2026-06-15, round 2): Codex vs pi vs OpenClaw, read from source

Three parallel investigations (reading openai/codex Rust source, earendil-works/pi source, and the locally-installed openclaw dist/ + docs) sharpened the picture and surfaced one architectural insight the matrix above was missing.

Corrections / sharper facts

  • OpenAI CAN fingerprint third-party clients — “no replay block to date” is too optimistic. Codex issues #14215 (non-official OAuth client passes authorize/callback but gets 403 at /oauth/token, while official codex-cli works on the identical account/network) and #2951/#14215 (third-party calls hit 429 quota) show the auth/quota layer can distinguish clients. OpenAI hasn’t slammed the door like Anthropic, but it has the same technical precursors Anthropic used right before it did. Treat OpenAI subscription replay as “unclear and revocable,” not “safe.” Defensible only as single-user, bring-your-own-subscription, personal-use — never multi-tenant/commercial.
  • Codex programmatically mints a real API key from the login: after the PKCE code exchange it does an RFC 8693 token-exchange (grant_type=…token-exchange, requested_token=openai-api-key) and stores the result as OPENAI_API_KEY in auth.json (falls back to the ChatGPT access token if the account lacks the Codex entitlement). Source: codex-rs/login/src/.
  • pi confirms mechanism (b) in-process, with client impersonation. pi runs its own PKCE OAuth (packages/ai/src/utils/oauth/), then in providers/anthropic.ts detects an OAuth token and injects Claude Code identity — anthropic-beta: claude-code-20250219,oauth-2025-04-20 + rewrites system/tool messages to the “Claude Code” identity. Anthropic now meters this as per-token “extra usage” (pi issue #3670) — i.e. the replay path is actively under pressure, exactly as predicted. This is the pattern we do NOT copy.
  • OpenClaw uses two different shapes, not one. claude-cli = a CLI subprocess backend: it spawns the real claude binary (-p --output-format stream-json … --resume {sessionId}), and clears all Anthropic key/OAuth env vars (CLAUDE_CLI_CLEAR_ENV) so the CLI falls back to its own stored subscription login — zero token replay by OpenClaw. Codex = an embedded harness over a long-lived app-server stdio process holding a stored Codex OAuth profile (avoids per-turn subprocess spawn latency). Both are per-vendor.

The insight the matrix missed: vendor CLIs are agents, not endpoints

claude (Claude Code) and codex are not raw “messages in → one completion out” endpoints. They are full agents that own their inner loop: they run their own tools, their own context compaction, and their own session state. OpenClaw’s claude-cli backend therefore sets ownsNativeCompaction: true and nativeToolMode: "always-on" — it cedes the loop to the CLI and can only observe.

That collides head-on with Truss’s whole thesis. Truss’s value is that we own a durable, signed, independently-verified loop. If we get subscription auth by shelling out to Claude Code in print mode, Claude Code runs the tools and the loop — our per-step journal, policy enforcement, and tool sandbox don’t apply to the inner work. You can’t have both “the vendor CLI authenticates and drives” and “Truss owns every step” at the same time.

Resolution: two integration depths, one trust boundary

Don’t force every provider into one mold. Truss supports a provider at one of two depths, and verification happens at the contract boundary either way:

  1. Inference provider (we own the loop). Raw model endpoint: API keys + local servers (Ollama/vLLM/etc.). Full Truss stack — per-step durable journal, policy, tool sandbox, reviewer panel. This is the registry we already built.
  2. Delegated agent (the vendor owns the inner loop). Subscription via the vendor CLI/app-server, env-cleared so the vendor’s own client authenticates — the ToS-clean path, no token replay by us. We don’t journal the inner steps; we wrap at the contract boundary: record the contract, let the delegated agent execute, then run our independent checks + N-reviewer panel on the result and sign the verdict. We lose inner-step replay; we keep the thing that matters most — verified delegation — and we stay ToS-clean.

So subscription auth doesn’t weaken the moat: a delegated-agent run is still a contract that produces a signed, independently-verified verdict. It just trusts the vendor for the inner loop instead of journaling it.

  • Keep the in-process registry for inference providers (API key / local). Done.
  • Subscription auth = a runtime-cli / delegated-agent tier that shells out to the vendor’s own CLI/app-server with credentials cleared (OpenClaw’s model), behind the same Provider/registry seam — i.e. ADR 0002’s ProviderConfig grows an auth/runtime variant; the CLI never learns a new shape. Per-vendor, ToS-robust, no token storage/refresh/replay on our side.
  • Explicitly reject in-process subscription-OAuth replay (pi’s approach). Anthropic: banned. OpenAI: grey/revocable — at most a personal-use, bring-your-own, off-by-default flag, never the default and never multi-tenant.
  • Anything commercial/multi-tenant routes to API keys — the documented, stable, sanctioned path.

Round-2 sources

codex: github.com/openai/codex (codex-rs/login/src/), discussions/8338, issues/14215, developers.openai.com/codex/auth, simonwillison.net/2026/Apr/25/romain-huet. pi: github.com/earendil-works/pi (packages/ai/src/utils/oauth/, providers/anthropic.ts, README), issue #3670. openclaw: local openclaw@2026.6.5 — docs/concepts/agent-runtimes.md, docs/plugins/cli-backend-plugins.md, dist/cli-backend-*.js, dist/cli-shared-*.js.


Working assumption & receipts (2026-06-15)

Receipt: the billing surface is volatile (live example)

On 2026-06-15 Anthropic emailed (“We’re pausing the Agent SDK credit change”): “The change today is on hold. Your SDK and -p usage keeps working as it did before today, and we’ll reach out with further updates.” They had a change scheduled to land that same day that would have altered how the Agent SDK and claude -p --model claude-opus-4-8 (print mode) consume credits — i.e. metering Max/subscription usage through those paths as paid usage — and walked it back at the last minute. Note claude -p --model claude-opus-4-8 is exactly the mechanism the delegated-agent / runtime-cli tier rides on, so even the ToS-clean subprocess path depends on billing terms the vendor can change unilaterally. This is the concrete receipt for “subscription billing is unclear and revocable, not stable.”

Decision (Matt, 2026-06-15): proceed assuming 3rd-party subscription stays supported

Working assumption: subscription usage through 3rd-party apps will be supported for the near future; we build for it now and adjust when the time comes. We do not gate the roadmap on revocation fear.

What this changes and doesn’t:

  • Does change: subscription auth is an explicit, supported roadmap feature (not a someday-maybe). Both the delegated-agent subprocess path and — where a vendor tolerates it — in-process OAuth are on the table as plugin tiers.
  • Doesn’t change the architecture: still behind the registry/plugin seam (ADR 0002), still per-vendor, ToS-sensitive logic out of core. So if a vendor flips (like Anthropic nearly did today), we disable/swap one plugin, and the durable+verified core and API-key baseline are untouched. The plugin boundary is the “adjust when the time comes” mechanism.
  • Still true: API keys remain the stable default; multi-tenant/commercial deployments route to API keys; subscription is opt-in. The assumption lets us build the subscription tiers, not depend on them.

Enterprise readiness — status

This maps the “what does it take to go from the simple loop to enterprise-ready” checklist to what v0.2 actually implements. Legend: ✅ done · 🟡 seam in place (stable signature, fuller impl later) · ⬜ not started.

The governing idea: one architectural move makes the rest bolt-on — thread a single Ctx handle (cancellation, deadline, budget, principal, policy, events) through Provider::complete and Tool::call, and use typed errors. Done once, in src/ctx.rs and src/error.rs, so no later feature changes those signatures.

Lifecycle & control

  • ✅ Cancellation token on Ctx; aborts in-flight HTTP and child processes
  • ✅ Per-run wall-clock deadline (--timeout); auto-cancels
  • ✅ Ctrl-C wired to cancellation
  • 🟡 Graceful drain on SIGTERM (cancellation primitive exists; no signal handler yet)
  • 🟡 Pause/resume (audit log is the resume seam; loader not built)

Reliability

  • ✅ Retries with exponential backoff + jitter (src/retry.rs)
  • ✅ 429 handling honoring Retry-After
  • ✅ Typed error taxonomy: retryable / fatal / policy (src/error.rs)
  • ⬜ Idempotency keys for retried tool side effects

Streaming & output

  • ✅ Provider-neutral complete_stream() contract: default fallback for every provider, real SSE for Anthropic and OpenAI-compatible/local backends
  • ✅ Incremental AssistantTextDelta events for live UIs + final assembled CompletionResponse for tool loop / usage / stop reason
  • ✅ Tool-result truncation in events (full result still goes to the model)
  • ⬜ Multimodal blocks (image/audio) — Block is the extension point

Cost & limits

  • ✅ Budget meter: tokens / cost / steps, enforced before & after each call
  • ✅ Escalation warning at a configurable fraction (default 80%)
  • ✅ Shared budget across sub-agents (clone of BudgetMeter)
  • 🟡 Context-window manager (capabilities expose window size; no trimming yet)

Observability

  • ✅ AgentEvent stream for everything (UI + audit)
  • ✅ Durable JSONL audit log with full provenance (src/state.rs)
  • ✅ tracing diagnostics (RUST_LOG)
  • ⬜ Metrics export (counters/histograms)

Security & policy

  • ✅ Enforced policy: write scope, read scope, forbidden shell ops, tool allowlist
  • ✅ Network policy model (none/allowlist/open) — enforced by net-using tools
  • ✅ Safe-by-default policy (deny writes, no network)
  • 🟡 Pluggable secret provider (keys via env today; trait seam later)
  • 🟡 Tool sandboxing (process isolation beyond kill-on-drop)

Multi-tenancy & scale

  • 🟡 Principal (tenant/user) on Ctx — carried, not yet isolating
  • ✅ Parallel tool execution within a step
  • 🟡 Sub-agent fan-out (Ctx::child exists; no orchestrator yet)
  • ⬜ Concurrency caps / backpressure across many runs

State & provider robustness

  • ✅ Model capability metadata (ModelCapabilities) per provider
  • ✅ Model override per request (CompletionRequest::model)
  • 🟡 Durable run store / resume (JSONL log written; loader pending)
  • ⬜ Record/replay of provider calls for CI

Config & DX

  • ✅ Declarative TOML config (provider/budget/policy), flags override
  • ✅ Single-binary CLI
  • ✅ Unit tests (budget, policy) + live end-to-end verified

Truss Testing Strategy

Status: active. Captured 2026-06-17. Principle: our testing IS our marketing. We sell “verified, auditable, can’t-silently-rot.” Every guarantee we sell needs a test category whose passing means an adversary or edge case cannot break the guarantee — not just that the happy path works. A buyer/ acquirer doing diligence will open this suite; it must make the claims true.

The trust ladder → test category mapping

Guarantee we sellTest category that defends it
Verified means a panel signed offinvariant/property test (unreachable any other way)
Ledger is tamper-evidentadversarial tamper suite (corrupt/forge/replay MUST fail verify)
Budget is bounded across a spawn treeproperty test over random tree shapes
Memory can’t silently rotinvariant: Unverified/Rejected/Stale never in trusted view
Everything has provenanceproperty test: every entry has a valid LedgerRef
Works on real modelsgated live smoke + nightly eval corpus

Current base (good, keep)

  • ~107 hermetic tests (fake provider), unit + per-feature integration.
  • Gates every PR: fmt --check, clippy --all-targets --all-features -D warnings, test, test --all-features, release build. ALINA re-runs on merge.

Priority additions

P0 — Adversarial tamper suite (the diligence question)

Prove --verify CATCHES bad chains, not just passes good ones. Negative tests that MUST fail verification: corrupt an entry hash, flip a signature byte, reorder/replay an entry, forge a verdict, truncate the chain, swap a child anchor. Same idea for the panel: a crafted tool output / result must not pass review as correct. This is the single most important category for a provenance product.

P1 — Property/invariant tests (proptest)

Randomized histories assert the moat invariants hold for ALL inputs:

  • Verified ⇒ a signed panel verdict exists (no other path sets it)
  • hash-chain validates after any write sequence
  • child budget never exceeds parent ceiling, any tree shape
  • Unverified/Rejected/Stale never surface in the trusted view
  • every entry carries a resolvable provenance ref

P2 — Live smoke + nightly eval corpus

  • Gated live-smoke job (behind API keys, pre-release): close the known gap that openai-codex was never tested against the live backend.
  • Nightly truss eval over a small contract corpus → track pass-rate + cost; regressions surface automatically. Turns SI-4 into a standing quality gate.

P3 — Confidence amplifiers (periodic, not per-PR)

  • Mutation testing (cargo-mutants): prove tests KILL bugs, not just pass. Highest signal that the suite is real.
  • Concurrency stress: fan-out/spawn budget + cancellation races.

Sequencing

P0 + P1 ship together (shared proptest harness; both back the sales claims) → P2 (CI/nightly + live smoke) → P3 as periodic confidence runs.

UX & Visual Presentation Principles (carry-forward)

Status: durable note, no UI yet. Captured 2026-06-17 (Matt): “we can’t overlook visual presentation — how it looks to the user is sometimes as important as functionality. We don’t have a UI yet but keep this in mind.” This doc exists so the principle isn’t lost before ClawTerm (the planned Warp-based front end) is built.

The core idea

For a trust product, presentation is not decoration — it’s how trust becomes felt. A signed ledger nobody can read is worth little; the same proof rendered legibly is the product’s most persuasive moment. Our unique UX surface is proof legibility: making verification, cost, and provenance immediately understandable.

Principles to carry into ClawTerm (and any front end)

  1. Show proofs, not logs. The hero view is the verdict + what the panel checked + the cost — not a raw transcript scroll. The ledger should be browsable and visually obviously tamper-evident (chain intact / broken).
  2. Make trust tiers visible. Verified vs Unverified vs Stale should read at a glance (color/badge), never buried in text. The trust label is the product; surface it.
  3. Cost & budget always in view. Spend, budget remaining, and per-step cost are first-class UI, tied to the local-first/cost-control story.
  4. Verification should feel tangible. A --verify pass (and a tamper catch) should be a satisfying, legible visual event — this is the demo moment that beats any competitor’s transcript.
  5. Spawn/fan-out trees are visual. Sub-agent trees, which branch won, and why, should render as a structure, not prose.
  6. Legible failure. When something fails, the message is actionable and visually distinct from success. Error legibility is a measurable UX metric (see testing-strategy + the sim-user harness).

How this connects to testing

The sim-user harness and instrumented UX proxies (turns-to-success, time-to-first- useful-output, error legibility, proof legibility) measure the interaction layer now. Visual-presentation UX gets validated with a small-N human comparative pass once ClawTerm is real. Keep these principles as acceptance criteria for that UI.

Verdict-Review Surface / Interactive TUI (Phase 4)

Status: spec. Created 2026-06-22 from docs/BUILD-PLAN.md Phase 4.

Truss’s moat is “review proofs, not transcripts.” That moat is only realized if a human can actually look at the proofs and act on them quickly. Phases 1–3 built the signed evidence (durable journal, N-reviewer panel, verdict bundles, diff-aware review, governed memory + verified playbooks) and the read surface for it (LedgerReader + truss ledger list|show|get|verdict|export|recall with stable --json). Phase 4 builds the human-facing review and sign-off surface on top of that read API.

North-star “done when”

Matt can approve or reject a real Truss task from the verdict view — seeing the definition-of-done clauses, the independent evidence, the reviewer panel result, the diff, and the ledger chain/head status — without reading the full transcript, and his decision becomes part of the signed chain.

Design decisions

  1. In-repo interactive TUI first, external front-ends via the JSON seam. We build a ratatui/crossterm TUI inside truss-cli as the first concrete surface because it is self-contained, testable, and hits the “done when” directly. The external ClawTerm/Warp front end consumes the same stable --json read surface (already shipped) plus the same core write primitives — we are not building a bespoke protocol. The TUI is a presentation/controller over LedgerReader and Store; it never becomes the provenance substrate.

  2. Provenance discipline (the load-bearing rule). Provenance-capturing = core. Stream-consuming = plugin/CLI.

    • The human decision (approve / reject / needs-changes) is provenance → it lives in core as a new signed journal entry (human_review) on the same hash chain, verified by verify_chain.
    • The screens, tabs, scroll position, note input, and layout are presentation → they live in the CLI/TUI and are never written to the ledger.
  3. Human sign-off attests to a head hash. A human_review entry records the decision, an optional note, the reviewer’s signing key id, and the session head hash it is attesting to at decision time. If later entries change the head, the sign-off is still cryptographically pinned to exactly the state the human reviewed — you can prove what was approved, and detect approve-then-mutate.

  4. No fake authority. A human_review decision is recorded and signed, but it does not silently overwrite the machine Verdict. The verdict bundle shows both: the independent machine verdict (panel + checks) and the human sign-off. They are distinct trust signals; the UI presents them side by side. (Default-to-fail is unchanged: absence of a human approval is not an approval.)

  5. TUI state is not durable. Underlying runs are durable (journal authoritative); the review window is not. Don’t engineer UI state into the ledger.

Milestones (one PR + one ADR section each)

VR-1 — signed human-review primitive (core + non-interactive CLI). FOUNDATION.

  • New journal entry kind human_review { decision: Approved|Rejected|NeedsChanges, note: Option<String>, attested_head_hash: String, reviewer_key_id: String }, signed and chained like every other entry; verify_chain covers it (incl. tamper tests).
  • Non-interactive CLI first (scriptable, testable without a terminal): truss review approve|reject|needs-changes <session> [--note ...] [--json].
  • LedgerReader gains read access to human-review entries; verdict bundle + truss ledger verdict render the human sign-off alongside the machine verdict.
  • Honest boundary: this is the trust primitive; no TUI yet. ADR 0034.

VR-2 — read-only interactive TUI (truss review / truss tui).

  • ratatui + crossterm, feature-gated (tui) so headless/CI builds stay lean.
  • Screens, all backed by LedgerReader: (1) list of recent/active contracts with machine verdict + human-review status; (2) verdict bundle detail — DoD clauses, clause evidence, self-validation evidence, reviewer panel majority + per-reviewer verdicts, diff summary, budget; (3) ledger chain view with head hash + verify status; (4) transcript drill-down (not the default view).
  • Pure consumer; no writes. Hermetic render tests over a fixture DB (ratatui TestBackend).

VR-3 — actions in the TUI.

  • Wire approve / reject / needs-changes keybindings (a / r / n) to the VR-1 signed primitive.
  • Add confirmation + note capture; the note is persisted only as the core human_review.note payload.
  • Add suspended-run signal control (s) by routing through Store::add_signal, plus a resume command control (x) that points back to the existing durable runner (truss --resume <session> --db <db>). The TUI does not duplicate provider/tool setup or create a second resume engine.
  • Everything routes through core (Store); no UI-side provenance.

VR-4 — approval gates. SHIPPED.

  • High-risk tool calls or spend over a configurable threshold suspend the durable run pending a human human_review decision (reuses the suspend/signal substrate).
  • Configurable (Directive 1): gate policy global→agent→contract; default off.
  • Honest no: don’t gate everything; gate is opt-in and policy-driven.
  • Release/block semantics (ADR 0037): approved human_review entries release the pending gate by queueing the existing approval-gate signal; rejected and needs-changes entries stay signed in the chain but keep the run suspended. A raw signal without the signed approval does not release the gate.

Lens (per BUILD-PLAN discipline)

  • Verification / crypto: human decision is a signed, chained, head-pinned entry (yes for the decision; the TUI rendering itself is not provenance).
  • Pause / resume: underlying runs are durable; VR-3 can deliver pending signals and points to the existing resume command; VR-4 gates ride the existing suspend/signal substrate; UI window state is presentation, not durable. Approval gates are signed suspended ledger entries waiting on an approval signal that only an approved human_review decision can release.
  • Honest no: the TUI does not replace the machine verdict, does not invent a protocol (reuses --json), and does not store layout/scroll in the ledger.

Reuse map (what already exists — do not rebuild)

  • truss_core::ledger::LedgerReader and its view structs (verdict bundle, evidence, panel result, verify report, export bundle).
  • truss ledger list|show|get|verdict|export|recall + stable --json.
  • identity::Signer (sign the human-review entry), Store/verify_chain (chain it), suspend/signal substrate (VR-3/VR-4), docs/ux-presentation-principles.md (proof-legibility principles to carry into the screens).
  • approval::ApprovalGatePolicy mirrors the existing layered-config pattern: global defaults, per-agent overrides, then per-contract overrides.

Session TUI — interactive daily-driver surface (ST slices)

Status: accepted direction (Matt, 2026-07-09). First slice ST-1.

Why

Truss is functional as a contract runner and one-shot CLI, but daily-driver use needs an interactive multi-turn session surface. The substrate already exists: durable sessions with resume/suspend/signal, the AgentEvent stream, approval gates (VR-4), and the ratatui verdict-review TUI (VR-2/VR-3). This work names a session front end over those seams — it adds no new trust machinery.

ClawTerm (external, egui) remains the rich front end and consumes the same seams later; the in-repo TUI is the minimal built-in surface. Boundary discipline from ADR 0036 holds: TUI is presentation/controller; core remains the provenance substrate. No UI state in the ledger.

ST-1 scope (first slice)

  • truss tui --session <id> (new session if absent): interactive chat view driving the durable agent loop on the existing resume semantics — each user message continues the same journal-authoritative session. Quitting mid-run and reopening resumes from recorded truth (that’s the demo).
  • Chat pane renders from the AgentEvent stream: streaming AssistantTextDelta, tool start/result activity lines, context-pressure and compaction notices.
  • Approval gates inline: when a gated run suspends, the TUI prompts approve / reject / needs-changes and routes through the existing Store::record_human_review + signal path (same as VR-3 actions).
  • One keybind to jump to the existing verdict-review view for the current session and back.
  • Input ergonomics: multi-line editor, Enter-to-send, Ctrl-C cancels the in-flight step (Ctx cancel), not the TUI.

Honest no (ST-1)

  • No multi-session tabs, no editor pane, no themes — ClawTerm’s job.
  • No token deltas persisted to the journal (journal records completed results only; streaming is presentation).
  • No new ledger entry kinds. Human decisions ride the existing signed human_review entry.
  • Delegated-CLI executors are out of scope for interactive mode (they own their own loop); ST-1 is the in-loop path.

Proof (hermetic)

  • Scripted-provider test: send message → streamed deltas rendered → journal gains the completed step; kill TUI mid-session → reopen → conversation reconstructed from journal, recorded steps not re-executed.
  • Gate test: gated tool call suspends → TUI approve → signed human_review appended → run completes; reject → stays blocked.

Later slices

  • ST-2: session list/picker + new-session flow, model/provider switcher.
  • ST-3: richer evidence inline (diff viewer for work_diff, check outputs).
  • ST-4: ClawTerm transport decision (direct crate vs serve-mcp vs WS/JSON event server) — decided in the clawterm project, consumed here.

Truss Memory & Identity Architecture

Status: DRAFT v0 spec, 2026-06-16. Build target — next workstream after the OpenAI subscription arc (v0.11.0). The data model and phasing should be pressure-tested before writing Rust.

Related docs: architecture.md (Provider/Executor/loop), durable-execution.md (ledger, resumability), and examples/contract.md (contracts and executable checks). This doc extends the same trust machinery (ledger + N-reviewer panel + signed verdict) from what the agent produces to what the agent believes and remembers.


0. Why this exists (the empirical finding)

We ran Fable 5 locally in two harnesses (native Claude Code CLI and OpenClaw-driven Claude Code) and inspected the artifacts. Findings:

  • The client-side memory tool (memory_20250818) is real and documented — the model issues view/create/str_replace/insert/delete/rename calls against a /memories directory the harness backs. But neither harness enabled it. Capability ≠ activation.
  • A native 470-turn local test run used zero memory-tool calls, had no /memories dir, and every context_management field resolved to {"applied_edits":[]} (present in the request envelope, never fired).
  • What Fable did reach for unprompted on that long-horizon task: a GOAL.md (7.7KB) it consulted and edited — i.e. it hand-rolled a goal ledger because the harness gave it nowhere structured to put durable state.
  • Current harnesses (OpenClaw included) inject identity + memory as one flat static markdown blob every call — AGENTS.md + SOUL.md + IDENTITY.md + USER.md + a 37KB MEMORY.md — undifferentiated, unverified, cache-hostile, push-only.

Fable-specific disposition (from Anthropic’s migration guidance): the memory tool is a 4.x-family feature, not Fable-exclusive — but Fable 5 is documented as notably better at using a memory surface: “performs notably better when it can write learnings somewhere for future reference… tell it where, tell it to consult that file in future sessions, and give it a format,” and “good at updating skills on the fly from what it learns mid-task.” So memory is the capability with the highest return on the leading model. Under-building it leaves Fable/Mythos’s best trait on the table.

Thesis: identity and memory are the same architecture viewed at different lifetimes. Truss should model the entire injected context as a layered, lifetime-tiered, cache-aligned stack with a typed, trust-tiered, provenance-tracked memory store the model reaches via the native memory tool — not a markdown dump.


1. Design principles

  1. Push the frozen essentials, pull the rest. Identity and pinned goals are pushed (small, stable). Durable memory is pulled on demand by the model via the memory tool — stop shoving 37KB every turn.
  2. Separate by lifetime, align to the cache. The Anthropic prompt cache is a prefix match (tools → system → messages). Each layer’s churn rate must match its position so a memory write never invalidates the identity prefix.
  3. No unverified belief is “trusted.” A memory write is quarantined until a verification pass promotes it — reusing the N-reviewer panel + signed verdict that already gate contract completion.
  4. Every memory carries provenance. Each entry links to the ledger event / tool result that produced it. “Why do you believe X?” is always answerable.
  5. The model curates; the harness governs. The model decides what to write (it’s good at this — especially Fable). The harness owns trust, lifetime, eviction, and injection policy.
  6. Substrate-agnostic, like Provider/Executor. The memory tool is the model-facing interface; the backing store is a Truss trait so we can swap flat-file → SQLite → vector index without touching the agent loop.

2. The layered injection model

Replace “inject all the markdown” with five lifetime tiers. Each has its own churn rate, cache position, trust level, and injection rule.

LayerContentLifetimeCache positionInjection
L0 Identitypersona, values, voice, operating boundaries~neverfront of cached system prefixpush, signed, frozen
L1 Principalwho the user is, prefs, standing constraintsper-user, slowafter L0, still cachedpush, per-user cache key
L2 Durable memoryverified facts, lessons, decisionsgrowsnot pushedpull via memory tool
L3 Goal ledgeractive contract intent + DoD, north-starper-taskhead of messages, re-pinned every N turnspush, never compacted
L4 Working scratchin-run notes, tool-result digestsephemeraltail of messagesmodel-written, compacted server-side

Key consequences:

  • Identity stops being a markdown file in the pile. L0 is a first-class, signed, frozen artifact at the very front of the prefix. It never sits next to volatile notes, so memory churn never busts the identity cache. This is the “agent identity, done better” Matt called for.
  • L0 splits into core + self (load-bearing — see §3.5). core = operator-signed boundaries, safety, mandate; never self-amendable. self = voice, learned preferences, working persona; self-amendable under policy. The agent can evolve its personality without touching its own guardrails.
  • L2 is addressable, not ambient. The model queries durable memory through the memory tool when relevant, instead of reading a 37KB wall it mostly ignores. Token spend drops; attention sharpens; the cache prefix stays stable.
  • L3 is the GOAL.md Fable already wanted — promoted to first-class, pinned, and re-injected on a cadence so the goal never drifts across a long run. This is the answer to the original “how do we keep the end goal from getting lost” question, now mechanized.

2.1 Cache alignment (concrete)

[ tools ]                      ← memory tool + task tools (stable set; don't churn mid-run)
[ system ]
   L0 identity (frozen, signed)        ┐ cached prefix — invalidated ~never
   L1 principal (per-user)             ┘
[ messages ]
   L3 goal ledger (re-pinned every N turns)   ← small, deliberate re-inject
   …conversation / tool calls…
   L2 pulls land here as tool_results          ← model-initiated, on demand
   L4 scratch (tail)                            ← compaction edits here

Rule: nothing above the goal ledger churns within a run. Identity, principal, and the tool set are frozen for the session. All volatility lives in messages.


3. The memory store (L2) — typed, trust-tiered, provenance-tracked

3.1 Entry types (typed, not flat)

TypeMeaningMutabilityCompaction
goalnorth-star DoD / intent for a contractappend + supersedenever
decisiona choice made + rationaleappend-only (immutable log)never
facta durable learned fact about the world/codebasesupersede-by-keysalience-scored
lessona correction / confirmed approach + whysupersede-by-keysalience-scored
artifacta verified output + where it livesappendby reference
scratchephemeral working notefreeaggressive

(Maps onto the SOUL/feedback/project/reference taxonomy ALINA already uses for durable memory — generalized and typed for the harness.)

3.2 Trust tiers (the novel part)

Every entry has a trust state. Writes do not enter trusted memory directly.

unverified ──(verification pass)──▶ verified ──(contradiction)──▶ stale/retired
     │                                  │
     └────────(refuted)────────▶ rejected
  • unverified — just written by the model. Visible to the writing run as scratch, but flagged; not promoted to L2 durable, not surfaced to future runs.
  • verified — promoted after a verification pass confirms it. Reuses the N-reviewer panel + signed verdict from the contract system: a claim like “the build passes” is checked (machine check) or judged (no-stake review) exactly as a DoD clause is. Only PASS promotes.
  • rejected — refuted by the panel. Kept (audit) but never surfaced.
  • stale — was verified, later contradicted by a newer verified entry or a failed re-check. Demoted, not deleted.

This is what kills the classic failure: a model hallucinates progress (“auth fixed”), writes it to memory, and a later run builds on the lie. In Truss the lie sits in unverified until evidence promotes it — and the same machinery that guards contract completion guards belief.

3.3 Provenance

Every entry links to the ledger (see durable-execution.md): the ledger_event_id (tool call / check / verdict) that produced or verified it. Querying memory can always return the evidence chain. Verified entries carry the signed verdict that promoted them — same signature scheme as contract verdicts. Memory becomes auditable, not vibes.

3.4 Rust-shaped data model (strawman)

// crate: truss-memory
pub struct MemoryEntry {
    pub id: MemoryId,              // content-addressed
    pub kind: EntryKind,          // Goal | Decision | Fact | Lesson | Artifact | Scratch
    pub key: Option<String>,      // supersede key for Fact/Lesson
    pub body: String,             // the content (model-authored)
    pub trust: Trust,             // Unverified | Verified | Rejected | Stale
    pub provenance: Provenance,   // ledger_event_id(s), produced_by, verified_by
    pub salience: f32,            // relevance score for eviction/ranking
    pub created: Timestamp,       // passed in (no Date::now in pure core)
    pub superseded_by: Option<MemoryId>,
}

pub struct Provenance {
    pub produced_by: LedgerEventId,
    pub verified_by: Option<SignedVerdict>,   // reuse contract verdict type
    pub sources: Vec<LedgerEventId>,
}

pub trait MemoryStore {           // substrate-agnostic, like Provider/Executor
    fn write(&mut self, draft: MemoryDraft) -> MemoryId;        // lands Unverified
    fn promote(&mut self, id: MemoryId, verdict: SignedVerdict);
    fn query(&self, q: MemoryQuery) -> Vec<MemoryEntry>;        // by kind/key/salience/trust
    fn delta_since(&self, cursor: MemoryCursor) -> MemoryDelta; // for L2 delta rehydration
}

v0 backing impl: flat content-addressed files + an index (mirrors the durable-execution ledger on disk). Later: SQLite, then optional vector index for salience/semantic recall — swapped behind MemoryStore with no agent-loop change.


3.5 Policy knobs (user-selectable, layered defaults)

Both verification cost and identity self-amendment are settings, not hard-coded. Granularity: global default → per-agent → per-contract override (contracts already carry config blocks).

VerificationPolicy — when/whether Unverified → Verified promotion runs:

ModeBehaviorCostUse
offwrites stay Unverified, usable in-run, never promoted/surfaced to future runs as trustednonethrowaway / solo speed
lazy (default)verify on first recall, not on write — pay only for memory actually re-usedamortizedmost work
eagersync N-reviewer pass before promotion on writehighhigh-stakes / shared memory
tieredcheap heuristic gate always; full panel only for fact/decision or flagged writesmediumbalanced

The invariant that makes this safe to expose: Verified always means “a panel signed off.” The knob changes when verification runs and what gets verified — never what the label asserts. off doesn’t relabel junk as trusted; it leaves everything Unverified and honestly labeled. Every consumer of memory branches on trust tier regardless of mode (it should anyway). So we lose almost nothing by making this selectable — the only real cost is test surface and keeping defaults honest. Don’t let off silently surface unverified content to future runs as if trusted; that’s the one footgun to guard.

M4 adds these layered knobs (all global → agent → contract, documented in ADR 0007 and examples/harness.toml):

KnobDefaultValues
scopeper-agentper-agent, per-project, shared
conflictverified-winsverified-wins, latest-wins, panel-arbitrated, keep-both-flagged
goal_repin_every8positive integer
projection.keep_tail0 (legacy no-projection)integer
projection.compaction_exempt_tiersl0_core, l0_self, l1_principal, l3_goaltier list
salienceoffoff, harness-recency, model-scored, hybrid (M5 seam only)

IdentityPolicy — who can amend L0 self:

ModeBehavior
lockedoperator-signed only; agent may propose, not commit
propose (default)agent drafts an amendment → verdict gate / operator approval → commit
self_amendagent commits directly (what Matt does in OpenClaw today)

self_amend is safe to expose only because of four structural guards that hold in every mode — these are the “what we’d lose” made into design, not bans:

  1. core is never amendable. Self-amend touches self only. Safety/boundary erosion is structurally impossible — the dangerous half of identity is locked.
  2. Amendments commit at run boundaries, not mid-run. L0 is the frozen cache prefix; a mid-run rewrite busts the most expensive cache and breaks the “identity never churns” guarantee. Buffer the change, apply at session start.
  3. Amendment is a deliberate signed tool call, provenance-logged to the ledger — never inducible from untrusted/injected content. Even in self_amend the proposed change is verdict-checked against core before commit (reuses the N-reviewer machinery — an identity edit is just a high-stakes memory write, so it also respects VerificationPolicy).
  4. Every identity change is a reversible ledger event — diff + rollback across all modes, non-negotiable.

With those four, self_amend loses nothing we care about: the agent evolves its voice/persona freely, can’t rewrite its guardrails, can’t be hijacked into doing so, and every change is auditable and reversible. Without them, self_amend risks value drift, cache thrash, and a prompt-injection takeover surface — so the guards ship with the knob, not as a later hardening pass.

The two knobs unify: an identity amendment is a high-stakes memory write, so it flows through the same trust-tier + N-reviewer + provenance machinery as any fact/decision. One mechanism, two surfaces.

4. Model-facing interface — the native memory tool

Expose L2 to the model through the documented memory_20250818 tool (so we ride Fable’s trained behavior, not a bespoke protocol). Truss intercepts the tool calls and maps them onto MemoryStore:

  • create/str_replace/insert → MemoryStore::write (lands Unverified), with the harness inferring kind from the path (/memories/goal/…, /memories/decisions/…, /memories/lessons/…).
  • view → MemoryStore::query filtered to Verified only by default (the model pulls trusted memory on demand — the “pull, not push” of L2).
  • A verification pass (sync or deferred) runs the N-reviewer panel on new Unverified writes and promotes the survivors.

Delta rehydration: on session resume / re-pin, inject only MemoryStore::delta_since(cursor) — what changed since the model last saw L2 — not the whole store.

This means the agent loop doesn’t learn a new memory API: it uses the tool Anthropic already trained Fable on, and Truss provides the trustworthy substrate underneath.


5. Fable-5 / Mythos tuning (optimize for the leading model)

Per the migration guidance, when the active provider is Fable-5/Mythos:

  • Give it the memory surface and tell it the format. Inject a short L0/L3 instruction: where memory lives, when to consult it, the one-lesson-per-entry + one-line-summary format. (Anthropic shows this materially improves Fable’s memory use.)
  • Let it update lessons mid-task. Don’t gate write; gate promotion. Fable is good at on-the-fly curation — capture it, verify it lazily.
  • Async sub-agent memory. Long-lived sub-agents keep their own L4 + shared L2; they communicate asynchronously with the orchestrator (Fable is documented as reliable at this) rather than spawn-and-block. Shared L2 is the substrate.
  • Don’t over-prescribe. Fable degrades with heavy step-by-step scaffolding — state the memory goal and format, not a procedure.

Provider-agnostic fallback: models without the memory tool get an L2 surfaced as a synthetic read_memory/write_memory tool pair over the same MemoryStore.


6. Build phasing

  • M1 — Store + provenance. truss-memory crate: MemoryStore trait, flat-file impl, typed entries, provenance into the existing ledger. No model yet; unit tests. (Mirrors how truss-core ledger was built.)
  • M2 — Memory tool binding. Intercept memory_20250818 in the agent loop, map to MemoryStore, path-based kind inference, view returns Verified-only. Hermetic tests with a fake provider emitting tool calls.
  • M3 — Trust tiers + promotion. Wire the N-reviewer panel + signed verdict to promote Unverified → Verified; contradiction → Stale. Reuse contract verdict types end-to-end.
  • M4 — Layered injection. L0–L4 assembly with cache-aligned placement; goal ledger re-pin cadence; delta rehydration. Identity (L0) becomes a signed frozen artifact.
  • M5 — Fable tuning + salience. Format-prompt injection per provider; salience scoring + eviction; optional vector backing behind MemoryStore.

Each milestone is independently testable and lands behind the same feature-flag/ADR discipline as the provider arc.


7. Open questions (pressure-test before M1)

  • Verification cost. RESOLVED → §3.5 VerificationPolicy (off / lazy (default) / eager / tiered, user-selectable, layered defaults).
  • Who writes L0 identity, and can the agent self-amend? RESOLVED → §3.5 IdentityPolicy (locked / propose (default) / self_amend) + the L0 core/self split in §2. Self-amend is exposable because of four structural guards (core locked, run-boundary commit, signed deliberate tool, reversible ledger event).
  • Salience / eviction policy. M4 adds a default-off SaliencePolicy seam; scoring and eviction remain M5.
  • Cross-run vs cross-agent scope. RESOLVED in M4 → configurable scope (per-agent default, per-project, shared).
  • Conflict resolution. RESOLVED in M4 → configurable conflict (verified-wins default, latest-wins, panel-arbitrated, keep-both-flagged).
  • Identity vs memory leakage. Keep L0/L1 out of L2 so persona can’t be silently rewritten by a memory write (prompt-injection surface).

Provenance of this doc: design discussion on 2026-06-16, grounded in local inspection of a 470-turn Fable 5 run and the Anthropic API documentation for memory tools and context editing.

Truss Memory & Identity — Implementation Plan

Status: build-ready, 2026-06-16. Companion to memory-identity-architecture.md (the design) and the drafted skeleton at crates/truss-core/src/memory/. Each milestone is independently shippable, behind the same ADR/feature discipline as the provider arc (v0.7→0.11). Acceptance = tests green + clippy/fmt clean.

Goal restated (from the #truss discussion): give Truss a typed, trust-tiered, provenance-tracked memory the model reaches via the native memory_20250818 tool, plus a layered, cache-aligned identity/context injection (L0–L4) that replaces flat-markdown dumping — optimized for Fable-5/Mythos, the model with the highest return on a real memory surface. Two user-selectable knobs: VerificationPolicy and IdentityPolicy.

Naming: truss-core::identity = ed25519 signing key (existing). Agent persona = memory::persona (new). Memory lives as a module in truss-core, reusing the SQLite store, Signer, and delegation::Verdict.


M1 — Store + provenance (foundation)

Why first: everything else writes to or reads from the store. It’s self-contained, pure-ish, and fully unit-testable with no model in the loop — exactly how the durable ledger was built.

Tasks:

  1. Wire the module: add pub mod memory; to lib.rs. Resolve any compile drift in the drafted skeleton (it references delegation::Verdict, store, identity).
  2. SqliteMemoryStore: implement the MemoryStore trait against the existing durable DB connection (share Arc<Mutex<Connection>> with crate::store::Store, or open the same file). Add the memory table from the schema sketch in store.rs (+ write_seq monotonic counter, index on (mem_key, trust) and write_seq).
  3. Content-addressing: MemoryId = hex sha256 over kind || key || body (reuse the sha2 dep already in store.rs).
  4. Supersede semantics: write of a Fact/Lesson with an existing mem_key marks the prior entry superseded_by + bumps write_seq.
  5. delta_since(cursor) = WHERE write_seq > cursor ORDER BY write_seq.
  6. Provenance: persist produced_by (session, seq) and (on promote) the full Verdict JSON.

Tests (crates/truss-core/tests/memory_store_test.rs):

  • write → get round-trips; lands Unverified.
  • supersede-by-key sets superseded_by, old entry no longer in default query.
  • query default returns Verified only; min_trust floor respected.
  • promote attaches verdict, flips tier, appears in next delta_since.
  • delta_since returns only post-cursor changes (incl. tier changes).

Acceptance: new tests green; existing 60 unchanged; clippy/fmt clean. ADR 0004 “Governed memory substrate.”


M2 — Memory tool binding (memory_20250818)

Why second: makes the store reachable by the model using the tool Anthropic already trained Fable on — no bespoke protocol.

Tasks:

  1. Register a memory tool (type: "memory_20250818") in the tool registry (tools/mod.rs), gated by a session flag so existing runs are unaffected.
  2. Map commands → MemoryStore:
    • create/insert/str_replace → write (Unverified). Infer kind from the path prefix (/memories/goal/…, /decisions/…, /lessons/…, /facts/…, /artifacts/…, else scratch).
    • view → query (default Verified-only); render as a dir/file listing the model expects.
    • delete/rename → supersede / mark_stale.
  3. Provenance capture: stamp produced_by with the current journal seq at the moment the tool call is journaled (the binding runs inside the agent loop, which already writes the ledger).
  4. Emit AgentEvents for memory ops (extend events.rs: MemoryWrite, MemoryRecall) so the UI/audit log sees them.

Tests (hermetic, fake provider emitting tool calls — mirror agent_loop_test.rs):

  • model create under /memories/lessons/x → entry exists, kind=Lesson, Unverified.
  • model view returns only Verified entries.
  • path→kind inference table.

Acceptance: hermetic tests green; flag-off path identical to today.


M3 — Trust tiers + promotion (the moat)

Why third: turns “stored” into “trusted.” Reuses the N-reviewer panel + signed verdict end-to-end — the same code that gates contracts now gates belief.

Tasks:

  1. VerificationPolicy resolution at session build (MemoryConfig), layered global → per-agent → per-contract (extend the contract [memory] block).
  2. Promotion driver:
    • Eager: on write, run the N-reviewer panel (delegation) on the claim → PASS calls promote, FAIL calls reject.
    • Lazy: on view/recall of an Unverified entry, verify-then-surface; cache the verdict so it runs once.
    • Tiered: heuristic gate (cheap model or rule) always; full panel only for Fact/Decision (MemoryConfig::verify_on_write).
    • Off: never promote; view may still return Unverified but tagged.
  3. Contradiction handling: a new Verified entry that supersedes a key demotes the prior to Stale.
  4. Recall honesty: entries surfaced to the model always carry their trust tier in the rendered view (no silent trust laundering).

Tests:

  • Eager: a verdict-PASS claim promotes; FAIL rejects and is never surfaced.
  • Lazy: Unverified entry triggers one verification on first recall, then cached.
  • Off: Unverified entries surface tagged, never as Verified.
  • supersede demotes prior to Stale.

Acceptance: each policy mode covered; verdict reuse verified by sharing delegation types (no new verdict path).


M4 — Layered injection (L0–L4) + identity

Why fourth: assembles the prompt from the tiers and aligns them to the cache. This is where the “stop dumping 37KB markdown” win actually lands.

Tasks:

  1. Context assembler: build tools → system(L0 core, L0 self, L1) → messages(L3 goal ledger, history, L2 pulls, L4 scratch). Place cache_control so L0/L1 are the stable cached prefix.
  2. Persona: load/persist PersonaCore (operator-signed via identity::Signer)
    • PersonaSelf; render() into L0. Verify the core signature on load.
  3. Goal ledger (L3): pin the active contract intent + DoD; re-inject every N steps; never compacted.
  4. Delta rehydration: on resume/re-pin, inject delta_since(cursor) for L2, not the whole store.
  5. IdentityPolicy + amendment flow: route_amendment → buffer CommitAtBoundary; apply at next session build as a reversible journal event; verdict-check self against core before commit. A dedicated signed amend_identity tool (deliberate, injection-resistant).

Tests:

  • assembled prompt has stable L0/L1 prefix across turns (cache key unchanged when only L3/L4 change).
  • core signature mismatch refuses to load.
  • amendment under Propose needs approval; under SelfAmend commits at boundary and is rollback-able; an amendment targeting core is Denied.

Acceptance: a long-run example shows cache-read tokens dominating (prefix held), and goal-ledger re-pin keeps intent across compaction.


M5 — Fable tuning + salience

Tasks:

  1. Provider-aware L0/L3 memory-format instruction (Fable: “write learnings here, this format, consult next session”); provider-agnostic fallback tool pair (read_memory/write_memory) for models without memory_20250818.
  2. Salience scoring + eviction (recency + recall-count + optional model score); Stale decay policy.
  3. Optional vector backing behind MemoryStore for semantic recall (no agent-loop change).
  4. Async sub-agent shared-L2 plumbing (ties into the multi-agent thread).

Acceptance: Fable run uses the memory tool unprompted-but-guided; salience evicts without losing Verified high-salience entries.


Cross-cutting

  • Config surface: [memory] contract block + global config: verification, identity, format, re-pin cadence N.
  • Open questions to settle before M4 (M1–M3 don’t depend on them): salience policy details, L2 scope (per-agent / per-project / shared trust boundaries), conflict resolution when two Verified facts collide.
  • Each milestone = one PR + one ADR section, same cadence as the provider arc.

Where to start

M1, task 1–2: wire the module + SqliteMemoryStore. It’s the foundation every later milestone reads/writes, it’s fully testable without a model or a provider, and it reuses machinery that already exists (SQLite store, sha2, Signer, delegation::Verdict). One PR, hermetic tests, zero risk to the green build until pub mod memory; flips on. Ship M1 → M2 → M3 in order; M1–M3 are the moat, M4 is the injection win, M5 is the Fable polish.

Memory Evolution Path (simple now → complex later, swap not rewrite)

Status: forward-compat contract. Captured 2026-06-17 per Matt: “the simple memory is OK for now, but know the path forward and build with it in mind — make it easy to update to a more complex memory scheme.” This doc names the seam, the staged upgrade path, and the invariants that must hold so each upgrade is a backend swap behind a stable interface.

The upgrade seam (already in place)

Everything above the substrate talks to memory through three stable types in truss-core::memory. A more complex scheme is a new implementation behind these, never a change to the callers:

  • trait MemoryStore — write / promote / reject / mark_stale / get / query / delta_since / write_policy / get_policy. The ONE thing the rest of the harness depends on. SqliteMemoryStore is the current impl; a complex scheme is a new impl (or a decorator) implementing the same trait.
  • MemoryEntry — already carries trust, provenance, salience (0–1 for ranking/eviction), key (supersede slug), superseded_by. The fields a richer scheme needs already exist.
  • MemoryQuery — kind / key / min_trust / contains / limit. contains is already annotated “backed by a vector index” — semantic recall slots in here without changing the call sites.

Rule we build by: callers (SI-1 extraction, SI-2 promotion, SI-3 recall, M4 injection) only ever go through MemoryStore + MemoryQuery. No caller reaches into SQLite or assumes exact-key lookup. (Verified: SI-3 recall is query-shaped and token-budgeted, not key-coupled.)

Invariants every future scheme must preserve

These are the moat; an upgrade may NOT weaken them:

  1. Writes always enter Unverified. Promotion stays a separate, panel-gated step.
  2. Verified always means “N-reviewer panel signed off.” Retrieval can get smarter; the trust label’s meaning is fixed.
  3. Provenance (LedgerRef) on every entry, validated under --verify.
  4. Reversibility — supersede/demote-to-Stale stays traceable in the ledger.
  5. Verified-only default view — richer recall must still default to trusted-only.

So long as a new backend honors these, it can change how entries are stored and ranked freely.

Staged path forward (each stage = new/extended MemoryStore, callers untouched)

  • S0 — current (simple). SQLite content-addressed store; exact-key + contains substring; trust tiers + provenance + supersede. Good enough for L2 now.
  • S1 — shipped in 0.55.0: semantic recall (vector). Adds an embedder seam, embedding-on-write for semantic stores, explicit truss memory reindex backfill, and vector-ranked MemoryQuery.contains behind query(). The config gate is memory.recall = "exact" | "semantic" | "hybrid", with exact as the default. The baseline ranker is exact brute-force cosine over stored vectors; sqlite-vec / ANN remains the S2-era scale swap.
  • S2 — hybrid ranking. BM25 + vector + salience + recency blend in query() (mirrors OpenClaw’s hybrid search). Still just query() internals.
  • S3 — salience & eviction / compaction. Activate the existing salience field: a scoring pass + budgeted eviction/summarization of cold entries (hierarchical/episodic compaction). Caps already exist (recall token budget).
  • S4 — episodic vs semantic split / associative (graph) memory. New EntryKinds and/or a graph decorator over MemoryStore for linked recall. Additive — new kinds, same trait.
  • S5 — scope tiers as backends. Per-agent / per-project / shared L2 can each be a different MemoryStore impl composed behind one facade (already an enumerated scope knob).

What this means for current building

  • Keep all memory access behind MemoryStore + MemoryQuery (enforced now).
  • Treat contains as the semantic-recall entry point; don’t add a parallel retrieval API.
  • Keep salience populated-or-zero so S3 can turn on without a migration.
  • S1 is now in place behind config. Keep future recall improvements behind MemoryStore::query; do not claim task-outcome gains until SI-4 paired eval measures them.

Net: the simple scheme ships now; the complex scheme is a backend swap we’ve already cleared the runway for.

Compaction Controller — proactive, governed, auditable context compaction

Status: design (pre-build). Target milestone arc: CC-1 → CC-4. ADR: 0020 (added with CC-1).

Problem

Long agent runs hit the model’s context window. When they do, something compacts the transcript — and today that something is outside Truss’s control: the host harness (Claude Code, OpenClaw, Codex) or an opted-in provider feature (Anthropic context_management / memory tool) summarizes or drops messages on its own schedule, by its own rules, with no record of what it threw away. The result is the momentum-killer Matt flagged: the run silently loses a decision, a goal, or a constraint mid-stream and slows to a crawl while it re-derives what it already knew.

Reframe: we don’t predict provider compaction — we own it

The LLM API does not silently compact. The context window is a hard wall; the API rejects an over-long request, it does not quietly trim it. What “compacts” is always a layer around the model. And in Truss’s design, we are that layer — agent.rs + projection.rs assemble every request, so at every step we already know:

  • exact projected token count (we build the prompt; BudgetMeter meters it),
  • the model’s context window (a provider capability, see CC-1),
  • therefore headroom to the wall as a first-class signal — a gauge, not a surprise.

So the goal is not to guess when an opaque provider will compact. It is to make Truss perform a governed compaction on our own terms, before any host/provider compaction can fire — and to record exactly what happened so nothing is ever truly lost.

Why Truss is uniquely positioned (the moat)

  • Journal is authoritative. Every turn is in the signed, hash-chained journal (store.rs); replay.rs::reconstruct can rebuild the model’s working context from any point. So compaction is only ever a presentation/projection concern, never data loss. A normal chat harness deletes; Truss elides a copy and keeps the original on disk, signed.
  • Already half-built. M4 shipped layered L0–L4 injection + projection.rs (Head + structural-summary-of-middle + verbatim tail) + compaction_exempt (l0_core, l0_self, l1_principal, l3_goal) + goal_repin_every. The goal ledger and identity are already protected from compaction. What’s missing is the active controller that decides when to compact and records it.
  • Governed + auditable + recallable. A compaction becomes a signed ledger entry plus a trust-tiered memory entry holding the summary, with provenance back to the elided journal range. You can audit what was summarized and recall the verbatim original. No frontier harness ships this.
  • Eval-able. Reuse the existing eval engine to run one contract under different compaction policies (off / threshold sweeps / summarize-vs-evict) and compare verdict + cost + turns-to-done. Per the everything-configurable directive, every knob is a setting so we can measure paths on real work.

Caveat — delegated tier

Full control only exists on the in-loop / API path, where Truss owns the context. On the delegated path (claude -p, codex exec), the vendor CLI owns its own compaction; Truss can observe and bound (budget/turns) but not govern it. This reinforces the existing thesis: in-loop is the real product; delegated is the convenience tier.

Design

Components

  1. ContextGauge (truss-core): computes pressure = projected_tokens / context_window at each step boundary. Requires a provider capability context_window() (CC-1) and a model→window map seeded from pricing.rs/capability metadata. Emits context_pressure.
  2. CompactionPolicy (configurable, layered global → agent → contract):
    • mode: off | summarize (default) | evict
    • threshold: pressure at which to act (default 0.70)
    • exempt: reuse M4 compaction_exempt layers (never compact L0/L1/L3)
    • min_tail: verbatim recent turns always kept
    • anticipate: forward-estimate on/off (CC-3)
  3. Governed compaction action (CC-2): at a run-step boundary (never mid-step — protects the KV-cache prefix and the determinism discipline, same rule as identity self-amend), when pressure ≥ threshold: summarize the elided middle via projection.rs into a memory entry (EntryKind::ContextSummary, trust per VerificationPolicy), stamp provenance LedgerRef to the elided journal range, keep exempt layers + min_tail verbatim, continue.
  4. Provenance (CC-2): signed compaction_performed ledger entry recording before/after token counts, the summary memory id, and the elided journal range; Store::verify_chain covers it. Fully recallable + auditable.

Anticipation (Matt’s question, CC-3)

Beyond reading current pressure, estimate steps-to-threshold from the running average per-step token growth (tool-result sizes dominate). That yields a true early warning (compaction_imminent) and lets the controller compact one boundary ahead of the wall instead of right at it — smoother, fewer cache-busting events. Optional CC-4 interop: when talking to Anthropic, set provider-side context_management to defer to ours (or leave it off) so there is exactly one compaction authority — Truss.

Event / ledger vocabulary (grows the provenance spine, per the discipline rule)

  • context_pressure { tokens, window, pressure } — observability (CC-1)
  • compaction_imminent { steps_to_threshold, pressure, window } — early warning (CC-3)
  • compaction_performed { before, after, summary_memory_id, elided_range } — signed ledger (CC-2)

Milestones (1 milestone = 1 PR + 1 ADR section)

  • CC-1 — Gauge + observability (FOUNDATION, no behavior change). Provider::context_window() capability + model→window map; ContextGauge::pressure(); context_pressure event each step. Pure measurement, nothing compacts yet. Tests: pressure math, window map, event emitted. → ADR 0020. Start here.
  • CC-2 — Governed compaction. CompactionPolicy (layered config) + boundary-safe summarize-elided-middle → ContextSummary memory entry + signed compaction_performed ledger; exempt layers honored; recallable. Hermetic tests incl. “exempt layers survive” and “elided range recoverable from journal.”
  • CC-3 — Anticipation. steps-to-threshold estimator + compaction_imminent + compact-ahead. → ADR 0022.
  • CC-4 — Eval. hermetic paired eval (off / governed threshold / anticipate) via a scripted provider workload, with a reproducible comparison table and ADR 0023. Live SWE-bench and Anthropic context_management interop stay out of scope for this milestone.

Invariants (any implementation must hold)

  1. The journal stays authoritative; compaction never deletes journal entries, only changes what is projected into the model’s context.
  2. Exempt layers (L0 core/self, L1 principal, L3 goal) are never compacted.
  3. Every compaction is a signed ledger entry and is recallable.
  4. Compaction happens only at a step boundary, never mid-step.
  5. Every knob is configurable (global → agent → contract) with a safe default.

Truss Self-Improvement Architecture (L2 now, L3 as north star)

Status: design / roadmap. Captured 2026-06-17 from the #truss discussion. Decision (Matt): L2 is the must-have; L3 is the goal but must not block L2. This doc shows why building L2 correctly is building most of L3’s substrate, so the two share one governance mechanism.

The ladder

  • L0 — none. Stateless; repeats mistakes forever. pi-class harnesses.
  • L1 — in-run self-correction. Model reviews/revises its own work within one task (retries, self-review, the verified loop catching a failure and re-attempting). Truss already has this (contract → execute → adversarial N-review → signed verdict).
  • L2 — verified cross-run learning. A lesson learned on one run changes behavior on a later run. The must-have. This is our wedge.
  • L3 — self-tuning the harness. The system adjusts its own config (routing thresholds, persona, tool selection, verification policy, salience) from measured outcomes. North star; non-blocking.
  • L4 — recursive self-modification. Agent rewrites its own code / spawns improved successors. Governance hard-line; explicitly NOT a target.

Why L2 is the differentiator

Every harness can “write notes to memory.” That’s the trap: ungoverned self-learning poisons itself — the agent stores a wrong lesson as fact and compounds it. Most products quietly accept that rot.

Truss already has the antidote (built for other reasons): trust tiers (unverified→verified), the N-reviewer panel that can gate a belief the same way it gates a result, and provenance — every belief chains back to the run that produced it in a signed ledger. So our L2 is not “self-improvement,” it is verified, auditable, reversible self-improvement: a learned belief is quarantined until an independent panel confirms it against evidence; if it later causes a regression, the ledger lets you trace the exact lesson and revoke it. That is the enterprise/regulated-industry version of L2. No frontier harness ships it today.

The unifying insight: L2 and L3 are the same mechanism, different write-target

  • L2 = an automated proposer writes beliefs/lessons → governed store → panel-gated promotion → recalled into future runs.
  • L3 = an automated proposer writes policy/config deltas → governed store → panel-gated promotion → applied to future runs.

Both reuse the same rails: trust tiers, delegation::Verdict (N-reviewer), provenance ledger, reversibility (supersede/demote), and the child_run substrate for evaluation. Therefore: if we give policy/config the same governed-write shape as memory now, L3 becomes incremental — no new trust model, no new safety story. That is exactly how L3 stays a goal without blocking L2.

L2 — buildable plan (the must-have)

Milestones, each ~1 PR + ADR section, all knobs configurable (per Directive 1):

  • SI-1 Lesson extraction at contract close. After a verified verdict, a step proposes generalizable lessons → written as EntryKind::Lesson Unverified entries with provenance (the run’s LedgerRef). Policy: off | on | model-scored.
  • SI-2 Lesson promotion. Route lessons through the existing N-reviewer panel (delegation::Verdict) to gate Unverified→Verified against evidence. Reuses M3 trust-tier machinery. Reversible: a regression supersedes/demotes the lesson.
  • SI-3 Lesson recall. Promoted lessons feed the M4 layered L2 injection (salience + scope already enumerated knobs). Verified-only by default.
  • SI-4 Paired eval harness. Run the same contract learning-on vs learning-off via child_run; compare verdict pass-rate + cost. This proves L2 helps instead of asserting it — and it is the signal source L3 will optimize against.

Shares substrate with build-order #2 (spawn/fan-out): best-of-N fan-out produces higher-quality lesson candidates and SI-4’s paired evals ride child_run.

L3 — forward seams to bake in now (so it never becomes a rewrite)

Do NOT build the optimizer yet. Just make the target shape right:

  • Tunables become typed, versioned, signed Policy objects living in the same governed store with provenance — structurally identical to a memory entry / identity amendment. (Routing thresholds, persona self layer, tool-set, VerificationPolicy, salience, goal_repin_every, etc.)
  • L3 = “an optimizer proposes a policy delta” → same panel + provenance + reversible-ledger gate before it takes effect. The optimizer can start dumb (bandit/grid over enumerated knobs using SI-4 eval results) and later become a small local routing/tuning model (ties to the local-first + smart-routing pillar).
  • Safety lines (hard): l0_core identity + operator-signed boundaries are never auto-tunable. Every L3 proposal is reversible, signed, panel-gated, and high-stakes deltas can require a human approval gate. Never crosses into L4 (self-rewriting code).

Sequencing recommendation

  1. Best-of-N fan-out on child_run (build-order #2) — also the lesson-quality and eval engine.
  2. SI-1 → SI-2 → SI-3 (the L2 learning loop).
  3. SI-4 paired eval harness (proves L2; feeds L3).
  4. In parallel/throughout: give policy/config the governed-write shape (L3 seam), but ship no optimizer until L2 is proven by SI-4.

Result: L2 delivered and measured; L3 reachable by adding one proposer against rails that already exist.

Self-Improvement Implementation Plan

Status: active build plan. Captured 2026-06-17. Companion to self-improvement-architecture.md (L2 now / L3 north-star, non-blocking) and extensibility-architecture.md (child-run keystone, merged v0.17.0). Each step = ~1 PR + 1 ADR section. All policy knobs configurable (Directive 1).

Where we start and why

The child-run substrate (v0.17.0) is the keystone. Step 1 is best-of-N fan-out on top of it, because that single primitive is also the lesson-quality engine and the paired-eval engine the L2 loop needs — so it is the shortest path that serves both the missing base primitive AND self-improvement.

Steps

Step 1 — Best-of-N fan-out on child-run (PR, v0.18.0) ← BUILD NOW

Run K child-runs of the same task concurrently (reusing child_run), collect their verified results, apply a Reducer. Reducers: Vote, JudgePanel (reuse delegation reviewers), BestScore, FirstSuccess. New events fanout_started / fanout_completed (per-child refs + which child won + why); ledger records the reduction decision and anchors each child. Hermetic tests. Explicitly leave as follow-up seams: the model-facing spawn tool, and refactoring the existing N-reviewer panel onto fan-out.

Step 2 — Spawn tool + panel refactor (PR, v0.19.0)

Expose child_run as a model-facing spawn tool (model can delegate a scoped sub-task and get a verified result back). Refactor the existing N-reviewer panel to be a JudgePanel fan-out instance (dedup the one-off). No new trust model.

Step 3 — SI-1 lesson extraction (PR)

At contract close, after a verified verdict, a step proposes generalizable lessons → EntryKind::Lesson written Unverified with provenance (LedgerRef to the producing run). Policy: extraction = off | on | model-scored. Also start the L3 seam here: introduce a typed/versioned/signed Policy governed-write object shape (same store/provenance as memory) — no optimizer.

Step 4 — SI-2 lesson promotion (PR)

Route lessons through the N-reviewer panel (delegation::Verdict) to gate Unverified→Verified against evidence. Reversible: regression supersedes/demotes. Reuses M3 trust-tier machinery end-to-end.

Step 5 — SI-3 lesson recall (PR)

Promoted lessons feed the M4 layered L2 injection. Verified-only by default; salience + scope are the already-enumerated knobs.

Step 6 — SI-4 paired eval harness (PR)

Run the same contract learning-on vs learning-off via child-run/fan-out; compare verdict pass-rate + cost; emit a report. Proves L2 helps; becomes the signal source an L3 optimizer would later optimize against.

L3 (north star — NOT built in this plan)

Once L2 is proven by SI-4: add a policy optimizer (dumb bandit/grid over enumerated knobs → later a small local tuning model) that proposes policy deltas through the SAME panel + provenance + reversible gate. l0_core / operator boundaries never auto-tunable; high-stakes deltas can require a human gate; never L4 (self-rewriting code).

Gate discipline (every step)

cargo fmt --check · cargo clippy --all-targets --all-features -- -D warnings · cargo test · cargo test --all-features · cargo build --release. ALINA re-runs gates on merge; PRs left open for review; ALINA tags on merge.

Self-Validation Sandbox — isolated, reproducible execution for in-loop testing

Status: design (pre-build). Milestone arc: SB-1 → SB-4. First ADR: 0025 (with SB-1).

Why (surfaced from the SWE in-loop run)

The in-loop SWE-bench finding was concrete: Truss’s own agent loop leaned on bash, did weak local validation, and never ran the task’s own tests during the solve — the Docker grader was the only source of truth. So the model coded blind until the verify boundary. Two problems fall out of that one observation:

  1. Capability gap — the agent has no trustworthy, reproducible way to check its own work mid-solve (run the project’s tests, see them pass) before we grade it. Tighter feedback = better solves, fewer wasted steps, richer evidence.
  2. Security gap — today’s “sandbox” is application-level only: Policy (write/read scope, network, forbidden_substrings, allow_tools) + bash kill_on_drop + Ctx deadline. That runs as the same user with substring matching — a permission layer, not isolation. It will not survive buyer security diligence and blocks multi-tenant.

These are different questions (capability vs. security) but they share one substrate: run tool/test execution inside a confined, reproducible environment. Build that substrate once; deliver the capability first (SB-1), harden it into the security boundary after (SB-2..4). This is the “don’t build the box twice” plan.

Design

The seam: a Sandbox trait (truss-core)

All tool execution (bash today; run_tests/file ops next) routes through the active Sandbox. Policy still applies on top (sandbox confines; policy authorizes). Default is NoSandbox = today’s behavior exactly → zero behavior change unless opted in (same discipline as the compaction controller).

trait Sandbox {
    fn run(&self, cmd: Command, ctx: &Ctx) -> Result<Output>;   // confined exec
    fn materialize(&self, checkout: &Path) -> Result<WorkDir>;  // reproducible workspace
    fn capabilities(&self) -> SandboxCaps;                       // fs/net/pid isolation level
}

Implementations, in build order:

  • NoSandbox — host exec under Policy (current). Default.
  • WorkingCopySandbox (SB-1) — ephemeral overlay/copy of the checkout so tests run without polluting the real tree; process reaped; result captured. Real reproducibility + blast-radius win even before kernel isolation.
  • LocalSandbox (SB-2) — unprivileged Linux namespaces (bubblewrap-style: mount/pid/net), Policy scopes enforced at the namespace boundary, optional network egress off.
  • ContainerSandbox (SB-3) — Docker/Podman image for full reproducibility (SWE-bench parity, commercial multi-tenant).

Self-validation flow (the capability)

A model-facing self_check path (or sandboxed run_tests) runs the project’s declared test command inside the active sandbox during the solve. Its result is EVIDENCE, never the verdict — the executor still cannot grade its own homework. The verify boundary still runs harness-owned checks + the N-reviewer panel. A self-validation run is recorded as a signed self_validation { cmd, exit, summary, workdir_hash } journal entry → it becomes part of the provenance the verdict can cite (“the agent’s own tests passed, and independently so did ours”).

Why this is on-thesis

  • Reproducible + recorded self-checks make the evidence bundle stronger, not just the solve.
  • It composes with everything: durable journal (records runs), Policy (authorizes), Ctx (deadline/cancel/budget), per-tenant Principal (SB-4 scoping).
  • It’s the substrate the security isolation needs anyway — capability now, security next, one box.

Milestones (1 milestone = 1 PR + 1 ADR section)

  • SB-1 — Sandbox trait + WorkingCopySandbox + self-validation evidence (START HERE). Sandbox trait; NoSandbox default (no behavior change); WorkingCopySandbox (ephemeral overlay/copy, reaped, captured); model-facing sandboxed test-run path; signed self_validation journal entry (evidence, NOT verdict). Config-selectable global→agent→ contract, default off. Hermetic tests (overlay isolation, evidence recorded, default-off unchanged). → ADR 0025.
  • SB-2 — LocalSandbox (namespaces). Unprivileged mount/pid/net isolation (bubblewrap-style); Policy scopes enforced at the namespace boundary; network egress toggle. → ADR 0026.
  • SB-3 — ContainerSandbox. Docker/Podman reproducible env; image pinning; SWE-bench parity path reuses it. → ADR 0027.
  • SB-4 — Per-tenant isolation + hardening. Principal → sandbox boundary; seccomp/egress policy; closes the documented execution-isolation gap. → ADR 0028.

Invariants

  1. Self-validation output is evidence, never the verdict (executor never grades own homework; verify boundary unchanged).
  2. Default NoSandbox — no behavior change unless opted in.
  3. Sandbox composes with Policy (sandbox confines, policy authorizes) — neither replaces the other.
  4. Every sandbox run is recorded in the signed journal (provenance/recallable).
  5. Every knob configurable global → agent → contract with a safe default.
  6. The trait is the stable seam; swapping WorkingCopy → Local → Container must not change the loop, the journal shape, or the verify boundary.

Configuration Surface

Truss accepts configuration from a TOML file and then lets CLI flags override the same resolved values. The normal shape is:

[provider]
kind = "anthropic"            # anthropic | openai
model = "claude-opus-4-8"
# base_url = "http://localhost:11434/v1"
# api_key_env = "ANTHROPIC_API_KEY"

[agent]
system = "You are a precise engineering assistant."
max_tokens = 4096
max_steps = 24
# reasoning = "high"          # minimal | low | medium | high
# temperature = 0.2

[budget]
max_cost_usd = 2.00
max_total_tokens = 500000
max_steps = 40

[tools]
memory = false
git = false
github = false
web = false

# [tools.web_search]
# endpoint = "https://api.search.brave.com/res/v1/web/search"
# api_key_env = "TRUSS_WEB_SEARCH_API_KEY"

[policy]
write_scope = ["/home/you/project"]
read_scope = ["/home/you/project"]
network = "none"              # none | open | allowlist
allow_tools = ["read_file", "write_file", "list_dir", "bash"]
forbidden = ["rm -rf /", "git push"]

[[mcp.servers]]
name = "github"
transport = "stdio"
command = "gh-mcp-server"
args = ["--repo", "ControlStackAI/truss"]
allow_tools = ["list_issues", "create_pr"]

Use it with:

truss --config examples/harness.toml --tools "summarize this repository"

Memory Recall

Memory recall is default-exact. Existing runs keep the same substring and lexical ranking behavior unless a config layer explicitly opts into semantic recall.

[memory]
store_backend = "sqlite"       # sqlite | ai_memory
# ai_memory_socket = "/run/user/1000/ai-memory.sock" # required for ai_memory
recall = "exact"              # exact | semantic | hybrid
embedding_backend = "open-ai" # open-ai | deterministic
embedding_model = "text-embedding-3-small"
embedding_api_key_env = "OPENAI_API_KEY"
embedding_dims = 1536
# embedding_base_url = "https://api.openai.com/v1"

semantic embeds new writes and backs MemoryQuery.contains with brute-force cosine ranking over stored vectors. hybrid combines exact substring matches with vector ranking and prefers exact hits before semantic-only hits. Both modes still compose with min_trust, kind, key, supersession, and query limits; the default memory view remains verified-only.

The real backend is OpenAI-compatible. embedding_api_key_env names the env var that holds the API key and defaults to OPENAI_API_KEY; embedding_base_url can point at a compatible endpoint. The deterministic backend is for hermetic tests and local smoke runs, not quality recall.

ai_memory is an explicit opt-in and currently supports exact recall only. The socket path must be absolute. SQLite remains the default when store_backend is omitted.

Rows written before S1 have no vector. Backfill them explicitly:

truss --config examples/harness.toml --db harness.db memory reindex
truss --config examples/harness.toml --db harness.db memory reindex --json

Semantic queries fail with a reindex-needed error if otherwise eligible entries lack vectors for the current embedder id/dimensions, so old memory is not silently missed.

See ADR 0040 for the backend decision and invariant preservation notes.

Memory Compaction

Governed compaction is default-off. When enabled, it only changes the projected context sent to the provider; the durable journal remains authoritative and every compaction is recorded as a signed ledger event.

[memory.compaction]
mode = "off"          # off | summarize | evict
threshold = 0.70      # context pressure, 0.0 through 1.0
min_tail = 6          # recent messages retained verbatim
anticipate = false    # opt in to compact-ahead behavior
# lookahead_steps = 1
# exempt_tiers = ["l0_core", "l0_self", "l1_principal", "l3_goal"]

The block can be set globally as [memory.compaction], under [agent.memory.compaction], or in a contract’s [memory.compaction]. Resolution is global, then agent, then contract. CLI flags override the resolved policy:

truss --durable \
  --compaction-mode summarize \
  --compaction-threshold 0.70 \
  --compaction-min-tail 6 \
  --compaction-anticipate \
  "..."

See ADR 0024 for the accepted CLI surface and Contract with Compaction for a worked example.

Approval Gates

Approval gates are default-off. When enabled, they suspend durable runs before configured high-risk tools execute or after spend crosses a configured threshold. The run releases only when a signed approved human_review entry is recorded; reject and needs-changes entries keep it suspended.

[approval_gates]
enabled = false
high_risk_tools = ["bash", "write_file", "mcp:payments:*"]
spend_threshold_usd = 1.00
per_call_spend_threshold_usd = 0.25

[agent.approval_gates]
enabled = true
high_risk_tools = ["bash"]

Contract files can set [approval_gates] as the highest-precedence layer. Tool patterns support exact names and * wildcards. See ADR 0037 for the release/block semantics.

Native Git and GitHub Tools

Native git and GitHub tools are default-off. Enable them with CLI flags:

truss --durable --tools --git-tools --github-tools --workdir /home/you/project "..."

Or enable them in config:

[tools]
git = true
github = true

[policy]
write_scope = ["/home/you/project"]
read_scope = ["/home/you/project"]
network = "allowlist"
allowlist = ["github.com", "api.github.com"]
allow_tools = [
  "read_file",
  "write_file",
  "git_status",
  "git_diff",
  "git_commit",
  "github_pr_create",
  "github_pr_checks",
]

Git tools are registered as git_status, git_diff, git_branch, and git_commit. GitHub tools are registered as github_pr_view, github_pr_create, github_pr_diff, github_pr_checks, github_issue_view, github_issue_list, and github_check_status.

In durable and contract runs, these tools record signed git_tool_call and github_tool_call provenance entries. Read-only outputs are hashed into the ledger; side-effecting operations such as git_commit and github_pr_create also memoize the result so resume does not repeat the side effect. See Native Git and GitHub Tools and ADR 0038.

Native Memory Tools

Native memory query/view tools are default-off. Enable them with a CLI flag:

truss --durable --tools --memory-tools --workdir /home/you/project "..."

Or with config:

[tools]
memory = true

[policy]
allow_tools = ["read_file", "memory_query", "memory_view"]

memory_query searches L2 durable memory using the resolved memory recall mode. memory_view fetches entries by id. Both tools are read-only and separate from the governed singular --memory-tool write binding.

Both default min_trust to verified; callers must explicitly opt in to widen the floor. Rejected memory is never surfaced. In durable and contract runs, both tools record signed memory_tool_call provenance and memoize the exact result. Resume replays the recorded memo after verifying it against the signed ledger. Pass revalidate = true to query the current memory store and record a new observation.

See Native Memory Tools and ADR 0041.

Native Web Tools

Native web tools are default-off. Enable them with a CLI flag:

truss --durable --tools --web-tools --workdir /home/you/project "..."

Or with config:

[tools]
web = true

[tools.web_search]
endpoint = "https://api.search.brave.com/res/v1/web/search"
api_key_env = "TRUSS_WEB_SEARCH_API_KEY"

[policy]
network = "allowlist"
allowlist = ["example.com", "api.search.brave.com"]
allow_tools = ["read_file", "web_fetch", "web_search"]

web_fetch fetches HTTP(S) URLs and returns status, final URL after redirects, content type, readable content, raw_sha256, and content_sha256. web_search returns ranked search results and results_sha256.

The default search backend is Brave Search. The API key is read from TRUSS_WEB_SEARCH_API_KEY unless [tools.web_search].api_key_env points at a different env var. The endpoint defaults to Brave Search and can be overridden by TRUSS_WEB_SEARCH_ENDPOINT or [tools.web_search].endpoint.

In durable and contract runs, both tools record signed web_tool_call provenance and memoize the exact result. Resume replays the recorded memo after verifying it against the signed ledger. Pass revalidate = true to fetch/search again and record a new observation.

The verification boundary is intentionally narrow: Truss hashes and signs content as retrieved. It does not certify that a fetched page or search result is true. See Native Web Tools and ADR 0039.

Sandbox

Sandbox selection is also layered global, then agent, then contract. The default is no_sandbox, which preserves the host shell execution path. working_copy materializes an ephemeral checkout for self-validation evidence and removes it when the command finishes.

[sandbox]
mode = "no_sandbox"       # no_sandbox | working_copy
checkout = "."

[agent.sandbox]
mode = "working_copy"

Contract files can set [sandbox] as their highest-precedence layer. See ADR 0025 and Self-Validation Sandbox Architecture for the verification boundary and limitations.

MCP Servers

MCP servers can be declared globally in --config or inside a contract. Stdio is the default transport. HTTP transport is available when Truss is built with the mcp-http feature. Tools are mounted as mcp:<server>:<tool> and still flow through Policy.allow_tools.

[[mcp.servers]]
name = "demo"
transport = "stdio"
command = "python3"
args = ["examples/fake-mcp-server.py"]
allow_tools = ["echo"]

HTTP servers use url. Static non-secret headers can go in headers; secret values should use env_headers, mapping header name to environment variable. Header values are used only at request time and are never written to the ledger. Network policy still applies, so set policy.network = "open" or an allowlist that includes the MCP host.

[[mcp.servers]]
name = "remote"
transport = "http"
url = "https://mcp.example.com/rpc"
allow_tools = ["search"]

[mcp.servers.headers]
"x-client" = "truss"

[mcp.servers.env_headers]
"authorization" = "REMOTE_MCP_AUTH_HEADER"

In durable and contract runs, mounted MCP calls record a signed mcp_tool_call ledger entry with transport, server/tool names, argument hash, result hash, and memo key. URLs and headers are intentionally omitted. See Contract with MCP Tool.

MCP tools can also return a Truss pending content item: { "type": "pending", "signal": "job-name", "reason": "optional reason" }. Durable runs record signed mcp_tool_pending provenance, suspend on that signal, and, when the signal is delivered, record the signal payload as the final signed mcp_tool_call result and memo. Resume therefore never re-invokes the external MCP server for the pending operation.

Operator diagnostics are available without starting a model run:

truss --config examples/harness.toml mcp list
truss --config examples/harness.toml mcp probe demo
truss --config examples/harness.toml mcp probe demo --json

list reads configured servers without launching them. probe connects to one server through the normal MCP mount path and reports the advertised mcp:<server>:<tool> definitions. HTTP probes obey the configured network policy.

CLI Flags

Common run flags:

FlagConfig equivalentPurpose
--confign/aLoad a TOML config file.
--providerprovider.kindSelect anthropic or openai.
--modelprovider.modelSelect the model id.
--base-urlprovider.base_urlPoint OpenAI-compatible providers at local or alternate endpoints.
--systemagent.systemOverride the system prompt.
--max-stepsagent.max_stepsCap model/tool loop steps.
--max-tokensagent.max_tokensCap output tokens per model call.
--reasoningagent.reasoningSet minimal, low, medium, or high reasoning effort.
--temperatureagent.temperatureSet sampling temperature where supported.
--toolsn/aRegister built-in tools.
--git-toolstools.gitRegister native git tools.
--github-toolstools.githubRegister native GitHub tools backed by gh.
--web-toolstools.webRegister native web tools (web_fetch, web_search).
--memory-toolstools.memoryRegister read-only native memory tools (memory_query, memory_view).
--memory-tooln/aRegister the governed memory tool for durable sessions.
--spawn-tooln/aRegister store-backed child-run spawning for durable or contract runs.
--workdirpolicy scopesConfine reads and writes to a directory.
--budget-usdbudget.max_cost_usdSet a hard run cost ceiling.
--budget-tokensbudget.max_total_tokensSet a hard token ceiling.
--timeoutn/aSet a wall-clock deadline in seconds.
--logn/aAppend JSONL events to a file.
--verbosen/aPrint every event as it happens.

Durable and verification flags:

FlagPurpose
--durableEnable SQLite-backed durable execution.
--dbSet the durable store path, default harness.db.
--sessionSet or target a durable session id.
--resumeResume a durable session by id.
--verifyVerify a durable session hash chain and signatures.
--keySelect the ed25519 signing identity path.
--expect-keyPin the expected public key while verifying.
--signal / --payloadDeliver a named durable signal.
--daemon / --poll-secsAuto-resume sessions whose waits are satisfied.
--anchor / --anchor-logAnchor a session head hash to a transparency log.
--verify-anchor-logVerify the transparency log chain.
--max-context-msgsLegacy projection: keep Head plus this many recent turns.

Contract flags:

FlagPurpose
--contractExecute a TOML contract, then independently verify it.
--reviewer-modelSelect the no-stake reviewer model.
--reviewersOverride the contract reviewer panel size.

Compaction flags:

FlagPurpose
--compaction-modeoff, summarize, or evict.
--compaction-thresholdContext pressure threshold, 0.0..=1.0.
--compaction-min-tailVerbatim tail messages retained after compaction.
--compaction-anticipateEnable compact-ahead behavior.

Native Memory Tools

Truss provides two default-off read-only native memory tools:

  • memory_query searches L2 durable memory using the configured recall mode (exact, semantic, or hybrid).
  • memory_view fetches one or more memory entries by id.

These tools let the model pull durable memory on demand instead of relying only on run-start recall injection. They do not create, edit, promote, reject, or stale memory entries.

Enabling

Enable the tools with the CLI flag:

truss --durable --tools --memory-tools --config examples/harness.toml "continue the task"

Or in TOML:

[tools]
memory = true

Tool registration and policy are separate. If policy.allow_tools is set, allow the exact tool names:

[policy]
allow_tools = ["read_file", "memory_query", "memory_view"]

The older singular --memory-tool flag still controls the governed memory_20250818 write binding. --memory-tools is read-only and does not open a model-facing write or promote path.

Trust Floor

Both tools default to:

{ "min_trust": "verified" }

The trust floor is applied before ranking. With semantic and hybrid recall, the store filters by trust, kind, and key before embedding similarity is computed or sorted. That keeps unverified, rejected, and stale-below-floor entries out of the candidate set.

Callers may explicitly widen the floor:

{ "query": "migration", "min_trust": "unverified" }

Rejected memory is never surfaced by these tools.

memory_query

Input:

{
  "query": "rust borrow checker",
  "kind": "lesson",
  "key": "optional-key",
  "limit": 32,
  "min_trust": "verified",
  "revalidate": false
}

query and contains are aliases. kind may be goal, decision, fact, lesson, playbook, artifact, scratch, or context_summary.

Output includes:

  • recall_mode: the resolved memory recall backend.
  • min_trust: the trust floor used.
  • entries: typed memory entries with id, kind, key, body, trust, ledger_ref, and full provenance.

ledger_ref is the entry’s producing LedgerRef; provenance also includes sources and verification verdict references when present.

memory_view

Input:

{
  "id": "memory-id",
  "ids": ["another-memory-id"],
  "min_trust": "verified",
  "revalidate": false
}

Output returns matching entries plus:

  • filtered: ids that exist but are below the trust floor.
  • missing: ids not found in the memory store.

Ledger And Replay

In durable and contract runs, both tools record signed memory_tool_call entries with:

  • tool name,
  • deterministic memo key,
  • input SHA-256,
  • result SHA-256,
  • error flag,
  • optional producing ledger reference.

Successful and failed results are written to the tool memo table in the same transaction as the signed journal entry. Resume/replay returns the recorded result and verifies that the memo still matches a signed ledger entry, instead of querying a mutated memory store.

To request a fresh observation, pass revalidate = true. Revalidation bypasses the existing memo, queries the current store, and records a new signed memory_tool_call entry plus replacement memo for the same logical input.

If a memo is changed without a matching signed ledger entry, replay fails with:

memory memo missing matching signed provenance

That failure prevents a replayed verdict from consuming unsigned memory output.

ai-memory Backend

Truss core includes an opt-in AiMemoryStore implementation of the governed MemoryStore contract. It connects to an OS-selected ai-memory Unix socket using broker protocol v1; requests contain no principal or caller-selected identity.

The backend preserves Truss’s memory invariants:

  • writes enter Unverified;
  • Verified and Rejected require reviewer verdicts;
  • promotion, rejection, and stale transitions use content-hash compare-and-set;
  • signed LedgerRef provenance remains attached to every record;
  • queries default to verified-only and never return rejected memory;
  • content-addressed IDs match SqliteMemoryStore.

Canonical Markdown is the durable source of truth. Truss’s signed execution journal remains local because it is the evidence chain referenced by memory, not a duplicate memory database.

Select the backend explicitly in Truss configuration:

[memory]
store_backend = "ai_memory"
ai_memory_socket = "/run/user/1000/ai-memory.sock"
recall = "exact"

SQLite remains the default. Selecting ai_memory without an absolute socket path fails closed, as does selecting semantic or hybrid recall, which the broker does not yet support.

Until the broker exposes a durable global mutation cursor, delta_since returns the full accessible projection with cursor zero. This is conservative and correct, but less efficient than the SQLite implementation.

Broker compare_set_many now preserves Truss’s cross-record supersede-by-key semantics without a partial-read window: replacement creation and predecessor linking commit together, promotion stales the verified predecessor, and rejection restores it.

Native Web Tools

Truss provides two default-off native web tools:

  • web_fetch fetches an HTTP(S) URL and returns readable content plus status, final URL after redirects, content type, and SHA-256 hashes.
  • web_search queries a configured search backend and returns ranked results with title, URL, snippet, and a hash of the returned result list.

These tools are for agentic browsing and research inside the verified runtime. They are not truth or source-quality validators. Truss attests that the content or search results were retrieved through the configured transport and binds the bytes/results into the signed ledger. It does not attest that a page is accurate, complete, current, unbiased, or authoritative.

Enabling

The tools are off by default. Enable them with the CLI flag:

truss --tools --web-tools --config examples/harness.toml "research the current docs"

Or in TOML:

[tools]
web = true

Tool registration and policy are separate. If policy.allow_tools is set, allow the exact tool names:

[policy]
network = "allowlist"
allowlist = ["example.com", "api.search.brave.com"]
allow_tools = ["read_file", "web_fetch", "web_search"]

policy.network still applies. web_fetch checks the target URL host. web_search checks the configured search endpoint host.

web_fetch

Input:

{
  "url": "https://example.com/page",
  "max_bytes": 1048576,
  "revalidate": false
}

Output includes:

  • url: requested URL.
  • final_url: final URL after redirects.
  • status: HTTP response status.
  • content_type: response content type when present.
  • raw_sha256: SHA-256 of the fetched response bytes retained by Truss.
  • content_sha256: SHA-256 of the readable content returned to the model.
  • content: text content. HTML is stripped with a small best-effort extractor.
  • attestation: currently attested-as-retrieved.

max_bytes is clamped to a 1 MiB default/ceiling. This keeps tool output bounded; it is not a completeness guarantee.

The first concrete backend is Brave Search. It is configured with environment variables by default:

  • TRUSS_WEB_SEARCH_API_KEY: Brave Search API key.
  • TRUSS_WEB_SEARCH_ENDPOINT: optional endpoint override. The compiled default is https://api.search.brave.com/res/v1/web/search.

The endpoint and API-key env var can also be overridden in config:

[tools.web_search]
endpoint = "https://api.search.brave.com/res/v1/web/search"
api_key_env = "TRUSS_WEB_SEARCH_API_KEY"

If the key env var is missing, web_search returns a clean signed tool error:

web_search unconfigured: missing env var TRUSS_WEB_SEARCH_API_KEY

Search quality, ranking, freshness, regional behavior, and filtering are provider properties. Truss hashes and signs what the provider returned; it does not verify that the provider found every relevant page.

Ledger And Replay

In durable and contract runs, both tools record signed web_tool_call entries with:

  • tool name,
  • deterministic memo key,
  • input SHA-256,
  • result SHA-256,
  • error flag,
  • optional producing ledger reference.

Because network reads are nondeterministic, both successful and failed results are also written to the tool memo table in the same transaction as the signed journal entry. Resume/replay returns the recorded result and verifies that the memo still matches a signed ledger entry. It does not re-fetch or re-query.

To request a fresh observation, pass revalidate = true. Revalidation bypasses the existing memo, performs the network operation again, and records a new signed web_tool_call entry plus replacement memo for the same logical input.

If a memo is changed without a matching signed ledger entry, replay fails with:

web memo missing matching signed provenance

That failure protects verdicts from consuming an unsigned or tampered web result.

Plugin Authoring Overview

Truss extension work should start from the smallest seam that preserves the runtime’s trust model. The safe path is:

  1. Use MCP when the extension is an external tool or already exists as a service.
  2. Use an in-process Rust Tool when the tool must share process memory or run hot/trusted logic.
  3. Use a provider plugin when adding a model backend or auth strategy.
  4. Use an event or ledger consumer when the extension only observes runs.
  5. Use a sandbox or anchor backend when swapping infrastructure behind an existing trust boundary.

The key rule is simple: outcome-affecting work must pass through Ctx, policy, events, and the signed ledger. Presentation-only work should stay off-journal.

Current seams

SeamCurrent APIBest forJournal rule
Providertruss_core::provider::Provider, built through truss_providers::registryModel backends, auth strategies, local OpenAI-compatible serversModel results are journaled by the agent/durable loop, not by the provider
In-process tooltruss_core::tools::ToolTrusted Rust tools that touch local state or need low latencyOverride call_with_ledger when the tool creates durable facts or side effects
MCP server tool[[mcp.servers]] + mcp:<server>:<tool>Out-of-process tools and enterprise integrationsTruss records mcp_tool_call provenance plus replayable tool_result
Event streamAgentEvent via Ctx::with_eventsLive UIs, logs, dashboards, telemetryUsually off-journal; observe without changing run outcome
Ledger readerLedgerReader, truss ledger ...Evidence exports, audits, buyer reports, recallRead-only; never rewrites signed entries
Sandboxtruss_core::sandbox::SandboxStronger command isolation behind existing self-check/tool flowsKeep the same self_validation journal shape
Anchortruss_core::anchor::AnchorRekor/transparency log/on-chain hash anchoringAnchor hashes only, never evidence or secrets
ContextProviderRoadmap seamEnterprise docs/vector/policy injectionNot a public trait yet; do not invent a parallel path

Pick the seam

Authoring checklist

  • Does this extension affect the model’s answer, contract verdict, filesystem, network, spend, memory, or identity? If yes, route it through a core seam.
  • Does it only render, summarize, notify, or visualize already-recorded data? If yes, keep it as a consumer and do not add journal entries.
  • Can it run out-of-process? Prefer MCP. In-process tools are for trusted code.
  • Does it need credentials? Resolve them inside the provider/MCP server/backend and avoid recording secret values.
  • Does it create a fact future runs may rely on? Stamp it with LedgerRef provenance.
  • Does it perform a side effect that should execute once? Make it replayable via the durable ledger/memo path instead of relying on best-effort idempotency.

Verification checklist

  • cargo test or a focused crate test for Rust seam changes.
  • truss mcp probe <server> for MCP tools.
  • truss ledger verify <run-id> or truss ledger export <run-id> for durable evidence.
  • mdbook build docs for docs and examples.
  • Confirm no secret values appear in logs, events, or ledger payloads.

Provider Plugins

A provider plugin turns a model backend into the one abstraction the agent loop understands:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait Provider: Send + Sync {
    fn name(&self) -> &str;
    fn default_model(&self) -> &str;
    fn capabilities(&self, model: Option<&str>) -> ModelCapabilities;
    async fn complete(
        &self,
        ctx: &Ctx,
        req: &CompletionRequest,
    ) -> Result<CompletionResponse, ProviderError>;
}
}

The CLI does not construct concrete providers. It passes a provider id and ProviderConfig to truss_providers::registry::build. Auth resolution belongs inside the provider’s build() function, not in truss-cli or truss-core.

Add a compile-time provider

  1. Add a module in crates/truss-providers/src/.
  2. Implement Provider.
  3. Add pub(crate) fn build(cfg: &ProviderConfig) -> Result<Arc<dyn Provider>, BuildError>.
  4. Add a cargo feature in crates/truss-providers/Cargo.toml.
  5. Add one available() entry and one build() match arm in registry.rs.

The provider should convert Truss-neutral CompletionRequest, ToolDef, SystemBlock, ReasoningEffort, and messages into the backend’s wire format. Keep provider-specific HTTP/auth/client code in truss-providers; truss-core should stay vendor-free.

Durability and provenance

Providers do not write durable journal entries themselves. The agent/durable loop records model results after the provider returns. That keeps replay centralized: on resume, Truss reuses the recorded result instead of asking the provider again.

Provider authors must:

  • honor ctx cancellation/deadlines;
  • return typed ProviderError values so retry policy can distinguish retryable transport errors from fatal config/model errors;
  • expose conservative ModelCapabilities;
  • keep secret material out of Debug, events, and errors;
  • implement complete_stream only when the backend has true streaming. The default wrapper is acceptable and still produces correct AgentEvents.

Runnable checks

These checks exercise the provider registry boundary without requiring live credentials:

cargo test -p truss-providers registry
cargo build -p truss-cli --no-default-features --features provider-openai
cargo build -p truss-core

For a provider with live credentials, add a wiremock/unit test first, then smoke the CLI:

truss --provider <id> --model <model> "say hello in one sentence"

In-Process Tools

An in-process tool is trusted Rust code registered in ToolRegistry. It receives the active Ctx, so it inherits cancellation, deadlines, budget, principal, policy, and event emission.

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait Tool: Send + Sync {
    fn def(&self) -> ToolDef;

    async fn call(&self, ctx: &Ctx, input: serde_json::Value)
        -> Result<String, ToolError>;

    async fn call_with_ledger(
        &self,
        ctx: &Ctx,
        input: serde_json::Value,
        produced_by: Option<LedgerRef>,
    ) -> Result<String, ToolError> {
        self.call(ctx, input).await
    }
}
}

Use this seam only when MCP is the wrong fit: hot trusted logic, direct access to in-process stores, or runtime primitives that must share Ctx internals.

Minimal shape

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use serde_json::json;
use truss_core::ctx::Ctx;
use truss_core::error::ToolError;
use truss_core::provider::ToolDef;
use truss_core::tools::{Tool, ToolRegistry};

struct BuildInfoTool;

#[async_trait]
impl Tool for BuildInfoTool {
    fn def(&self) -> ToolDef {
        ToolDef {
            name: "build_info".into(),
            description: "Return static build metadata".into(),
            input_schema: json!({ "type": "object", "properties": {} }),
        }
    }

    async fn call(&self, ctx: &Ctx, _input: serde_json::Value)
        -> Result<String, ToolError>
    {
        ctx.check().map_err(|_| ToolError::Cancelled)?;
        Ok("build=local".into())
    }
}

fn register(registry: &mut ToolRegistry) {
    registry.register(std::sync::Arc::new(BuildInfoTool));
}
}

The tool name is the policy name. It is visible to the model only when Policy::can_use_tool(name) allows it.

When to override call_with_ledger

Override call_with_ledger when the tool creates state that future decisions may trust:

  • memory writes,
  • identity/persona amendments,
  • child-run or fan-out decisions,
  • external side effects that must be tied to a model request,
  • evidence artifacts used by a contract verdict.

The produced_by value points to the signed journal entry that caused the tool call. Store that reference with the created fact, emit an AgentEvent if a UI should observe it, and keep secret values out of the payload.

If the tool only renders a view, lists files, or returns deterministic local metadata, the default call_with_ledger is fine.

Verification checklist

  • Add a unit test for schema/name stability.
  • Add a policy denial test when the tool touches outside state.
  • Confirm ctx.check() is called before expensive or blocking work.
  • If call_with_ledger is overridden, test that missing provenance fails when provenance is required.

MCP Server Tools

MCP is the preferred seam for out-of-process tools. A configured MCP server is mounted into the normal tool registry. The model sees a normal tool name:

mcp:<server>:<tool>

The mounted tool still passes through Truss policy, budget, cancellation, durable memoization, and signed provenance.

Stdio server

examples/contract-mcp.toml mounts the tiny stdio server in examples/fake-mcp-server.py:

[[mcp.servers]]
name = "demo"
command = "python3"
args = ["examples/fake-mcp-server.py"]
allow_tools = ["echo"]

[constraints]
allow_tools = ["read_file", "write_file", "list_dir", "bash", "mcp:demo:echo"]

Probe it without a model:

truss --config examples/harness.toml mcp list
truss --config examples/contract-mcp.toml mcp probe demo

HTTP server

When Truss is built with mcp-http, the same config shape supports HTTP:

[[mcp.servers]]
name = "search"
transport = "http"
url = "https://mcp.example.com"
allow_tools = ["query"]

[mcp.servers.env_headers]
authorization = "SEARCH_MCP_AUTHORIZATION"

Use headers only for non-secret metadata. Put bearer tokens and API keys in env_headers; Truss records header names, server name, tool name, input hash, and result hash, but not secret header values.

Durable behavior

Durable MCP calls write a signed mcp_tool_call provenance entry and an atomic tool memo entry. A resumed run reuses the memo after verifying it against the signed provenance hashes, so the MCP server is not invoked again.

Long-running MCP tools may return a pending content item:

{ "type": "pending", "signal": "job-123", "reason": "remote job still running" }

Durable Truss runs suspend until that signal is delivered. The delivered signal payload becomes the recorded tool result and is memoized for future resumes.

Authoring checklist

  • Keep server and tool names stable; they become policy names.
  • Use allow_tools on the server and in contract constraints.
  • Keep secrets in environment variables, not TOML.
  • Return structured text that reviewers/checks can use as evidence.
  • For long jobs, return pending instead of holding the process open forever.

Context Providers

ContextProvider is a planned seam, not a public Rust trait in this release. That distinction matters: context injection changes what the model sees, so it must be governed by trust tier and provenance rather than bolted on as a prompt string.

The current runtime already has the substrate a future ContextProvider will use:

  • GoalLedger for contract intent and definition-of-done context;
  • layered memory/persona context;
  • governed compaction and signed compaction_performed journal entries;
  • LedgerRef provenance on memory entries;
  • AgentEvent for recall/write observations.

Desired shape

A provider should eventually answer: “for this run, principal, policy, goal, and trust tier, what context fragments may enter which layer?”

#![allow(unused)]
fn main() {
// Roadmap sketch, not current API.
#[async_trait::async_trait]
pub trait ContextProvider: Send + Sync {
    async fn project(
        &self,
        ctx: &Ctx,
        request: ContextRequest,
    ) -> Result<Vec<ContextFragment>, ContextError>;
}
}

Each fragment needs provenance:

  • source system and stable id,
  • trust tier,
  • content hash,
  • redaction/secret handling,
  • optional LedgerRef when produced by a prior Truss run.

What to do today

  • For durable facts produced by Truss, use the governed memory tool and LedgerReader.
  • For passive presentation, consume AgentEvent or ledger exports.
  • For enterprise docs/vector DB injection, keep the adapter outside the runtime until the public ContextProvider trait lands. Do not fork prompt assembly or write directly into system prompts if the data affects outcomes.

Done criteria for the future seam

  • Context fragments are selected by policy and trust tier.
  • Injected fragments can be traced back to source ids/hashes.
  • Summaries and compactions point back to signed ledger ranges.
  • Presentation-only context stays off-journal; outcome-affecting context is provenance-bearing.

Sandbox And Anchor Backends

Sandbox and anchor backends replace infrastructure behind existing trust boundaries. They should not change model prompts, verifier policy, or journal entry shapes.

Sandbox

truss_core::sandbox::Sandbox controls command execution:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait Sandbox: Send + Sync {
    async fn run(&self, cmd: SandboxCommand, ctx: &Ctx)
        -> Result<SandboxOutput, ToolError>;

    fn materialize(&self, checkout: &Path) -> Result<WorkDir, ToolError>;
    fn capabilities(&self) -> SandboxCaps;
}
}

Current implementations:

  • NoSandbox: direct local execution, process reaping only.
  • WorkingCopySandbox: copies a checkout to an ephemeral workdir and hashes the result.

Stronger backends can add containers, VM isolation, network isolation, or remote builders as long as they keep the same output contract: stdout, stderr, exit code, and workdir_hash.

Verification checklist:

  • The backend honors ctx cancellation/deadlines.
  • Child processes are reaped.
  • capabilities() is honest.
  • The same command produces a stable workdir_hash for the same output tree.
  • Self-validation still emits the same evidence shape.

Anchor

truss_core::anchor::Anchor publishes a session head hash:

#![allow(unused)]
fn main() {
pub trait Anchor {
    fn anchor(
        &self,
        session_id: &str,
        head_hash: &[u8],
        pubkey: &str,
    ) -> anyhow::Result<AnchorReceipt>;
}
}

Current implementations:

  • NoopAnchor: default stand-in.
  • FileTransparencyLog: local append-only JSONL hash chain.

A Rekor, transparency-log, or chain backend should anchor only hashes. Never send contract text, tool outputs, evidence data, prompts, file contents, or secrets to the anchor service.

Verification checklist:

  • Anchor receipts include log id, position, head hash, entry hash, and timestamp.
  • Anchoring failure is explicit; it must not silently claim a receipt.
  • The backend has a verification story for the external log.
  • Private evidence stays local; only salted roots or head hashes leave the box.

Event And Ledger Consumers

Most enterprise plugins should be consumers, not runtime extensions. If a plugin only renders, exports, alerts, indexes, or visualizes a run, it should consume the event stream or ledger without adding new journal entries.

Live event consumers

AgentEvent is the live stream:

#![allow(unused)]
fn main() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let ctx = Ctx::new().with_events(tx);

while let Some(event) = rx.recv().await {
    println!("{}", serde_json::to_string(&event)?);
}
}

Events are for observation. A UI can show AssistantTextDelta, tool requests, tool results, context pressure, child runs, fan-out, memory writes, and run completion without becoming part of the signed execution source of truth.

If an event consumer decides something that affects the run, that decision must come back through a governed runtime seam and be journaled there.

Ledger consumers

LedgerReader and the truss ledger commands are for durable, read-only evidence:

truss --db harness.db ledger list
truss --db harness.db ledger show <run-id>
truss --db harness.db ledger verdict <run-id> --json
truss --db harness.db ledger export <run-id> \
  | python3 examples/plugin-ledger-consumer.py --fail-on-unverified

The export bundle is self-contained: contract summary, evidence, signed entries, anchor refs, child sessions, and offline verification status.

Consumer checklist

  • Treat ledger data as immutable.
  • Verify before trusting: use ledger verify, ledger verdict, or export offline_verification.
  • Keep consumer-derived presentation state off-journal.
  • If you cache derived views, store the source run id and head hash with the cache entry.
  • Do not re-run tools or model calls from a consumer.

Journal Discipline

The signed ledger is for facts Truss may need to replay, prove, or trust later. It is not a dumping ground for every UI notification.

Journal it

Write or reuse a journaled path when the extension:

  • calls a model or delegated executor;
  • invokes a tool whose output feeds the model or verifier;
  • creates or modifies memory, identity, policy, or durable context;
  • performs a side effect that should not run twice after resume;
  • records contract evidence, self-validation, reviewer output, verdicts, child anchors, fan-out reductions, compactions, or MCP provenance;
  • publishes an anchor receipt.

Keep it off-journal

Do not journal work that only:

  • renders a UI;
  • streams token deltas for presentation;
  • emits desktop/chat/web notifications;
  • indexes an already-signed export for search;
  • draws charts or dashboards from ledger data;
  • records local debug traces that are not evidence.

These consumers should keep a pointer to the source run id and head hash if they persist derived state.

Secret handling

  • Providers resolve credentials internally.
  • MCP HTTP secrets belong in env_headers, not static config.
  • Anchor backends publish hashes only.
  • Errors and event previews should be sanitized.
  • Ledger payloads should contain evidence and hashes, not raw secrets.

Replay rule

If resume would be wrong after re-executing it, the operation needs a recorded result or memo. If resume can recompute it safely from signed data, keep it as a consumer.

Verified Playbooks

Verified Playbooks are the on-moat replacement for skills and slash-command snippets. A playbook is a governed memory entry, not an untrusted prompt blob. It starts as Unverified, carries signed ledger provenance, and can later move through the same reviewer/eval promotion machinery as lessons.

The M1 substrate shipped:

  • EntryKind::Playbook in governed memory;
  • markdown/SKILL.md import into structured playbook JSON;
  • signed playbook_imported journal entries;
  • truss playbook import|list|show|revoke;
  • revocation by demoting the memory entry to Stale and recording playbook_revoked.

M2 adds the trust gate and recall path:

  • truss playbook promote <id-or-key>;
  • the same N-reviewer panel used for lesson promotion and verified delegation;
  • signed playbook_promoted or playbook_promotion_rejected journal entries;
  • promote_with_verdict_ref provenance for passing verdicts;
  • opt-in L2 recall of verified playbooks.

M3 adds eval-backed promotion:

  • truss playbook promote <id-or-key> --eval --contract <file> [--repeats N];
  • paired learning-on/off evidence over a real contract;
  • signed promotion or rejection events containing the EvalReport;
  • default-to-fail semantics when the eval shows no positive pass-rate lift.

Imported files are still drafts. Truss does not pretend an imported file is trusted.

Import

truss --db harness.db playbook import skills/coding/SKILL.md --key coding-agent

The importer extracts a stable schema from headings when present:

  • purpose,
  • trigger,
  • prerequisites,
  • procedure,
  • checks,
  • examples,
  • known failures.

If a section is absent, the corresponding field is empty. The original file is not executed; import is a local transform plus a signed journal event.

Inspect

truss --db harness.db playbook list
truss --db harness.db playbook show coding-agent
truss --db harness.db playbook show <memory-id> --json

list shows playbooks at Unverified or above by default so operators can see fresh imports. Use --verified-only when a caller wants only promoted playbooks.

Promote

truss --db harness.db --provider openai --model gpt-5.5 \
  --reviewers 3 playbook promote coding-agent
truss --db harness.db playbook promote <memory-id> --json
truss --db harness.db --provider openai --model gpt-5.5 \
  playbook promote coding-agent --eval --contract contracts/rust-review.toml --repeats 3

Promotion loads the playbook draft and asks the existing no-stake reviewer panel whether the playbook is accurate, non-misleading, and safe to rely on later. A strict majority must pass. A passing panel records a signed playbook_promoted event and promotes the memory entry to Verified with verified_by_ref pointing at that event.

With --eval, promotion uses paired eval evidence instead of the local panel. The command requires a real TOML contract and executor. The learning-on arm enables verified-memory recall, including bounded playbook recall; the learning-off arm disables learned-memory recall. During this promotion eval, the candidate draft is visible only to the eval memory view as a temporary verified playbook so Truss can measure whether recalling it would improve the task. The stored draft remains Unverified unless the eval passes the promotion rule.

Eval promotion is stricter than the eval report’s general verdict: the playbook is promoted only when learning_on.pass_rate is greater than learning_off.pass_rate. Zero lift and regressions record a signed playbook_promotion_rejected event containing the EvalReport, exit nonzero, and leave the draft Unverified.

Failing or unverifiable panels record a signed playbook_promotion_rejected event and leave the playbook Unverified. This is the honest boundary: rejected promotion attempts are auditable, but they do not create fake trust and do not surface as trusted recall material.

Revoke

truss --db harness.db playbook revoke coding-agent --reason "regressed on rustfmt"

Revocation records a signed playbook_revoked journal entry and marks the memory entry Stale. It does not delete the old playbook, because the audit trail should still explain what changed and why.

Trust Model

  • Imported playbooks are drafts.
  • Drafts can be listed, inspected, and submitted for promotion, but they are not trusted recall material.
  • Promotion attaches a reviewer-panel verdict and flips the trust tier to Verified only on Pass.
  • Failing or unverifiable promotion attempts leave the draft Unverified.
  • Verified playbooks can be recalled into L2 context only when playbook recall limits are explicitly enabled. Unverified playbooks are never recalled as trusted playbooks.
  • Future regression should demote the entry to Stale, not erase it.

Examples

The examples in this section render the live files from examples/. The Markdown adds context, but the TOML and prompt examples are included from the repository source so they stay in sync with code and tests.

ExampleUse
Harness ConfigA full --config file for provider, agent, memory, budget, and policy defaults.
Verified ContractThe minimal contract-to-verdict flow with checks and reviewer clauses.
Contract with MCP ToolContract-scoped stdio MCP tool mounting with signed provenance.
Contract with CompactionA contract-level [memory.compaction] example; compaction remains opt-in.
Claude Subscription ContractDelegated execution through Claude Code while Truss owns verification.
Codex Exec ContractDelegated execution through codex exec while Truss owns verification.
Ledger Export ConsumerA read-only plugin that consumes exported signed evidence without touching the runtime.
Layered Long RunIntended long-run prompt shape for layered memory and projection.

Run examples from the repository root unless the example says otherwise.

Harness Config

examples/harness.toml is the general-purpose configuration file. It shows the provider block, agent defaults, optional memory/projection/compaction knobs, budget ceilings, and enforced policy.

truss --config examples/harness.toml --tools "summarize this repository"
# Example truss config. Pass with `--config harness.toml`.
# CLI flags override anything set here.

[provider]
kind = "anthropic"            # "anthropic" | "openai"
model = "claude-opus-4-8"
# base_url = "http://localhost:11434/v1"   # e.g. Ollama for kind = "openai"
# api_key_env = "ANTHROPIC_API_KEY"        # defaults per provider kind

[agent]
system = "You are a precise engineering assistant. Prefer tools over guessing."
max_tokens = 4096
max_steps = 24
# reasoning = "high"   # minimal | low | medium | high (models that support thinking)
# temperature = 0.2
# [agent.memory] overrides [memory] for this agent.

[memory]
# store_backend = "sqlite"       # sqlite | ai_memory
# ai_memory_socket = "/run/user/1000/ai-memory.sock" # required for ai_memory
# recall = "exact"              # exact | semantic | hybrid
# embedding_backend = "open-ai" # open-ai | deterministic
# embedding_model = "text-embedding-3-small"
# embedding_api_key_env = "OPENAI_API_KEY"
# embedding_dims = 1536
# embedding_base_url = "https://api.openai.com/v1" # optional OpenAI-compatible endpoint
# verification = "lazy"          # off | lazy | eager | tiered
# identity = "propose"           # locked | propose | self_amend
# scope = "per-agent"            # per-agent | per-project | shared
# conflict = "verified-wins"     # verified-wins | latest-wins | panel-arbitrated | keep-both-flagged
# goal_repin_every = 8           # re-inject active contract intent + DoD every N model steps
# salience = "off"               # off | harness-recency | model-scored | hybrid (M5 seam; no eviction yet)

# Context projection keeps the cached L0/L1 prefix stable and compacts only
# non-exempt message history when enabled. keep_tail = 0 preserves legacy behavior.
# [memory.projection]
# keep_tail = 0
# compaction_exempt_tiers = ["l0_core", "l0_self", "l1_principal", "l3_goal"]

# Governed compaction is opt-in. When enabled, Truss compacts only at run-step
# boundaries and records a signed compaction_performed journal entry.
# [memory.compaction]
# mode = "off"                 # off | summarize | evict
# threshold = 0.70
# min_tail = 6
# anticipate = false
# exempt_tiers = ["l0_core", "l0_self", "l1_principal", "l3_goal"]

[budget]
max_cost_usd = 2.00
max_total_tokens = 500000
max_steps = 40

# Native memory/git/GitHub/web tools are opt-in and still obey
# [policy].allow_tools. Network policy also gates web tools.
[tools]
memory = false
git = false
github = false
web = false

# Brave Search is the first concrete web_search backend. Leave the key out of
# config; point at the env var that holds it.
# [tools.web_search]
# endpoint = "https://api.search.brave.com/res/v1/web/search"
# api_key_env = "TRUSS_WEB_SEARCH_API_KEY"

# Approval gates are opt-in. When enabled, gated durable runs suspend until a
# signed approved human_review releases them.
# [approval_gates]
# enabled = false
# high_risk_tools = ["bash", "write_file", "mcp:payments:*"]
# spend_threshold_usd = 1.00
# per_call_spend_threshold_usd = 0.25

[policy]
write_scope = ["/home/you/project"]
read_scope = ["/home/you/project"]
network = "none"             # "none" | "open" | "allowlist"
# allowlist = ["example.com", "api.search.brave.com"]
allow_tools = ["read_file", "write_file", "list_dir", "bash"]
forbidden = ["rm -rf /", ":(){", "git push"]

# MCP servers can be mounted globally or per contract. Stdio is default.
#
# [[mcp.servers]]
# name = "demo"
# transport = "stdio"
# command = "python3"
# args = ["examples/fake-mcp-server.py"]
# allow_tools = ["echo"]
#
# HTTP MCP transport is available when built with `--features mcp-http`.
# Secret header values should come from env vars; they are not journaled.
#
# [[mcp.servers]]
# name = "remote"
# transport = "http"
# url = "https://mcp.example.com/rpc"
# allow_tools = ["search"]
#
# [mcp.servers.env_headers]
# "authorization" = "REMOTE_MCP_AUTH_HEADER"

Verified Contract

examples/contract.toml is the baseline verified-delegation contract. The executor does the work, then Truss independently runs check clauses and asks the reviewer panel to judge review clauses before signing a verdict.

truss --contract examples/contract.toml --tools
# Example contract. Run with: ./target/release/truss --contract examples/contract.toml --tools
#
# The executor does the work; the harness then INDEPENDENTLY verifies it —
# `check` clauses are shell commands the harness runs, `review` clauses are
# judged by a panel of no-stake reviewer models. You read the verdict, not the
# transcript.

# Size of the adversarial reviewer panel: N reviewers judge each `review` clause
# in parallel (each from a different lens) and a strict majority must agree it's
# met. 1 = single reviewer. CLI `--reviewers` overrides this.
reviewers = 3

[memory]
# Contract-level overrides layer after global [memory] and [agent.memory].
# verification = "eager"
# identity = "propose"
# scope = "per-project"
# conflict = "verified-wins"
# goal_repin_every = 8
# salience = "off"

[contract]
id = "demo-greeting-0001"
title = "Write a greeting file"
intent = """
Create a file named greeting.txt in the working directory whose contents are
exactly the line: hello from the harness
"""

# ── Definition of done ────────────────────────────────────────────────
[[done]]
kind = "check"
desc = "greeting.txt exists and contains the exact line"
run = "printf 'hello from the harness\\n' | cmp - greeting.txt"
expect_exit = 0

[[done]]
kind = "check"
desc = "greeting.txt exists"
run = "test -f greeting.txt && echo ok"
expect_exit = 0
expect_contains = ["ok"]

[[done]]
kind = "review"
desc = "the greeting reads naturally and matches the intent"
# reviewer_context = ["greeting.txt"]

# ── Budget (hard ceilings) ────────────────────────────────────────────
[budget]
max_cost_usd = 1.00
max_steps = 12

# ── Constraints (enforced) ────────────────────────────────────────────
[constraints]
write_scope = ["."]
network = "none"
allow_tools = ["read_file", "write_file", "list_dir", "bash"]
forbidden = ["rm -rf /", "git push"]
system = "You are a careful engineer. Use the tools to complete the task, then stop."

Contract With MCP Tool

examples/contract-mcp.toml shows the stdio MCP shape: a contract-scoped MCP server is mounted into the normal Truss tool registry as mcp:<server>:<tool>. That mounted tool still has to pass the contract’s allow_tools policy.

Durable and contract runs re-mount MCP tools with store-backed provenance, so each call records a signed mcp_tool_call ledger entry with argument and result hashes. The normal tool_result remains replayable for the model, and repeated same-session MCP calls replay from the memo instead of re-invoking the server.

HTTP MCP servers use the same contract/config block with transport = "http" and url = "..." when Truss is built with --features mcp-http. Use env_headers for secret header values; URLs and headers are not recorded in the signed MCP provenance entry.

Long-running MCP tools may return a Truss pending content item with a signal name. Durable runs suspend until that signal arrives, then treat the delivered payload as the tool result and memoize it, so resume does not reconnect to the MCP server.

# Example MCP contract. The MCP server is scoped to this contract and its
# mounted tool must also appear in [constraints].allow_tools.

reviewers = 1

[contract]
id = "demo-mcp-0001"
title = "Use a scoped MCP tool"
intent = """
Use the scoped MCP echo tool to produce a short greeting, then write it to
mcp-greeting.txt.
"""

[[mcp.servers]]
name = "demo"
command = "python3"
args = ["examples/fake-mcp-server.py"]
allow_tools = ["echo"]

[[done]]
kind = "check"
desc = "mcp-greeting.txt contains the MCP-produced greeting"
run = "cat mcp-greeting.txt"
expect_exit = 0
expect_contains = ["hello from mcp"]

[budget]
max_cost_usd = 1.00
max_steps = 12

[constraints]
write_scope = ["."]
network = "none"
allow_tools = ["read_file", "write_file", "list_dir", "bash", "mcp:demo:echo"]
forbidden = ["rm -rf /", "git push"]
system = "Use the MCP tool if it helps. Write only the requested file."

Serving Truss Over MCP

Run Truss as an MCP server over stdio:

truss --db harness.db --key harness-identity.key serve-mcp \
  --transport stdio \
  --principal-tenant local \
  --principal-user alice

An MCP client can call truss_run_contract with a contract TOML path:

{
  "name": "truss_run_contract",
  "arguments": {
    "contract_path": "examples/contract.toml",
    "session_id": "example-mcp-contract"
  }
}

The result includes the contract verdict, the signed ledger verdict bundle, the session head hash, and a bundle_ref pointing at truss_ledger_export.

For long-running work, pass "async": true and poll truss_contract_status. Ledger reads are read-only through truss_ledger_verdict and truss_ledger_export.

HTTP serving is not implemented in this slice. The server also does not expose raw bash, filesystem, or internal write tools over MCP; callers delegate a contract and read signed proof bundles.

Contract with Compaction

examples/contract-compaction.toml enables governed compaction at the contract layer. This is default-off everywhere else; selecting summarize here affects only the projected context for this contract run.

truss --contract examples/contract-compaction.toml --tools
# Contract example showing governed compaction enabled at the contract layer.
# Run with: truss --contract examples/contract-compaction.toml --tools

reviewers = 1

[memory.compaction]
mode = "summarize"
threshold = 0.70
min_tail = 6
anticipate = false

[contract]
id = "demo-compaction-0001"
title = "Summarize at context pressure"
intent = """
Create a file named compaction-demo.txt in the working directory whose contents
are exactly the line: governed compaction is opt-in
"""

[[done]]
kind = "check"
desc = "compaction-demo.txt contains the expected line"
run = "cat compaction-demo.txt"
expect_exit = 0
expect_contains = ["governed compaction is opt-in"]

[budget]
max_cost_usd = 1.00
max_steps = 12

[constraints]
write_scope = ["."]
network = "none"
allow_tools = ["read_file", "write_file", "list_dir", "bash"]
forbidden = ["rm -rf /", "git push"]
system = "You are a careful engineer. Use the tools to complete the task, then stop."

Claude Subscription Contract

examples/contract-claude-subscription.toml uses a delegated CLI executor. The Claude CLI owns the inner loop on its subscription login, while Truss still owns the contract boundary, independent checks, reviewer panel, and signed verdict.

truss --contract examples/contract-claude-subscription.toml
# Delegated-executor contract (ADR 0003, Tier 2) — run the work on a Claude
# *subscription* instead of an API key, then verify it the same way.
#
#   truss --contract contract-claude-subscription.toml
#
# How it differs from contract.toml: the executor doesn't drive a Provider we own.
# It shells out to the real `claude` CLI in print mode (`-p`). We CLEAR the Anthropic
# key/OAuth env vars from the child, so the CLI falls back to its OWN stored
# Claude Pro/Max login — Truss replays no tokens (ToS-clean). Claude Code runs its
# own inner loop/tools; Truss captures the result and verifies it at the contract
# boundary with independent `check` clauses + the reviewer panel, then signs the
# verdict. You still review a proof, not a transcript.
#
# Prereq: `claude` on PATH and already logged in (`claude` → /login once).

reviewers = 3

[contract]
id = "demo-greeting-claude-sub-0001"
title = "Write a greeting file (on a Claude subscription)"
intent = """
Create a file named greeting.txt in the working directory whose contents are
exactly the line: hello from the harness
"""

# ── Execute via the Claude CLI on its own subscription (no API key) ───────────
[executor]
kind = "delegated-cli"
command = "claude"
# Print mode = one-shot; pin the live subscription model because the Claude CLI
# default can move to aliases unavailable on this account.
args = [
  "-p",
  "--model",
  "claude-opus-4-8",
  "--output-format",
  "text",
  "--permission-mode",
  "acceptEdits",
]
# Stripped from the child so the CLI uses its stored subscription login, not a key.
clear_env = [
  "ANTHROPIC_API_KEY",
  "ANTHROPIC_AUTH_TOKEN",
  "ANTHROPIC_OAUTH_TOKEN",
  "CLAUDE_CODE_OAUTH_TOKEN",
]
prompt_via = "stdin"   # pipe the task prompt to the CLI's stdin (default)

# ── Definition of done — verified by Truss, independently of the executor ─────
[[done]]
kind = "check"
desc = "greeting.txt exists and contains the exact line"
run = "cat greeting.txt"
expect_exit = 0
expect_contains = ["hello from the harness"]

[[done]]
kind = "review"
desc = "greeting.txt contains exactly one line, `hello from the harness`, with no extra text"
reviewer_context = ["greeting.txt"]

# ── Budget + constraints (the reviewer panel still runs on your normal provider) ─
[budget]
max_cost_usd = 1.00
max_steps = 12

Codex Exec Contract

examples/contract-codex-exec.toml delegates the inner solve to codex exec. Truss clears API-key environment variables from the child and verifies the result at the same contract boundary as API-backed runs.

truss --contract examples/contract-codex-exec.toml
# OpenAI subscription via the DELEGATED path (ADR 0003, Tier 2) — uses the `codex`
# CLI's own ChatGPT login. No API key, no token replay by Truss; `codex` owns the
# inner loop and Truss verifies the result at the contract boundary.
#
#   truss --contract contract-codex-exec.toml
#
# Compare with `--provider openai-codex` (Tier 1, in-process), which drives Truss's
# OWN loop on the same subscription — better for dev/testing. This delegated path is
# the safer production answer for OpenAI subscriptions.
#
# Prereq: `codex` on PATH and signed in with ChatGPT (`codex` → Sign in).

reviewers = 3

[contract]
id = "demo-greeting-codex-exec-0001"
title = "Write a greeting file (via codex on a ChatGPT subscription)"
intent = """
Create a file named greeting.txt in the working directory whose contents are
exactly the line: hello from the harness
"""

[executor]
kind = "delegated-cli"
command = "codex"
# `codex exec` is the non-interactive, one-shot mode; prompt arrives on stdin.
args = ["exec", "--skip-git-repo-check"]
# Stripped so the CLI uses its stored ChatGPT login, not a key Truss is holding.
clear_env = ["OPENAI_API_KEY", "OPENAI_BASE_URL"]
prompt_via = "stdin"

[[done]]
kind = "check"
desc = "greeting.txt exists and contains the exact line"
run = "cat greeting.txt"
expect_exit = 0
expect_contains = ["hello from the harness"]

[[done]]
kind = "review"
desc = "the greeting reads naturally and matches the intent"

[budget]
max_cost_usd = 1.00
max_steps = 12

Ledger Export Consumer

examples/plugin-ledger-consumer.py is a tiny read-only consumer for truss ledger export. It demonstrates the safest plugin class: observe a signed evidence bundle, verify the status, and render a derived view without touching the runtime database.

truss --db harness.db ledger export <run-id> \
  | python3 examples/plugin-ledger-consumer.py --fail-on-unverified
#!/usr/bin/env python3
"""Read a Truss ledger export bundle from stdin and summarize it.

This is intentionally a consumer, not a runtime plugin: it never opens the Truss
SQLite database and never writes journal entries. Feed it:

    truss --db harness.db ledger export <run-id> | python3 examples/plugin-ledger-consumer.py
"""

import argparse
import json
import sys


def status_name(value):
    if isinstance(value, str):
        return value
    if isinstance(value, dict):
        return value.get("status", "unknown")
    return "unknown"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--fail-on-unverified",
        action="store_true",
        help="exit non-zero when offline verification is not verified",
    )
    args = parser.parse_args()

    bundle = json.load(sys.stdin)
    ledger = bundle.get("ledger", {})
    verdict = bundle.get("verdict", {})
    evidence = bundle.get("evidence", {})
    verification = bundle.get("offline_verification", {})

    status = status_name(verification.get("status"))
    entries = ledger.get("entries", [])
    children = ledger.get("child_sessions", [])
    anchors = ledger.get("anchor_refs", [])

    print(f"schema: {bundle.get('schema_version', 'unknown')}")
    print(f"session: {ledger.get('session_id', 'unknown')}")
    print(f"overall: {verdict.get('overall', 'unknown')}")
    print(f"offline_verification: {status}")
    print(f"entries: {len(entries)}")
    print(f"child_sessions: {len(children)}")
    print(f"anchors: {len(anchors)}")
    print(f"checks: {len(evidence.get('checks', []))}")
    print(f"panel_results: {len(evidence.get('panel', []))}")
    print(f"self_validation: {len(evidence.get('self_validation', []))}")

    if args.fail_on_unverified and status != "verified":
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Layered Long Run

examples/m4-layered-long-run.md captures the intended prompt shape for a long-running contract with layered memory, stable prefix caching, and projection.

# M4 Layered Long-Run Example

This example documents the intended long-run prompt shape for a contract with
`goal_repin_every = 8`, `projection.keep_tail = 3`, and the default compaction
exempt tiers.

## Initial Call

Stable prefix:

1. tools
2. L0 core persona
3. L0 self persona
4. L1 principal with Anthropic `cache_control = { type = "ephemeral" }`

Messages:

1. L3 goal ledger: active contract intent and definition of done
2. original user task
3. live history tail

The provider can cache-read the tools + L0/L1 prefix on subsequent calls because
the goal ledger and scratch notes are below the cache breakpoint.

## Step 8 After Compaction

Stable prefix:

1. unchanged tools
2. unchanged L0 core persona
3. unchanged L0 self persona
4. unchanged L1 principal with the same cache breakpoint

Messages:

1. re-pinned L3 goal ledger
2. original user task
3. deterministic context summary of the compacted middle
4. last three history messages
5. L2 delta since the previous cursor, if any
6. L4 scratch, if any

Expected effect: input tokens above the cache breakpoint are stable across the
run, so cache-read tokens dominate once the prefix is warm. The active contract
intent remains visible even when the middle history is summarized.

ADR 0001 — Tiered plugin architecture (small, rock-solid core)

  • Status: Proposed
  • Date: 2026-06-15
  • Deciders: Matt (owner), ALINA
  • Supersedes: —

Naming note: crate names below use a <prefix>- placeholder. The product name (working name “Truss”) is not yet decided — see the “Open questions” section. Nothing in this ADR depends on the final name.

Context

The harness today is a single Rust crate. The runtime, durable execution, signed ledger, and verified delegation are vendor-agnostic and depend only on the Provider / Tool traits — that boundary is already clean. But vendor-specific code (HTTP backends, and soon several subscription-auth strategies that are messy, frequently-broken, and ToS-sensitive — see docs/auth-strategy.md) lives inside the crate. As we add providers, the core’s dependency surface and compile size grow, and vendor churn risks destabilizing the part we most want to keep rock-solid.

OpenClaw hit the same wall and responded with a major revision to a plugin scheme to keep its core small. We want the same property — the core gets smaller and more stable as providers are added, not bigger — adapted to Rust.

Key constraint that shapes the design: Rust has no stable dynamic-plugin ABI, so “plugin” cannot mean “drop a .so in at runtime” the way it does for OpenClaw’s Node/JS. Rust gives us two real extension mechanisms — compile-time (Cargo features + workspace crates) and runtime (an external process speaking a protocol). The right design uses both, tiered.

Decision

Adopt a three-tier architecture.

Tier 0 — the core (small, frozen, rock-solid)

A minimal core that contains only:

  • the contracts: Provider, Auth, Tool traits, message/usage/error types, the run Ctx;
  • the runtime value: the agent loop, durable execution (journal + replay), the signed ledger, verified delegation.

Invariants (enforced, not aspirational):

  • No vendor dependencies. The core’s Cargo.toml cannot reach a vendor SDK or even an HTTP client — HTTP providers move out of the core. Core deps shrink to essentials (tokio, serde, rusqlite, sha2, ed25519, …).
  • Stable, semver-versioned plugin API. The trait contracts are the public surface; breaking them is a major-version event.
  • Quality is enforced at the boundary by a provider compliance kit (below), so a small core does not mean a loose one.

Tier 1 — first-party plugins = feature-gated workspace crates

Each provider / auth strategy is its own crate, behind a Cargo feature. A build compiles only the ones enabled; an unused provider is not in the binary at all (this is the bloat fix — opt-in compilation, not runtime toggles).

crates/
  <prefix>-core/         # contracts + runtime (Tier 0)
  <prefix>-provider-openai/      # feature: provider-openai
  <prefix>-provider-anthropic/   # feature: provider-anthropic
  <prefix>-auth-apikey/          # feature: auth-apikey  (default)
  <prefix>-auth-oauth/           # feature: auth-oauth   (generic OAuth login/refresh)
  <prefix>-runtime-cli/          # feature: runtime-cli  (drive a vendor CLI as a backend)
  <prefix>-plugin-host/          # Tier 2 external-process host
  <prefix>-cli/                  # the binary; selects features

Adding a first-party provider = a new crate + one feature line. The core never changes. Default features stay lean (proposed: auth-apikey, provider-openai, provider-anthropic); everything else opt-in.

Tier 2 — external plugins = a minimal process protocol

For third-party, experimental, out-of-tree, or ToS-sensitive providers, the core ships one generic ExternalProvider that speaks a tiny JSON-over-stdio contract (the model-provider analog of MCP-for-tools). Any executable implementing the protocol is a provider — no recompile, no core change, any language.

Sketch (line-delimited JSON-RPC over stdio):

→ {"method":"describe"}                      ← {"name","models","capabilities"}
→ {"method":"complete","params":{request}}   ← {"content","stop_reason","usage"}
→ {"method":"complete_stream","params":…}     ← stream of {delta} then {done}
→ {"method":"shutdown"}

This is where future subscription providers live. It isolates the per-vendor token-replay logic (which breaks often and carries ToS risk) entirely outside the core.

How the auth/subscription problem maps onto this

The core knows only Auth (produce credentials + headers for a request) and Provider. Every subscription strategy is a plugin:

  • API key — auth-apikey (Tier 1, default).
  • OAuth subscription — a generic auth-oauth login/refresh engine (Tier 1) + a small per-vendor descriptor (endpoint, headers, beta flags). Viable for OpenAI (tolerated); not for Anthropic (ToS-banned Feb 2026 — see docs/auth-strategy.md).
  • Vendor CLI runtime — runtime-cli (Tier 1) or a Tier-2 plugin; the compliant path for Anthropic subscriptions. This is where the prototype claude-cli provider (PR #10) belongs — as one feature-gated plugin, off by default, not a core feature and not “the” approach.

Provider compliance kit

A shared test suite (in <prefix>-core under a testkit feature) that every provider — Tier 1 or Tier 2 — must pass: cancellation honored, deadline honored, typed-error classification, usage accounting, tool-call translation round-trip, streaming contract. Keeps the core small while guaranteeing plugins behave.

Phasing (protects the June 28 demo)

A big-bang workspace split right before a demo is how things break. So:

  • Phase 0 — now, non-breaking: feature-gate the existing providers in the current single crate. Instant “lean default build” win, near-zero risk, no file moves, demo unaffected.
  • Phase 1 — after the demo: mechanically extract Tier 0 + Tier 1 into the workspace. Pure refactor, covered by the existing test suite.
  • Phase 2: define + ship the Tier-2 external provider protocol. From then on, new subscription providers never touch the core.

Consequences

Positive

  • Core shrinks and stabilizes as providers grow — the opposite of bloat.
  • Vendor churn / ToS risk is isolated outside the core.
  • Lean default binary; users compile in only what they need.
  • Tier 2 gives the OpenClaw-style “drop in a plugin” without Rust dynamic-loading.

Negative / costs

  • A workspace is more moving parts than one crate (Phase 1 cost).
  • Tier-2 plugins pay IPC/serialization overhead vs. in-process (acceptable; they’re for the long tail, not the hot default).
  • The compliance kit is upfront work — but it’s how we keep the core honest.

Alternatives considered

  • Stay a monolith, gate with features only. Simpler, but vendor deps still live in the core crate and the dependency surface keeps growing. (Phase 0 is this — as a stepping stone, not the destination.)
  • Dynamic dylib/abi_stable plugins. True runtime loading, but Rust’s unstable ABI makes this fragile and a maintenance trap. Rejected.
  • WASM plugins. Sandboxed, dynamic, language-agnostic — attractive long term, but heavier to build now and overkill for providers. Deferred; Tier-2 process plugins get 90% of the benefit sooner.

Open questions (need decisions)

  1. Product name / crate prefix. Blocks crate naming. “Truss” is the working name but unsettled — to be decided before Phase 1.
  2. Default-feature matrix. Proposed: auth-apikey, provider-openai, provider-anthropic on by default; all else opt-in. Confirm.
  3. Disposition of PR #10 (claude-cli). Keep as an off-by-default plugin example, or hold until Phase 1?

ADR 0002 — Provider registry boundary (auth lives in the provider)

  • Status: Accepted
  • Date: 2026-06-15
  • Deciders: Matt (owner), ALINA
  • Builds on: ADR 0001

Context

ADR 0001 set the strategy: a small, rock-solid core, with providers as plugins, realized in Rust as a compile-time tier (Cargo features + workspace crates) now and an optional runtime tier (external process / subprocess protocol) later. The workspace split shipped — truss-core is vendor-free and only knows the Provider trait; truss-providers holds the vendor code.

But the boundary wasn’t real yet. The CLI still reached across it: truss-cli contained two near-identical functions that hardcoded match args.provider { ... } arms, read ANTHROPIC_API_KEY / OPENAI_API_KEY directly, and knew each provider’s default model. Adding a provider meant editing the CLI. Worse, the CLI was the place auth was resolved — which is exactly the line that would have to branch for subscription/OAuth vs. API key (see docs/auth-strategy.md). Auth was in the wrong layer.

Decision

Introduce a provider registry in truss-providers (registry.rs) as the one seam callers use. Two changes:

  1. Dispatch by id, in truss-providers. registry::build(name, &ProviderConfig) -> Result<Arc<dyn Provider>, BuildError> owns the match over provider ids, with each arm #[cfg]-gated by its feature. registry::available() -> Vec<&'static str> lists what’s compiled in (drives help + error messages). The CLI now just forwards args.provider + a ProviderConfig; it no longer names a concrete provider or knows which features exist.

  2. Auth resolution moves into the provider. Each provider gets a pub(crate) fn build(&ProviderConfig) that resolves its own credentials and default model. ProviderConfig carries only intent (model, base_url) — never credentials. Today every provider resolves an API key from the environment; a future subscription/OAuth provider resolves its token behind the identical signature, so it slots in without touching truss-core or truss-cli.

Adding a provider is now: new module + a cargo feature + two lines in registry.rs (one in available(), one match arm in build()). The core and CLI are untouched.

BuildError

Construction failures get their own typed error, surfaced before any model call:

  • Unknown { name, available } — no provider with this id is compiled in; the message lists the alternatives.
  • Auth(String) — credentials missing/unusable (e.g. an env var isn’t set).
  • Config(String) — structurally invalid request (e.g. OpenAI needs a model).

This keeps “couldn’t even start” distinct from transport/runtime ProviderError.

Why not a global inventory/linkme-style auto-registry?

Crates like inventory let providers self-register via life-before-main static constructors, removing even the two-line edit. Rejected for now: it adds a dependency and link-time magic for a registry with a handful of entries, and it hides the provider list (harder to reason about what a given build contains). An explicit match is trivially auditable and costs two lines. Revisit only if the provider count grows large enough that the central list becomes a real merge-point.

Consequences

Good

  • The boundary is now compiler-enforced and used: the CLI cannot reach into a vendor. Verified by building truss-cli --no-default-features --features provider-openai (Anthropic fully excluded) and by a dynamic “unknown provider” error that lists exactly what’s compiled in.
  • Auth lives where the provider-specific knowledge is. The subscription-auth work (ADR 0001’s messy tier) now has a clean home and a stable signature to target — without building it yet.
  • Default model + env-var names left the CLI; one source of truth per provider.

Costs / trade-offs

  • Still compile-time: a third party can’t add a provider without recompiling. That is the explicit ADR 0001 boundary; the runtime/subprocess tier remains the answer for external providers and is unaffected by this change.
  • ProviderConfig will grow (e.g. an auth field) when subscription auth lands. That’s expected and additive — the seam is in the right place.

Follow-ups

  • Subscription/OAuth provider(s) as new registry entries, per docs/auth-strategy.md.
  • Optionally surface available() in --provider’s help text.
  • The runtime (subprocess-protocol) plugin tier, if/when an external provider needs it.

ADR 0003 — Subscription execution tiers (delegated CLI + OAuth-replay provider)

Context

docs/auth-strategy.md (incl. the 2026-06-15 deep dive) established: only OAuth login/refresh generalizes across vendors; the inference call is irreducibly per-vendor; and vendor CLIs (claude, codex) are full agents that own their own loop/tools/compaction — not raw model endpoints. ADR 0002 gave us the registry seam where each provider resolves its own auth.

Matt’s direction (2026-06-15): proceed assuming 3rd-party subscription usage stays supported near-term; build Claude first; for OpenAI, build both a delegated path and in-process subscription replay — because a replay path that “works just like the API path” is ideal for development/testing (no API credits burned, full Truss loop).

That last point is the crux: there are two structurally different ways a subscription can drive work, and they live at different layers.

Decision

Support subscription auth at two tiers, chosen per vendor by what that vendor’s subscription surface actually allows:

Tier 1 — In-loop OAuth-replay Provider (we own the loop)

A normal Provider (ADR 0002 registry entry) whose build() resolves an OAuth subscription token instead of an API key, then calls the vendor’s HTTP endpoint with the vendor’s mandatory client headers. Because it’s one-request-in/out, it behaves exactly like the API path — it drives Truss’s full durable, signed, policy-enforced, N-reviewer-verified loop. Best for development/testing and for any vendor whose subscription endpoint accepts a replayed token.

  • OpenAI: viable now (~/.codex/auth.json → Bearer access-token + ChatGPT-Account-ID against the Responses endpoint; refresh via the OAuth token endpoint). Build it — it’s our cheap dev driver for the whole stack.
  • Anthropic: not this tier — replay is ToS-banned (2026-02-19). Anthropic subscription only via Tier 2.

Tier 2 — Delegated-CLI Executor (the vendor owns the inner loop)

Not a Provider. A new Executor abstraction at the delegation layer: shell out to the vendor’s own agent CLI (claude -p --model claude-opus-4-8, codex exec) with subscription credentials cleared from the child env so the CLI uses its own stored login (OpenClaw’s model — zero token replay by us, ToS-robust). The CLI runs its own loop/tools; we do not journal inner steps. We wrap at the contract boundary: record the contract, capture the delegated result, then run our independent check clauses + N-reviewer panel on it and sign the verdict.

  • Anthropic: this is the path. claude -p --model claude-opus-4-8 with ANTHROPIC_API_KEY/OAuth vars cleared.
  • OpenAI: also available here (codex exec) for parity / when you want the vendor’s full agent rather than just its model.

The Executor seam (new)

run_contract stops taking executor: Arc<dyn Provider> and takes:

pub enum Executor {
    /// Truss owns the loop: durable agent drives a Provider. Full per-step journal,
    /// policy, tool sandbox. (API key OR Tier-1 OAuth-replay provider.)
    InLoop(Arc<dyn Provider>),
    /// A vendor agent owns the inner loop (Tier 2). Shell out, creds cleared,
    /// capture result, verify at the contract boundary.
    DelegatedCli(CliExecutor),
}

Verification (checks + reviewer panel + signed verdict) is identical for both — that’s the invariant. Subscription auth never weakens the moat: every contract still yields a signed, independently-verified verdict. The only thing that varies is whether the inner loop was ours (journaled per-step) or the vendor’s (verified at the boundary).

CliExecutor mirrors the load-bearing bits of OpenClaw’s CliBackendConfig: command, args, clear_env (vars stripped so the CLI uses its own login), prompt_via (stdin | arg). Honors Ctx cancellation + deadline. Configurable via a contract [executor] block.

Why this split (not one mold)

You cannot have “the vendor CLI authenticates and drives” and “Truss journals every inner step” simultaneously — the CLI owns the loop. Forcing Tier 2 into the Provider trait would be a lie (it’s not one-shot). Forcing Tier 1 into an Executor would throw away the per-step durability we get for free when we own the loop. Two honest abstractions beat one leaky one.

Consequences

Good

  • Anthropic subscription works ToS-cleanly (vendor CLI owns login, no replay).
  • OpenAI gives us a subscription-backed Provider that drives the full stack — cheap dev/testing without API credits, behaves just like the API path.
  • Both tiers sit behind existing seams (registry for Tier 1, the new Executor enum for Tier 2). Core stays small; vendor/ToS-sensitive logic stays at the edges.
  • If a vendor flips billing/ToS (as Anthropic nearly did 2026-06-15), we disable one tier for one vendor; the API-key baseline and the verified core are untouched.

Costs / trade-offs

  • Tier 2 loses inner-step journaling (we verify at the boundary instead). Acceptable: verified delegation is the value, not inner-step replay.
  • Tier 1 for OpenAI is the grey/revocable path (docs/auth-strategy.md): off by default, dev/personal-use framing, never multi-tenant. Tier 2 is the safer production answer for OpenAI subscriptions.
  • Tier 2 couples to each CLI’s argv/stdin contract → needs a live smoke test per CLI (static inspection isn’t enough); hermetic tests use a fake CLI to prove the subprocess wiring (env-clearing, stdin piping, capture, cancellation).

Build order

  1. (this ADR’s PR) Tier-2 Executor seam + CliExecutor + Anthropic (claude -p --model claude-opus-4-8), hermetic tests with a fake CLI, contract [executor] block. “Claude first.”
  2. Tier-1 OpenAI OAuth-replay provider (registry entry; dev driver) + Tier-2 codex exec. Next PR.

Status (2026-06-16)

  • Tier 2 (delegated CLI): shipped v0.10.0. Anthropic claude -p --model claude-opus-4-8 (examples/contract-claude-subscription.toml) and OpenAI codex exec (examples/contract-codex-exec.toml) both work via the same CliExecutor — no provider code, just an [executor] block.
  • Tier 1 (OpenAI in-process OAuth-replay): shipped v0.11.0 as the openai-codex registry provider, behind the provider-openai-codex cargo feature (off by default — the ToS-grey path). Reads ~/.codex/auth.json (codex_auth.rs), talks the ChatGPT-backend Responses API (openai_codex.rs), refreshes the access token reactively on a 401. Request shaping + SSE parsing are unit-tested against the source-exact schema, but the live request hasn’t been smoke-tested against the real backend — the client headers (originator, version, user-agent) may need tuning, and the backend can fingerprint non-official clients. Treat as dev/personal-use until a real-login smoke test confirms it. Anthropic in-process replay stays banned — Tier 2 only.

ADR 0004 — Governed memory substrate

Context

Truss needs durable memory that is typed, trust-tiered, and provenance-tracked instead of flat markdown injection. M1 establishes the substrate only: storage, content-addressing, trust state transitions, and deltas. Tool binding, verification drivers, persona injection, salience policy, L2 scope, and conflict resolution are layered on by later milestones.

Decision

Add truss_core::memory with a substrate-agnostic MemoryStore trait and an M1 SqliteMemoryStore implementation. The SQLite implementation shares the durable execution database connection, stores model writes as Unverified, persists ledger provenance, persists promotion verdict JSON, and uses a monotonic write_seq cursor for delta rehydration.

MemoryId is content-addressed as lowercase SHA-256 hex over kind || key || body. Fact and Lesson entries supersede the active prior entry with the same kind/key; superseded entries remain auditable but are not returned by default queries. Default queries return only active Verified entries.

Consequences

This gives later milestones a governed storage boundary without changing the agent loop yet. M4 resolves the formerly-deferred L2 scope and verified-conflict questions as configurable policy knobs rather than one hard-coded answer: scope = "per-agent" by default and conflict = "verified-wins" by default. Salience remains an M5 implementation seam with a default-off configurable field.

ADR 0005 — Memory tool binding

Context

ADR 0004 landed the governed SQLite memory substrate but left it unreachable by the model. M2 binds that substrate to the native Anthropic-trained memory_20250818 surface while preserving the M1 trust boundary: model writes are stored, but not trusted.

Decision

Register a memory tool in the Truss tool registry and advertise it as the native memory_20250818 tool only when a per-session memory_tool flag is enabled. The default remains off, so existing durable and non-durable runs do not see or execute the memory tool.

The binding maps filesystem-like memory commands onto MemoryStore:

  • create, insert, and str_replace write MemoryDrafts that always land as Unverified.
  • view queries with the default Verified trust floor and renders a directory/file-style listing.
  • delete marks active entries stale.
  • rename writes a replacement entry and marks the source stale.

Entry kind is inferred from path prefixes:

  • /memories/goal/ or /goals/ → Goal
  • /decisions/ → Decision
  • /lessons/ or /memories/lessons/ → Lesson
  • /facts/ → Fact
  • /artifacts/ → Artifact
  • anything else → Scratch

Durable runs pass the journal model_result sequence containing the tool call into tool execution as LedgerRef { session, seq }. Memory writes stamp that reference as produced_by, so each entry points at the signed journal record that caused it.

AgentEvent now includes MemoryWrite and MemoryRecall so UIs and audit consumers can observe memory activity without scraping tool output.

Consequences

The model can now use governed memory through a native tool surface, but no memory appears unless the session explicitly enables it. Default recall remains verified-only; M3 can add promotion policy without changing the tool protocol.

ADR 0006: Trust Tiers + Promotion

Status: Accepted

Context

M1 added the governed SQLite memory substrate and M2 bound the native memory_20250818 tool. M3 turns model-authored memory from “stored” into “trusted” by reusing the verified-delegation reviewer panel and delegation::Verdict that already gate contract completion.

Decision

Truss resolves memory policy at session build with layered overrides: global [memory], then [agent.memory], then contract [memory].

VerificationPolicy controls promotion timing:

  • lazy verifies an unverified entry on first recall, then caches the verdict in the entry trust tier.
  • eager verifies on write and promotes only a passing panel verdict.
  • tiered runs a deterministic cheap hygiene gate on every write and runs the full reviewer panel on write for fact and decision entries.
  • off never promotes; recalls may surface unverified entries, but the rendered view always tags them as trust: unverified.

Promotion and rejection both store the existing delegation::Verdict. A verified replacement for the same supersede key demotes the prior verified entry to stale; a rejected replacement restores the prior verified entry as active.

M4 extends the same layered policy surface with configurable L2 scope and conflict handling. Defaults are conservative: scope = "per-agent" and conflict = "verified-wins". Other modes (per-project, shared, latest-wins, panel-arbitrated, keep-both-flagged) are present for evaluation, not because the trust model assumes one answer fits every run.

Consequences

Verified keeps one meaning: an N-reviewer panel signed off. Lower-cost modes change when verification happens, not what the label asserts. Memory views always render the trust tier, preventing silent trust laundering.

ADR 0007: Layered Injection + Identity Persona

Status: Accepted

Context

M1-M3 gave Truss a governed memory store, the native memory_20250818 tool, and trust-tier promotion. M4 changes how that state reaches the model: stop dumping all identity, memory, and task notes as one volatile markdown blob; assemble the prompt by lifetime tier and align the slow tiers to the provider prompt cache.

Matt’s design directive for M4 is that policy choices remain configurable so real work can A/B evaluate alternatives. Even knobs not exposed in a friendly UI are typed config with layered resolution: global [memory] / [identity], then [agent.memory], then contract [memory].

Decision

Truss assembles context in this order:

  1. tools
  2. system blocks: L0 core identity, L0 self identity, L1 principal
  3. messages: L3 goal ledger, projected history, L2 memory deltas, L4 scratch

The last L0/L1 system block carries cache_control = { type = "ephemeral" } for providers that support prompt caching. The cache-prefix key is computed only from tools plus L0/L1 system content, so L3 goal changes and L4 scratch changes do not invalidate the stable prefix.

Persona is first-class:

  • PersonaCore is operator-signed with identity::Signer, verified on load, and refused if the signature no longer matches.
  • PersonaSelf contains amendable voice/working identity and version metadata.
  • Persona::render() emits L0 core before L0 self.

Identity amendments are deliberate:

  • amend_identity is a dedicated signed tool.
  • Amendments targeting core are denied.
  • IdentityPolicy::propose buffers for approval.
  • IdentityPolicy::self_amend buffers CommitAtBoundary, applies on the next session build, and records a reversible journal event.
  • The amendment is verdict-checked against core before commit.

L3 goal ledger pins the active contract intent and definition of done. It is injected on the configured cadence and remains outside compaction.

Resume/re-pin uses MemoryStore::delta_since(cursor) for L2 rehydration. The model sees only memory changes after its cursor, never the whole store unless the cursor is zero.

Configurable Knobs

KnobDefaultValuesLayering
verificationlazyoff, lazy, eager, tieredglobal -> agent -> contract
identityproposelocked, propose, self_amendglobal -> agent -> contract
scopeper-agentper-agent, per-project, sharedglobal -> agent -> contract
conflictverified-winsverified-wins, latest-wins, panel-arbitrated, keep-both-flaggedglobal -> agent -> contract
goal_repin_every8positive integerglobal -> agent -> contract
projection.keep_tail0integer, 0 disables projectionglobal -> agent -> contract
projection.compaction_exempt_tiersl0_core, l0_self, l1_principal, l3_goalany ContextTier listglobal -> agent -> contract
salienceoffoff, harness-recency, model-scored, hybridglobal -> agent -> contract

Salience is an M5 seam only. M4 parses and resolves the policy but does not evict or rank memory differently based on it.

Consequences

The default path stays conservative: no projection unless configured, memory is per-agent, conflicts prefer verified entries, identity self-amendment still needs approval, and salience does nothing. When M4 surfaces the layered context, the provider can reuse the L0/L1 cached prefix across long runs while the L3 goal ledger keeps the active intent fresh across compaction.

ADR 0008: Child-Run Substrate

Status: Accepted

Context

docs/extensibility-architecture.md identifies child-run as the keystone primitive beneath model-facing spawn, best-of-N fan-out, and the eventual N-reviewer panel refactor. The core constraint is that child work must not become a side channel: all lifecycle, budget, and provenance facts must flow through the existing Ctx, events, and signed durable ledger.

Decision

Truss adds an internal child_run substrate in truss-core.

A child-run executes a sub-task with a scoped Ctx:

  • cancellation uses Ctx child tokens, so cancelling the parent cancels the child;
  • the child deadline is the tighter of the parent’s inherited deadline and any requested child timeout;
  • the child can inherit the parent Principal or run under an explicitly tighter principal/scope label;
  • spend is recorded into the parent’s shared tree meter while an additional per-child BudgetLimits slice is enforced.

The result is a verified result, not a transcript:

  • child_run::ChildRunOutput contains the existing delegation::Verdict plus produced ArtifactRefs;
  • the runner records the result in the child ledger as child_result;
  • the returned VerifiedChildResult includes the verdict, artifacts, the child root/head hashes, the verdict_ref, and the parent anchor ref.

Event Vocabulary

The typed AgentEvent stream now includes:

  • child_started — parent id, child id, task, scope, and budget slice;
  • child_completed — parent id, child id, child head hash, verdict ref, and artifacts;
  • child_failed — parent id, child id, and error.

These events are the future UI/SDK projection point for spawn and fan-out.

Ledger Anchoring

The child ledger is its own signed session. The parent ledger records a child_anchor entry containing:

  • parent_run_id
  • child_run_id
  • child_root_hash
  • child_head_hash
  • task
  • scope
  • budget_slice
  • status
  • verdict_ref when completed
  • error when failed

Store::verify_chain now recursively verifies child_anchor payloads: the parent chain must verify, the child chain must verify, and the child’s current root/head hashes must match the anchor. The CLI --verify path uses the same store method, so parent-to-child provenance is checked by the existing verification command.

Budget Slice Semantics

Budget governance is tree-aware:

  • each child has a local slice meter that enforces its own ceiling;
  • every child usage record also increments the parent’s shared tree meter;
  • multiple child-runs therefore cannot collectively exceed the parent ceiling, even when each individual slice is larger than the remaining parent budget.

The existing BudgetMeter still has root-run semantics when no slice is present.

Follow-Up Seams

This ADR deliberately stops before the model-facing surface.

  • Spawn tool: expose child_run::run_child as a tool that packages a model task into ChildRunOptions and returns a VerifiedChildResult.
  • Best-of-N fan-out: launch K child-runs with sibling budget slices and reduce their verified results.
  • Reviewer panel refactor: the current N-reviewer panel is structurally a fan-out instance. It should be re-expressed on top of child-run/fan-out after this substrate is reviewed, not changed in this PR.

Consequences

The primitive is conservative: no new transcript store, no special event bus, and no parallel reviewer rewrite. The cost is that child-run users must produce a Verdict and artifacts explicitly, but that keeps the trust boundary aligned with the rest of Truss: signed verified evidence first, model-facing ergonomics later.

ADR 0009: Best-of-N Fan-Out on Child-Run

Status: Accepted

Context

ADR 0008 introduced the child-run substrate as the provenance-preserving primitive beneath spawn, best-of-N fan-out, and the eventual reviewer-panel refactor. Step 1 of docs/self-improvement-impl-plan.md builds fan-out first because it unlocks both the lesson-quality engine and paired-eval substrate without changing the trust model.

The constraint is the same as child-run: fan-out must not execute work through a side channel. Every branch is a child-run, with its own scoped Ctx, budget slice, child ledger, and parent child_anchor.

Decision

Truss adds truss_core::fan_out as a thin orchestration layer over child_run::run_child.

run_fan_out launches k sibling child-runs of the same task concurrently, collects the verified ChildRunOutput results, and applies a pluggable Reducer. It returns the winning child plus the signed parent-ledger reduction decision.

The reducer set is:

  • Vote — strict majority over verified child overall verdicts. This is the default because it needs no external scorer or model reviewer dependency.
  • JudgePanel — reuses the existing delegation::run_review_panel no-stake reviewer machinery to evaluate candidate child results. This PR exposes that helper instead of copying or forking the panel logic.
  • BestScore — calls a caller-supplied BestScore trait object and picks the highest finite score.
  • FirstSuccess — picks the first child whose verified verdict is Pass, then cancels sibling branches.

The serializable reducer knob is FanOutReducerKind / ReducerConfig; concrete runtime dependencies such as reviewer providers and scorer trait objects are bound by the caller.

Budget and Cancellation Semantics

Fan-out creates a same-run cancellation scope for its sibling branches. That scope preserves the parent run id, so child-run anchors still land in the parent ledger, but it can be cancelled independently of the caller. FirstSuccess uses that scope to cancel losing branches after the first passing verified result.

Parent cancellation and deadlines propagate through the same Ctx child-token chain as ordinary child-runs.

Budget remains tree-aware:

  • every branch receives a per-child BudgetLimits slice;
  • branch spend accrues to the shared parent BudgetMeter;
  • fan-out does not create a separate meter or bypass child_run;
  • step accounting saturates at the configured step ceiling so concurrent sibling branches cannot collectively push the shared step counter past the parent limit before enforcement observes it.

Event Vocabulary

The typed AgentEvent stream now includes:

  • fanout_started — parent run id, fan-out id, task, k, and reducer kind;
  • fanout_completed — reducer kind, per-child anchor/result refs, winner, why, and the parent-ledger decision ref.

These events are the future UI/SDK projection point for best-of-N work.

Ledger and Verification

Child branches are still anchored by the existing child_anchor entry from ADR 0008. Fan-out adds one parent-ledger entry:

  • fanout_reduction — parent run id, fan-out id, reducer kind, all child refs, winner, and reducer explanation.

Store::verify_chain now validates fan-out decisions in addition to recursively validating child anchors. A fanout_reduction must reference real child anchors in the same parent ledger; winner refs must point at one of those completed children and match its recorded verdict_ref.

The CLI --verify path uses Store::verify_chain, so parent-to-fan-out-to-child provenance is checked by the existing verification command.

Follow-Up Seams

This PR deliberately leaves two surfaces unbuilt:

  • Model-facing spawn tool: expose child_run::run_child as a tool that packages a model-requested subtask into ChildRunOptions.
  • Reviewer-panel refactor: the current N-reviewer panel is structurally a fan-out instance, but this PR only reuses its helper for JudgePanel. A later PR should re-express the panel itself on top of fan-out after the primitive is reviewed.

Consequences

Fan-out inherits child-run’s trust boundary: branches produce verified results, not transcripts, and parent provenance remains a signed hash chain. The cost is that callers must provide a cloneable branch runner and, for advanced reducers, bind reviewer providers or scoring logic explicitly. That is acceptable because the model-facing ergonomics are intentionally deferred to the spawn-tool PR.

ADR 0010: Spawn Tool + Reviewer Panel/Fan-Out Unification

Status: Accepted

Context

ADR 0008 introduced child_run as the provenance-preserving substrate beneath model-facing spawn, fan-out, and reviewer-panel refactors. ADR 0009 added best-of-N fan-out but deliberately left two follow-up seams: expose child-run as a model-facing tool, and close the dependency loop between delegation’s N-reviewer panel and fan-out’s JudgePanel reducer.

The constraints remain unchanged:

  • spawned work must not be a side channel;
  • tree-wide parent budget ceilings must still apply;
  • child work must have a scoped Ctx, signed child ledger, parent child_anchor, and cancellation/deadline propagation;
  • the existing delegation Verdict shape and reviewer semantics must not change.

Decision

Truss adds an opt-in model-facing tool named spawn, gated by the CLI flag --spawn-tool.

The tool input is:

  • task (required string);
  • budget_slice (optional BudgetLimits object with max_total_tokens, max_cost_usd, and max_steps);
  • scope (optional provenance label).

The tool output is a JSON verified-result summary:

  • child_run_id;
  • verdict.overall, verdict.summary, and clause results;
  • artifacts as ArtifactRef values;
  • ledger.verdict_ref;
  • ledger.parent_anchor;
  • ledger.child_root_hash and ledger.child_head_hash;
  • usage for the child slice.

spawn is registered like the governed memory tool: default-off, added only by an explicit registration path. The CLI exposes that as --spawn-tool, and only for store-backed durable/contract/daemon runs. Non-durable runs do not have a signed Store, so they reject --spawn-tool instead of creating an unverifiable side ledger.

When the durable loop calls spawn, it uses the existing Tool::call_with_ledger provenance pointer to anchor the child into the durable parent session that produced the tool call. The child itself runs through child_run::run_child; no code path bypasses child-run. The child durable loop records its own signed model/tool steps, and child_run records the child_result plus parent child_anchor. Existing child_started, child_completed, and child_failed events are reused.

Reviewer-panel unification is behavior-preserving: the no-stake panel implementation now lives behind fan_out::run_judge_panel, and both delegation::run_review_panel and fan-out’s Reducer::JudgePanel call that one helper. The public delegation API remains intact, and the strict-majority semantics, adversarial lenses, prompt shape, evidence text, and ClauseResult output are unchanged.

Consequences

The spawn surface is intentionally narrow: it returns verified child-run provenance, not a child transcript. Recursive spawn is not registered into child tool registries by the CLI helper; children get the current non-spawn toolset (including governed memory when enabled). This avoids accidental recursive fan-out while preserving the tree-wide budget ceiling as the hard stop.

The panel refactor removes the one-off reviewer implementation without changing signed verdict semantics. Existing delegation tests continue to assert the same majority pass/fail behavior and reviewer-context handling, while fan-out tests continue to cover JudgePanel selection.

ADR 0011: Lesson Extraction + Governed Policy Seam

Status: Accepted

Context

docs/self-improvement-impl-plan.md Step 3 starts SI-1: after a contract produces a signed, evidence-backed verdict, Truss may propose durable lessons from that run. The lesson is not trusted on arrival. It is L2 candidate material: written as Unverified, tied to the producing ledger event, and withheld from trusted recall until a later promotion milestone.

docs/self-improvement-architecture.md also identifies the L3 seam: L2 lesson writes and L3 policy/config writes should use the same governance rails. L2 writes beliefs; L3 later writes policy deltas. Both need trust tiers, provenance, signed ledger references, panel-gated promotion, and reversibility.

Decision

Truss adds LessonExtractionPolicy to the layered MemoryConfig:

  • off is the default-safe mode. It performs no extractor model call and writes no memory.
  • on runs the extractor after the contract verdict is recorded.
  • model-scored runs the same extractor, but only writes lessons whose model salience is at or above extraction_min_salience (default 0.70).

Extraction runs only after the verdict journal entry lands. Each accepted lesson is written through the existing MemoryStore as EntryKind::Lesson with Trust::Unverified. Its Provenance.produced_by points at the contract verdict ledger entry, and the same LedgerRef is included as a source. The existing AgentEvent::MemoryWrite event is emitted for each lesson.

The extractor uses the existing provider path (Provider::complete) and is bound by the caller via ContractCloseLearning. When policy is off, Truss returns before touching the provider, so the feature is zero-cost in the default configuration.

Truss also adds a governed policy object seam:

  • memory::Policy is a typed, versioned policy object.
  • memory::PolicyTarget enumerates tunable policy surfaces such as routing, PersonaSelf, tool-set, verification, salience, and goal repinning.
  • memory::PolicyRecord mirrors memory’s governance shape: Trust, Provenance, creation time, and supersession pointer.
  • SqliteMemoryStore stores policy records in the same SQLite-backed governed substrate as memory. A policy record’s provenance points to a signed journal entry, so signature validation remains Store::verify_chain.

Deferred

This ADR does not build SI-2 promotion. Lessons remain unverified and are not auto-promoted.

This ADR does not build an L3 optimizer, proposer, evaluator, auto-application path, or policy-delta promotion flow. Policy records can be written and read back, but nothing applies them to runtime behavior.

The L0 core identity and operator-signed boundaries are never auto-tunable. They are intentionally absent from PolicyTarget; future L3 work must preserve that boundary even when adding proposers and promotion gates.

Consequences

SI-1 is useful but conservative: contract runs can now produce durable lesson candidates, but default trusted memory views still exclude them because MemoryQuery::default() remains verified-only.

The L3 seam is now incremental. A future optimizer can propose policy records through the same store/provenance/trust shape instead of inventing a separate configuration trust model.

ADR 0012: Lesson Promotion Through the Reviewer Panel

Status: Accepted

Context

ADR 0011 added SI-1 lesson extraction: contract-close learning can write EntryKind::Lesson records as Unverified, with provenance pointing at the signed contract verdict that produced the candidate lesson. Those records are not trusted on write.

docs/self-improvement-impl-plan.md Step 4 adds SI-2: the L2 learning loop must be useful without becoming self-poisoning. A model-authored lesson may influence future runs only after an independent panel verifies the claim against its evidence, and the decision must be reversible if a later verified lesson supersedes or contradicts it.

Decision

Lesson promotion reuses the existing M3 trust-tier machinery and the existing delegation reviewer panel:

  • extracted lessons still land as Trust::Unverified;
  • the promotion gate calls delegation::run_review_panel through the existing memory-claim verifier, now including the lesson claim, provenance ledger refs, and the producing contract verdict evidence;
  • a passing delegation::Verdict calls MemoryStore::promote, which stores the verdict in Provenance.verified_by;
  • a failing or unverifiable verdict calls MemoryStore::reject, retaining the record for audit while excluding it from trusted views;
  • the promoting/rejecting verdict is also recorded as a signed memory_verdict journal event when a durable store is available, so normal ledger verification validates the promotion trail.

No new policy knob is introduced. Lesson promotion uses the resolved MemoryConfig.verification (VerificationPolicy) layered global → agent → contract:

  • off: never auto-promote; lessons remain honestly labeled Unverified;
  • lazy: do not promote at extraction time; first recall through the governed memory tool runs the same panel before surfacing the lesson as trusted;
  • eager: run the panel at extraction/write time;
  • tiered: keeps the existing tiered semantics from M3.

The invariant is explicit: Verified always means “the panel signed off.” The policy changes when and which entries are verified, never the meaning of the label. Unverified lessons are excluded from the default trusted memory query.

Reversibility

Reversibility stays in the M3 store path. Fact and Lesson entries supersede by key. If a newer candidate with the same key is later promoted, the prior verified entry is demoted to Trust::Stale and linked through superseded_by. The stale record, the new verified record, and both signed verdicts remain in the ledger-backed audit trail.

Consequences

SI-2 completes the trust moat for extracted lessons: lesson recall can later use verified lessons without inventing a separate safety story. The cost/control surface remains the user-selectable VerificationPolicy, so operators can pick eager freshness, lazy amortized verification, or off without weakening the meaning of Verified.

The verifier still judges only the evidence passed into the panel. SI-3 recall must continue to default to trusted/verified views and avoid pushing unverified lessons into future prompts.

ADR 0013: Verified Lesson Recall Into L2 Context

Status: Accepted

Context

ADR 0011 added SI-1 lesson extraction: contract-close learning can write EntryKind::Lesson records as Unverified. ADR 0012 added SI-2 promotion: lessons become Verified only after the reviewer panel signs the claim, and superseded lessons are demoted to Stale.

docs/self-improvement-impl-plan.md Step 5 completes the L2 loop. A verified lesson must influence a later run, but only through the existing M4 layered context machinery and only under the same trust and provenance rules as other governed memory.

Decision

Truss adds verified lesson recall to the existing M4 L2 memory injection path. At fresh durable run start, the loop asks the configured MemoryStore for active EntryKind::Lesson entries using the existing verified-only query shape:

  • kind = Lesson;
  • min_trust = Verified;
  • Rejected, Unverified, and Stale entries are excluded by the store/query trust invariant;
  • no vector store or separate injector is introduced.

The selected lessons are rendered as an L2 message block labeled [L2 verified learned lessons]. The label is intentional: learned lessons are not silently blended into ground truth or L0/L1 identity. They remain auditable, trusted L2 context.

Relevance And Budget

Recall uses the current task context (contract goal/definition of done and the fresh prompt) and ranks candidates by simple lexical overlap with the lesson body/key, then by existing salience metadata and recency. This matches the current store’s simple query/scoring level; M5 can improve retrieval behind the same store trait without changing the injection layer.

Two new layered MemoryConfig knobs bound recall:

  • lesson_recall_max_lessons defaults to 5;
  • lesson_recall_token_budget defaults to 512 approximate tokens.

Both resolve through the existing global → agent → contract layering. Setting either to 0 disables lesson recall. If no verified lesson is relevant, Truss injects no L2 lesson block, so the run pays no additional prompt-token cost.

Provenance

Promotion now persists the durable ledger reference for the signed memory_verdict event when a journal is available. Provenance therefore keeps both:

  • verified_by: the signed reviewer-panel Verdict;
  • verified_by_ref: the LedgerRef of the journal event that recorded that verdict.

Injected lessons render their producing ledger ref and verifying verdict ledger ref, so a recalled belief can be traced back to the run and panel that verified it. Existing Store::verify_chain continues to validate the chain.

Consequences

SI-3 closes the write → promote → recall L2 loop: lessons written by SI-1 and promoted by SI-2 can now affect future runs through trusted L2 context.

The trust invariant is deliberately narrow. Only Verified lessons enter this trusted injection path. Unverified, rejected, and stale lessons remain available for audit and governed tool views where policy allows, but they are not injected as trusted run-start context.

Deferred

SI-4 is still required to prove that learning helps instead of merely existing. The next and final L2 step is the paired learning-on/off evaluation harness, comparing verdict pass-rate and cost with recall enabled versus disabled.

ADR 0014: Paired Learning-On/Off Eval Harness

Status: Accepted

Context

ADR 0011 added SI-1 lesson extraction, ADR 0012 added panel-gated lesson promotion, and ADR 0013 added verified lesson recall into L2 context. Those steps make cross-run learning possible, but existence is not evidence that L2 helps.

docs/self-improvement-impl-plan.md Step 6 requires SI-4: run the same contract with learning enabled and disabled, compare outcomes and cost, and produce the measurement signal that a future L3 optimizer can optimize against.

Decision

Truss adds truss_core::eval, a paired evaluation harness that runs the same contract in two arms:

  • learning_on: lesson extraction on, lesson promotion eager, verified lesson recall enabled.
  • learning_off: lesson extraction off, promotion off, verified lesson recall disabled.

Every repeat in each arm runs through child_run::run_child; the eval harness does not reimplement execution. The child ledger is the durable contract execution ledger for that repeat, and the parent eval ledger records each child_anchor as usual. The contract verifier still uses the existing delegation path, reviewer panel, budget meter, and provider path.

The CLI exposes the measurement as:

truss eval --contract path/to/contract.toml --repeats 3

The command prints a concise human summary to stderr and the structured report as pretty JSON on stdout. It also records the report as a signed eval_report journal entry in the eval parent session, so truss --db <db> --verify <eval_run_id> verifies the report session and recursively verifies the anchored arm child-runs.

EvalReport Schema

EvalReport is serializable with serde:

  • eval_run_id: parent eval session id.
  • contract_id: source contract id.
  • repeats: repeats per arm.
  • verdict: pass or fail for whether the learning-on arm improved or tied with no extra measured cost/tokens.
  • arms: one EvalArmReport per arm, including:
    • arm: learning_on or learning_off;
    • resolved memory config for that arm;
    • pass/fail/unverifiable counts and pass-rate;
    • total budget usage (cost, steps, input/output tokens);
    • child-run ids plus verdict refs, parent anchor refs, and child root/head hashes for traceability.
  • delta: learning-on minus learning-off pass-rate, cost, steps, and tokens.
  • summary: concise human-readable comparison.
  • report_ref: ledger ref of the signed eval_report event after recording.

Measurement Only

SI-4 does not tune policy, change routing, promote configuration deltas, or auto-apply anything based on the result. The report is a signal source only.

L3 remains deferred: a future optimizer may propose policy/config deltas from many EvalReports, but those proposals must use the same governed store/provenance/reviewer gate described in the L2/L3 architecture docs.

Consequences

L2 can now be judged by paired evidence instead of assertion. The cost is that evals may spend roughly twice the budget of a single contract times repeat count, but the report makes that spend visible and auditable.

ADR 0015: Adversarial and Property Testing for Trust Claims

Status: Accepted

Date: 2026-06-17

Context

Truss sells trust: verified results, tamper-evident durable ledgers, bounded child execution, and governed memory that does not silently promote unreviewed claims. Happy-path tests are not enough for that claim. Diligence needs negative tests that prove forged or corrupted evidence is rejected, and randomized invariant tests that keep core trust properties from regressing as orchestration paths multiply.

Decision

Add two standing trust-test categories to truss-core:

  • An adversarial tamper suite that starts from valid signed ledgers, mutates the persisted state, and asserts verification rejects the result.
  • Modest-count proptest invariant tests that randomize operation histories and assert the trust ladder remains coherent.

The adversarial suite covers:

  • corrupt stored entry hash
  • flipped signature byte
  • reordered ledger entries
  • replayed/duplicated ledger entry
  • truncated chain with dropped head
  • forged verdict signature mismatch
  • repointed child anchor head

The property suite covers:

  • Verified memory implies an attached panel verdict and signed memory_verdict ledger ref
  • ledger hash chains validate after arbitrary append sequences
  • branch step usage never exceeds the parent step ceiling for arbitrary branch counts and slice sizes
  • default memory queries surface only Verified entries
  • stored entries carry resolvable ledger provenance

Consequences

These tests become part of the normal PR gate and are intentionally hermetic: in-memory SQLite, fake providers where needed, and no network. Test-only ledger mutation hooks are allowed only to simulate persistence-layer tampering; they do not weaken append-time production invariants.

The suite also identified and fixed one verification gap: verify_chain now checks the session head hash against the final journal entry, so deleting the tail/head entry is detectable instead of verifying the remaining prefix.

ADR 0016: Truss Bench Crate Boundary

Status: Accepted

Date: 2026-06-17

Context

truss_core::eval is now a product primitive: it records paired learning-on/off contract runs through child_run, emits signed EvalReports, and gives Truss a way to prove governed learning helps. That primitive belongs in core because it is part of the shipped runtime and CLI surface.

Real-world benchmark and simulated-user tooling have a different lifecycle. They will accrete external dataset adapters, local fixtures, test personas, research-only glue, and eventually heavier optional dependencies. Keeping that surface inside core would blur the runtime boundary and make shipped crates carry testing-only concerns.

Decision

Add crates/truss-bench as a separate workspace crate for benchmark and interaction-UX tooling. It depends on truss-core; truss-core does not depend on it.

The crate introduces:

  • BenchmarkTask: the shared identity surface for external benchmark items.
  • BenchmarkAdapter: a trait that maps a benchmark task into a Truss Contract.
  • SweBenchStyleAdapter: a local SWE-bench-style adapter where a repo fixture plus shell/test command becomes the contract’s harness-owned check clause.
  • BenchmarkRunner: a thin runner that executes adapted contracts via truss_core::eval::run_paired_eval and aggregates a serializable BenchmarkReport.
  • SimulatedUser: a provider-backed persona/goal harness that drives multi-turn interaction with an agent provider and records turn metrics plus a score/legibility rating.

The key model is that benchmark graders become contract done checks. For the SWE-bench-style local adapter, this means { workdir, intent, test_cmd } maps to a Contract whose done clause runs cd <workdir> && <test_cmd> and expects exit 0. No real SWE-bench data or network access is pulled into this scaffold.

Consequences

The core eval primitive stays in truss-core. truss-bench builds on it instead of moving it because the primitive is useful to shipped Truss workflows while dataset adapters and simulated-user personas are explicitly testing tooling.

The first scaffold remains hermetic: in-repo fixture, fake providers, no external datasets, and no network. Heavy adapters should be feature-gated when added.

Follow-up dataset adapters are pending RECON’s landscape research, especially:

  • SWE-bench Verified
  • GAIA
  • τ-bench

ADR 0017: SWE-bench Verified Adapter

Status: Accepted

Date: 2026-06-17

Context

RECON’s benchmark landscape ranked SWE-bench Verified as the first real-world corpus to integrate: 500 human-verified GitHub issue tasks, MIT license, and a grader that is exactly Truss-shaped: apply a candidate patch, run the repository tests, and resolve the task only when the selected tests pass.

The dataset itself is not part of this repository. Real runs require a cached HuggingFace export of princeton-nlp/SWE-bench_Verified, candidate predictions, the SWE-bench harness, Docker, and task images. Those dependencies are useful for nightly/live evaluation, but they do not belong in default CI.

GAIA remains excluded from CI and this adapter because it is gated and cannot be redistributed into public/open test fixtures.

Decision

Add a real SWE-bench Verified adapter to truss-bench:

  • SweBenchTask is a typed record for the HuggingFace fields: instance_id, repo, base_commit, problem_statement, patch, test_patch, FAIL_TO_PASS, PASS_TO_PASS, and optional metadata.
  • load_swebench_tasks(path) reads local JSON arrays, single JSON objects, or JSONL records. It never fetches the dataset.
  • SweBenchAdapter maps a task into a Truss Contract whose intent is the problem_statement and whose check clause applies the candidate patch, applies the task test_patch, and runs FAIL_TO_PASS + PASS_TO_PASS.
  • The benchmark runner records a signed swebench_result ledger event: { instance_id, patch_hash, resolved }.

Default tests use a tiny in-repo fake SWE-bench record and local repo fixture. They exercise parsing, contract mapping, paired learning-on/off reporting, and the signed ledger record without Docker, network, or external data.

Live Execution Boundary

The official SWE-bench harness path is behind the disabled-by-default swebench-live Cargo feature. With that feature enabled, the CLI exposes:

cargo run -p truss-cli --features swebench-live -- \
  swebench \
  --dataset /path/to/SWE-bench_Verified.jsonl \
  --checkout-root /path/to/checkouts \
  --predictions /path/to/predictions.jsonl \
  --limit 5 \
  --repeats 1

The command assumes the operator has already cached the dataset and installed the official SWE-bench harness with Docker access. The code passes local paths to the harness; it does not fetch HuggingFace data and it does not make Docker a default build dependency.

Consequences

SWE-bench becomes a first-class benchmark adapter while preserving the product boundary: truss-core still owns contracts, evals, and the signed ledger; truss-bench owns dataset shape and benchmark mapping.

Default CI stays hermetic. Live SWE-bench runs are intentionally opt-in because they are heavyweight, stateful, and environment-dependent.

ADR 0018: SWE-bench In-Loop Generation

Status: Accepted

Date: 2026-06-18

Context

The first SWE-bench Verified adapter graded existing prediction JSONL files. That was enough to validate the official fail-closed Docker grading path, but it did not exercise Truss as an agent harness. In particular, truss swebench could prove whether a patch resolved an instance, but the patch itself had to come from outside Truss, often from a delegated vendor CLI.

That left a product gap: SWE-bench should also be able to measure Truss’s own loop, where Executor::InLoop(provider) drives the provider through Truss’s filesystem and bash tools inside the task checkout.

Decision

Add an in-loop SWE-bench solve mode to the live CLI:

truss --provider openai-codex swebench \
  --solve in-loop \
  --dataset /path/to/verified.jsonl \
  --checkout-root /path/to/checkouts \
  --predictions /path/to/generated.jsonl \
  --run-id unique-run-id

--solve predictions remains the default and requires --predictions. It keeps the existing product tier for externally generated patches.

--solve in-loop does the following for each selected task:

  1. Prepare a per-instance checkout under --checkout-root, checking out the task base_commit.
  2. Build a Truss Contract whose intent is the SWE-bench problem_statement and whose constraints scope reads and writes to that checkout.
  3. Run Executor::InLoop(provider) with the default filesystem and bash tool registry.
  4. Capture git diff --binary HEAD as model_patch in an official predictions JSONL row.
  5. Hand that predictions file to the existing official SWE-bench Docker grading path.

The grading wrapper uses a no-op in-process provider in generated mode so the official Docker harness is the sole pass/fail authority after generation. Empty or whitespace-only diffs are intentionally written as empty model_patch values, which the existing preflight rejects, preserving the fail-closed invariant.

Provider selection is the normal provider registry path. openai-codex is the subscription-backed in-process provider used for development validation. anthropic remains selectable for API-key-backed runs, but is metered and should only be run when explicitly authorized.

Consequences

SWE-bench now has three honest tiers:

  • predictions JSONL: grade patches generated elsewhere.
  • delegated CLI: a vendor agent owns the inner loop; Truss verifies at the contract boundary.
  • in-loop: Truss owns the loop, tools, policy, durable ledger, and diff capture.

The in-loop path gives direct debugging signal about the harness itself: whether models can discover files through Truss tools, edit correctly, run tests, and stop with a useful diff. Its score may initially be low; that is useful product evidence rather than a grading failure.

ADR 0019: Provider-Aware Typed Tool Schemas

Status: Accepted

Date: 2026-06-18

Context

Truss already exposes local filesystem and shell capabilities through generic custom function schemas. That keeps execution local, policy-gated, and recorded in the durable ledger, but it misses provider-specific schemas that frontier models were trained to use for coding workflows.

Anthropic already had a precedent in Truss: the governed memory tool advertises the native memory_20250818 schema while still executing through Truss’s local memory substrate. The same principle applies to file and shell tools.

Decision

Provider request serialization may map a generic internal tool definition onto a provider-native, client-executed typed schema when the provider exposes one. Execution stays inside the existing Truss tool registry and policy boundary.

For Anthropic:

  • read_file, write_file, and list_dir are advertised as the text_editor_20250728 tool named str_replace_based_edit_tool.
  • bash is advertised as bash_20250124 named bash.
  • memory continues to use memory_20250818.

The Anthropic text-editor tool name is accepted as a provider alias at local execution time. Its view, create, str_replace, and insert commands are translated into local filesystem operations guarded by the same read/write policy checks as the generic tools. Generic providers do not advertise this alias.

OpenAI-compatible chat-completions providers and the current subscription Codex provider continue to use generic function tools. OpenAI documents a Responses API shell tool with local runtime mode, but Truss’s OpenAI provider is currently chat-completions based and the Codex subscription provider uses a reverse-engineered Responses-compatible path whose shell item protocol is not yet wired into the internal Block model. Mapping those providers should be a separate Responses-provider change, not a partial schema-only fork.

Consequences

Anthropic gets the file and shell schemas it was trained to use without moving execution to Anthropic infrastructure or weakening ledger provenance.

The provider-neutral tool definitions and generic provider behavior remain stable. The only provider-specific execution accommodation is a narrow alias that converts Anthropic typed editor calls back into local tool operations.

ADR 0020: Context Pressure Gauge

Status: Accepted

Date: 2026-06-18

Context

Long in-loop runs can approach a model’s context limit before the harness has a first-class signal that it is near the wall. The compaction-controller design splits the work into milestones; CC-1 must be measurement-only so existing runs do not change behavior before governed compaction is designed and tested.

Truss already assembles the request at each model step and records provider usage. The provider-reported input token count is the best available already-computed projected prompt size for the request that just ran, so CC-1 can observe pressure without adding a second tokenizer or changing projection.

Decision

Providers expose a context_window(model) capability in tokens. Known model families are mapped centrally from the existing pricing/capability metadata: Anthropic Claude families use 200k, GPT-5/Codex-family models use 256k, and existing OpenAI-compatible families retain the prior 128k default. Unknown models fall back to the conservative DEFAULT_CONTEXT_WINDOW of 128k tokens.

truss-core adds ContextGauge, which computes:

pressure = projected_tokens / context_window

At each model step boundary, the agent loop emits:

{"event":"context_pressure","tokens":50000,"window":200000,"pressure":0.25}

The durable loop emits the same semantic event after each live model call. This is observability only: no compaction policy, summarization, elision, ledger compaction_performed entry, or run-control decision is introduced in CC-1.

Consequences

Operators and future controllers can watch context headroom using the same AgentEvent stream as usage, tool, and lifecycle events.

The journal remains authoritative and unchanged. CC-1 only measures the prompt that was already sent; it does not alter history, projection, tool execution, or completion behavior.

Future milestones can build threshold policy, anticipation, governed summaries, and eval on this signal without changing the provider abstraction again.

ADR 0021: Governed Compaction

Status: Accepted

Date: 2026-06-18

Context

ADR 0020 added the context-pressure gauge without changing run behavior. CC-2 activates the next step: when a run opts in, Truss can compact its own projected history at a run-step boundary before an outer harness or provider-specific context manager is forced to intervene.

The durable journal is still the source of truth. Compaction must therefore be presentation-only: it may change the message history sent to the next model call, but it must never delete or rewrite the signed journal entries that produced the original context.

Decision

MemoryConfig now includes a layered CompactionPolicy with safe defaults:

  • mode = "off" by default, so existing runs are unchanged.
  • threshold = 0.70 is the default pressure trigger when a caller opts in.
  • min_tail = 6 keeps recent messages verbatim.
  • exempt_tiers defaults to L0 core, L0 self, L1 principal, and L3 goal.

The durable in-loop executor checks the policy only at the boundary after a model result has been journaled and context pressure has been measured. If pressure is at or above the configured threshold, it uses projection.rs to summarize the elided middle of the message history. L0/L1 remain in the stable system prefix and L3 goal ledger is injected outside the compacted history, so the existing exempt layers remain protected.

For summarize mode, the next request keeps:

  • the original head message,
  • one deterministic context-summary message,
  • the configured verbatim tail.

For evict mode, Truss stores the same summary for audit and recall but omits the summary from the active context. Both modes write EntryKind::ContextSummary through the governed memory substrate. The entry is initially Unverified; promotion remains governed by the existing verification policy rather than relabeling deterministic summaries as verified without a verdict.

Every compaction appends a signed compaction_performed journal entry with:

  • before and after token counts,
  • the context-summary memory id,
  • the inclusive elided journal range,
  • the policy mode, threshold, min-tail, exempt tiers, and verification policy.

The before count comes from the already-observed CC-1 projected input-token count. The after count is a conservative projection estimate from the compacted message shape, avoiding a second tokenizer pass.

Consequences

Governed compaction is opt-in and boundary-only, preserving prompt-cache prefix stability and deterministic replay discipline. Disabled and below-threshold runs continue to send the same request shapes as before.

The journal remains authoritative and fully auditable. The elided range can be recovered from signed journal rows, and Store::verify_chain covers the compaction_performed entry like every other durable event.

CC-2 intentionally does not add anticipation, compaction_imminent, eval variants, or provider-side context_management interop. Those remain CC-3 and CC-4 work.

ADR 0022: Compaction Anticipation

Status: Accepted

Date: 2026-06-18

Context

ADR 0020 made context pressure observable from the already-projected provider input token count. ADR 0021 added opt-in governed compaction at step boundaries when pressure reaches the configured threshold. CC-3 answers the next control question: whether Truss can warn and compact one boundary ahead of threshold crossing without introducing a second compaction authority or changing existing runs.

Decision

ContextGrowthEstimator tracks a short moving average of recent per-step input token deltas. It is fed only with provider-reported projected input token counts, the same counts used by ContextGauge, so it does not run a tokenizer or inspect message bodies. If the moving average is zero or unavailable, no steps-to-threshold estimate is produced.

CompactionPolicy gains two safe-default knobs:

  • anticipate = false, preserving CC-2 behavior unless explicitly enabled.
  • lookahead_steps = 1, the horizon used when anticipation is enabled.

When compaction mode is enabled and anticipate is true, the durable loop emits:

{"event":"compaction_imminent","steps_to_threshold":1,"pressure":0.60,"window":100}

at a step boundary when the estimate is within the configured horizon. If the current pressure is still below threshold, the same CC-2 maybe_compact_history path runs immediately, producing the normal signed compaction_performed journal entry and governed context-summary memory entry.

Consequences

Default runs, including CC-2 users who enabled threshold compaction but did not enable anticipation, keep identical behavior: no compaction_imminent event and no early compaction. Threshold-triggered compaction remains unchanged.

The journal remains authoritative. CC-3 does not delete journal entries, compact mid-step, add an eval harness, or configure provider-side context_management. Those evaluation and interop questions remain CC-4.

ADR 0023: Compaction Eval Harness

Status: Accepted

Date: 2026-06-18

Context

ADR 0020 made context pressure observable, ADR 0021 added opt-in governed compaction, and ADR 0022 added compact-ahead anticipation. CC-4 needs to put a number on what those controller primitives buy back without changing runtime behavior or running live benchmarks.

The eval must be hermetic: no network, no API keys, no Docker, and no SWE-bench. The journal remains authoritative, and the eval only observes the normal event stream and signed ledger entries produced by durable runs.

Decision

truss-core::compaction_eval runs the same deterministic scripted workload under three arms:

  • OFF: compaction disabled.
  • THRESHOLD: governed CC-2 summarization at the configured threshold with anticipate = false.
  • ANTICIPATE: the same governed policy with CC-3 anticipate = true.

The scripted provider does not call any external model. It emits the same tool use sequence in every arm and reports deterministic input-token usage from the request shape it receives. Tool results carry explicit synthetic token weights; a compacted context-summary message has a fixed synthetic weight. That keeps the run reproducible while still letting existing projection and compaction behavior change the provider-reported input-token count that ContextGauge observes.

Metrics are collected from the existing event and journal stream:

  • completed, and completed without crossing the context wall,
  • peak observed context pressure,
  • compaction_performed ledger entries,
  • model steps completed,
  • total configured verbatim tail messages preserved across compactions,
  • tokens summarized away, computed as the signed ledger before_tokens - after_tokens totals,
  • momentum-preserved steps.

The momentum proxy is:

momentum_preserved_steps =
  count of model steps completed before the first context_pressure > 1.0

If no overflow pressure is observed, it equals total steps completed. This is a synthetic pressure-headroom proxy: it says whether the controller kept the scripted run away from the context wall. It does not prove model quality, task success on real software work, human momentum, or SWE-bench performance.

A small example prints the comparison table:

cargo run -p truss-core --example compaction_cc4_eval

Consequences

The eval harness adds no new controller behavior and does not alter existing run semantics. It reuses ContextGauge, CompactionPolicy, durable compaction, the event stream, and signed journal entries.

The default workload is intentionally small enough for unit tests and PR review. It is useful for regression detection and for comparing controller arms, but its numbers must be described as synthetic.

To replace the scripted workload later, keep the report shape and metrics but swap the provider/workload driver:

  1. run a live in-loop Truss workload or SWE-bench instance through the same durable loop,
  2. keep CC-4’s arm definitions and metric collection unchanged,
  3. derive token and compaction metrics from the real provider’s usage events and Truss journal, and
  4. separately record task-quality verdicts from the live grader.

That live version is intentionally out of scope for CC-4.

ADR 0024: Compaction CLI and Config Surface

Status: Accepted

Date: 2026-06-18

Context

ADR 0020 through ADR 0023 built the compaction controller: context pressure, governed compaction, compact-ahead anticipation, and a hermetic eval harness. The runtime policy already resolves through MemoryConfig, but operators still had to hand-edit TOML to enable it, and the supported config surface was not documented at the CLI boundary.

The invariant remains unchanged: compaction must be safe and opt-in. Existing runs must keep the same request shape unless a caller selects a compaction mode.

Decision

Expose the resolved [memory.compaction] policy through the normal memory config layering:

[memory.compaction]
mode = "off"          # off | summarize | evict
threshold = 0.70      # 0.0 through 1.0
min_tail = 6
anticipate = false

The block can be set globally, under [agent.memory.compaction], or in a contract’s [memory.compaction]. Resolution stays global → agent → contract, then CLI flags override the resolved policy:

truss --compaction-mode summarize \
  --compaction-threshold 0.70 \
  --compaction-min-tail 6 \
  --compaction-anticipate \
  --durable "..."

truss-cli owns the Clap-specific surface. It mirrors CompactionMode with a CLI-local ValueEnum, the same pattern used for reasoning effort, so truss-core remains free of Clap. The threshold flag validates 0.0..=1.0 at parse time; config resolution keeps its existing defensive clamp.

Consequences

The default remains mode = "off", so this ADR does not change runtime behavior for existing users. Operators can now select governed compaction without editing core code, and tests cover TOML parsing, config precedence, flag precedence, threshold validation, and a hermetic CLI-flag-to-durable-compaction smoke path.

The CLI does not expose controller internals such as summary construction, provenance shape, gauge math, eval metrics, or Anthropic context-management interop. Those remain in the controller/runtime and future integration work.

ADR 0025: SB-1 Self-Validation Sandbox

Status: Accepted

Date: 2026-06-18

Context

The SWE in-loop run showed a concrete weakness: the executor could edit code and use bash, but it did not have a reproducible, first-class path for running the project’s own tests during the solve. The external verify boundary remained the truth, but the agent lacked useful local evidence before reaching that boundary.

At the same time, today’s policy layer is authorization, not isolation. Policy can deny tools, write scopes, network, and forbidden command substrings, while the process still runs as the host user. SB-1 does not try to solve kernel-level isolation. It creates the stable execution seam and ships a working-copy implementation that contains common test pollution and records evidence.

Decision

Add truss_core::sandbox::Sandbox as the command-execution seam:

#[async_trait]
pub trait Sandbox: Send + Sync {
    async fn run(&self, cmd: SandboxCommand, ctx: &Ctx) -> Result<SandboxOutput, ToolError>;
    fn materialize(&self, checkout: &Path) -> Result<WorkDir, ToolError>;
    fn capabilities(&self) -> SandboxCaps;
}

Ctx carries the active sandbox alongside policy, budget, cancellation, and the event sink. Tool execution composes both layers: ToolRegistry and individual tools still consult Policy; shell execution then runs through the active Sandbox.

SB-1 provides two implementations:

  • NoSandbox: the default. It preserves the existing host sh -c execution path and process reaping discipline.
  • WorkingCopySandbox: materializes an ephemeral copy of the checkout, runs the command there with kill_on_drop(true), captures stdout/stderr/exit, hashes the resulting workdir, and deletes the copy when finished.

Add model-facing self-validation tools, self_check and run_tests, for store-backed runs when a working-copy sandbox is selected. They run either the contract’s first declared check command or an explicit command supplied by the model inside the active sandbox. Each run appends a signed journal entry:

{
  "kind": "self_validation",
  "payload": {
    "cmd": "...",
    "exit": 0,
    "summary": "...",
    "workdir_hash": "...",
    "evidence_only": true,
    "verdict_unchanged": true
  }
}

A matching AgentEvent::SelfValidation is emitted. The verify boundary is unchanged: harness-owned check clauses and the reviewer panel still determine the verdict. Self-validation is evidence only.

Sandbox selection uses the same layered pattern as governed memory:

[sandbox]
mode = "no_sandbox"      # no_sandbox | working_copy
checkout = "."

[agent.sandbox]
mode = "working_copy"

[sandbox]               # in a contract file
mode = "working_copy"

Resolution is global → agent → contract, with NoSandbox as the default. truss-cli performs TOML wiring and checkout selection; truss-core owns only the typed config and runtime seam. Clap stays out of core.

Consequences

Existing runs keep the same behavior unless sandbox mode is explicitly changed. NoSandbox remains the default and the host execution output formatting stays unchanged.

WorkingCopySandbox improves reproducibility and blast-radius containment for test runs that use the checkout-relative working directory. It is not a security boundary: absolute paths, host permissions, network, namespaces, seccomp, and container images are intentionally out of scope for SB-1.

The stable seam allows SB-2 and SB-3 to swap in namespace or container-backed implementations without changing the agent loop, self-validation journal shape, or verify-boundary verdict semantics.

ADR 0026: mdBook Documentation Site

Status: Accepted

Date: 2026-06-18

Context

Truss has accumulated architecture docs, implementation plans, go-to-market notes, and ADRs as loose Markdown under docs/. That keeps authoring simple, but it makes the material hard to browse, search, and publish. Matt explicitly asked for documentation plus a way to view it in the browser with examples.

The site should not fork the content. Existing design docs and ADRs remain the source of truth, and example pages should render the checked-in files under examples/ rather than copying TOML into a second place.

Decision

Use mdBook as the documentation site generator.

The repository now treats docs/ as the mdBook root:

  • docs/book.toml configures the book and emits generated HTML to top-level book/.
  • docs/SUMMARY.md is the navigation tree for architecture docs, configuration, examples, go-to-market/evaluation notes, and ADRs.
  • docs/introduction.md is the landing page.
  • docs/examples/*.md are thin wrappers that explain each example and include the live source files from examples/.

GitHub Pages publishing is handled by .github/workflows/docs.yml. It builds the book with mdbook build docs, uploads the top-level book/ artifact, and deploys it with actions/deploy-pages on pushes to main. The workflow is secret-free; Matt still needs to enable GitHub Pages for the repository and select GitHub Actions as the source.

Consequences

Truss gets a searchable, browsable, Rust-native documentation site with a single binary local preview path:

cargo install mdbook
mdbook serve docs --open

Generated HTML is ignored and not committed. Existing docs stay Markdown-native, so future design work can keep using the same files while the book navigation decides how readers discover them.

mdBook is intentionally a documentation site, not a product app. Richer product UI, generated diagrams, verified playbooks, and per-agent MCP surfaces remain separate milestones.

ADR 0027: Ledger Read Tools LR-1

Status: Accepted

Date: 2026-06-18

Context

Truss already records a signed, hash-chained durable journal for each run. Before LR-1, the supported read surface was effectively truss --verify, which answers whether a session verifies but does not show the proof chain itself. The product direction is to review proofs, not transcripts, so the ledger needs a read API that can list runs, inspect entries, and retrieve exact payloads without mutating the store.

Decision

Add a read-only LedgerReader in truss-core that composes over the existing Store. It exposes session listing, session lookup, ordered entries, last entry, head hash, anchor count, and chain verification. The only new store primitive is Store::list_sessions, a read-only query over existing sessions and journal tables. No schema changes or write paths are introduced.

Add truss ledger CLI commands in truss-cli:

  • truss ledger list
  • truss ledger show <run-id>
  • truss ledger get <run-id> <seq>

Each command supports --json. JSON responses use schema version truss.ledger.v1 and include verification status alongside content. Human output also shows verification state. Tampered or unsigned entries are surfaced as unverified; readers do not panic or silently present them as trusted.

JSON Schema

LR-1 intentionally keeps the schema small and stable:

  • list: { schema_version, command: "list", sessions: [...] }
  • show: { schema_version, command: "show", session: { session_id, head_hash, verification, entries } }
  • get: { schema_version, command: "get", session_id, entry: { seq, kind, hash, short_hash, signed, payload, verification_status } }

Session and entry records include hash and verification metadata so future UI and diligence tooling can consume the read API without interpreting terminal text.

Consequences

The ledger can now be inspected without running providers or changing durable state. This unblocks later LR milestones:

  • LR-2 verdict bundle and evidence export
  • LR-3 recall and verify command consolidation
  • LR-4 run diffing

Those later views are explicitly out of scope for LR-1.

ADR 0028: Ledger Read Tools LR-2

Status: Accepted

Date: 2026-06-18

Context

LR-1 made the durable ledger readable as a signed chain. The next buyer-facing question is not “what happened in every transcript turn,” but “was the contract met, what evidence supports that verdict, and can I take the proof away and verify it offline?”

The existing durable journal already contains the required material: the recorded contract intent, harness-owned check results, optional self_validation evidence, reviewer-panel evidence, the signed verdict entry, hash-chain material, the session signing key id, and any anchor receipts.

Decision

Extend the read-only LedgerReader with two LR-2 views:

  • verdict(<run-id>): renders the recorded verdict bundle for a run.
  • export(<run-id>): emits a portable JSON evidence bundle for offline verification.

Add CLI commands:

  • truss ledger verdict <run-id> with human-readable default output and --json.
  • truss ledger export <run-id> as JSON-native output.

Both commands keep LR-1’s stable schema version, truss.ledger.v1. LR-2 adds new command shapes instead of changing the LR-1 list, show, or get schemas.

JSON Schema

verdict --json:

{
  "schema_version": "truss.ledger.v1",
  "command": "verdict",
  "verdict": {
    "session_id": "...",
    "contract": {
      "id": "...",
      "title": "...",
      "intent": "...",
      "definition_of_done": []
    },
    "evidence": {
      "checks": [],
      "self_validation": [],
      "panel": []
    },
    "recorded_overall": "PASS",
    "overall": "PASS",
    "verdict_entry": {},
    "head_hash": "...",
    "anchor_refs": [],
    "verification": {}
  }
}

export:

{
  "schema_version": "truss.ledger.v1",
  "command": "export",
  "contract": {},
  "evidence": {
    "checks": [],
    "self_validation": [],
    "panel": []
  },
  "verdict": {},
  "ledger": {
    "session_id": "...",
    "pubkey_id": "...",
    "head_hash": "...",
    "verification": {},
    "entries": [
      {
        "seq": 0,
        "kind": "run_started",
        "payload": {},
        "prev_hash": "...",
        "entry_hash": "...",
        "signature": "...",
        "signed": true,
        "verification_status": "verified"
      }
    ],
    "anchor_refs": [],
    "child_sessions": []
  },
  "offline_verification": {
    "algorithm": "sha256(prev_hash || kind || canonical_json(payload) || seq_le)",
    "signature_algorithm": "ed25519",
    "status": "verified"
  }
}

Consequences

The verdict view is a render of already-recorded journal data. It never reruns checks, reconvenes reviewers, or recomputes a verdict. If chain or signature verification fails, the view reports unverified and treats the trusted overall as UNVERIFIABLE while still showing the recorded value as untrusted evidence.

The export bundle is self-contained for offline integrity checks: it carries the signed entries, previous hashes, entry hashes, signatures, session public key id, head hash, anchor references, and recursively included child sessions referenced by child anchors. No storage schema changes or write paths are introduced.

Out of scope remains LR-3/LR-4: recall, ledger verify consolidation, and diffing between runs.

ADR 0032: Verified Playbook Promotion and Recall

Status: Accepted

Context

Verified Playbooks M1 added EntryKind::Playbook, markdown/SKILL.md import, and signed playbook import/revoke events. Imported playbooks deliberately landed as Unverified drafts. The next tranche needs playbooks to become trusted only through the same trust path lessons already use, and then become available to future runs through the existing M4 L2 context injector.

Decision

Truss adds truss playbook promote <id-or-key>.

Promotion reuses the existing memory-claim review lane:

  • load the playbook draft from governed memory;
  • run delegation::run_memory_claim_with_evidence, which routes to the same N-reviewer panel used by contracts and lessons;
  • require a strict majority of reviewers to pass;
  • treat failed and unparsable panels as not promoted.

A passing panel records a signed playbook_promoted journal event and calls MemoryStore::promote_with_verdict_ref. The resulting memory entry becomes Trust::Verified, carries the reviewer Verdict, and stores Provenance.verified_by_ref pointing at the signed event.

A failing or unverifiable panel records a signed playbook_promotion_rejected event and leaves the playbook Unverified. That differs from lesson extraction’s eager reject path because playbook promote is an operator-invoked draft promotion attempt; failed attempts should be auditable without destroying the draft.

Trusted Recall

Truss adds recall_verified_playbooks, mirroring recall_verified_lessons:

  • query EntryKind::Playbook;
  • require min_trust = Verified;
  • rank by lexical overlap with the task context, then salience and recency;
  • render an L2 block labeled [L2 verified playbooks] with provenance.

Playbook recall is opt-in through playbook-specific recall limits. Defaults keep the request shape unchanged:

  • playbook_recall_max_playbooks = 0;
  • playbook_recall_token_budget = 512.

Setting either limit to 0 disables playbook recall. Unverified, rejected, and stale playbooks never enter the trusted L2 playbook block.

Consequences

Verified Playbooks now share the same reviewer-panel trust semantics and ledger-backed provenance as verified lessons. The only new path is the playbook-specific CLI/audit event surface; trust mutation still uses the kind-agnostic memory store.

The no-fake-trust boundary remains explicit: import creates a draft, promotion must pass the panel, and recall is verified-only.

ADR 0033: Eval-Driven Playbook Promotion

Status: Accepted

Context

ADR 0032 made Verified Playbooks promote through the same reviewer-panel trust lane as lessons, then recall only when playbook recall is explicitly enabled. That proves a draft is plausible, but it does not prove that recalling the playbook improves a real durable task. Verified Playbooks M3 needs the same paired learning-on/off evidence discipline introduced for SI-4 lessons.

Decision

Truss adds an eval-backed promotion route:

truss playbook promote <id-or-key> --eval --contract contract.toml --repeats 3

The command runs the paired eval harness against the supplied contract. The learning-on arm enables all verified-memory recall under the learning umbrella: lesson extraction, lesson recall, and bounded playbook recall. The learning-off arm disables extraction, verification, lesson recall, and playbook recall.

During playbook promotion only, the candidate draft is exposed to the eval’s memory view as a temporary verified playbook. The stored memory entry is not mutated during the eval. This lets the on-arm measure the honest question: “would this playbook help if it were trusted?” The off-arm keeps playbook recall disabled, so any pass-rate delta must come from learning-enabled context.

Promotion requires a positive pass-rate delta:

  • learning_on.pass_rate > learning_off.pass_rate promotes;
  • zero lift and regressions reject.

A passing eval records a signed playbook_promoted ledger event containing the EvalReport, derives a memory Verdict from that report, and calls promote_with_verdict_ref. A rejected eval records a signed playbook_promotion_rejected event containing the same report and leaves the draft Unverified.

Consequences

The local panel route remains available as:

truss playbook promote <id-or-key>

Eval-driven promotion is heavier: it requires a real contract and executor, and it consumes durable child runs. That cost is intentional. The eval path is the “prove lift before trust” route, not a lightweight import or panel convenience.

The no-fake-trust boundary remains explicit. A playbook draft is never marked Verified because it exists, parses, or sounds useful. Eval promotion needs signed eval evidence with positive pass-rate lift; no lift means no promotion.

ADR 0034: Signed Human Review Primitive

Status: Accepted

Context

Phase 4 adds a human-facing verdict-review surface. The surface is only trustworthy if a human approval, rejection, or needs-changes decision becomes ledger provenance instead of presentation state. The decision must be tamper-evident, attributable to the reviewer key, and pinned to the exact ledger state the human reviewed.

Decision

Truss adds a signed human_review journal entry in core. Its payload contains:

  • decision: approved, rejected, or needs_changes;
  • note: an optional reviewer note;
  • attested_head_hash: the session head hash being reviewed at decision time;
  • reviewer_key_id: the ed25519 public-key id of the signer that recorded the review.

Store::record_human_review is the single write API. It reads the session head hash, builds the typed payload, and appends it through the existing journal append path so the entry is hash-chained and signed. Store::verify_chain continues to verify normal run entries against the session key and verifies human_review entries against their payload reviewer_key_id.

truss review approve|reject|needs-changes <session> records the entry without requiring a terminal UI. truss ledger verdict surfaces the latest human sign-off alongside the machine verdict and reports whether the attested reviewed head still matches the current reviewable head. The machine Verdict remains unchanged; human sign-off is a separate trust signal.

Consequences

Human review decisions are durable provenance and survive export/verification. An approval does not mutate, overwrite, or launder the machine verdict. Absence of a human approval remains non-approval.

Because the review entry itself advances the hash chain, stale-signoff detection compares the attested hash with the latest non-human_review entry hash while the full chain head remains visible separately. If later work appends new non-review evidence after approval, the verdict bundle reports the sign-off as not matching the current reviewable head.

ADR 0035: Read-Only Review TUI

Status: Accepted

Context

ADR 0034 made the human approve / reject / needs-changes decision signed, chained ledger provenance, reachable from a scriptable, terminal-free CLI. The verdict-review surface is only realized, though, if a human can actually look at the proofs — the definition-of-done clauses, the independent evidence, the reviewer panel result, the diff, and the chain head/verify status — and form a judgement without scrolling a raw transcript. That viewing surface is VR-2.

The load-bearing rule from the Phase 4 spec is the provenance boundary: provenance-capturing is core; stream-consuming is plugin/CLI. The viewing surface captures no provenance, so it must not touch core.

Decision

Truss ships a read-only interactive review TUI inside truss-cli, built on ratatui + crossterm, behind an off-by-default tui cargo feature so headless/CI and --no-default-features builds never pull in the terminal stack.

The TUI is a pure consumer over truss_core::ledger::LedgerReader. It calls list_sessions, verdict, verify, and entries, snapshots the results into transient in-memory state, and renders four screens:

  1. List — recent/active sessions with the ledger trust label, the machine verdict status, and the human review status (with a stale marker when the sign-off no longer matches the reviewable head).
  2. Verdict bundle — definition-of-done clauses, clause evidence, self-validation evidence, the reviewer panel majority and per-reviewer verdicts, the diff summary, and the budget/spend. The machine verdict and the human sign-off render side by side (design decision #4): they are distinct trust signals and neither launders the other.
  3. Ledger chain — head hash plus the verify status and per-entry hashes, so an intact-vs-tampered chain reads at a glance.
  4. Transcript — reachable but never the default view.

truss tui launches it; truss review with no decision subcommand launches it too. The existing non-interactive truss review approve|reject|needs-changes forms are unchanged. When the binary is built without the tui feature, both entry points fail with a clear “rebuild with --features tui” message rather than panicking.

UI state — selected session, active screen, scroll offset — is presentation and is never written to the ledger (design decision #5). The rendering layer is a pure function of the in-memory snapshot, so every screen is exercised by hermetic render tests over a fixture database using ratatui’s TestBackend; the tests require no terminal, network, or credentials.

Consequences

The “done when” viewing half of the verdict-review surface is met: a human can see the verdict, evidence, panel, diff, budget, and chain status without reading the transcript, and the trust tiers read at a glance. No new provenance is introduced and no core code changes — the TUI rides the stable read API, the same seam an external front end (ClawTerm/Warp) would consume.

The TUI is read-only by construction: it has no write path to the ledger. Wiring the on-screen approve / reject / needs-changes actions to the ADR 0034 signed primitive is deferred to VR-3, and policy-driven approval gates to VR-4.

ADR 0036: TUI Review Actions

Status: Accepted

Context

VR-1 made human review decisions signed ledger provenance through human_review. VR-2 made the proof surface readable in a terminal, but still forced the operator to leave the view and run a separate truss review approve|reject|needs-changes command. That split is clunky: the human forms the judgment in the TUI, then has to context-switch to record it.

The risk is architectural drift. The TUI must not become a parallel provenance system, and it must not duplicate the durable runner just to provide a “resume” button. The load-bearing boundary remains: core records facts; the TUI only drives core APIs.

Decision

The review TUI now exposes action keybindings:

  • a approve
  • r reject
  • n needs changes
  • s deliver the pending signal for a suspended run
  • x show the existing resume command for the selected run

Approve/reject/needs-changes open an in-terminal note prompt. Pressing enter calls Store::record_human_review, which appends the same signed human_review entry introduced by ADR 0034. The TUI then refreshes its LedgerReader snapshot, so the human sign-off appears beside the machine verdict.

Signal delivery opens an optional payload prompt, then calls Store::add_signal with the pending signal name found in the selected run’s latest suspended ledger entry. This reuses the existing durable suspend/signal substrate. The TUI does not record a bespoke UI event.

Resume is intentionally not executed inside the TUI. Resuming requires provider construction, tool registry setup, memory config, MCP mounting, budgets, and policy resolution from the normal CLI path. The TUI exposes a resume control by showing the exact command:

truss --resume <session> --db <db>

That keeps one durable runner in the system.

Consequences

The “review proofs, then act” loop is now single-surface for human sign-off: the same view that shows evidence can append the signed decision. The ledger remains the source of truth and the machine verdict remains separate from the human sign-off.

Suspended runs can be nudged from the review surface without adding a new signal mechanism. Full resume remains on the established CLI runner path, which is less cute but much cleaner: no duplicated provider/tool setup, no silent semantic fork, no UI-side provenance.

The TUI is no longer read-only, but its write set is deliberately tiny: human_review entries and pending durable signals, both through core Store APIs.

ADR 0037: Approval Gates

Status: Accepted

Context

VR-1 made human review decisions signed, chained, and head-pinned. VR-3 gave the verdict TUI a controller path for those decisions and for existing durable signals. VR-4 closes Phase 4 by making selected risky work actually stop until a human decision exists.

The boundary matters: the TUI cannot become a provenance substrate, and a gate cannot be a separate pause mechanism. A gate must be durable, visible in the ledger, opt-in by policy, and default-to-fail.

Decision

Truss adds an opt-in approval-gate policy resolved global -> agent -> contract:

  • enabled
  • high_risk_tools
  • spend_threshold_usd
  • per_call_spend_threshold_usd

Tool patterns support exact names and * wildcards. The default policy is off and has no thresholds, so existing runs do not change.

When a configured high-risk tool is about to execute, or model spend crosses a configured threshold, the durable loop appends a signed suspended entry whose payload carries an approval_gate object and awaits the existing approval_gate signal. This reuses the durable suspend/signal substrate; it does not introduce a second pause table or UI-only state.

Store::record_human_review remains the single write primitive for the human decision. An approved human_review entry on a pending approval gate also queues the existing wake signal. On resume, the durable runner releases an approval gate only if the ledger contains a signed approved human_review entry after that gate. A raw signal without the signed review does not release the run.

Reject and needs-changes are blocking decisions. They are recorded as signed human_review entries, but they do not queue the release signal; the run stays suspended and later resumes return still-waiting. A later approved review can release the same pending gate. This keeps rejection reversible for human operations while remaining default-to-fail.

Consequences

High-risk tools are gated before execution. Spend gates are raised after the model result and spend are journaled, but before the run can complete or act on the assistant’s next tool request.

The machine verdict is never overwritten. Human approval is separate signed provenance that releases a pause; it does not launder a failing verdict into a passing one.

The TUI surfaces gated runs from the same ledger data and records approve / reject / needs-changes through Store::record_human_review. It cannot bypass an approval gate with the generic signal keybinding.

ADR 0038: Native Git and GitHub Tools

Status: Accepted

Context

Phase 5 starts filling daily-driver parity gaps. Coding tasks need git and GitHub operations, but routing every operation through bash makes the tool surface untyped and weakens Truss’s provenance story. A normal coding loop should be able to run status, diff, commit, PR creation, and check inspection through first-class tools.

The design gate from docs/BUILD-PLAN.md applies:

  • git diffs, PR diffs, issue text, and check results can support a verdict, so fetched/read artifacts need signed hashes in the ledger;
  • commit and PR creation are side effects and must not silently execute twice after a durable resume;
  • read-only diagnostics should stay lightweight and avoid unnecessary replay machinery.

Decision

Truss adds native tool modules for:

  • git_status
  • git_diff
  • git_branch
  • git_commit
  • github_pr_view
  • github_pr_create
  • github_pr_diff
  • github_pr_checks
  • github_issue_view
  • github_issue_list
  • github_check_status

The git tools invoke the git executable directly via tokio::process::Command with structured arguments. GitHub tools invoke the gh CLI directly for auth and transport, while exposing typed Truss tool schemas and structured JSON returns. This keeps the implementation consistent with existing in-process native tools and avoids turning bash into the only integration layer.

Tools are opt-in through --git-tools, --github-tools, or:

[tools]
git = true
github = true

Tool registration is separate from authorization. Policy.allow_tools still decides which registered tools are advertised and callable. Git tools check read/write scope against the target repo. GitHub tools require network policy allowing github.com or api.github.com.

Durable runs attach the current store/session to the tools. Each durable call records a signed git_tool_call or github_tool_call entry with:

  • tool name;
  • memo key;
  • input SHA-256;
  • result SHA-256;
  • error state;
  • side-effect flag;
  • optional produced_by ledger ref.

Read-only calls record signed provenance but do not memoize. Side-effecting calls (git_commit, github_pr_create, and mutating branch actions) use Store::append_with_memo so the ledger entry and replay memo commit atomically. On a later identical call, the tool replays the memo only if a matching signed provenance entry exists; otherwise it refuses to use the memo.

Consequences

Coding contracts can use native git and GitHub operations without shelling out through the generic bash tool.

Verdicts can cite ledgered hashes for status/diff/PR/check artifacts. The full tool output is still available in the normal tool_result entry created by the durable loop, while the provenance entry carries compact hashes suitable for proof review.

Durable resume replays recorded commit/PR-create results instead of double-applying side effects. A raw memo row or tampered memo cannot bypass the ledger because replay requires matching signed provenance.

The first milestone deliberately does not add web search/fetch, artifact input, or multi-channel delivery. Those remain later Phase 5 work.

ADR 0039: MCP Server Tier

Status: Accepted

Context

Phase 7 adds the secondary MCP direction: Truss can be called by another harness as a verification backend. This is distinct from the existing MCP client tier, where Truss consumes external MCP servers as contract-scoped tools.

The design gate from docs/BUILD-PLAN.md applies:

  • the server exposes the signed-verdict substrate, so returned results must point at the ledger head and the read-only verdict/export bundle;
  • long-running delegated work must reconnect through durable sessions instead of re-executing work on resume;
  • the MCP server must not become remote shell access. The unit of work is a contract.

Decision

Truss adds truss serve-mcp over stdio. It speaks the same Content-Length framed JSON-RPC style used by the MCP client transport and advertises only four tools:

  • truss_run_contract
  • truss_contract_status
  • truss_ledger_verdict
  • truss_ledger_export

truss_run_contract accepts a contract path and optional durable session id. In synchronous mode it runs the contract through the existing delegation path and returns:

  • the contract verdict object;
  • the signed ledger verdict bundle;
  • the session head hash;
  • a bundle_ref pointing callers to truss_ledger_export.

When called with async = true, the server returns a handle immediately and runs the contract in the server process. truss_contract_status reads the in-process job table when available and falls back to the durable store by session id, so reconnects use recorded ledger state rather than rerunning the contract. A future HTTP deployment can use the same handle shape with stronger per-caller isolation.

Ledger tools are read-only wrappers around LedgerReader::verdict and LedgerReader::export. They return signed store views only; there is no write path through the MCP ledger surface.

Stdio mode carries a single configured Principal for the process. The CLI exposes --principal-tenant and --principal-user, and the contract execution context threads that principal into the durable run. HTTP mode is an honest stub in this slice: auth, request-scoped principals, and network serving are reserved for MCP-S3.

Consequences

Other harnesses can delegate high-stakes work to Truss and receive a signed verdict with a durable bundle reference, without being granted Truss’s internal tool surface.

The trust boundary is intentionally narrow. bash, read_file, write_file, list_dir, and other raw internal tools are neither advertised nor callable over the server. If a contract needs tools, they remain scoped inside Truss policy, budget, sandbox, provenance, and durable replay.

The first server slice does not implement HTTP transport, HTTP auth, MCP progress notifications, or raw resource/prompt surfaces. Those can be layered later without changing the contract-level trust boundary.

ADR 0039: Native Web Search And Fetch Tools

Status

Accepted

Context

Phase 5 adds daily-driver native tools without moving work outside Truss’s verification and replay substrate. Web fetch and search are common model actions, but delegating them through shell commands or unjournaled adapters loses the signed evidence trail that verdicts depend on.

Network reads are also nondeterministic. A page can change, a search provider can rerank results, credentials can expire, or a request can fail differently on resume. The runtime needs an exact replay path for the observation that influenced the model.

Decision

Add two typed, opt-in native tools:

  • web_fetch for HTTP(S) GET requests.
  • web_search for provider-backed web search.

Both tools are registered only when --web-tools or [tools] web = true is set. Policy remains separate: policy.allow_tools may still restrict the exact tool names, and policy.network controls reachable hosts.

web_fetch uses the existing Rust async HTTP stack (reqwest) and returns status, content type, final URL after redirects, readable content, and SHA-256 hashes.

web_search is behind a small WebSearchProvider trait. The first concrete backend is Brave Search, configured by environment/config rather than hardcoded secrets:

  • TRUSS_WEB_SEARCH_API_KEY by default for the API key.
  • TRUSS_WEB_SEARCH_ENDPOINT optionally overrides the endpoint, unless TOML provides [tools.web_search].endpoint.
  • [tools.web_search].api_key_env can point at a different key env var.

If no key is configured, web_search returns a normal signed tool error instead of panicking.

Durable runs record both tools as signed web_tool_call entries and write the result into the tool memo table in the same transaction. On resume, Truss verifies the memo against the signed ledger entry and replays it. Passing revalidate = true bypasses the memo and records a new observation.

Consequences

  • Verdicts can cite web observations by signed ledger entry and result hash.
  • Resume is deterministic for web observations unless the caller explicitly asks to revalidate.
  • Tests can inject fake HTTP/search transports and never hit the live network.
  • The core runtime now directly depends on reqwest because native web tools are compiled into the tool registry; runtime use remains default-off.
  • The attestation is deliberately narrow: Truss proves what it retrieved, not whether the retrieved content or search result is true.

Alternatives Considered

  • Only expose web via MCP. MCP is useful, but web fetch/search are baseline parity tools and should inherit first-class typed schemas, policy checks, ledger entries, and replay semantics without requiring a separate server.
  • Use shell commands such as curl. That would make network access opaque to the tool policy and would not give structured final URL, content type, hashes, or provider-specific search results.
  • Hardcode one search provider with no seam. This would make tests brittle and lock search quality to a single backend. The trait keeps provider behavior swappable while preserving one concrete implementation.

ADR 0040: Semantic Memory Recall Backend

Status: Accepted

Context

The governed memory substrate already exposes the right seam: MemoryStore::query(MemoryQuery) with contains reserved for richer recall. S1 uses that seam to improve retrieval quality behind the backend boundary while preserving the trust, provenance, and reversibility invariants from memory-evolution-path.md.

This is not an outcome claim. Semantic recall is default-off infrastructure; SI-4 paired eval remains responsible for proving whether it improves task results.

Decision

Add memory.recall = "exact" | "semantic" | "hybrid" to the existing layered memory config. The default is exact, which preserves the previous substring and lexical-ranking behavior. semantic enables vector-backed MemoryQuery.contains. hybrid is accepted as a named future mode but returns a clear S2-reserved error instead of blending scores prematurely.

truss-core::memory owns a small synchronous Embedder trait plus a deterministic feature-hashing embedder for hermetic tests. Real provider-backed embedding lives outside core. The first real backend is an OpenAI-compatible /v1/embeddings embedder in truss-providers; it reads credentials from memory.embedding_api_key_env, defaulting to OPENAI_API_KEY, and can target an alternate endpoint with memory.embedding_base_url.

SQLite stores vectors in a side table:

  • memory_id
  • embedder_id
  • dims
  • vector_json
  • created_at

Semantic query first applies the same active-entry, min_trust, kind, key, and limit constraints. It then embeds the query text and brute-force ranks eligible vectors by cosine similarity. At current memory scale this avoids an ANN dependency. sqlite-vec or another ANN index is the documented S2 scale swap.

Existing rows written before S1 have no vector. Truss therefore exposes an explicit backfill path:

truss --config examples/harness.toml --db harness.db memory reindex

Semantic queries fail with a reindex-needed error if otherwise eligible entries lack a vector for the current embedder id/dims, so old memory is not silently missed.

Invariant Preservation

Writes still enter Unverified; embedding persistence does not change trust.

Verified still means the reviewer panel signed off. No recall mode promotes, relabels, or launders unverified content.

Every entry still carries LedgerRef provenance. Embeddings are derived indexes, not provenance.

Reversibility is unchanged. Supersede and stale transitions still update memory entries and remain ledger-traceable.

The default view remains verified-only. Semantic ranking composes with MemoryQuery::default().min_trust = Verified; tests cover an unverified entry that is semantically closer but excluded from the default trusted view.

Consequences

Default runs have zero behavior change and do not require embedding credentials.

Semantic mode requires an embedder. The OpenAI-compatible backend uses OPENAI_API_KEY by default and returns a configuration error when the env var is missing. Tests use the deterministic embedder and never call the network.

Changing embedding model, dimensions, or backend changes the stored embedder_id/dims and requires truss memory reindex before semantic recall can safely use existing entries.

ADR 0041: Native Model-Pulled Memory Tools

Status

Accepted

Context

S1 added semantic durable-memory recall, but recall was still primarily pushed at run start. Long agent runs need a model-pulled path so the model can ask for L2 memory only when it becomes relevant.

Memory is not a normal scratchpad. Truss keeps five invariants:

  1. Model writes enter Unverified.
  2. Verified means a panel-signed or paired-eval verdict promoted the entry.
  3. Every entry carries provenance and a LedgerRef.
  4. Supersede and demote-to-Stale remain reversible ledgered state changes.
  5. Recall defaults to verified-only and applies the trust floor before ranking.

The model should be able to read memory on demand without gaining a new write, promote, or demote path.

Decision

Add two typed native tools, both default-off:

  • memory_query: query L2 durable memory with the configured recall mode (exact, semantic, or hybrid).
  • memory_view: fetch entries by id.

Enable them with --memory-tools or [tools] memory = true.

The tools are read-only. The existing governed memory_20250818 binding remains the write path and is still controlled by the separate singular --memory-tool flag.

Both read tools default min_trust to Verified. Operators may explicitly widen the floor to stale or unverified, but rejected entries are never surfaced. For semantic and hybrid recall, trust/kind/key filters are applied before vector similarity scoring or ranking.

Durable and contract runs record signed memory_tool_call observations and memo their outputs. Replay verifies the memo against a signed ledger entry and returns the same recall result originally observed. revalidate = true bypasses the memo and records a fresh observation.

Consequences

  • The model can pull durable memory when it needs it, reducing eager L2 context pressure.
  • Verdicts can cite memory retrievals by signed memory_tool_call entries and the returned entries’ LedgerRef provenance.
  • Replayed runs do not silently see a mutated memory store unless the caller explicitly revalidates.
  • Enabling read tools does not enable writes, promotion, rejection, or staling.

Alternatives Considered

  • Reuse the existing memory tool. That binding includes model-facing write commands, so it is too broad for a read-only recall surface.
  • Only push larger run-start recall. That spends context before the model knows what it needs and does not help mid-run topic shifts.
  • Skip memoing because memory reads are local. The store can mutate between resume attempts; replay must preserve the exact observation that influenced the original model turn.