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:
- System Architecture for the contract-to-verdict path, runtime, provider boundary, and ledger model.
- Configuration Surface for the supported TOML and CLI knobs.
- Example Overview for runnable config and contract files.
- Decision Records for accepted design choices.
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
- Provider registry: feature-gated model backends.
- MCP tests and server tests: protocol-facing integration behavior.
- Child-run tests and fan-out tests: delegation state and aggregation.
- Compaction tests: projected context remains separate from the durable record.
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 > 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
| Dimension | Typical harness | Truss |
|---|---|---|
| Unit of work | prompt | contract (intent + checkable done + budget + constraints) |
| What you review | transcript | signed verdict + independent evidence |
| Who verifies | the executor (self-reported “done”) | harness-run checks + N-reviewer adversarial panel |
| Run state | in-memory; dies with the process | durable journal (SQLite); suspend / crash / resume |
| Audit | logs you trust | hash-chained, ed25519-signed ledger; externally anchored |
| Privacy | n/a | only salted hashes leave the box (NDA-safe) |
| Provider lock-in | usually one vendor | Provider 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:
- 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?
- 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?
- 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_callledger 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:
Playbookmemory 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 installpath 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_contracttool returning verdict/head hash/bundle reference.truss_contract_statusfor async handles.- Ledger read tools exposed over MCP.
- Per-caller
Principaland 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 docswhen docs change.
For docs-only changes, git diff --check and mdbook build docs are enough.
8. Immediate Next Moves
- Open MCP-1 implementation branch.
- Decide
truss-mcpcrate vs provider module and SDK vs hand-rolled JSON-RPC. - Implement fake stdio MCP server tests first.
- Draft the plugin-authoring docs skeleton in parallel after the MCP API surface stops moving.
- 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
.soplugins. - 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 durablesleep_until. We appendsuspended, 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; appendssignal_received, then resumes.- Durable timers: an optional long-lived
truss daemonholds 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/effectfulin 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_idrecorded on the session. - Verify:
truss verify <session>recomputes the chain and checks every signature againstpubkey_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 (setbytes = 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. Thehead_hashover 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_versionstamps 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— addsession_id; add durable accessorsnow()/sleep_until()/ random()(journaled in A; thin in C).Principalalready present.src/state.rs— evolveRunLog(JSONL) into a SQLite-backedJournal+LedgerSnapshotStore. JSONL becomes an export of the journal, not the store.
src/agent.rs— wrap model/tool calls through aDurablelayer (memoize via journal; per-step transactional append of result + snapshot); recognize aSuspendoutcome; rehydrate on resume.src/provider.rs/src/tools— tools gain an effect class (pure|idempotent| effectful) and may returnSuspend { await }.- New module
src/journal.rs(store + hash-chain + signing) andsrc/anchor.rs(trait + Noop). New deps:rusqlite(bundled),ed25519-dalek,sha2. - CLI:
resume,signal,verify, and an optionaldaemon.
15. Milestones
- 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. - M2 — ledger: hash-chain + ed25519 signing per entry;
verify <session>. - M3 — signals & timers: external
signalsuspend/resume; optionaldaemonfor durable timers. - M4 — anchoring + context projection:
Anchortrait + transparency-log anchor; Head/Tail/compacted-middle projection hook. - 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:
- Stable extension traits (backends, tools, context sources, routing).
- 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
eventsstream + 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
ContextProviderchain 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’sagent.mdblob-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)
- 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. - spawn tool + best-of-N fan-out on top of #1; refactor the N-reviewer panel onto the same substrate.
- ContextProvider trait + MCP tool tier — independent seams; can run in parallel with #2.
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
Ctxshared 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_callledger entry — server id, tool name, argument hash, result hash, provenanceLedgerRef. Results that feed a claim are available to the N-reviewer panel and totruss 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
- Connect declared servers at run start (stdio child process with
kill_on_drop, or HTTP client). Handshake, list tools. - Mount advertised tools (post-allowlist) into the loop’s tool registry alongside
in-proc tools (
ReadFile/WriteFile/ListDir/Bash/memory/spawn). Namespace asmcp:<server>:<tool>to avoid collisions. - 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. - Invoke inside the agent loop: enforce
Policy(network/forbidden) +Ctxbudget/deadline/cancel → call server → record signedmcp_tool_callledger entry + memo → return result to the model. - Resume: memoized result replays; no re-call. Async tool → suspend/await_signal.
- Verify:
truss ledgershows MCP calls with arg/result hashes; the panel can cite them.
1.4 New surface
- Core:
truss_core::mcpmodule (client) —McpServerConfig, stdio JSON-RPC framing, and a tool adapter onto the existingTooltrait. Ledger entry kindmcp_tool_call. - CLI:
truss mcp list(declared servers + advertised tools),--mcp-configoverlay,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_callledger. 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_callprovenance hash and existingtool_memosubstrate. Implemented as the second PR. - MCP-3 HTTP transport + auth (env-injected, never journaled — see crypto note).
Implemented behind the off-by-default
mcp-httpfeature sotruss-coreremains vendor-free by default. - MCP-4 async tool suspend via
await_signal. Implemented with structured MCP pending content that recordsmcp_tool_pending, suspends the durable run, and records the delivered signal payload as the final signedmcp_tool_call. - MCP-5 CLI diagnostics surface. Implemented as
truss mcp listandtruss 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 ledgerreads. 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_hashhandle 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 + atruss_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 exposingtruss_run_contract+truss_contract_status+ ledger read tools, backed by existingdelegation+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
Principalisolation. 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-mcptransport code, shared tool-schema types).
2.4 Implemented stdio server slice
truss serve-mcp --transport stdio advertises exactly these tools:
truss_run_contracttruss_contract_statustruss_ledger_verdicttruss_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.rshas noDebug). Argument hashes in the ledger must redact declared-secret fields. - Shared code: one
truss-mcpmodule 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 (likelytruss-providersor a newtruss-mcpcrate; decide at MCP-1).
- tool-schema types, used by both client and server. Adding MCP must not add a vendor
dep to
- 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)
- New
truss-mcpcrate vs module intruss-providers? (Vendor-dep boundary.) - MCP Rust SDK (
rmcp/official) vs hand-rolled JSON-RPC? (Dep weight vs control — mirrors the SSE hand-roll decision.) - Async-tool suspend: reuse
await_signalas-is, or a dedicatedmcp_pendingawait kind? - 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.
| Tool | Purpose | Side effect |
|---|---|---|
git_status | Structured git status --porcelain=v1 --branch output. | No |
git_diff | Diff patch plus patch_sha256. Supports staged, base, head, and paths. | No |
git_branch | current, list, create, checkout, or create_checkout. | Only create/checkout actions |
git_commit | Optionally 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.
| Tool | Purpose | Side effect |
|---|---|---|
github_pr_view | View PR metadata, commits, files, and review state. | No |
github_pr_create | Create a PR with title, body, optional base, head, and draft. | Yes |
github_pr_diff | Fetch PR diff plus diff_sha256. | No |
github_pr_checks | Read PR checks through gh pr checks --json. | No |
github_issue_view | View issue metadata, labels, comments, and body. | No |
github_issue_list | List issues by state and limit. | No |
github_check_status | Fetch 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
- 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,spawnare these.) - 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.
- (a) typed, client-executed — e.g. Anthropic
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_20250818schema (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_editorfor file edits,bashfor 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.
- Anthropic →
- 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
- (this) agree the principle: typed schema + local execution.
- implement provider-aware mapping for file/shell tools (reuse the memory-tool pattern).
- re-run the in-loop benchmark → measure improvement vs the current generic-tool baseline (the ~50-task run starting now is that baseline).
- 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-cliand 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 namespacedhttps://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/messageswithAuthorization: Bearer <accessToken>,anthropic-beta: claude-code-20250219,oauth-2025-04-20,anthropic-version: 2023-06-01, nox-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-cliruntime + 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 asOPENAI_API_KEYinauth.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 inproviders/anthropic.tsdetects 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 realclaudebinary (-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:
- 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.
- 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.
Recommended Truss design (still: no code yet)
- 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 sameProvider/registry seam — i.e. ADR 0002’sProviderConfiggrows anauth/runtimevariant; 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
AssistantTextDeltaevents for live UIs + final assembledCompletionResponsefor tool loop / usage / stop reason - ✅ Tool-result truncation in events (full result still goes to the model)
- ⬜ Multimodal blocks (image/audio) —
Blockis 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
- ✅
AgentEventstream for everything (UI + audit) - ✅ Durable JSONL audit log with full provenance (
src/state.rs) - ✅
tracingdiagnostics (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) onCtx— carried, not yet isolating - ✅ Parallel tool execution within a step
- 🟡 Sub-agent fan-out (
Ctx::childexists; 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 sell | Test category that defends it |
|---|---|
Verified means a panel signed off | invariant/property test (unreachable any other way) |
| Ledger is tamper-evident | adversarial tamper suite (corrupt/forge/replay MUST fail verify) |
| Budget is bounded across a spawn tree | property test over random tree shapes |
| Memory can’t silently rot | invariant: Unverified/Rejected/Stale never in trusted view |
| Everything has provenance | property test: every entry has a valid LedgerRef |
| Works on real models | gated 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,releasebuild. 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-codexwas never tested against the live backend. - Nightly
truss evalover 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)
- 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).
- 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.
- Cost & budget always in view. Spend, budget remaining, and per-step cost are first-class UI, tied to the local-first/cost-control story.
- Verification should feel tangible. A
--verifypass (and a tamper catch) should be a satisfying, legible visual event — this is the demo moment that beats any competitor’s transcript. - Spawn/fan-out trees are visual. Sub-agent trees, which branch won, and why, should render as a structure, not prose.
- 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
-
In-repo interactive TUI first, external front-ends via the JSON seam. We build a
ratatui/crosstermTUI insidetruss-clias 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--jsonread surface (already shipped) plus the same core write primitives — we are not building a bespoke protocol. The TUI is a presentation/controller overLedgerReaderandStore; it never becomes the provenance substrate. -
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 byverify_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.
- The human decision (approve / reject / needs-changes) is provenance → it lives in
core as a new signed journal entry (
-
Human sign-off attests to a head hash. A
human_reviewentry 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. -
No fake authority. A
human_reviewdecision is recorded and signed, but it does not silently overwrite the machineVerdict. 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.) -
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_chaincovers it (incl. tamper tests). - Non-interactive CLI first (scriptable, testable without a terminal):
truss review approve|reject|needs-changes <session> [--note ...] [--json]. LedgerReadergains read access to human-review entries; verdict bundle +truss ledger verdictrender 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.notepayload. - Add suspended-run signal control (
s) by routing throughStore::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_reviewdecision (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_reviewentries 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
suspendedledger entries waiting on an approval signal that only an approvedhuman_reviewdecision 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::LedgerReaderand 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::ApprovalGatePolicymirrors 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
AgentEventstream: streamingAssistantTextDelta, 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_reviewentry. - 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_reviewappended → 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-mcpvs WS/JSON event server) — decided in theclawtermproject, 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 issuesview/create/str_replace/insert/delete/renamecalls against a/memoriesdirectory the harness backs. But neither harness enabled it. Capability ≠ activation. - A native 470-turn local test run used zero memory-tool
calls, had no
/memoriesdir, and everycontext_managementfield 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 37KBMEMORY.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
- 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.
- 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. - 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.
- Every memory carries provenance. Each entry links to the ledger event / tool result that produced it. “Why do you believe X?” is always answerable.
- 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.
- 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.
| Layer | Content | Lifetime | Cache position | Injection |
|---|---|---|---|---|
| L0 Identity | persona, values, voice, operating boundaries | ~never | front of cached system prefix | push, signed, frozen |
| L1 Principal | who the user is, prefs, standing constraints | per-user, slow | after L0, still cached | push, per-user cache key |
| L2 Durable memory | verified facts, lessons, decisions | grows | not pushed | pull via memory tool |
| L3 Goal ledger | active contract intent + DoD, north-star | per-task | head of messages, re-pinned every N turns | push, never compacted |
| L4 Working scratch | in-run notes, tool-result digests | ephemeral | tail of messages | model-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.mdFable 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)
| Type | Meaning | Mutability | Compaction |
|---|---|---|---|
goal | north-star DoD / intent for a contract | append + supersede | never |
decision | a choice made + rationale | append-only (immutable log) | never |
fact | a durable learned fact about the world/codebase | supersede-by-key | salience-scored |
lesson | a correction / confirmed approach + why | supersede-by-key | salience-scored |
artifact | a verified output + where it lives | append | by reference |
scratch | ephemeral working note | free | aggressive |
(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-stakereview) exactly as a DoD clause is. OnlyPASSpromotes. - 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:
| Mode | Behavior | Cost | Use |
|---|---|---|---|
off | writes stay Unverified, usable in-run, never promoted/surfaced to future runs as trusted | none | throwaway / solo speed |
lazy (default) | verify on first recall, not on write — pay only for memory actually re-used | amortized | most work |
eager | sync N-reviewer pass before promotion on write | high | high-stakes / shared memory |
tiered | cheap heuristic gate always; full panel only for fact/decision or flagged writes | medium | balanced |
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):
| Knob | Default | Values |
|---|---|---|
scope | per-agent | per-agent, per-project, shared |
conflict | verified-wins | verified-wins, latest-wins, panel-arbitrated, keep-both-flagged |
goal_repin_every | 8 | positive integer |
projection.keep_tail | 0 (legacy no-projection) | integer |
projection.compaction_exempt_tiers | l0_core, l0_self, l1_principal, l3_goal | tier list |
salience | off | off, harness-recency, model-scored, hybrid (M5 seam only) |
IdentityPolicy — who can amend L0 self:
| Mode | Behavior |
|---|---|
locked | operator-signed only; agent may propose, not commit |
propose (default) | agent drafts an amendment → verdict gate / operator approval → commit |
self_amend | agent 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:
coreis never amendable. Self-amend touchesselfonly. Safety/boundary erosion is structurally impossible — the dangerous half of identity is locked.- 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.
- Amendment is a deliberate signed tool call, provenance-logged to the
ledger — never inducible from untrusted/injected content. Even in
self_amendthe proposed change is verdict-checked againstcorebefore commit (reuses the N-reviewer machinery — an identity edit is just a high-stakes memory write, so it also respectsVerificationPolicy). - 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(landsUnverified), with the harness inferringkindfrom the path (/memories/goal/…,/memories/decisions/…,/memories/lessons/…).view→MemoryStore::queryfiltered toVerifiedonly 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
Unverifiedwrites 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-memorycrate:MemoryStoretrait, flat-file impl, typed entries, provenance into the existing ledger. No model yet; unit tests. (Mirrors howtruss-coreledger was built.) - M2 — Memory tool binding. Intercept
memory_20250818in the agent loop, map toMemoryStore, path-basedkindinference,viewreturns 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.5VerificationPolicy(off/lazy(default) /eager/tiered, user-selectable, layered defaults).Who writes L0 identity, and can the agent self-amend?RESOLVED → §3.5IdentityPolicy(locked/propose(default) /self_amend) + the L0core/selfsplit 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
SaliencePolicyseam; scoring and eviction remain M5. Cross-run vs cross-agent scope.RESOLVED in M4 → configurablescope(per-agentdefault,per-project,shared).Conflict resolution.RESOLVED in M4 → configurableconflict(verified-winsdefault,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:
- Wire the module: add
pub mod memory;tolib.rs. Resolve any compile drift in the drafted skeleton (it referencesdelegation::Verdict,store,identity). SqliteMemoryStore: implement theMemoryStoretrait against the existing durable DB connection (shareArc<Mutex<Connection>>withcrate::store::Store, or open the same file). Add thememorytable from the schema sketch instore.rs(+write_seqmonotonic counter, index on(mem_key, trust)andwrite_seq).- Content-addressing:
MemoryId= hex sha256 overkind || key || body(reuse thesha2dep already instore.rs). - Supersede semantics:
writeof aFact/Lessonwith an existingmem_keymarks the prior entrysuperseded_by+ bumpswrite_seq. delta_since(cursor)=WHERE write_seq > cursor ORDER BY write_seq.- Provenance: persist
produced_by(session, seq) and (on promote) the fullVerdictJSON.
Tests (crates/truss-core/tests/memory_store_test.rs):
- write →
getround-trips; landsUnverified. - supersede-by-key sets
superseded_by, old entry no longer in default query. querydefault returnsVerifiedonly;min_trustfloor respected.promoteattaches verdict, flips tier, appears in nextdelta_since.delta_sincereturns 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:
- Register a
memorytool (type: "memory_20250818") in the tool registry (tools/mod.rs), gated by a session flag so existing runs are unaffected. - Map commands →
MemoryStore:create/insert/str_replace→write(Unverified). Inferkindfrom the path prefix (/memories/goal/…,/decisions/…,/lessons/…,/facts/…,/artifacts/…, elsescratch).view→query(defaultVerified-only); render as a dir/file listing the model expects.delete/rename→ supersede /mark_stale.
- Provenance capture: stamp
produced_bywith the current journalseqat the moment the tool call is journaled (the binding runs inside the agent loop, which already writes the ledger). - Emit
AgentEvents for memory ops (extendevents.rs:MemoryWrite,MemoryRecall) so the UI/audit log sees them.
Tests (hermetic, fake provider emitting tool calls — mirror agent_loop_test.rs):
- model
createunder/memories/lessons/x→ entry exists,kind=Lesson, Unverified. - model
viewreturns 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:
VerificationPolicyresolution at session build (MemoryConfig), layered global → per-agent → per-contract (extend the contract[memory]block).- Promotion driver:
Eager: on write, run the N-reviewer panel (delegation) on the claim → PASS callspromote, FAIL callsreject.Lazy: onview/recall of anUnverifiedentry, verify-then-surface; cache the verdict so it runs once.Tiered: heuristic gate (cheap model or rule) always; full panel only forFact/Decision(MemoryConfig::verify_on_write).Off: never promote;viewmay still return Unverified but tagged.
- Contradiction handling: a new Verified entry that supersedes a key demotes the
prior to
Stale. - 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:
- Context assembler: build
tools → system(L0 core, L0 self, L1) → messages(L3 goal ledger, history, L2 pulls, L4 scratch). Placecache_controlso L0/L1 are the stable cached prefix. Persona: load/persistPersonaCore(operator-signed viaidentity::Signer)PersonaSelf;render()into L0. Verify the core signature on load.
- Goal ledger (L3): pin the active contract intent + DoD; re-inject every N steps; never compacted.
- Delta rehydration: on resume/re-pin, inject
delta_since(cursor)for L2, not the whole store. IdentityPolicy+ amendment flow:route_amendment→ bufferCommitAtBoundary; apply at next session build as a reversible journal event; verdict-checkselfagainstcorebefore commit. A dedicated signedamend_identitytool (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
Proposeneeds approval; underSelfAmendcommits at boundary and is rollback-able; an amendment targetingcoreisDenied.
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:
- 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 withoutmemory_20250818. - Salience scoring + eviction (recency + recall-count + optional model score);
Staledecay policy. - Optional vector backing behind
MemoryStorefor semantic recall (no agent-loop change). - 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 cadenceN. - 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.SqliteMemoryStoreis the current impl; a complex scheme is a new impl (or a decorator) implementing the same trait.MemoryEntry— already carriestrust,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.containsis 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:
- Writes always enter
Unverified. Promotion stays a separate, panel-gated step. Verifiedalways means “N-reviewer panel signed off.” Retrieval can get smarter; the trust label’s meaning is fixed.- Provenance (
LedgerRef) on every entry, validated under--verify. - Reversibility — supersede/demote-to-
Stalestays traceable in the ledger. - 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 +
containssubstring; 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 reindexbackfill, and vector-rankedMemoryQuery.containsbehindquery(). The config gate ismemory.recall = "exact" | "semantic" | "hybrid", withexactas 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 inquery()(mirrors OpenClaw’s hybrid search). Still justquery()internals. - S3 — salience & eviction / compaction. Activate the existing
saliencefield: 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 overMemoryStorefor linked recall. Additive — new kinds, same trait. - S5 — scope tiers as backends. Per-agent / per-project / shared L2 can each be
a different
MemoryStoreimpl 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
containsas the semantic-recall entry point; don’t add a parallel retrieval API. - Keep
saliencepopulated-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;
BudgetMetermeters 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::reconstructcan 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
evalengine 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
- ContextGauge (
truss-core): computespressure = projected_tokens / context_windowat each step boundary. Requires a provider capabilitycontext_window()(CC-1) and a model→window map seeded frompricing.rs/capability metadata. Emitscontext_pressure. - CompactionPolicy (configurable, layered global → agent → contract):
mode:off|summarize(default) |evictthreshold: pressure at which to act (default0.70)exempt: reuse M4compaction_exemptlayers (never compact L0/L1/L3)min_tail: verbatim recent turns always keptanticipate: forward-estimate on/off (CC-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 viaprojection.rsinto a memory entry (EntryKind::ContextSummary, trust perVerificationPolicy), stamp provenanceLedgerRefto the elided journal range, keep exempt layers +min_tailverbatim, continue. - Provenance (CC-2): signed
compaction_performedledger entry recording before/after token counts, the summary memory id, and the elided journal range;Store::verify_chaincovers 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_pressureevent 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 →ContextSummarymemory entry + signedcompaction_performedledger; 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_managementinterop stay out of scope for this milestone.
Invariants (any implementation must hold)
- The journal stays authoritative; compaction never deletes journal entries, only changes what is projected into the model’s context.
- Exempt layers (L0 core/self, L1 principal, L3 goal) are never compacted.
- Every compaction is a signed ledger entry and is recallable.
- Compaction happens only at a step boundary, never mid-step.
- 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::LessonUnverified entries with provenance (the run’sLedgerRef). 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
Policyobjects living in the same governed store with provenance — structurally identical to a memory entry / identity amendment. (Routing thresholds, personaselflayer, 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_coreidentity + 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
- Best-of-N fan-out on
child_run(build-order #2) — also the lesson-quality and eval engine. - SI-1 → SI-2 → SI-3 (the L2 learning loop).
- SI-4 paired eval harness (proves L2; feeds L3).
- 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) andextensibility-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:
- 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.
- Security gap — today’s “sandbox” is application-level only:
Policy(write/read scope, network,forbidden_substrings,allow_tools) + bashkill_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).
Sandboxtrait;NoSandboxdefault (no behavior change);WorkingCopySandbox(ephemeral overlay/copy, reaped, captured); model-facing sandboxed test-run path; signedself_validationjournal 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
- Self-validation output is evidence, never the verdict (executor never grades own homework; verify boundary unchanged).
- Default
NoSandbox— no behavior change unless opted in. - Sandbox composes with
Policy(sandbox confines, policy authorizes) — neither replaces the other. - Every sandbox run is recorded in the signed journal (provenance/recallable).
- Every knob configurable global → agent → contract with a safe default.
- 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:
| Flag | Config equivalent | Purpose |
|---|---|---|
--config | n/a | Load a TOML config file. |
--provider | provider.kind | Select anthropic or openai. |
--model | provider.model | Select the model id. |
--base-url | provider.base_url | Point OpenAI-compatible providers at local or alternate endpoints. |
--system | agent.system | Override the system prompt. |
--max-steps | agent.max_steps | Cap model/tool loop steps. |
--max-tokens | agent.max_tokens | Cap output tokens per model call. |
--reasoning | agent.reasoning | Set minimal, low, medium, or high reasoning effort. |
--temperature | agent.temperature | Set sampling temperature where supported. |
--tools | n/a | Register built-in tools. |
--git-tools | tools.git | Register native git tools. |
--github-tools | tools.github | Register native GitHub tools backed by gh. |
--web-tools | tools.web | Register native web tools (web_fetch, web_search). |
--memory-tools | tools.memory | Register read-only native memory tools (memory_query, memory_view). |
--memory-tool | n/a | Register the governed memory tool for durable sessions. |
--spawn-tool | n/a | Register store-backed child-run spawning for durable or contract runs. |
--workdir | policy scopes | Confine reads and writes to a directory. |
--budget-usd | budget.max_cost_usd | Set a hard run cost ceiling. |
--budget-tokens | budget.max_total_tokens | Set a hard token ceiling. |
--timeout | n/a | Set a wall-clock deadline in seconds. |
--log | n/a | Append JSONL events to a file. |
--verbose | n/a | Print every event as it happens. |
Durable and verification flags:
| Flag | Purpose |
|---|---|
--durable | Enable SQLite-backed durable execution. |
--db | Set the durable store path, default harness.db. |
--session | Set or target a durable session id. |
--resume | Resume a durable session by id. |
--verify | Verify a durable session hash chain and signatures. |
--key | Select the ed25519 signing identity path. |
--expect-key | Pin the expected public key while verifying. |
--signal / --payload | Deliver a named durable signal. |
--daemon / --poll-secs | Auto-resume sessions whose waits are satisfied. |
--anchor / --anchor-log | Anchor a session head hash to a transparency log. |
--verify-anchor-log | Verify the transparency log chain. |
--max-context-msgs | Legacy projection: keep Head plus this many recent turns. |
Contract flags:
| Flag | Purpose |
|---|---|
--contract | Execute a TOML contract, then independently verify it. |
--reviewer-model | Select the no-stake reviewer model. |
--reviewers | Override the contract reviewer panel size. |
Compaction flags:
| Flag | Purpose |
|---|---|
--compaction-mode | off, summarize, or evict. |
--compaction-threshold | Context pressure threshold, 0.0..=1.0. |
--compaction-min-tail | Verbatim tail messages retained after compaction. |
--compaction-anticipate | Enable compact-ahead behavior. |
Native Memory Tools
Truss provides two default-off read-only native memory tools:
memory_querysearches L2 durable memory using the configured recall mode (exact,semantic, orhybrid).memory_viewfetches 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 withid,kind,key,body,trust,ledger_ref, and fullprovenance.
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; VerifiedandRejectedrequire reviewer verdicts;- promotion, rejection, and stale transitions use content-hash compare-and-set;
- signed
LedgerRefprovenance 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_fetchfetches an HTTP(S) URL and returns readable content plus status, final URL after redirects, content type, and SHA-256 hashes.web_searchqueries 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: currentlyattested-as-retrieved.
max_bytes is clamped to a 1 MiB default/ceiling. This keeps tool output bounded;
it is not a completeness guarantee.
web_search
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 ishttps://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:
- Use MCP when the extension is an external tool or already exists as a service.
- Use an in-process Rust
Toolwhen the tool must share process memory or run hot/trusted logic. - Use a provider plugin when adding a model backend or auth strategy.
- Use an event or ledger consumer when the extension only observes runs.
- 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
| Seam | Current API | Best for | Journal rule |
|---|---|---|---|
| Provider | truss_core::provider::Provider, built through truss_providers::registry | Model backends, auth strategies, local OpenAI-compatible servers | Model results are journaled by the agent/durable loop, not by the provider |
| In-process tool | truss_core::tools::Tool | Trusted Rust tools that touch local state or need low latency | Override 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 integrations | Truss records mcp_tool_call provenance plus replayable tool_result |
| Event stream | AgentEvent via Ctx::with_events | Live UIs, logs, dashboards, telemetry | Usually off-journal; observe without changing run outcome |
| Ledger reader | LedgerReader, truss ledger ... | Evidence exports, audits, buyer reports, recall | Read-only; never rewrites signed entries |
| Sandbox | truss_core::sandbox::Sandbox | Stronger command isolation behind existing self-check/tool flows | Keep the same self_validation journal shape |
| Anchor | truss_core::anchor::Anchor | Rekor/transparency log/on-chain hash anchoring | Anchor hashes only, never evidence or secrets |
| ContextProvider | Roadmap seam | Enterprise docs/vector/policy injection | Not a public trait yet; do not invent a parallel path |
Pick the seam
- Add a model backend: Provider Plugins.
- Add a local Rust tool: In-Process Tools.
- Add an external tool: MCP Server Tools.
- Add context injection: Context Providers.
- Add isolation or notarization infrastructure: Sandbox and Anchor Backends.
- Build a UI, exporter, dashboard, or audit bot: Event and Ledger Consumers.
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
LedgerRefprovenance. - 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 testor a focused crate test for Rust seam changes.truss mcp probe <server>for MCP tools.truss ledger verify <run-id>ortruss ledger export <run-id>for durable evidence.mdbook build docsfor 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
- Add a module in
crates/truss-providers/src/. - Implement
Provider. - Add
pub(crate) fn build(cfg: &ProviderConfig) -> Result<Arc<dyn Provider>, BuildError>. - Add a cargo feature in
crates/truss-providers/Cargo.toml. - Add one
available()entry and onebuild()match arm inregistry.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
ctxcancellation/deadlines; - return typed
ProviderErrorvalues 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_streamonly when the backend has true streaming. The default wrapper is acceptable and still produces correctAgentEvents.
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_ledgeris 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_toolson 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:
GoalLedgerfor contract intent and definition-of-done context;- layered memory/persona context;
- governed compaction and signed
compaction_performedjournal entries; LedgerRefprovenance on memory entries;AgentEventfor 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
LedgerRefwhen 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
AgentEventor ledger exports. - For enterprise docs/vector DB injection, keep the adapter outside the runtime
until the public
ContextProvidertrait 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
ctxcancellation/deadlines. - Child processes are reaped.
capabilities()is honest.- The same command produces a stable
workdir_hashfor 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 exportoffline_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::Playbookin governed memory;- markdown/SKILL.md import into structured playbook JSON;
- signed
playbook_importedjournal entries; truss playbook import|list|show|revoke;- revocation by demoting the memory entry to
Staleand recordingplaybook_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_promotedorplaybook_promotion_rejectedjournal entries; promote_with_verdict_refprovenance 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
Verifiedonly onPass. - 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.
| Example | Use |
|---|---|
| Harness Config | A full --config file for provider, agent, memory, budget, and policy defaults. |
| Verified Contract | The minimal contract-to-verdict flow with checks and reviewer clauses. |
| Contract with MCP Tool | Contract-scoped stdio MCP tool mounting with signed provenance. |
| Contract with Compaction | A contract-level [memory.compaction] example; compaction remains opt-in. |
| Claude Subscription Contract | Delegated execution through Claude Code while Truss owns verification. |
| Codex Exec Contract | Delegated execution through codex exec while Truss owns verification. |
| Ledger Export Consumer | A read-only plugin that consumes exported signed evidence without touching the runtime. |
| Layered Long Run | Intended 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,Tooltraits, message/usage/error types, the runCtx; - 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.tomlcannot 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-oauthlogin/refresh engine (Tier 1) + a small per-vendor descriptor (endpoint, headers, beta flags). Viable for OpenAI (tolerated); not for Anthropic (ToS-banned Feb 2026 — seedocs/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 prototypeclaude-cliprovider (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_stableplugins. 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)
- Product name / crate prefix. Blocks crate naming. “Truss” is the working name but unsettled — to be decided before Phase 1.
- Default-feature matrix. Proposed:
auth-apikey,provider-openai,provider-anthropicon by default; all else opt-in. Confirm. - 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:
-
Dispatch by id, in
truss-providers.registry::build(name, &ProviderConfig) -> Result<Arc<dyn Provider>, BuildError>owns thematchover 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 forwardsargs.provider+ aProviderConfig; it no longer names a concrete provider or knows which features exist. -
Auth resolution moves into the provider. Each provider gets a
pub(crate) fn build(&ProviderConfig)that resolves its own credentials and default model.ProviderConfigcarries 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 touchingtruss-coreortruss-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.
ProviderConfigwill grow (e.g. anauthfield) 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)
- Status: Accepted
- Date: 2026-06-15
- Deciders: Matt (owner), ALINA
- Builds on: ADR 0001, ADR 0002
- Research:
docs/auth-strategy.md
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-IDagainst 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-8withANTHROPIC_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
Executorenum 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
- (this ADR’s PR) Tier-2
Executorseam +CliExecutor+ Anthropic (claude -p --model claude-opus-4-8), hermetic tests with a fake CLI, contract[executor]block. “Claude first.” - 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 OpenAIcodex exec(examples/contract-codex-exec.toml) both work via the sameCliExecutor— no provider code, just an[executor]block. - Tier 1 (OpenAI in-process OAuth-replay): shipped v0.11.0 as the
openai-codexregistry provider, behind theprovider-openai-codexcargo 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
- Status: Proposed
- Date: 2026-06-16
- Deciders: Matt (owner), ALINA
- Builds on: Memory & Identity Architecture
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
- Status: Proposed
- Date: 2026-06-17
- Deciders: Matt (owner), ALINA
- Builds on: ADR 0004 — Governed memory substrate
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, andstr_replacewriteMemoryDrafts that always land asUnverified.viewqueries with the defaultVerifiedtrust floor and renders a directory/file-style listing.deletemarks active entries stale.renamewrites 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:
lazyverifies an unverified entry on first recall, then caches the verdict in the entry trust tier.eagerverifies on write and promotes only a passing panel verdict.tieredruns a deterministic cheap hygiene gate on every write and runs the full reviewer panel on write forfactanddecisionentries.offnever promotes; recalls may surface unverified entries, but the rendered view always tags them astrust: 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:
- tools
- system blocks: L0 core identity, L0 self identity, L1 principal
- 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:
PersonaCoreis operator-signed withidentity::Signer, verified on load, and refused if the signature no longer matches.PersonaSelfcontains amendable voice/working identity and version metadata.Persona::render()emits L0 core before L0 self.
Identity amendments are deliberate:
amend_identityis a dedicated signed tool.- Amendments targeting
coreare denied. IdentityPolicy::proposebuffers for approval.IdentityPolicy::self_amendbuffersCommitAtBoundary, applies on the next session build, and records a reversible journal event.- The amendment is verdict-checked against
corebefore 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
| Knob | Default | Values | Layering |
|---|---|---|---|
verification | lazy | off, lazy, eager, tiered | global -> agent -> contract |
identity | propose | locked, propose, self_amend | global -> agent -> contract |
scope | per-agent | per-agent, per-project, shared | global -> agent -> contract |
conflict | verified-wins | verified-wins, latest-wins, panel-arbitrated, keep-both-flagged | global -> agent -> contract |
goal_repin_every | 8 | positive integer | global -> agent -> contract |
projection.keep_tail | 0 | integer, 0 disables projection | global -> agent -> contract |
projection.compaction_exempt_tiers | l0_core, l0_self, l1_principal, l3_goal | any ContextTier list | global -> agent -> contract |
salience | off | off, harness-recency, model-scored, hybrid | global -> 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
Ctxchild 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
Principalor run under an explicitly tighter principal/scope label; - spend is recorded into the parent’s shared tree meter while an additional
per-child
BudgetLimitsslice is enforced.
The result is a verified result, not a transcript:
child_run::ChildRunOutputcontains the existingdelegation::Verdictplus producedArtifactRefs;- the runner records the result in the child ledger as
child_result; - the returned
VerifiedChildResultincludes the verdict, artifacts, the child root/head hashes, theverdict_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_idchild_run_idchild_root_hashchild_head_hashtaskscopebudget_slicestatusverdict_refwhen completederrorwhen 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_childas a tool that packages a model task intoChildRunOptionsand returns aVerifiedChildResult. - 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 childoverallverdicts. This is the default because it needs no external scorer or model reviewer dependency.JudgePanel— reuses the existingdelegation::run_review_panelno-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-suppliedBestScoretrait object and picks the highest finite score.FirstSuccess— picks the first child whose verified verdict isPass, 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
BudgetLimitsslice; - 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_childas a tool that packages a model-requested subtask intoChildRunOptions. - 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, parentchild_anchor, and cancellation/deadline propagation; - the existing delegation
Verdictshape 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(optionalBudgetLimitsobject withmax_total_tokens,max_cost_usd, andmax_steps);scope(optional provenance label).
The tool output is a JSON verified-result summary:
child_run_id;verdict.overall,verdict.summary, and clause results;artifactsasArtifactRefvalues;ledger.verdict_ref;ledger.parent_anchor;ledger.child_root_hashandledger.child_head_hash;usagefor 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:
offis the default-safe mode. It performs no extractor model call and writes no memory.onruns the extractor after the contract verdict is recorded.model-scoredruns the same extractor, but only writes lessons whose model salience is at or aboveextraction_min_salience(default0.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::Policyis a typed, versioned policy object.memory::PolicyTargetenumerates tunable policy surfaces such as routing,PersonaSelf, tool-set, verification, salience, and goal repinning.memory::PolicyRecordmirrors memory’s governance shape:Trust,Provenance, creation time, and supersession pointer.SqliteMemoryStorestores 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 remainsStore::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_panelthrough the existing memory-claim verifier, now including the lesson claim, provenance ledger refs, and the producing contract verdict evidence; - a passing
delegation::VerdictcallsMemoryStore::promote, which stores the verdict inProvenance.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_verdictjournal 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 labeledUnverified;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, andStaleentries 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_lessonsdefaults to5;lesson_recall_token_budgetdefaults to512approximate 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-panelVerdict;verified_by_ref: theLedgerRefof 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:passorfailfor whether the learning-on arm improved or tied with no extra measured cost/tokens.arms: oneEvalArmReportper arm, including:arm:learning_onorlearning_off;- resolved
memoryconfig 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 signedeval_reportevent 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
proptestinvariant 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:
Verifiedmemory implies an attached panel verdict and signedmemory_verdictledger 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
Verifiedentries - 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 TrussContract.SweBenchStyleAdapter: a local SWE-bench-style adapter where a repo fixture plus shell/test command becomes the contract’s harness-ownedcheckclause.BenchmarkRunner: a thin runner that executes adapted contracts viatruss_core::eval::run_paired_evaland aggregates a serializableBenchmarkReport.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:
SweBenchTaskis 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.SweBenchAdaptermaps a task into a TrussContractwhose intent is theproblem_statementand whosecheckclause applies the candidate patch, applies the tasktest_patch, and runsFAIL_TO_PASS + PASS_TO_PASS.- The benchmark runner records a signed
swebench_resultledger 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:
- Prepare a per-instance checkout under
--checkout-root, checking out the taskbase_commit. - Build a Truss
Contractwhose intent is the SWE-benchproblem_statementand whose constraints scope reads and writes to that checkout. - Run
Executor::InLoop(provider)with the default filesystem and bash tool registry. - Capture
git diff --binary HEADasmodel_patchin an official predictions JSONL row. - 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, andlist_dirare advertised as thetext_editor_20250728tool namedstr_replace_based_edit_tool.bashis advertised asbash_20250124namedbash.memorycontinues to usememory_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.70is the default pressure trigger when a caller opts in.min_tail = 6keeps recent messages verbatim.exempt_tiersdefaults 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 withanticipate = false.ANTICIPATE: the same governed policy with CC-3anticipate = 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_performedledger entries,- model steps completed,
- total configured verbatim tail messages preserved across compactions,
- tokens summarized away, computed as the signed ledger
before_tokens - after_tokenstotals, - 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:
- run a live in-loop Truss workload or SWE-bench instance through the same durable loop,
- keep CC-4’s arm definitions and metric collection unchanged,
- derive token and compaction metrics from the real provider’s usage events and Truss journal, and
- 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 hostsh -cexecution path and process reaping discipline.WorkingCopySandbox: materializes an ephemeral copy of the checkout, runs the command there withkill_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.tomlconfigures the book and emits generated HTML to top-levelbook/.docs/SUMMARY.mdis the navigation tree for architecture docs, configuration, examples, go-to-market/evaluation notes, and ADRs.docs/introduction.mdis the landing page.docs/examples/*.mdare thin wrappers that explain each example and include the live source files fromexamples/.
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 listtruss 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_ratepromotes;- 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, orneeds_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:
- 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).
- 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.
- Ledger chain — head hash plus the verify status and per-entry hashes, so an intact-vs-tampered chain reads at a glance.
- 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:
aapproverrejectnneeds changessdeliver the pending signal for a suspended runxshow 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:
enabledhigh_risk_toolsspend_threshold_usdper_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_statusgit_diffgit_branchgit_commitgithub_pr_viewgithub_pr_creategithub_pr_diffgithub_pr_checksgithub_issue_viewgithub_issue_listgithub_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_byledger 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_contracttruss_contract_statustruss_ledger_verdicttruss_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_refpointing callers totruss_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_fetchfor HTTP(S) GET requests.web_searchfor 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_KEYby default for the API key.TRUSS_WEB_SEARCH_ENDPOINToptionally overrides the endpoint, unless TOML provides[tools.web_search].endpoint.[tools.web_search].api_key_envcan 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
reqwestbecause 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_idembedder_iddimsvector_jsoncreated_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:
- Model writes enter
Unverified. Verifiedmeans a panel-signed or paired-eval verdict promoted the entry.- Every entry carries provenance and a
LedgerRef. - Supersede and demote-to-
Staleremain reversible ledgered state changes. - 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, orhybrid).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_callentries and the returned entries’LedgerRefprovenance. - 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
memorytool. 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.