Date: 2026-04-05
Branch: hal/trust-chain-benchmark (from compound/daemon-local-runtime-foundation)
Spec refs: §3 §6, §4 §2, §5 §2-3, §7 §2-3, §9 §2, §10 §5, §11 §3
Issues: Advances #20 (disclosure verification), advances #21 (gateway binding verification)
Priority: HIGH — closes 4 spec gaps that block the end-to-end "10-minute trust loop" MVP
Estimated: ~650 LoC net new across 8 files
Coverage lift: overall 79% → ~84%
After a thorough spec-vs-code audit (SPEC-0.2.0 §00–§11), we identified 4 concrete gaps that remain between the implementation and the spec's normative requirements. These gaps collectively break the trust chain: an agent cannot yet complete the full "10-minute trust loop" described in §9 because (a) anti-Sybil enforcement is missing, (b) disclosure packages aren't fully verified at admission, (c) gateway doesn't verify announcement bindings, and (d) there is no Benchmark World to bootstrap cold-start reputation.
| # | Gap | Spec Section | Severity | Impact |
|---|---|---|---|---|
| 1 | maxAgentsPerWallet defined but NOT enforced |
§4 §2, §7 §2 | HIGH | Sybil attackers can join with unlimited agents per wallet |
| 2 | Disclosure Merkle proofs not verified during admission | §3 §5, §7 §3, §10 §3 | MEDIUM | Trust-gated worlds accept unverified disclosure packages |
| 3 | Gateway doesn't verify announcement fields against binding record | §5 §2-3 | MEDIUM | Malicious operators can announce fake metadata |
| 4 | No Benchmark World template | §9 §2, §3 §6 | HIGH | Cold-start agents have no standardized reputation path |
These related items were verified as correctly implemented during the audit:
| Requirement | Status | Location |
|---|---|---|
| source_world_stage_factor (0.50/0.80/1.00) | ✅ | scoring.rs:71-81, 467, 473 |
| Beta reputation model (α/β) | ✅ | scoring.rs:499-504 |
| Rate limiting enforcement | ✅ | server.rs:502-650 |
| Protocol version header (X-AWN-Protocol-Version) | ✅ | server.rs:1189-1204 |
| Receipt count endpoint | ✅ | trust_endpoints.rs |
| Identity Binding Proof endpoint | ✅ | trust_endpoints.rs |
| Version skew ±1 minor enforcement | ✅ | daemon/client.rs:353-374 |
| Timeout escalation (consecutive + rolling) | ✅ | world_api/timeout.rs:14-117 |
| Gateway liveness (120s/300s thresholds) | ✅ | gateway/liveness.rs:9-12, 64-73 |
| World Binding Record struct | ✅ | world_api/world_binding.rs:18-33 |
| Receipt lifecycle (all 6 states) | ✅ | trust/lifecycle.rs:45-61 |
| Challenge window / timeout finalization | ✅ | trust/lifecycle.rs:270-343 |
| SSE event signing | ✅ | world_api/sse.rs:77-91 |
| Daemon state checkpoint | ✅ | daemon/compound.rs:365-458 |
| Receipt staleness rejection (30 days) | ✅ | trust/lifecycle.rs:35-36, 378-386 |
Goal: Prevent a single wallet from joining a world with multiple agent identities, as required by §4 and §7.
Spec: §4 §2 — "maxAgentsPerWallet: 1 — Optional: anti-Sybil"; §7 §2 — "Per-World multi-accounting: Enforce maxAgentsPerWallet: 1"
Current State: AdmissionConfig has max_agents_per_wallet: Option<u32> field defined in src/schema/mod.rs:381, but check_admission() in endpoints.rs explicitly skips it with a comment saying "enforced at a different layer". No layer enforces it.
Step 1: Add a wallet_tracker to EndpointState:
// In EndpointState (src/world_api/endpoints.rs)
/// Maps wallet subjectId → set of awnIds currently joined from that wallet.
/// Used for maxAgentsPerWallet enforcement.
pub wallet_tracker: RwLock<HashMap<String, HashSet<String>>>,Step 2: In join_handler(), after admission check passes, enforce the limit:
// After check_admission() succeeds, before world_runtime.on_join()
if let Some(max_per_wallet) = state.admission_config.as_ref()
.and_then(|c| c.max_agents_per_wallet)
{
if let Some(ref subject_id) = join_request.subject_id {
// Extract wallet from subject_id: "eip155:8453:0xRegistry:tokenId" → "eip155:8453:0xRegistry"
let wallet = extract_wallet_from_subject_id(subject_id);
let tracker = state.wallet_tracker.read().await;
if let Some(agents) = tracker.get(&wallet) {
if agents.len() >= max_per_wallet as usize {
return Err(ApiError::new(
ApiErrorCode::AdmissionDenied,
format!("wallet already has {}/{} agents joined", agents.len(), max_per_wallet),
));
}
}
}
}Step 3: On successful join, register in tracker. On leave, remove from tracker.
Step 4: Add helper extract_wallet_from_subject_id():
/// Extracts the wallet portion from a CAIP-10 subject ID.
/// "eip155:8453:0xRegistry:tokenId" → "eip155:8453:0xRegistry"
/// Used for maxAgentsPerWallet grouping (all tokens from same registry = same wallet).
fn extract_wallet_from_subject_id(subject_id: &str) -> String {
// Split on ':' — first 3 parts are the wallet identifier
let parts: Vec<&str> = subject_id.splitn(4, ':').collect();
if parts.len() >= 3 {
format!("{}:{}:{}", parts[0], parts[1], parts[2])
} else {
subject_id.to_string()
}
}Files: src/world_api/endpoints.rs
AC:
-
maxAgentsPerWalletenforced at join time when configured - Wallet extracted from CAIP-10
subjectId(registry-level grouping) - Join rejected with
ADMISSION_DENIEDwhen limit exceeded - Leave handler cleans up wallet tracker
- Works with
None(no limit, default behavior unchanged) - Test: 2 agents from same wallet, limit=1 → second rejected
- Test: 2 agents from different wallets, limit=1 → both admitted
- Test: no limit configured → unlimited agents per wallet
Goal: When a trust-gated world verifies a disclosure package during admission, actually verify the Merkle inclusion proofs — not just deserialize them.
Spec: §3 §5 — "Receipt validity proven via Ed25519 signatures + Merkle inclusion proofs"; §10 §3 — "Verify Merkle proofs against on-chain roots"
Current State: verify_disclosure_receipts() in src/trust/disclosure.rs:113-276 verifies Ed25519 receipt signatures but does NOT call verify_merkle_proof() (which exists in src/trust/merkle.rs:160-184) on the disclosure package's AnchorProof entries.
Step 1: In verify_disclosure_receipts(), after signature verification succeeds for each receipt, check if a matching AnchorProof exists and verify it:
// After signature verification (around line 270 in disclosure.rs)
// Verify Merkle proofs if provided
for proof in &package.proofs {
// Find the receipt this proof belongs to
let receipt_id = &proof.receipt_id;
// Compute leaf hash: SHA256(JCS(receipt))
let receipt_json = package.receipts.iter()
.find(|r| r.get("receiptId").and_then(|v| v.as_str()) == Some(receipt_id));
if let Some(receipt_val) = receipt_json {
let canonical = jcs_canonicalize(receipt_val)?;
let leaf_hash = sha256(&canonical);
if !verify_merkle_proof(&leaf_hash, proof.leaf_index, &proof.proof, &proof.merkle_root) {
warnings.push(DisclosureWarning {
code: "MERKLE_PROOF_INVALID".to_string(),
message: format!("Merkle proof for receipt {} failed verification", receipt_id),
});
}
}
}Step 2: Add MERKLE_PROOF_INVALID warning code to DisclosureWarning.
Step 3: Optionally, worlds with strict admission can reject on any Merkle proof failure. For now, return as warning (consistent with spec's "conditional completeness" approach).
Files: src/trust/disclosure.rs, src/trust/merkle.rs (minor — ensure public API)
AC:
-
verify_disclosure_receipts()verifies Merkle proofs whenAnchorProofentries present - Invalid proofs produce
MERKLE_PROOF_INVALIDwarning - Missing proofs do NOT fail (proofs are optional per spec)
- Receipt leaf hash computed via JCS canonicalization + SHA256
- Test: valid proof → no warning
- Test: tampered proof → warning generated
- Test: no proofs in package → passes (backward compatible)
Goal: Gateway must verify that announcement fields match the World Binding Record, not just the signature.
Spec: §5 §2 — "Announced worldId, slug, name, category, endpoint, agentCardUrl MUST match bound values in binding record. Announcement signature MUST verify against bound worldKey."
Current State: handle_announce() in src/gateway/handlers.rs:40-92 verifies the announcement signature and checks non-empty required fields, but does NOT verify fields against a resolved World Binding Record. The verify_binding flag defaults to false.
Step 1: Add an optional WorldBindingResolver to the gateway engine:
// In GatewayEngine or GatewayState
pub binding_resolver: Option<Arc<WorldBindingResolver>>,Step 2: In handle_announce(), when binding_resolver is available, resolve the binding record and compare fields:
if let Some(ref resolver) = state.binding_resolver {
match resolver.resolve(&announcement.world_id).await {
Ok(binding) => {
// Verify fields match binding record
let mismatches = verify_announcement_binding(&announcement, &binding);
if !mismatches.is_empty() {
return Err(GatewayError::BindingMismatch {
world_id: announcement.world_id.clone(),
mismatches,
});
}
}
Err(e) => {
// If binding resolution fails, accept with warning (graceful degradation)
tracing::warn!(world_id = %announcement.world_id, "binding resolution failed: {e}");
}
}
}Step 3: Implement verify_announcement_binding():
fn verify_announcement_binding(
announcement: &WorldAnnouncement,
binding: &WorldBindingRecord,
) -> Vec<String> {
let mut mismatches = Vec::new();
if announcement.world_id != binding.world_id {
mismatches.push(format!("worldId: '{}' != '{}'", announcement.world_id, binding.world_id));
}
// Check operator_subject_id if both present
if let (Some(ref ann_op), Some(ref bind_op)) = (&announcement.operator_subject_id, &binding.operator_subject_id) {
if ann_op != bind_op {
mismatches.push(format!("operator: '{}' != '{}'", ann_op, bind_op));
}
}
// Check signing key matches binding's ed25519 key
// (already verified by signature, but explicit check)
mismatches
}Files: src/gateway/handlers.rs, src/gateway/engine.rs
AC:
- Gateway resolves World Binding Record on announcement (when resolver available)
- worldId mismatch → announcement rejected with
BindingMismatch - operator subjectId mismatch → rejected
- Binding resolution failure → graceful degradation (accept with warning)
- No resolver configured → existing behavior preserved (backward compatible)
- Test: matching announcement → accepted
- Test: worldId mismatch → rejected
- Test: no resolver → accepted (no binding check)
Goal: Implement the Benchmark World specified in §9 §2, enabling cold-start agents to build initial reputation through standardized coding challenges.
Spec: §9 §2 — "Standardized coding benchmark for single-agent protocol testing. 5 deterministic challenges."; §3 §6 — "Only world-attestation receipts. No payment-derived weighting. Single-agent interactions."
Current State: Benchmark scoring rules are implemented in src/trust/scoring.rs:50 (BENCHMARK_CATEGORY), with special handling for world-attestation receipts, value_weight=1.0, participantCount=1. But no actual Benchmark World template exists in src/worlds/.
Step 1: Create src/worlds/benchmark/ module with runtime.rs and engine.rs:
// src/worlds/benchmark/runtime.rs
pub struct BenchmarkWorld {
manifest: WorldManifest,
challenges: Vec<Challenge>,
sessions: HashMap<String, BenchmarkSession>,
config: BenchmarkConfig,
}
pub struct BenchmarkConfig {
pub challenges: Vec<ChallengeSpec>,
}
pub struct ChallengeSpec {
pub id: String, // "fizzbuzz", "binary-search", etc.
pub difficulty: String, // "easy", "medium", "hard"
pub time_limit_secs: u64, // per-challenge timeout
}
pub struct BenchmarkSession {
pub agent_awn_id: String,
pub started_at: Instant,
pub submissions: HashMap<String, Submission>, // challenge_id → Submission
pub completed: bool,
}
pub struct Submission {
pub solution: String,
pub language: String,
pub submitted_at: Instant,
pub score: u32, // 0-100
}Step 2: Implement WorldRuntime for BenchmarkWorld:
impl WorldRuntime for BenchmarkWorld {
fn manifest(&self) -> &WorldManifest { &self.manifest }
fn on_join(&mut self, agent: &AgentSlot) -> Result<JoinResponse, WorldError> {
// Single-agent: one session per agent
let session = BenchmarkSession::new(agent.awn_id.clone());
self.sessions.insert(agent.awn_id.clone(), session);
Ok(JoinResponse {
slot_index: 0,
session_id: format!("ses_{}", uuid::Uuid::now_v7()),
initial_state: self.challenge_list_json(),
})
}
fn on_action(&mut self, agent: &AgentSlot, action: &Action) -> Result<ActionResult, WorldError> {
match action.name.as_str() {
"submit-solution" => self.handle_submit(agent, &action.params),
_ => Err(WorldError::InvalidAction(format!("unknown action: {}", action.name))),
}
}
fn on_leave(&mut self, agent: &AgentSlot) -> Result<LeaveResponse, WorldError> {
let session = self.sessions.remove(&agent.awn_id)
.ok_or(WorldError::InvalidState("not joined".into()))?;
// Generate world-attestation receipt with aggregate score
let total_score = self.compute_aggregate_score(&session);
let receipt = ReceiptDraft {
receipt_type: ReceiptType::WorldAttestation,
category: "benchmark".to_string(),
outcome_result: if total_score >= 60 { OutcomeResult::Success } else { OutcomeResult::Partial },
dimensions: Some({
let mut d = HashMap::new();
d.insert("quality".to_string(), total_score);
d
}),
participant_count: 1,
task_id: Some("benchmark-session".to_string()),
settlement: None,
};
Ok(LeaveResponse { receipts: vec![receipt] })
}
fn state_for(&self, agent: &AgentSlot) -> Result<serde_json::Value, WorldError> {
// Return challenge list + submission status
// ...
}
fn tick(&mut self, now: Instant) -> Vec<WorldEvent> {
// Check for timed-out challenges
// ...
vec![]
}
}Step 3: Implement handle_submit() — deterministic scoring for 5 challenges:
The spec defines 5 challenges: fizzbuzz, binary-search, json-parser, rate-limiter, merkle-tree. For MVP, scoring is structural validation (not code execution):
fn handle_submit(&mut self, agent: &AgentSlot, params: &serde_json::Value) -> Result<ActionResult, WorldError> {
let challenge_id = params.get("challengeId").and_then(|v| v.as_str())
.ok_or(WorldError::InvalidParams("challengeId required".into()))?;
let solution = params.get("solution").and_then(|v| v.as_str())
.ok_or(WorldError::InvalidParams("solution required".into()))?;
let language = params.get("language").and_then(|v| v.as_str())
.ok_or(WorldError::InvalidParams("language required".into()))?;
// Validate challenge exists
let challenge = self.challenges.iter().find(|c| c.id == challenge_id)
.ok_or(WorldError::InvalidParams(format!("unknown challenge: {challenge_id}")))?;
let session = self.sessions.get_mut(&agent.awn_id)
.ok_or(WorldError::InvalidState("not joined".into()))?;
// Check time limit
let elapsed = session.started_at.elapsed();
if elapsed > Duration::from_secs(challenge.time_limit_secs) {
return Err(WorldError::InvalidState("challenge time expired".into()));
}
// Score: structural validation (presence of key patterns)
let score = score_submission(challenge_id, solution, language);
session.submissions.insert(challenge_id.to_string(), Submission {
solution: solution.to_string(),
language: language.to_string(),
submitted_at: Instant::now(),
score,
});
Ok(ActionResult {
accepted: true,
seq: 0, // filled by endpoint layer
events: vec![WorldEvent {
event_type: "benchmark.scored".to_string(),
data: serde_json::json!({
"challengeId": challenge_id,
"score": score,
"difficulty": challenge.difficulty,
}),
visibility: EventVisibility::Private(agent.awn_id.clone()),
}],
state_patch: None,
})
}Step 4: Implement score_submission() — deterministic structural checks:
fn score_submission(challenge_id: &str, solution: &str, _language: &str) -> u32 {
match challenge_id {
"fizzbuzz" => score_fizzbuzz(solution),
"binary-search" => score_binary_search(solution),
"json-parser" => score_json_parser(solution),
"rate-limiter" => score_rate_limiter(solution),
"merkle-tree" => score_merkle_tree(solution),
_ => 0,
}
}
// Each scorer checks for structural correctness patterns:
// - fizzbuzz: checks for modulo operations, conditional branching, "fizz"/"buzz" strings
// - binary-search: checks for midpoint calculation, comparison, array narrowing
// - json-parser: checks for recursive descent patterns, string/number/object handling
// - rate-limiter: checks for token bucket or sliding window patterns
// - merkle-tree: checks for hashing, tree construction, proof verification patterns
//
// Score range: 0-100 per challenge
// Aggregate: weighted average (easy=15%, medium=25%, hard=35%)Step 5: Register in WorldFactory:
// src/daemon/factory.rs
"benchmark" | "code-bench-v1" => Ok(Box::new(BenchmarkWorld::new(config))),Step 6: Add module exports:
// src/worlds/mod.rs
pub mod benchmark;Files: src/worlds/benchmark/runtime.rs (new, ~250 LoC), src/worlds/benchmark/engine.rs (new, ~150 LoC), src/worlds/benchmark/mod.rs (new), src/worlds/mod.rs, src/daemon/factory.rs
AC:
-
BenchmarkWorldimplementsWorldRuntimewith 5 challenges per §9 §2 - Challenges: fizzbuzz (easy), binary-search (easy), json-parser (medium), rate-limiter (medium), merkle-tree (hard)
- Single-agent sessions (
participantCount: 1) - Generates
world-attestationreceipts with quality score 0-100 - Category is
"benchmark"(hooks into existing scoring special case) - Per-challenge time limits enforced
- Registered in
WorldFactoryas"benchmark"/"code-bench-v1" - Test: join → submit all 5 → leave → receipts generated with correct scores
- Test: submit after time limit → error
- Test: unknown challenge → InvalidParams error
- Test: aggregate score ≥80% on 5 challenges → ~0.78 score per §3 §6
| File | Change Type | Description |
|---|---|---|
src/worlds/benchmark/mod.rs |
New | ~5 LoC — module exports |
src/worlds/benchmark/runtime.rs |
New | ~250 LoC — BenchmarkWorld implementing WorldRuntime |
src/worlds/benchmark/engine.rs |
New | ~150 LoC — structural scoring for 5 challenges |
src/worlds/mod.rs |
Modified | ~1 LoC — pub mod benchmark; |
src/daemon/factory.rs |
Modified | ~5 LoC — register benchmark slug |
src/world_api/endpoints.rs |
Modified | ~60 LoC — wallet tracker + maxAgentsPerWallet enforcement |
src/trust/disclosure.rs |
Modified | ~40 LoC — Merkle proof verification in disclosure |
src/gateway/handlers.rs |
Modified | ~50 LoC — binding verification in announce handler |
src/gateway/engine.rs |
Modified | ~10 LoC — optional binding resolver |
Total: ~650 LoC net new across 9 files
| Section | Before | After | Delta | What Changed |
|---|---|---|---|---|
| §03 Contextual Scoring | 90% | 95% | +5% | Merkle proof verification in disclosure |
| §04 World Trust | 82% | 88% | +6% | maxAgentsPerWallet enforcement |
| §05 Gateway | 75% | 82% | +7% | Announcement binding verification |
| §07 Security | 80% | 86% | +6% | Anti-Sybil enforcement |
| §09 Implementation Plan | 60% | 75% | +15% | Benchmark World template |
| §11 Platform Architecture | 72% | 78% | +6% | 6th world template |
| Overall | 79% | ~84% | +5% |
| Issue | Status After This PR | What Remains |
|---|---|---|
| #20 (Durable receipt outbox) | 80% (up from 75%) | Full receipt state machine (Anchored/Replicated states) |
| #21 (Pluggable DiscoveryBackend) | 30% started | Gateway binding resolver is first step; full trait extraction in future PR |
This PR depends on compound/daemon-local-runtime-foundation (post PR #37 merge).
US-001 (maxAgentsPerWallet) ─── independent
US-002 (Merkle proof verification) ─── independent
US-003 (Gateway binding verification) ─── independent
US-004 (Benchmark World) ─── independent
All 4 stories are independent. Recommended order:
US-001 first (small, high-impact anti-Sybil)
→ US-002 (small, strengthens trust chain)
→ US-003 (small, gateway hardening)
→ US-004 last (largest, new world template)
| Test | Story | Validates |
|---|---|---|
wallet_limit_enforced |
US-001 | 2 agents same wallet, limit=1 → second rejected |
wallet_limit_different_wallets |
US-001 | 2 agents different wallets, limit=1 → both admitted |
wallet_limit_none |
US-001 | No limit → unlimited agents |
wallet_cleanup_on_leave |
US-001 | After leave, wallet slot freed |
extract_wallet_from_subject_id |
US-001 | CAIP-10 parsing correctness |
merkle_proof_verified_in_disclosure |
US-002 | Valid proof → no warning |
merkle_proof_tampered |
US-002 | Bad proof → MERKLE_PROOF_INVALID warning |
disclosure_no_proofs |
US-002 | No proofs → passes (backward compat) |
binding_match_accepted |
US-003 | Matching announcement → accepted |
binding_mismatch_rejected |
US-003 | worldId mismatch → rejected |
binding_no_resolver |
US-003 | No resolver → accepted |
benchmark_join_leave |
US-004 | Join → leave → receipt generated |
benchmark_submit_scored |
US-004 | Submit solution → score returned |
benchmark_time_limit |
US-004 | Expired challenge → error |
benchmark_unknown_challenge |
US-004 | Bad challengeId → InvalidParams |
benchmark_aggregate_score |
US-004 | 5 perfect submissions → high aggregate |
benchmark_receipt_type |
US-004 | Receipt type is WorldAttestation |
benchmark_category |
US-004 | Category is "benchmark" |
| Test | Story | Validates |
|---|---|---|
benchmark_e2e_trust_loop |
US-004 | Join → solve 5 → leave → receipt → score computed correctly |
benchmark_scoring_special_case |
US-004 | Benchmark receipts use value_weight=1.0, multi_agent_weight=1.0 |
sybil_prevention_e2e |
US-001 | Multi-wallet scenario with maxAgentsPerWallet enforcement |
| Feature | Why | Where |
|---|---|---|
| Actual code execution for benchmark | Requires sandboxed runtime; structural scoring sufficient for MVP | Future PR |
| On-chain Merkle root verification | Requires blockchain integration; local proofs sufficient for now | Future PR |
| Full DiscoveryBackend trait extraction | Separate architectural concern | Issue #21, future PR |
| Anchored/Replicated receipt states | Requires on-chain integration | Issue #20, future PR |
| ZK reputation proofs | Phase 2+ per spec | Future |
| Benchmark challenge addition/versioning | Standard 5 challenges for MVP | Future |
| Risk | Level | Mitigation |
|---|---|---|
| CAIP-10 subject ID format variations | Low | Use prefix-based extraction; test with multiple formats |
| Merkle proof verification slows admission | Low | Proofs are small (log₂ tree); verification is O(log n) |
| Benchmark structural scoring too lenient/strict | Medium | Start with basic pattern matching; calibrate with real agent submissions |
| Binding resolver adds latency to gateway announce | Low | Cache binding records (already LRU-cached in WorldBindingResolver) |
| Wallet tracker memory growth | Low | Cleaned up on leave; bounded by maxAgents per world |
- US-001: Check
EndpointStatefields carefully — useRwLock<HashMap<String, HashSet<String>>>for the wallet tracker. Clean up in bothleave_handler()ANDevict_agent(). - US-002: The
verify_merkle_proof()function insrc/trust/merkle.rsalready exists and is tested. Just wire it intoverify_disclosure_receipts(). Ensure you compute the leaf hash the same way the anchor batch does (JCS + SHA256). - US-003: The
WorldBindingResolverinsrc/world_api/world_binding.rsalready has resolve + cache logic. Share it viaArcinto the gateway state. - US-004: Keep the Benchmark World simple. No code execution — just structural pattern matching on submitted solutions. The scoring doesn't need to be perfect for MVP; it needs to generate valid
world-attestationreceipts with reasonable quality scores. - Run
cargo fmtandcargo clippybefore every commit. CI is strict. - Keep
#[serde(rename_all = "camelCase")]on any new structs that touch JSON. - Test the Benchmark World against the existing scoring engine — ensure that benchmark receipts trigger the special case path in
scoring.rs(category == "benchmark", value_weight=1.0, participantCount=1).
After this PR, the main remaining gaps are:
| Gap | Spec Section | Priority | Estimated PR |
|---|---|---|---|
| Anchored/Replicated receipt states | §2 §5 | P1 | PR #39 |
| Full DiscoveryBackend trait | §5 §4 | P1 | PR #39 or #40 |
| Dispute resolution Tier 2/3 | §8 §4 | P2 | Future |
| On-chain Merkle root anchoring (Base L2) | §2 §6, §5 | P2 | Future |
| ZK reputation proofs | §7 §5 | P3 | Phase 2+ |
| Payment pool settlement (smart contract) | §6 §3 | P2 | Future |
| System service install (launchd/systemd) | §11 | P3 | Future |