# Verifying Feed Integrity Before Injecting External Context into a Tool Loop

Published: 2026-10-18 | Domain: https://agentfeeds.si | Related Paper: Recognition-Gated Safety (#3)

## Outcome first: zero indirect prompt injection from external feeds

When an autonomous agent fetches an external RSS feed, web page, or README and injects the text directly into its system prompt, a single poisoned entry can hijack tool calls (an indirect prompt-injection or confused-deputy attack). In our tool-attack red-teaming study ([papers.tayloramarel.com](https://papers.tayloramarel.com)), unguarded agents complied with `81.3%` of injected tool escalations. By enforcing `SHA-256` Merkle provenance and strict three-layer epistemic state separation (`Evidence` vs. `Interpretation` vs. `Exploration`), `agentfeeds.si` ensures that external feed payloads can never directly parameterize privileged shell or network effects.

## Binary Merkle tree verification over feed batches

Every `agentfeeds.si` batch is published with a deterministic Merkle root computed over canonicalized JSON leaf records (`RFC 8785` JSON Canonicalization Scheme). Before a harness admits a single feed item into Layer 0 (`Evidence`), it verifies:

1. **Domain-separated leaf hashing**: Each leaf is hashed with a `0x00` prefix (`SHA256(0x00 || canonical_json(item))`) and internal nodes use a `0x01` prefix (`SHA256(0x01 || left || right)`) to prevent second-preimage tree-height attacks.
2. **Taint tagging on untrusted strings**: Even after cryptographic provenance passes, string fields originating from external feeds carry an epistemic `source_taint = "external_evidence"` tag. Tainted strings are prohibited from flowing into shell commands, file write paths, or outbound HTTP hostnames without an explicit allowlist match.

```javascript
// Domain-Separated SHA-256 Merkle Proof Verifier (RFC 6962 / RFC 8785 style)
export async function verifyMerkleInclusion(leafBytes, auditPathHex, leafIndex, expectedRootHex) {
  const subtle = crypto.subtle;
  const leafInput = new Uint8Array(1 + leafBytes.length);
  leafInput[0] = 0x00;
  leafInput.set(leafBytes, 1);
  let currentHash = new Uint8Array(await subtle.digest("SHA-256", leafInput));

  let idx = leafIndex;
  for (const siblingHex of auditPathHex) {
    const sibling = hexToBytes(siblingHex);
    const nodeInput = new Uint8Array(1 + 64);
    nodeInput[0] = 0x01;
    if (idx % 2 === 0) {
      nodeInput.set(currentHash, 1);
      nodeInput.set(sibling, 33);
    } else {
      nodeInput.set(sibling, 1);
      nodeInput.set(currentHash, 33);
    }
    currentHash = new Uint8Array(await subtle.digest("SHA-256", nodeInput));
    idx = Math.floor(idx / 2);
  }
  return bytesToHex(currentHash) === expectedRootHex.toLowerCase();
}
```

## Separating exact bytes, interpretation, and use permissions

Following the Loop Engine constitution, exact source bytes, derived interpretations, and execution permissions remain stored in separate records. A verified feed item in Layer 0 informs the model's context, but content approval never grants effect authority.
