<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: willamhou</title>
    <description>The latest articles on Forem by willamhou (@willamhou).</description>
    <link>https://forem.com/willamhou</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3856634%2F4545fd93-d2d7-46b4-a778-faf990fee34f.jpg</url>
      <title>Forem: willamhou</title>
      <link>https://forem.com/willamhou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/willamhou"/>
    <language>en</language>
    <item>
      <title>How to Add Tamper-Evident Audit Trails to Your OpenClaw Assistant</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Tue, 12 May 2026 06:17:29 +0000</pubDate>
      <link>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-openclaw-assistant-532</link>
      <guid>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-openclaw-assistant-532</guid>
      <description>&lt;p&gt;Your OpenClaw assistant just deleted a file, sent an email, or ran a shell command on your machine. Can you &lt;em&gt;prove&lt;/em&gt; what it did? When? Authorized by whom?&lt;/p&gt;

&lt;p&gt;Standard log files don't answer that. They can be edited. They can be rotated. They can be deleted. After an incident, "the agent did X" is your word against the runtime that produced the log.&lt;/p&gt;

&lt;p&gt;This post walks through adding cryptographic audit trails to OpenClaw using &lt;a href="https://www.npmjs.com/package/@signet-auth/openclaw-plugin" rel="noopener noreferrer"&gt;&lt;code&gt;@signet-auth/openclaw-plugin&lt;/code&gt;&lt;/a&gt;. Every tool call gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Ed25519 signature over the canonical action payload (RFC 8785 JCS → SHA-256 → Ed25519)&lt;/li&gt;
&lt;li&gt;A hash-chained entry in &lt;code&gt;~/.signet/audit/*.jsonl&lt;/code&gt; — deletion or reordering breaks the chain&lt;/li&gt;
&lt;li&gt;Optional policy enforcement (deny dangerous tools before they run)&lt;/li&gt;
&lt;li&gt;Optional encryption of tool params at rest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total setup time: under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenClaw (&lt;code&gt;&amp;gt;=2026.3.24-beta.2&lt;/code&gt;) — the gateway you already run&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;signet&lt;/code&gt; CLI on &lt;code&gt;$PATH&lt;/code&gt;. Install via &lt;code&gt;cargo install signet-cli&lt;/code&gt;, or grab a &lt;a href="https://github.com/Prismer-AI/signet/releases" rel="noopener noreferrer"&gt;release binary&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Two minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a signing identity
&lt;/h2&gt;

&lt;p&gt;The plugin signs with a local Ed25519 key. Generate one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signet identity generate &lt;span class="nt"&gt;--name&lt;/span&gt; openclaw-agent &lt;span class="nt"&gt;--owner&lt;/span&gt; you@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes the keypair to &lt;code&gt;~/.signet/identities/openclaw-agent/&lt;/code&gt;. The private key stays on disk; the public key is what verifiers (auditors, you, anyone) use to check signatures later.&lt;/p&gt;

&lt;p&gt;If you want the key passphrase-protected, add &lt;code&gt;--passphrase&lt;/code&gt;. We'll set the passphrase env var below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Install the plugin
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @signet-auth/openclaw-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenClaw checks ClawHub first, falls back to npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Configure
&lt;/h2&gt;

&lt;p&gt;Add the plugin entry to &lt;code&gt;~/.openclaw/config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"keyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw://gateway/local"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fields are enough to get started. Everything else has sensible defaults.&lt;/p&gt;

&lt;p&gt;If your key is passphrase-protected, export the passphrase before launching the gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SIGNET_PASSPHRASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'...'&lt;/span&gt;
openclaw start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Use OpenClaw normally
&lt;/h2&gt;

&lt;p&gt;Run any task that exercises tools — file operations, web search, shell commands, anything. Every tool call now produces a signed receipt before execution. Tool errors are logged. Nothing in your workflow changes.&lt;/p&gt;

&lt;p&gt;While OpenClaw runs, watch the gateway log for lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[signet] signed file_read (call=tc_abc123 session=ses_xyz789) → rec_a1b2c3d4...
[signet] signed shell_exec (call=tc_def456 session=ses_xyz789) → rec_e5f6a7b8...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;rec_...&lt;/code&gt; is a receipt id derived from the signature itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify the audit trail
&lt;/h2&gt;

&lt;p&gt;After OpenClaw has done some work, verify the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signet audit &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash chain integrity:    valid (records=42)
Signature verification:  42 / 42 valid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the cryptographic guarantee: 42 receipts, every one of them signed by the key whose public half lives in &lt;code&gt;~/.signet/identities/openclaw-agent/openclaw-agent.pub.json&lt;/code&gt;. Modify any field of any receipt and the verification fails. Delete a record and the chain breaks.&lt;/p&gt;

&lt;p&gt;To browse what your assistant actually did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signet explore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press a tool name to see the full action payload, params, timestamp, and the run id that ties together the &lt;code&gt;before_tool_call&lt;/code&gt; and &lt;code&gt;after_tool_call&lt;/code&gt; events.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a receipt looks like
&lt;/h2&gt;

&lt;p&gt;Each line of &lt;code&gt;~/.signet/audit/audit.jsonl&lt;/code&gt; is one record. The signed receipt inside looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rec_a1b2c3d4e5f6a7b8..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell_exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git status"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw://gateway/local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed25519:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-26T14:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rnd_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed25519:..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature covers the entire payload. Modify &lt;code&gt;action.params.command&lt;/code&gt; from &lt;code&gt;"git status"&lt;/code&gt; to &lt;code&gt;"rm -rf /"&lt;/code&gt; and the signature stops verifying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding policy enforcement
&lt;/h2&gt;

&lt;p&gt;Audit-after-the-fact is good. Blocking dangerous calls before they run is better.&lt;/p&gt;

&lt;p&gt;Create a policy at &lt;code&gt;~/.signet/policies/openclaw.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openclaw-safe&lt;/span&gt;
&lt;span class="na"&gt;default_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;

&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny-rm-rf&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shell_exec&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-rf"&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destructive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;never&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;without&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;approval"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate-limit-network&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;one_of&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;http_request&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;fetch_url&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate_limit&lt;/span&gt;
    &lt;span class="na"&gt;rate_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;window_secs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
      &lt;span class="na"&gt;max_calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into the plugin config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"keyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw://gateway/local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.signet/policies/openclaw.yaml"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart OpenClaw. When the policy denies a call, you'll see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[signet] policy denied shell_exec: destructive command — never run without human approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenClaw skips the tool call entirely. The denial itself isn't signed — that's by design. Only allowed actions produce receipts. The denial is logged at &lt;code&gt;warn&lt;/code&gt; level so it's still observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without Signet&lt;/th&gt;
&lt;th&gt;With Signet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"OpenClaw ran shell_exec" — log entry, editable&lt;/td&gt;
&lt;td&gt;Ed25519 signature proving exactly what command, when, by which key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering can be falsified&lt;/td&gt;
&lt;td&gt;Hash chain breaks if any entry is removed or reordered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust your local logs&lt;/td&gt;
&lt;td&gt;Verify offline with just the public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No regulatory mapping&lt;/td&gt;
&lt;td&gt;Maps to EU AI Act Article 12 "automatic event logging" requirement (effective August 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Verifying as an external auditor
&lt;/h2&gt;

&lt;p&gt;If someone else needs to verify your audit log — a security team, a regulator, you on a different machine — they only need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The audit log file (&lt;code&gt;~/.signet/audit/audit.jsonl&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The agent's public key (&lt;code&gt;~/.signet/identities/openclaw-agent/openclaw-agent.pub.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No private key. No access to the OpenClaw runtime. They run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signet audit &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="nt"&gt;--keys-dir&lt;/span&gt; ./received-keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the chain is intact and every signature checks out, the audit log is authentic. If anyone tampered with anything, verification fails at the modified record.&lt;/p&gt;

&lt;p&gt;This is the property that "we keep good logs" can never give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The plugin is open source (&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Apache-2.0 OR MIT&lt;/a&gt;). If you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bilateral co-signing&lt;/strong&gt;: server-side keys signing alongside the agent for two-party non-repudiation. Already in Signet core; can be wired into OpenClaw via a follow-up plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust bundles&lt;/strong&gt;: pin a published bundle of trusted public keys so verifiers don't need to track keys out-of-band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted params&lt;/strong&gt;: set &lt;code&gt;encryptParams: true&lt;/code&gt; in the plugin config to wrap &lt;code&gt;action.params&lt;/code&gt; in an XChaCha20-Poly1305 envelope keyed off the signing key. The signature chain stays verifiable; only key holders see the params.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;https://github.com/Prismer-AI/signet&lt;/a&gt;&lt;br&gt;
Plugin: &lt;a href="https://github.com/Prismer-AI/signet/tree/main/packages/signet-openclaw-plugin" rel="noopener noreferrer"&gt;https://github.com/Prismer-AI/signet/tree/main/packages/signet-openclaw-plugin&lt;/a&gt;&lt;br&gt;
Issues: &lt;a href="https://github.com/Prismer-AI/signet/issues" rel="noopener noreferrer"&gt;https://github.com/Prismer-AI/signet/issues&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build something with this, I'd love to know. The OpenClaw maintainers in particular — feedback on the hook integration and security audit collector behavior would be valuable.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>security</category>
      <category>audit</category>
    </item>
    <item>
      <title>5 things missing from your AI agent audit logs (and how we fixed them in Signet v0.10)</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Thu, 07 May 2026 09:39:02 +0000</pubDate>
      <link>https://forem.com/willamhou/5-things-missing-from-your-ai-agent-audit-logs-and-how-we-fixed-them-in-signet-v010-5gi0</link>
      <guid>https://forem.com/willamhou/5-things-missing-from-your-ai-agent-audit-logs-and-how-we-fixed-them-in-signet-v010-5gi0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — If your AI agent audit log only signs the &lt;em&gt;intent&lt;/em&gt; (tool name + args), you're shipping demo-ware. Real audit needs 5 things most projects skip: outcome binding inside the signature scope, durable nonce stores, persistent server identity, portable forensic bundles, and encrypted-but-verifiable payloads. v0.10 of &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; ships all five today.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The gap between "demo passes" and "compliance team approves"
&lt;/h2&gt;

&lt;p&gt;I've spent the last few months building &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; — an Ed25519-signed cryptographic audit SDK for AI agents. Along the way I noticed a pattern across every "let's add cryptographic audit to AI agents" project I looked at:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a keypair.&lt;/li&gt;
&lt;li&gt;Agent calls a tool.&lt;/li&gt;
&lt;li&gt;Sign &lt;code&gt;{tool, params}&lt;/code&gt; with Ed25519.&lt;/li&gt;
&lt;li&gt;Stuff the signature into the audit log.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; passes. Demo done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a working prototype. It is not what a compliance team can sign off on.&lt;/p&gt;

&lt;p&gt;Below are the five things v0.9 of Signet did &lt;em&gt;not&lt;/em&gt; have, that v0.10 (shipped today) does. If you're building anything in this space, I think you have to answer these before claiming production-ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Outcome binding — sign what &lt;em&gt;happened&lt;/em&gt;, not just what was &lt;em&gt;requested&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Most agent receipts sign the request: &lt;code&gt;{tool: "delete_user", params: {id: 42}}&lt;/code&gt;. The receipt becomes a record that the agent &lt;em&gt;intended&lt;/em&gt; to call that tool.&lt;/p&gt;

&lt;p&gt;But here's the audit question that actually matters: &lt;strong&gt;what did the server actually do?&lt;/strong&gt; Did it succeed? Did it reject the call (policy violation)? Did it crash mid-flight? An intent-only receipt can't tell you.&lt;/p&gt;

&lt;p&gt;In v0.10, Signet's bilateral receipt (v3) embeds an &lt;code&gt;Outcome&lt;/code&gt; field with one of four states — &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;rejected&lt;/code&gt;, &lt;code&gt;executed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt; — &lt;em&gt;inside the signature scope&lt;/em&gt;. If a server claims it logged "executed" but the agent observed "failed", the receipt's signature is invalid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rust: signing a bilateral receipt with outcome&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign_bilateral_with_outcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;server_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;agent_receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Outcome&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;OutcomeStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Executed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The status field is now under the signature.&lt;/span&gt;
&lt;span class="c1"&gt;// Tampering with it after the fact breaks verification.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python — same shape
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign_bilateral_with_outcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent_receipt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is small in code but big in semantics. The receipt becomes a record of &lt;em&gt;what actually happened&lt;/em&gt;, not just &lt;em&gt;what was requested&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Durable nonce store — replay protection that survives a restart
&lt;/h2&gt;

&lt;p&gt;Replay attacks against signed receipts are well-known. The standard mitigation: track nonces, reject any receipt whose nonce you've seen before.&lt;/p&gt;

&lt;p&gt;The catch: most implementations use an in-memory hash set. Process restarts? Set is empty. Replay protection: gone for the duration of the warm-up window.&lt;/p&gt;

&lt;p&gt;v0.10 ships &lt;code&gt;FileNonceChecker&lt;/code&gt; — a JSON-file-backed nonce store, single-host pilot grade. It survives process restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FileNonceChecker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/var/lib/signet/nonces.json"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;BilateralVerifyOptions&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// now includes replay check by default&lt;/span&gt;
    &lt;span class="nf"&gt;.with_nonce_checker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nf"&gt;verify_bilateral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;server_pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python equivalent
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_bilateral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;server_pubkey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nonce_store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/signet/nonces.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript — packages/signet-mcp-server FileNonceCache&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileNonceCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@signet-auth/mcp-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileNonceCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/lib/signet/nonces.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;verifyRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nonceCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single-host only — distributed nonce stores (Redis-backed, etc.) are post-1.0. But "single-host pilot" is what most teams need first, and v0.9 didn't even have that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Behavioral break to note:&lt;/strong&gt; &lt;code&gt;BilateralVerifyOptions::default()&lt;/code&gt; now enables in-memory replay protection by default. Use &lt;code&gt;BilateralVerifyOptions::insecure_no_replay_check()&lt;/code&gt; for forensic replay flows where nonce reuse is expected.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Persistent server identity — your trust bundle finally has something to anchor
&lt;/h2&gt;

&lt;p&gt;Bilateral signing means both the agent &lt;em&gt;and&lt;/em&gt; the server sign the receipt. The server's public key needs to be stable — otherwise nothing can pin it. Trust bundles, allowlists, audit replay — all break the moment the server pubkey rotates.&lt;/p&gt;

&lt;p&gt;In v0.9, the server keypair was ephemeral: generated on &lt;code&gt;signet proxy&lt;/code&gt; startup. New process, new pubkey. Compliance teams loved that.&lt;/p&gt;

&lt;p&gt;In v0.10:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate identities — agent and server must be different keys&lt;/span&gt;
signet identity generate &lt;span class="nt"&gt;--name&lt;/span&gt; agent-prod
signet identity generate &lt;span class="nt"&gt;--name&lt;/span&gt; openclaw-gateway

&lt;span class="c"&gt;# Use them persistently — same pubkeys across every restart&lt;/span&gt;
signet proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; ./my_mcp_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; agent-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-key&lt;/span&gt; openclaw-gateway

&lt;span class="c"&gt;# Compose the trust bundle by hand — CLI bundle creation is on the roadmap;&lt;/span&gt;
&lt;span class="c"&gt;# `signet trust` today exposes inspect / list / disable / revoke / rotate for&lt;/span&gt;
&lt;span class="c"&gt;# editing existing bundles. Full schema lives in docs/guides/team-deployment.md.&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; trust.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;
{
  "version": 1,
  "bundle_id": "pilot-2026-Q2",
  "org": "your-org",
  "env": "pilot",
  "generated_at": "2026-05-11T00:00:00Z",
  "agents": [{
    "id": "agent-prod-2026-05",
    "name": "agent-prod",
    "owner": "you",
    "pubkey": "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;signet identity &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; agent-prod&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;",
    "status": "active",
    "created_at": "2026-05-11T00:00:00Z"
  }],
  "servers": [{
    "id": "openclaw-gateway-2026-05",
    "name": "openclaw-gateway",
    "owner": "you",
    "pubkey": "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;signet identity &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; openclaw-gateway&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;",
    "status": "active",
    "created_at": "2026-05-11T00:00:00Z"
  }],
  "roots": []
}
&lt;/span&gt;&lt;span class="no"&gt;JSON

&lt;/span&gt;&lt;span class="c"&gt;# Sanity-check the bundle parses&lt;/span&gt;
signet trust inspect ./trust.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI also refuses to use the &lt;em&gt;same&lt;/em&gt; key for both agent and server roles — a common foot-gun that silently invalidates the entire bilateral protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Forensic bundle / restore — the artifact compliance teams actually want
&lt;/h2&gt;

&lt;p&gt;A compliance team's question is rarely "is this audit log valid right now on this machine?" It's usually:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Six months from now, on a different machine, with no access to your keystore, can you prove that this set of audit records was signed by [these specific keys] and that nothing was tampered with?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That requires the audit data + the keys + the hash chain proof + version metadata, packaged together, self-verifying.&lt;/p&gt;

&lt;p&gt;v0.10 ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Producer side — package up an evidence bundle&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--bundle&lt;/span&gt; ./evidence-2026-Q2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include-trust-bundle&lt;/span&gt; ./trust.json

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;#   evidence-2026-Q2/&lt;/span&gt;
&lt;span class="c"&gt;#   ├── records.jsonl       # the audit records&lt;/span&gt;
&lt;span class="c"&gt;#   ├── manifest.json       # version + chain root + signer pubkeys&lt;/span&gt;
&lt;span class="c"&gt;#   ├── hash-summary.txt    # human-readable chain summary&lt;/span&gt;
&lt;span class="c"&gt;#   └── trust-bundle.json   # (optional) signer key set&lt;/span&gt;

&lt;span class="c"&gt;# Verifier side — re-verify on ANY machine, no keystore required&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--restore&lt;/span&gt; ./evidence-2026-Q2
&lt;span class="c"&gt;# Verifies:&lt;/span&gt;
&lt;span class="c"&gt;#   - every receipt's signature&lt;/span&gt;
&lt;span class="c"&gt;#   - hash chain integrity&lt;/span&gt;
&lt;span class="c"&gt;#   - timestamps in expected window&lt;/span&gt;
&lt;span class="c"&gt;#   - trust bundle attestation chain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--restore&lt;/code&gt; flow is replay-tolerant by design: it uses &lt;code&gt;BilateralVerifyOptions::forensic()&lt;/code&gt; so re-verifying the same receipt twice doesn't fail nonce-replay checks. Forensic verification is a different mode from live verification, and v0.10 makes that explicit instead of confusing the two.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Encrypted audit envelope — verifiable AND confidential
&lt;/h2&gt;

&lt;p&gt;Audit logs that are cryptographically verifiable are great for forensics. Audit logs that contain customer data in plaintext are great for incident response. These two goods are usually in tension.&lt;/p&gt;

&lt;p&gt;v0.10 wraps the &lt;code&gt;params&lt;/code&gt; field of audit records in an XChaCha20-Poly1305 encrypted envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# When signing, opt the audit record into encrypted params&lt;/span&gt;
signet sign &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tool&lt;/span&gt; send_email &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--params&lt;/span&gt; &lt;span class="s1"&gt;'{"to": "alice@example.com"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; agent-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--encrypt-params&lt;/span&gt;

&lt;span class="c"&gt;# Forensic decrypt during export — uses the matching local identity from&lt;/span&gt;
&lt;span class="c"&gt;# the keystore (no separate "encryption-only" key flag today)&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--export&lt;/span&gt; ./audit-decoded.jsonl &lt;span class="nt"&gt;--decrypt-params&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature still covers the encrypted ciphertext — meaning verifiability is preserved without ever exposing the plaintext at rest. Anyone holding the audit log without the keystore sees only ciphertext; the &lt;code&gt;--decrypt-params&lt;/code&gt; export materialises plaintext into the export file, scoped to that one operation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;v0.10 limitation:&lt;/strong&gt; &lt;code&gt;--encrypt-params&lt;/code&gt; reuses the signing agent's identity to derive the envelope key, so today "decrypt access" and "sign access" share one key. A dedicated encryption-only identity is on the roadmap; if you need that separation today, please &lt;a href="https://github.com/Prismer-AI/signet/issues/new" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the thing your security team has been asking for since you turned audit on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it together — a pilot deployment
&lt;/h2&gt;

&lt;p&gt;Here's roughly what a single-host Signet pilot looks like with all 5 in play:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Generate identities (agent and server must be different keys)&lt;/span&gt;
signet identity generate &lt;span class="nt"&gt;--name&lt;/span&gt; agent-prod
signet identity generate &lt;span class="nt"&gt;--name&lt;/span&gt; openclaw-gateway   &lt;span class="c"&gt;# stable server key&lt;/span&gt;

&lt;span class="c"&gt;# 2. Compose the trust bundle JSON by hand&lt;/span&gt;
&lt;span class="c"&gt;#    (paste pubkeys from `signet identity export --name &amp;lt;NAME&amp;gt;` — see Section 3)&lt;/span&gt;
&lt;span class="nv"&gt;$EDITOR&lt;/span&gt; ./trust.json
signet trust inspect ./trust.json   &lt;span class="c"&gt;# verify it parses&lt;/span&gt;

&lt;span class="c"&gt;# 3. Run the proxy with persistent agent + server identities + a policy&lt;/span&gt;
signet proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; ./my_mcp_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; agent-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-key&lt;/span&gt; openclaw-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy&lt;/span&gt; /etc/signet/policy.yaml

&lt;span class="c"&gt;# 4. On the verifier side (same host or compliance host) — durable nonce store&lt;/span&gt;
&lt;span class="c"&gt;#    is a `signet verify` flag, not a proxy flag in v0.10&lt;/span&gt;
signet verify &amp;lt;receipt-path&amp;gt; &lt;span class="nt"&gt;--nonce-store&lt;/span&gt; /var/lib/signet/nonces.json

&lt;span class="c"&gt;# 5. Periodically bundle audit evidence for off-host handoff&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--bundle&lt;/span&gt; ./evidence-2026-05-14 &lt;span class="nt"&gt;--include-trust-bundle&lt;/span&gt; ./trust.json

&lt;span class="c"&gt;# 6. Compliance team can verify on a different machine&lt;/span&gt;
scp &lt;span class="nt"&gt;-r&lt;/span&gt; ./evidence-2026-05-14 compliance-host:
ssh compliance-host
signet audit &lt;span class="nt"&gt;--restore&lt;/span&gt; ./evidence-2026-05-14
&lt;span class="c"&gt;# ✓ all signatures valid&lt;/span&gt;
&lt;span class="c"&gt;# ✓ hash chain intact&lt;/span&gt;
&lt;span class="c"&gt;# ✓ trust bundle attestation chain valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full operator runbook: &lt;a href="https://github.com/Prismer-AI/signet/blob/main/docs/guides/team-deployment.md" rel="noopener noreferrer"&gt;docs/guides/team-deployment.md&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's not in v0.10 (and where the work is going)
&lt;/h2&gt;

&lt;p&gt;Honest disclaimers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-host only.&lt;/strong&gt; Distributed nonce stores (Redis, FoundationDB-backed) are post-1.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No managed key rotation.&lt;/strong&gt; You can rotate manually with new identities and bundle merges, but there's no operator-friendly flow yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi-tenant isolation.&lt;/strong&gt; Each pilot is a single-tenant deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BilateralVerifyOptions::default()&lt;/code&gt; is a behavioral break.&lt;/strong&gt; It now defaults to in-memory replay protection. If you were calling &lt;code&gt;verify_bilateral()&lt;/code&gt; repeatedly on the same receipt for forensic replay, you need &lt;code&gt;insecure_no_replay_check()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw plugin is fail-closed by default.&lt;/strong&gt; That means a misconfigured Signet will block all tool calls, not silently allow them. This is the right default but trips people up — see the &lt;a href="https://github.com/Prismer-AI/signet/blob/main/docs/guides/team-deployment.md" rel="noopener noreferrer"&gt;pilot runbook&lt;/a&gt; for the readiness signal flow.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Python&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth&lt;span class="o"&gt;==&lt;/span&gt;0.10.0

&lt;span class="c"&gt;# Rust&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;signet-cli

&lt;span class="c"&gt;# OpenClaw gateway plugin&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @signet-auth/openclaw-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;github.com/Prismer-AI/signet&lt;/a&gt;&lt;br&gt;
v0.10 release notes: &lt;a href="https://github.com/Prismer-AI/signet/releases/tag/v0.10.0" rel="noopener noreferrer"&gt;v0.10.0 release&lt;/a&gt;&lt;br&gt;
Compliance mapping (SOC 2 / ISO 27001 / EU AI Act / NIST AI RMF): &lt;a href="https://github.com/Prismer-AI/signet/blob/main/docs/COMPLIANCE.md" rel="noopener noreferrer"&gt;COMPLIANCE.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your AI agent audit log is missing any of these five things, I'd love to hear which one bites first in your environment. The fastest way to shape v0.11 is to tell me what doesn't work for you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>compliance</category>
    </item>
    <item>
      <title>NIST NCCoE AI Agent Identity &amp; Authorization: What Developers Need to Build</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Sat, 02 May 2026 01:00:00 +0000</pubDate>
      <link>https://forem.com/willamhou/nist-nccoe-ai-agent-identity-authorization-what-developers-need-to-build-1kp1</link>
      <guid>https://forem.com/willamhou/nist-nccoe-ai-agent-identity-authorization-what-developers-need-to-build-1kp1</guid>
      <description>&lt;p&gt;Your agent can send an email, place an order, or merge a PR. If an auditor asks "prove it," what artifact do you hand them?&lt;/p&gt;

&lt;p&gt;Plaintext logs aren't an answer. They're editable, deletable, and reorderable by anyone who controls the runtime. NIST has been quiet about this gap until recently — but in early 2026 they started lining up the answer.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;February 5, 2026&lt;/strong&gt;, NIST NCCoE published a &lt;a href="https://www.nccoe.nist.gov/projects/software-and-ai-agent-identity-and-authorization" rel="noopener noreferrer"&gt;concept paper on AI agent identity and authorization&lt;/a&gt; surfacing four control areas any production agent deployment must address. Twelve days later, &lt;strong&gt;February 17, 2026&lt;/strong&gt;, NIST CAISI launched the &lt;a href="https://www.nist.gov/caisi/ai-agent-standards-initiative" rel="noopener noreferrer"&gt;AI Agent Standards Initiative&lt;/a&gt; — more deliverables coming, exact timelines still emerging.&lt;/p&gt;

&lt;p&gt;The concept paper is scoping work, not a prescriptive standard yet. But the four control areas are settled, and if you're building AI agents today, they tell you what you'll need to have working before NIST's normative output lands.&lt;/p&gt;

&lt;p&gt;This post walks through each area, what it actually requires, and where the implementation gaps are today. Python code throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four control areas
&lt;/h2&gt;

&lt;p&gt;The NCCoE concept paper surfaces four areas (the paper itself doesn't call them "pillars" — that's my framing for this post):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identification&lt;/strong&gt; — How are AI agents identified? Persistent vs task-specific identities, metadata for action scoping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication &amp;amp; Authorization&lt;/strong&gt; — OAuth 2.0 extensions, ABAC, policy-based access control for agents as distinct principals. Delegation is discussed &lt;em&gt;under&lt;/em&gt; authorization, not as a standalone area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Delegation&lt;/strong&gt; (sub-area of authorization) — Linking user identities to agents while preventing privilege escalation through delegation chains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditing &amp;amp; Non-repudiation&lt;/strong&gt; — "Mechanisms by which specific AI agent actions are attributed to their non-human entity for audit and forensic purposes."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth area is where most production deployments are weakest today. Most agents generate logs. Few generate evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 1: Identification
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What NCCoE asks:&lt;/strong&gt; A mechanism for issuing and resolving agent identities. Either persistent (the agent is a long-lived entity) or task-specific (a new identity per task).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "good" looks like:&lt;/strong&gt; Public-key cryptographic identity. Each agent has a keypair. The public key is the verifiable identifier. No central registry required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal working code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent identity: create once, reuse across tasks
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# X9kF2mN8pQ3...   (raw base64 Ed25519 public key, 44 chars)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Ed25519 public key &lt;em&gt;is&lt;/em&gt; the agent identifier. A verifier who receives any artifact signed by this key knows it came from this agent. No registry lookup, no external service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap to watch:&lt;/strong&gt; NCCoE mentions SPIFFE/SPIRE for workload identity. If you're in a Kubernetes environment, the integration point is real — SPIRE issues short-lived SVIDs that can back your agent identity instead of long-lived local keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 2: Authorization
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What NCCoE asks:&lt;/strong&gt; OAuth 2.0 extensions, ABAC, or policy-based access control. The agent is a distinct principal, not a user. Decisions about what the agent can do happen at policy evaluation time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "good" looks like:&lt;/strong&gt; The policy decision is evaluated &lt;em&gt;before&lt;/em&gt; the action executes, and the decision is captured cryptographically so an auditor can later verify that the policy ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal working code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parse_policy_yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sign_with_policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load_signing_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_signet_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Define policy in YAML. Rules use `id:` and numeric comparisons use operator
# objects (e.g. {gt: 1000}), not string expressions.
&lt;/span&gt;&lt;span class="n"&gt;policy_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
version: 1
name: procurement-safe
default_action: deny
rules:
  - id: allow-search
    match:
      tool: web_search
    action: allow
  - id: require-approval-over-threshold
    match:
      tool: place_order
      params:
        amount: {gt: 1000}
    action: require_approval
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Canonical JSON policy is what gets hashed into the attestation.
&lt;/span&gt;&lt;span class="n"&gt;policy_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_policy_yaml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy_yaml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;action_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;laptop prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# The low-level binding takes string args and returns (receipt_json, eval_json).
# It reads the signing key off disk; pass the raw key bytes yourself so the
# signing agent's in-memory key handle is not re-exported.
&lt;/span&gt;&lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_signing_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;default_signet_dir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;receipt_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign_with_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;action_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# receipt.policy contains the attestation: which policy hash, which rule id,
# which decision. All inside the Ed25519 signature scope.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI-equivalent one-liner is &lt;code&gt;signet sign --key procurement-bot-01 --tool web_search --params '{"query":"laptop prices"}' --policy policy.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this gives you:&lt;/strong&gt; The policy version (hashed), the matched rule, and the decision are co-signed with the action. A verifier can confirm "the policy evaluated &lt;code&gt;allow-search&lt;/code&gt; for this exact call" without trusting the runtime that produced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap to watch:&lt;/strong&gt; NCCoE emphasizes that enforcement and attestation are separate concerns. The policy must actually run before the action, not after. If your implementation signs the policy decision post-hoc, it's not enforcement — it's reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 3: Access Delegation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What NCCoE asks:&lt;/strong&gt; Mechanisms for linking user identities to agents while preventing privilege escalation. Actions must trace back to the human authority that delegated them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "good" looks like:&lt;/strong&gt; A cryptographically signed delegation chain. The root is a human (or org). Each delegation narrows scope, never widens. Every delegation has an expiration. Verification is offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal working code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;

&lt;span class="c1"&gt;# Assumes both keys exist already (signet identity create alice-human, etc.).
# Use SigningAgent.create(name, owner=...) on first run to mint them.
&lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice-human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Alice delegates scoped authority to bot, expiring in 1 hour.
# Scope fields are passed as keyword args; permissions can only narrow from here.
&lt;/span&gt;&lt;span class="n"&gt;token_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;place_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp://procurement-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# cannot re-delegate
&lt;/span&gt;    &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-06-30T23:59:59Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Bot signs an action carrying the delegation chain as proof.
# chain_json is a JSON array string of delegation tokens.
&lt;/span&gt;&lt;span class="n"&gt;receipt_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign_authorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;place_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LAPTOP-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp://procurement-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chain_json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token_json&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The v4 receipt carries:
# - authorization.chain_hash: SHA-256 of the delegation chain
# - authorization.root_pubkey: Alice's public key
# - All inside the signature scope
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification is offline.&lt;/strong&gt; A third party with Alice's public key can verify the chain without contacting Alice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scope_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_authorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;receipt_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;trusted_roots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;clock_skew_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Returns the effective scope as a JSON string, or raises if:
# - Signature invalid
# - Chain scope narrowing violated
# - Delegation expired
# - Root not in trusted_roots
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gap to watch:&lt;/strong&gt; NCCoE calls out the privilege escalation risk explicitly. Without "permissions only narrow, never widen," a compromised intermediate agent could issue itself broader permissions. The scope narrowing check must be enforced at verification time, not just at delegation time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar 4: Logging and Transparency
&lt;/h2&gt;

&lt;p&gt;This is where most deployments fail the auditor test. NCCoE asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Mechanisms by which specific AI agent actions are attributed to their non-human entity for audit and forensic purposes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most current implementations answer this with: "We write logs." That is not what NCCoE is asking for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "good" looks like:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Non-repudiation&lt;/strong&gt;: The agent cannot later deny it took an action. Ed25519 signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper-evident&lt;/strong&gt;: Modifying a log entry is detectable. Signatures break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper-evident ordering&lt;/strong&gt;: Deleting or reordering entries is detectable. SHA-256 hash chains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independently verifiable&lt;/strong&gt;: An auditor doesn't need access to the original runtime. Offline verification with the public key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most audit log implementations satisfy 0 of 4. The concept paper's language is specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Mechanisms by which agent actions can be logged in a &lt;strong&gt;tamper-proof&lt;/strong&gt; manner."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Minimal working code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_signet_dir&lt;/span&gt;

&lt;span class="c1"&gt;# Assumes the key already exists; .create(...) on first run.
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Every action signed and appended to hash-chained audit log
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;laptop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;place_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LAPTOP-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# An auditor — who never ran this code — verifies the chain:
&lt;/span&gt;&lt;span class="n"&gt;signet_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;default_signet_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signet_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chain intact: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total records: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_records&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any entry is modified: the Ed25519 signature fails. If any entry is deleted: the SHA-256 hash chain breaks. If any entry is reordered: the hash chain breaks. All detectable independently of the runtime that produced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the four areas compose
&lt;/h2&gt;

&lt;p&gt;The four areas aren't independent — they compose into a single verifiable artifact. Real Signet receipt shape (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receipt {
    v: 4,
    id: "rec_...",
    action: {
        tool: "place_order",
        params: { ... },
        params_hash: "sha256:...",
        target: "mcp://procurement-api",
        transport: "stdio",
    },
    signer: {                         // Pillar 1 (Identification)
        pubkey: "...",                // raw base64 Ed25519
        name: "procurement-bot-01",
        owner: "acme-corp",
    },
    policy: {                         // Pillar 2 (Authorization)
        policy_hash: "sha256:...",
        policy_name: "procurement-safe",
        decision: "allow",
        matched_rules: ["allow-search"],
    },
    authorization: {                  // Pillar 3 (Delegation)
        chain_hash: "sha256:...",
        root_pubkey: "...",           // Alice's public key, raw base64
    },
    ts: "2026-04-30T12:00:00Z",
    nonce: "rnd_...",
    sig: "ed25519:...",               // binding the whole thing (Pillar 4)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field inside the signature scope is tamper-evident. A verifier with the root public key can confirm, offline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt;: this specific agent produced this receipt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt;: this agent was delegated specific scope by this root&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy&lt;/strong&gt;: this policy was evaluated and returned this decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: this tool was called with these parameters at this time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain integrity&lt;/strong&gt;: this receipt is part of an unbroken sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's what NCCoE is asking for. Not logs. Not telemetry. Cryptographic receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;The four areas are additive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with Auditing &amp;amp; Non-repudiation&lt;/strong&gt; (signed receipts). This is the foundation — without it, the other three don't produce verifiable evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Identification&lt;/strong&gt;. Name the agent with a public-key ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Delegation&lt;/strong&gt; if your agents act on behalf of humans or other agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Authorization (policy)&lt;/strong&gt; if you have deny rules that must be provably enforced.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams start by retrofitting signed receipts onto an existing agent framework via callbacks. The examples above use &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt;, which handles all four areas. The specific tool matters less than the pattern. Whatever you build, the verifier should be able to answer four questions without calling back to your infrastructure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who did this? (signer pubkey)&lt;/li&gt;
&lt;li&gt;Were they allowed? (authorization chain root)&lt;/li&gt;
&lt;li&gt;Did the policy approve it? (policy hash + decision)&lt;/li&gt;
&lt;li&gt;Is the audit trail intact? (hash chain)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your current implementation can't answer all four from a receipt file alone, that's the gap the NCCoE concept paper will push you to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;NIST has signaled that more deliverables are coming under the AI Agent Standards Initiative — an Interoperability Profile is on the roadmap, but the published timeline and exact contents are still emerging. The direction is clear (cryptographic identity, signed delegation, tamper-evident audit) even if the final profile is not.&lt;/p&gt;

&lt;p&gt;The IETF draft &lt;a href="https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/" rel="noopener noreferrer"&gt;draft-farley-acta-signed-receipts&lt;/a&gt; is currently the most advanced concrete receipt specification — check Datatracker for the latest revision before citing a specific version.&lt;/p&gt;

&lt;p&gt;If you want to follow the standards track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.nist.gov/caisi/ai-agent-standards-initiative" rel="noopener noreferrer"&gt;NIST CAISI AI Agent Standards Initiative&lt;/a&gt; (main page)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nccoe.nist.gov/projects/software-and-ai-agent-identity-and-authorization" rel="noopener noreferrer"&gt;NCCoE Concept Paper&lt;/a&gt; (the four control areas)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nist.gov/news-events/news/2025/12/express-interest-working-caisi" rel="noopener noreferrer"&gt;Express interest in working with CAISI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/" rel="noopener noreferrer"&gt;IETF draft-farley-acta-signed-receipts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The window between now and whenever the normative profile lands is when the formats get locked in. What you ship in the next quarter will probably dictate whether you're ahead of or behind the NIST curve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; (Apache-2.0 OR MIT) is one open-source implementation in this space — Rust core, Python and TypeScript bindings — used as the working code in this post. &lt;code&gt;pip install signet-auth&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>compliance</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Add Tamper-Evident Audit Trails to Your CrewAI Agents</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Wed, 29 Apr 2026 08:39:24 +0000</pubDate>
      <link>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-crewai-agents-3i4c</link>
      <guid>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-crewai-agents-3i4c</guid>
      <description>&lt;p&gt;Your CrewAI crew kicks off a task. Agents delegate to each other, call tools, return results. But can you &lt;em&gt;prove&lt;/em&gt; what each agent actually did?&lt;/p&gt;

&lt;p&gt;CrewAI's built-in logs capture what happened. Cryptographic receipts prove it. The difference matters when an auditor, a customer, or a regulator asks "show me exactly what the agent did and prove it wasn't altered after the fact."&lt;/p&gt;

&lt;p&gt;This tutorial adds Ed25519-signed, hash-chained audit trails to a CrewAI crew in under 5 minutes. &lt;strong&gt;Signet itself needs no external service, no signing API, no infrastructure&lt;/strong&gt; — receipts verify offline with a public key. (Your CrewAI agent and any tools it uses still need their own keys; that part is unchanged.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;A CrewAI crew where every tool call produces a signed receipt containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt;: which tool was called, with what parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt;: the agent's Ed25519 public key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt;: timestamp&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof&lt;/strong&gt;: Ed25519 signature over JCS-canonicalized (RFC 8785) payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain&lt;/strong&gt;: SHA-256 hash linking to the previous receipt (tamper-evident ordering)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anyone modifies a receipt after the fact, the signature breaks. If anyone deletes or reorders receipts, the hash chain breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth[crewai] crewai-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires a CrewAI release that exposes the &lt;code&gt;crewai.hooks&lt;/code&gt; global tool-hook API (tested with the current PyPI release). &lt;code&gt;crewai-tools&lt;/code&gt; ships the &lt;code&gt;SerperDevTool&lt;/code&gt; used below; it is not bundled into the &lt;code&gt;[crewai]&lt;/code&gt; extra.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SerperDevTool&lt;/code&gt; calls Serper's web-search API and the CrewAI agent itself calls an LLM provider, so this exact demo also wants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather run zero-key, swap &lt;code&gt;SerperDevTool()&lt;/code&gt; for any local tool — Signet signs whatever flows through &lt;code&gt;crewai.hooks&lt;/code&gt; regardless of what the tool does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a signing identity
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KeyNotFoundError&lt;/span&gt;

&lt;span class="c1"&gt;# Load an existing Ed25519 identity, or create one on first run.
# Keys live at ~/.signet/keys/ — the private key stays local.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-crewai-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;KeyNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-crewai-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Public key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No key server, no certificate authority. The private key stays on disk, the public key is what verifiers use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Install the signing hooks
&lt;/h2&gt;

&lt;p&gt;CrewAI exposes global tool hooks. Signet plugs into them with one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth.crewai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;install_hooks&lt;/span&gt;

&lt;span class="nf"&gt;install_hooks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every tool call across every agent in every crew is now signed automatically.&lt;/p&gt;

&lt;p&gt;The hooks cover the full lifecycle: &lt;code&gt;before_tool_call&lt;/code&gt; (what was called, signed before the tool runs) and &lt;code&gt;after_tool_call&lt;/code&gt; (what was returned, hashed and signed). Recoverable signing errors (&lt;code&gt;SignetError&lt;/code&gt; — bad payload, audit-log IO trouble) are caught and logged as a warning. Programmer errors that imply your code is in a bad state (e.g. calling &lt;code&gt;agent.close()&lt;/code&gt; and then continuing to use the agent) still raise — by design, so you find them at dev time rather than silently dropping receipts in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run your crew normally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crewai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Crew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crewai_tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SerperDevTool&lt;/span&gt;

&lt;span class="n"&gt;researcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research Analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Find information on a given topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backstory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Experienced analyst with attention to detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;SerperDevTool&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research the weather in Tokyo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A summary of today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;crew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Crew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickoff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No changes to your agents, tools, or crew. The signing happens transparently through the hooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Inspect the receipts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth.crewai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_receipts&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_receipts&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Receipt #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Tool:       &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Params hash: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;params_hash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Signature:   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sig&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Timestamp:   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receipt #1
  Tool:       serper_dev
  Params hash: sha256:a1b2c3...
  Signature:   ed25519:Mz4xNTk2NjQ0NDgw...
  Timestamp:   2026-04-20T14:30:00Z

Receipt #2
  Tool:       _tool_end
  Params hash: sha256:d4e5f6...
  Signature:   ed25519:Nk5yODk3MjE1Njg4...
  Timestamp:   2026-04-20T14:30:02Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the start/end pair: the first receipt captures the tool call, the second captures a SHA-256 hash of the output. Together they prove what was called &lt;em&gt;and&lt;/em&gt; the hash of what it returned. (The full output stays in your application; the receipt only signs the hash, which is enough to detect any after-the-fact tampering of an output you've stored elsewhere.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify a receipt
&lt;/h2&gt;

&lt;p&gt;Anyone with the public key can verify, offline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;

&lt;span class="n"&gt;receipts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_receipts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;receipts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valid: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tamper with any field and verification fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evil_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# tamper
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;
&lt;span class="n"&gt;tampered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tampered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Verify the audit chain
&lt;/h2&gt;

&lt;p&gt;The audit log is a hash-chained JSONL file. Each entry's hash covers the previous entry, so deleting or reordering receipts breaks the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_signet_dir&lt;/span&gt;

&lt;span class="n"&gt;signet_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;default_signet_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;chain_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signet_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chain intact: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chain_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entries: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chain_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_records&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Clean up (optional)
&lt;/h2&gt;

&lt;p&gt;When you're done signing, uninstall the hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth.crewai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uninstall_hooks&lt;/span&gt;

&lt;span class="nf"&gt;uninstall_hooks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters for CrewAI specifically
&lt;/h2&gt;

&lt;p&gt;CrewAI's strength is agent-to-agent delegation. A researcher agent delegates to a writer agent, who calls tools, who returns results that feed back into the chain. When something goes wrong, "which agent did what" becomes a real question.&lt;/p&gt;

&lt;p&gt;Signed receipts answer that question independently of CrewAI's own logs. CrewAI's &lt;code&gt;ToolCallHookContext&lt;/code&gt; does expose &lt;code&gt;agent&lt;/code&gt; to the hook today, but Signet's current &lt;code&gt;install_hooks()&lt;/code&gt; binding ties one signing identity to the global hook registration; per-call routing to a per-agent key is on the roadmap. For now, if you need separate keys per agent, the practical pattern is: install with one agent's key, run that agent's task, call &lt;code&gt;uninstall_hooks()&lt;/code&gt;, then re-install with the next agent's key.&lt;/p&gt;

&lt;p&gt;Either way, the audit trail proves not just &lt;em&gt;what&lt;/em&gt; happened but &lt;em&gt;who&lt;/em&gt; signed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without Signet&lt;/th&gt;
&lt;th&gt;With Signet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"The research agent called serper_dev" (log entry)&lt;/td&gt;
&lt;td&gt;Ed25519 signature proving it, verifiable by anyone with the public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs can be edited after the fact&lt;/td&gt;
&lt;td&gt;Signature breaks if any field is modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ordering proof&lt;/td&gt;
&lt;td&gt;Hash chain breaks if receipts are deleted or reordered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust CrewAI's logs&lt;/td&gt;
&lt;td&gt;Verify independently, offline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When you need this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regulated industries&lt;/strong&gt;: EU AI Act Article 12 requires "automatic recording of events" for high-risk systems. Tamper-evident signed receipts can support that traceability + log-integrity requirement (the legal sufficiency check is your auditor's call, not Signet's — but they need something better than rotatable plaintext logs to point at).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise deployments&lt;/strong&gt;: When the question is "can you prove what the agent did?", signed receipts are the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-to-agent&lt;/strong&gt;: CrewAI's core pattern — when one agent verifies another's work, signatures make it cryptographic, not just log-based.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident response&lt;/strong&gt;: After something goes wrong in a multi-agent crew, tamper-evident receipts let you reconstruct exactly what happened without trusting anyone's claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bilateral co-signing&lt;/strong&gt;: Have both the agent and the tool server sign each interaction independently. Neither party can fabricate receipts. See &lt;code&gt;signet proxy&lt;/code&gt; for MCP integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy attestation&lt;/strong&gt;: Evaluate YAML policy rules and include the decision (allow/deny/require_approval) inside the signed receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chains&lt;/strong&gt;: Prove that Agent A was authorized by Human B to perform a specific action with scoped constraints. Useful when CrewAI agents are acting on behalf of specific users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are in &lt;code&gt;signet-auth&lt;/code&gt; today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth[crewai]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Prismer-AI/signet&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Signet is open source (Apache-2.0 OR MIT). Rust core with Python and TypeScript bindings. The signing layer needs no external service or signing API — receipts verify offline with the public key.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crewai</category>
      <category>python</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>Auto-updating Kubernetes workloads: an annotation-driven rollout, with circuit breaker</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Mon, 27 Apr 2026 04:20:33 +0000</pubDate>
      <link>https://forem.com/willamhou/auto-updating-kubernetes-workloads-an-annotation-driven-rollout-with-circuit-breaker-280o</link>
      <guid>https://forem.com/willamhou/auto-updating-kubernetes-workloads-an-annotation-driven-rollout-with-circuit-breaker-280o</guid>
      <description>&lt;p&gt;You have ten agent pods on a cluster, each running a different runtime image. Every Tuesday somebody publishes a new version of one of them. Are you going to &lt;code&gt;kubectl set image&lt;/code&gt; ten things by hand? Are you sure you'll know if v1.4.2 was the one that wedged the pods?&lt;/p&gt;

&lt;p&gt;This post is about the auto-update controller in &lt;a href="https://github.com/Prismer-AI/k8s4claw" rel="noopener noreferrer"&gt;k8s4claw&lt;/a&gt;, a Kubernetes operator for AI agent runtimes. It polls OCI registries on cron, picks the highest semver tag that matches your constraint, flips a single annotation, and lets the main reconciler do the rollout. If the rollout doesn't go ready inside a timeout, it rolls back. If it rolls back too many times, it stops trying and asks for a human.&lt;/p&gt;

&lt;p&gt;The whole controller is one Go file (&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/controller/autoupdate_controller.go" rel="noopener noreferrer"&gt;&lt;code&gt;autoupdate_controller.go&lt;/code&gt;&lt;/a&gt;), about 470 lines. This is the design walkthrough — not the API reference, not the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Claw&lt;/code&gt; resource looks like this when auto-update is on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openclaw&lt;/span&gt;
  &lt;span class="na"&gt;autoUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;           &lt;span class="c1"&gt;# daily at 3 AM&lt;/span&gt;
    &lt;span class="na"&gt;versionConstraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=1.0.0,&amp;lt;2"&lt;/span&gt;
    &lt;span class="na"&gt;healthTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;
    &lt;span class="na"&gt;maxRollbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five fields, and the controller has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wake up on schedule (cron expression, not "every N seconds").&lt;/li&gt;
&lt;li&gt;Ask the registry what tags exist for &lt;code&gt;ghcr.io/prismer-ai/k8s4claw-openclaw&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Filter to semver tags inside the constraint.&lt;/li&gt;
&lt;li&gt;Pick the highest one that's strictly greater than what's running, skipping any version we've already tried and rolled back.&lt;/li&gt;
&lt;li&gt;Apply it — but not by patching the StatefulSet directly.&lt;/li&gt;
&lt;li&gt;Watch readiness for &lt;code&gt;healthTimeout&lt;/code&gt; (10 min default).&lt;/li&gt;
&lt;li&gt;If both &lt;code&gt;sts.Status.UpdatedReplicas&lt;/code&gt; and &lt;code&gt;sts.Status.ReadyReplicas&lt;/code&gt; reach the desired count: record success, reset rollback counter.&lt;/li&gt;
&lt;li&gt;If it times out: clear the &lt;code&gt;target-image&lt;/code&gt; annotation so the main reconciler reverts to the runtime adapter's default image, mark this version as failed, increment rollback counter.&lt;/li&gt;
&lt;li&gt;After &lt;code&gt;maxRollbacks&lt;/code&gt; consecutive failures: open the circuit and stop trying. Subsequent version checks then emit a "version available, circuit open" event/condition instead of applying the new image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The non-obvious bits are &lt;em&gt;where the state lives&lt;/em&gt; and &lt;em&gt;how the rollout actually happens&lt;/em&gt;. Both turn out to use the same trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 1 — annotations drive the in-flight rollout
&lt;/h2&gt;

&lt;p&gt;The auto-update controller never holds in-memory state across reconciles. State lives in two places on the &lt;code&gt;Claw&lt;/code&gt; resource:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Annotations&lt;/strong&gt; drive the in-flight update — what image we want, what phase we're in, when we started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status.autoUpdate&lt;/code&gt;&lt;/strong&gt; holds the durable bookkeeping — current version, available version, rollback count, circuit-breaker flag, failed-version list, version history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The three annotations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;annotationTargetImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claw.prismer.ai/target-image"&lt;/span&gt;
    &lt;span class="n"&gt;annotationUpdatePhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claw.prismer.ai/update-phase"&lt;/span&gt;
    &lt;span class="n"&gt;annotationUpdateStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claw.prismer.ai/update-started"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;target-image&lt;/code&gt; — the full image reference we want running (&lt;code&gt;ghcr.io/.../openclaw:1.2.0&lt;/code&gt;). Stays set after a successful update.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update-phase&lt;/code&gt; — currently only &lt;code&gt;HealthCheck&lt;/code&gt; or absent. Absent = idle. Anything else falls through to the idle path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update-started&lt;/code&gt; — RFC3339 timestamp of when we set the phase annotation. Used by the health-check timer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reconcile is a two-way fork on the phase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;annotationUpdatePhase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"HealthCheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconcileHealthCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// otherwise: idle — check if a version poll is due&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the controller is &lt;strong&gt;stateless and idempotent&lt;/strong&gt;. If the operator pod restarts mid-update, the next reconcile reads the annotation back from etcd and picks up exactly where the old one left off. There's no &lt;code&gt;map[types.NamespacedName]updateState&lt;/code&gt; to rehydrate, no leader-election dance for in-flight work. Kubernetes is the database. The controller is a function over its current state.&lt;/p&gt;

&lt;p&gt;The other thing this gets you: &lt;code&gt;kubectl describe claw foo&lt;/code&gt; shows the in-flight update verbatim. No tracing, no controller logs to grep. The state is on the resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 2 — the rollout is one annotation
&lt;/h2&gt;

&lt;p&gt;Here's the thing that surprised me when I wrote this controller. The auto-update logic does not patch the StatefulSet. It does not touch pods. It does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;targetImage&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;baseImage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;newVersion&lt;/span&gt;
&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;annotationTargetImage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;targetImage&lt;/span&gt;
&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;annotationUpdatePhase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"HealthCheck"&lt;/span&gt;
&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;annotationUpdateStart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RFC3339&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole "apply a new version" code path.&lt;/p&gt;

&lt;p&gt;The rollout actually happens because the &lt;strong&gt;main&lt;/strong&gt; &lt;code&gt;ClawReconciler&lt;/code&gt; watches the same &lt;code&gt;Claw&lt;/code&gt; resource and rebuilds the pod template every reconcile. It checks the annotation when it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// claw_controller.go&lt;/span&gt;
&lt;span class="n"&gt;podTemplate&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PodTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Auto-update: override runtime image if target-image annotation is set.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;targetImage&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"claw.prismer.ai/target-image"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;targetImage&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Containers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Containers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"runtime"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Containers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;targetImage&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the auto-update controller is purely a &lt;em&gt;signal source&lt;/em&gt;. It says "I want this image to be running." The main reconciler is responsible for translating that into a StatefulSet update, which then translates into a rolling pod replacement, which the auto-update controller observes via &lt;code&gt;sts.Status.UpdatedReplicas&lt;/code&gt; and &lt;code&gt;sts.Status.ReadyReplicas&lt;/code&gt; (both required — see Mechanism 4).&lt;/p&gt;

&lt;p&gt;This separation matters because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is mostly just deleting annotations.&lt;/strong&gt; When we roll back, we &lt;code&gt;delete(claw.Annotations, annotationTargetImage)&lt;/code&gt; and the main reconciler reverts to the adapter's default image on the next pass. No special "rollback path" in the StatefulSet logic. (The &lt;code&gt;update-phase&lt;/code&gt; and &lt;code&gt;update-started&lt;/code&gt; annotations also get cleared.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual image overrides keep working.&lt;/strong&gt; If somebody set &lt;code&gt;target-image&lt;/code&gt; by hand for a hotfix, the main reconciler honors it for the pod template. The auto-update controller compares against &lt;code&gt;status.CurrentVersion&lt;/code&gt; (not the annotation) when deciding whether to propose a new version, so a manual override doesn't accidentally redirect what the controller thinks "current" means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The auto-update controller can be removed entirely&lt;/strong&gt; without breaking anything. Stale annotation, sure, but the cluster doesn't fall over.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're writing a new controller and you find yourself directly mutating sub-resources, ask whether you could mutate annotations on the parent CR instead and let the existing reconciler do the work. It's almost always cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 3 — semver resolution
&lt;/h2&gt;

&lt;p&gt;The version-picking logic is in &lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/registry/resolver.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/registry/resolver.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ResolveBestVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failedVersions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;semver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;constraint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentVer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;semver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;currentVer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;semver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;failedSet&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failedVersions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;failedVersions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;failedSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;semver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;semver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c"&gt;// skip non-semver tags like "latest", "sha-abc"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failedSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Original&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currentVer&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentVer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Original&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three subtleties worth flagging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-semver tags are silently dropped.&lt;/strong&gt; &lt;code&gt;latest&lt;/code&gt;, &lt;code&gt;sha-abc1234&lt;/code&gt;, &lt;code&gt;nightly&lt;/code&gt; — they all fail &lt;code&gt;semver.NewVersion()&lt;/code&gt; and get skipped. This is the right default for an auto-updater: anything you can't compare to a version constraint is something you don't want to roll into automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;failedVersions&lt;/code&gt; is checked after the constraint check, by exact original tag string.&lt;/strong&gt; A version that has been rolled back gets recorded in &lt;code&gt;Status.AutoUpdate.FailedVersions&lt;/code&gt; and is excluded from future auto-selection. The match is on &lt;code&gt;v.Original()&lt;/code&gt;, so &lt;code&gt;"1.2.0"&lt;/code&gt; and &lt;code&gt;"v1.2.0"&lt;/code&gt; would be treated as different strings — the constraint check is semver-aware, but the failed-version filter is not. To retry a failed version automatically you have to clear it from status manually; you can also force a manual rollout via the annotations (see the circuit-breaker section). This is conservative on purpose — the assumption is that if v1.2.0 wedged your pods once, the next 3 AM cron run isn't going to fix that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;!v.GreaterThan(currentVer)&lt;/code&gt; excludes equal.&lt;/strong&gt; Reinstalling the same version on every cron tick would be a noisy mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The auto-update controller also has an early bail-out for digest-pinned images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;currentImage&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;annotationTargetImage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currentImage&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDigestPinned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentImage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"skipping auto-update: image is digest-pinned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentImage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requeueAtNextCron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks the &lt;code&gt;target-image&lt;/code&gt; annotation, not the actual running image. &lt;code&gt;IsDigestPinned&lt;/code&gt; is just &lt;code&gt;strings.Contains(image, "@sha256:")&lt;/code&gt;. If you set &lt;code&gt;target-image&lt;/code&gt; to a digest-pinned reference (manually or via a previous override), the controller stops touching that Claw on its cron schedule. If the annotation is absent, the check is skipped and version polling proceeds normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 4 — health verification
&lt;/h2&gt;

&lt;p&gt;Once the annotation is set and the main reconciler has rolled the StatefulSet, the auto-update controller requeues every 15 seconds and watches readiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;desiredReplicas&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replicas&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;desiredReplicas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replicas&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedReplicas&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;desiredReplicas&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
   &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadyReplicas&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;desiredReplicas&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Health check passed.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two conditions, both required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UpdatedReplicas&lt;/code&gt; — pods running the &lt;em&gt;new&lt;/em&gt; template, not the old one. Without this check, you'd "succeed" the moment the old pods are still ready before the rollout has even started.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReadyReplicas&lt;/code&gt; — pods passing their readiness probes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If both clear within &lt;code&gt;healthTimeout&lt;/code&gt; (10 min default), we record success: reset rollback counter, reset circuit breaker, append a &lt;code&gt;Healthy&lt;/code&gt; entry to version history, and clear the &lt;code&gt;update-phase&lt;/code&gt; and &lt;code&gt;update-started&lt;/code&gt; annotations. Note we deliberately &lt;em&gt;keep&lt;/em&gt; &lt;code&gt;target-image&lt;/code&gt; — it's still the signal the main reconciler uses to override the runtime container image, and clearing it would silently revert the running pods to the adapter default on the next reconcile.&lt;/p&gt;

&lt;p&gt;If the timer expires first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;healthTimeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"health check timed out"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also roll back if the StatefulSet itself disappears past the timeout (the resource was deleted while we were watching), or if the start-time annotation is somehow malformed (you have to handle that — annotations are just strings).&lt;/p&gt;

&lt;p&gt;15 seconds is a polling interval, not a deadline. The actual deadline is &lt;code&gt;healthTimeout&lt;/code&gt;, parsed from the spec. If you're upgrading a heavyweight runtime that takes 8 minutes to warm up, set &lt;code&gt;healthTimeout: 15m&lt;/code&gt; and the controller will wait that long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 5 — circuit breaker
&lt;/h2&gt;

&lt;p&gt;Rolling back once is a hiccup. Rolling back three times in a row is a system telling you to stop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;maxRollbacks&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;defaultMaxRollbacks&lt;/span&gt;  &lt;span class="c"&gt;// 3&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRollbacks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;maxRollbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRollbacks&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RollbackCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxRollbacks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CircuitOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;SetAutoUpdateCircuit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventTypeWarning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventAutoUpdateCircuitOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Circuit breaker opened after %d rollbacks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RollbackCount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the circuit is open, the main Reconcile path detects new versions and emits an event saying "version X is available, but we're not applying it." The user sees this on &lt;code&gt;kubectl describe claw foo&lt;/code&gt; and can decide whether to investigate or override.&lt;/p&gt;

&lt;p&gt;The recovery story is deliberately blunt: &lt;strong&gt;the controller does not auto-recover the circuit&lt;/strong&gt;. There's no "wait 24 hours and try again" timer, no exponential backoff, no separate trial deployment. The gating check is &lt;code&gt;if status.CircuitOpen&lt;/code&gt; — it doesn't look at &lt;code&gt;RollbackCount&lt;/code&gt;. So the recovery paths are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A human patches &lt;code&gt;status.autoUpdate.circuitOpen&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; (and usually &lt;code&gt;rollbackCount&lt;/code&gt; to 0 for a clean slate). The next cron tick will resume normal version polling.&lt;/li&gt;
&lt;li&gt;A human forces an update path some other way — for example, setting all three annotations (&lt;code&gt;target-image&lt;/code&gt; to a known-good image, &lt;code&gt;update-phase&lt;/code&gt; to &lt;code&gt;HealthCheck&lt;/code&gt;, &lt;code&gt;update-started&lt;/code&gt; to a fresh RFC3339 timestamp) by hand. The phase check happens before the circuit check, so the next reconcile enters &lt;code&gt;reconcileHealthCheck&lt;/code&gt; directly and, on a successful rollout, resets &lt;code&gt;RollbackCount&lt;/code&gt; and &lt;code&gt;CircuitOpen&lt;/code&gt;. (&lt;code&gt;FailedVersions&lt;/code&gt; is left intact, so the controller still won't auto-pick the versions that failed before.) Skipping the timestamp or pointing &lt;code&gt;target-image&lt;/code&gt; at something that won't go ready will just cause an immediate rollback, so the manual path needs all three pieces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The argument for this design: three consecutive bad versions probably means something is wrong outside the controller's view (broken upstream image, broken probe, broken cluster networking). Auto-recovery would just rediscover the broken state on a fresh schedule and burn through more rollouts. We'd rather page somebody.&lt;/p&gt;

&lt;p&gt;If you wanted to add a "soak then retry" mode, the natural place is to have the recovery logic clear &lt;code&gt;CircuitOpen&lt;/code&gt; after, say, the third consecutive successful version-poll-with-no-update — i.e., a stable period where there's nothing new to try. That's a reasonable PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 6 — version history (with a cap)
&lt;/h2&gt;

&lt;p&gt;Every successful update &lt;em&gt;and&lt;/em&gt; every rollback appends an entry to &lt;code&gt;Status.AutoUpdate.VersionHistory&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistoryEntry&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AppliedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metav1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistoryHealthy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;// or VersionHistoryRolledBack&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;trimVersionHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;trimVersionHistory&lt;/code&gt; exists because etcd objects have size limits, and a &lt;code&gt;Claw&lt;/code&gt; that's been updating daily for two years can otherwise accumulate 700+ history entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;maxVersionHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;trimVersionHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxVersionHistory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionHistory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;maxVersionHistory&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;50 entries is enough to debug the last few months of activity. If you need long-term audit, scrape the controller's events into your observability stack. Status fields are not an audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Update vs Status.Update dance
&lt;/h2&gt;

&lt;p&gt;Annotations live on the resource (under &lt;code&gt;metadata&lt;/code&gt;). Status fields live under &lt;code&gt;.status&lt;/code&gt;. In Kubernetes, these are written through different subresources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;r.Update(ctx, claw)&lt;/code&gt; — writes &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;spec&lt;/code&gt;. Bumps &lt;code&gt;resourceVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;r.Status().Update(ctx, claw)&lt;/code&gt; — writes &lt;code&gt;.status&lt;/code&gt;. Also bumps &lt;code&gt;resourceVersion&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a single reconcile needs to write both — like the "start an update" path, which sets three annotations &lt;em&gt;and&lt;/em&gt; writes status fields — the in-memory &lt;code&gt;claw&lt;/code&gt; object goes stale between the two calls. The controller does an explicit re-fetch in between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Update annotations first, then re-fetch and merge status.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to set target-image annotation: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// Re-fetch to get updated resourceVersion before status update.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NamespacedName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to re-fetch after annotation update: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;mergeAutoUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;pendingConditions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;apimeta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetStatusCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to update status: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The re-fetch picks up the new &lt;code&gt;resourceVersion&lt;/code&gt; so &lt;code&gt;Status().Update&lt;/code&gt; doesn't conflict with the write we just did. Without it you'll see 409 errors under any non-trivial reconcile rate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mergeAutoUpdateStatus&lt;/code&gt; is the other half. It copies our locally-tracked status fields one at a time into the freshly-fetched object instead of swinging &lt;code&gt;claw.Status.AutoUpdate&lt;/code&gt; to a different pointer. Field-by-field copy is conservative: if a future field is added to &lt;code&gt;AutoUpdateStatus&lt;/code&gt; and we forget to track it locally, a wholesale pointer replacement would silently zero it. The merge style makes the controller's status writes additive within the auto-update sub-object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;mergeAutoUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdate&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;clawv1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdate&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentVersion&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AvailableVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AvailableVersion&lt;/span&gt;
    &lt;span class="c"&gt;// ... field-by-field copy ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your controller writes both annotations and status, you need this dance. If it only writes one, you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testability: Clock and TagLister
&lt;/h2&gt;

&lt;p&gt;Two interfaces, both for tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TagLister&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ListTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TagLister&lt;/code&gt; lets unit tests inject &lt;code&gt;[]string{"1.0.0", "1.1.0", "2.0.0-rc1"}&lt;/code&gt; instead of hitting GHCR. &lt;code&gt;Clock&lt;/code&gt; lets them advance time without &lt;code&gt;time.Sleep&lt;/code&gt;. Both have one-line production implementations and one-line fake implementations.&lt;/p&gt;

&lt;p&gt;These get wired in the manager setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// cmd/operator/main.go&lt;/span&gt;
&lt;span class="n"&gt;registryClient&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;clawregistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRegistryClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoUpdateReconciler&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetClient&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Scheme&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetScheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Recorder&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEventRecorderFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"autoupdate-controller"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;TagLister&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;registryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;// Clock is left nil; clock() falls back to realClock{}.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the reconcile-path tests, both fields get fakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;cl&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClientBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;WithScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;WithObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;WithStatusSubresource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AutoUpdateReconciler&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Scheme&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Recorder&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFakeRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;TagLister&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;testTagLister&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1.1.0"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;Clock&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;testClock&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The autoupdate unit tests use &lt;code&gt;controller-runtime/pkg/client/fake&lt;/code&gt; — no envtest API server, no kube-apiserver process, just an in-memory client backed by typed scheme. They create a &lt;code&gt;Claw&lt;/code&gt;, run a single &lt;code&gt;Reconcile&lt;/code&gt; pass with a controlled clock, and assert on annotations and &lt;code&gt;Status.AutoUpdate&lt;/code&gt;. No real registry calls, no real timers, no flake. Total run time is sub-second per test.&lt;/p&gt;

&lt;p&gt;If you find yourself reaching for &lt;code&gt;time.Now()&lt;/code&gt; or hitting an external API directly inside a reconciler, stop and define the interface first. Future-you writing tests will thank present-you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we didn't do (on purpose)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight image probe.&lt;/strong&gt; We don't pull the new image and try &lt;code&gt;docker run&lt;/code&gt; it on a node before flipping the StatefulSet. That would be a much heavier dependency (DaemonSet? privileged container?) and the StatefulSet rollout is itself a kind of probe — the readiness check just runs in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary deploys.&lt;/strong&gt; Roll one pod, observe, then the rest. For most agent workloads we have, replicas is 1 and there's nothing to canary against. For higher-replica deployments, this is a worthwhile follow-up — the existing state machine could grow a &lt;code&gt;Canary&lt;/code&gt; phase between idle and &lt;code&gt;HealthCheck&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook-driven updates.&lt;/strong&gt; Push from registry instead of poll. Simpler operationally but creates an inbound dependency from the registry to the cluster, which is not a thing most clusters want. Cron-poll wins on operational simplicity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-namespace coordination.&lt;/strong&gt; If you have ten Claws on the same image and a bad version drops, they will all roll back independently. We considered tying them together via a shared &lt;code&gt;ClawImageGroup&lt;/code&gt; resource and decided the complexity wasn't worth it. The circuit breaker + failed-versions list is good enough: each Claw learns from its own pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image signature verification.&lt;/strong&gt; Sigstore / cosign integration would slot in at &lt;code&gt;IsDigestPinned&lt;/code&gt;'s level — verify, &lt;em&gt;then&lt;/em&gt; set &lt;code&gt;target-image&lt;/code&gt;. We didn't ship it because the projects we serve aren't there yet, but it's an obvious next step for security-sensitive deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Unit tests are split across three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/controller/autoupdate_reconcile_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/controller/autoupdate_reconcile_test.go&lt;/code&gt;&lt;/a&gt; — the largest reconcile-path set. Covers initiating an update, skipping digest-pinned images, health-check success, rollback on timeout, circuit-breaker opening after consecutive rollbacks, StatefulSet-not-found behavior, invalid &lt;code&gt;update-started&lt;/code&gt; triggering an immediate rollback, custom &lt;code&gt;healthTimeout&lt;/code&gt; override, and the schedule-not-due requeue path.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/controller/autoupdate_controller_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/controller/autoupdate_controller_test.go&lt;/code&gt;&lt;/a&gt; — a mix: helper-function coverage (&lt;code&gt;extractVersionFromImage&lt;/code&gt;, &lt;code&gt;trimVersionHistory&lt;/code&gt;, &lt;code&gt;containsString&lt;/code&gt;, cron-due math, the &lt;code&gt;realClock&lt;/code&gt; fallback inside &lt;code&gt;clock()&lt;/code&gt;) plus a smaller batch of reconcile tests for the disabled/no-new-version/not-found/circuit-already-open paths.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/controller/autoupdate/autoupdate_controller_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/controller/autoupdate/autoupdate_controller_test.go&lt;/code&gt;&lt;/a&gt; — an older parallel suite kept alive against the same controller code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reconcile-path tests pre-load a &lt;code&gt;Claw&lt;/code&gt; (and optionally a &lt;code&gt;StatefulSet&lt;/code&gt; with the desired readiness state), run a single &lt;code&gt;Reconcile&lt;/code&gt; pass, and assert on annotations or &lt;code&gt;Status.AutoUpdate&lt;/code&gt;. Most tests are under 50 lines. The fake clock and fake tag lister make timing deterministic, which is the main reason the tests aren't flaky.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bought us
&lt;/h2&gt;

&lt;p&gt;A ~470-line controller that does cron-driven, semver-filtered, health-verified, automatically-rolling-back image updates for a CRD, with a circuit breaker and version history. All in-flight state lives on the &lt;code&gt;Claw&lt;/code&gt; resource (annotations for phase, &lt;code&gt;.status&lt;/code&gt; for durable bookkeeping), so the controller has no in-memory state to lose across restarts. Supported runtime types are mapped to their base OCI images via a small &lt;code&gt;ImageForRuntime(string) string&lt;/code&gt; helper — adding a new runtime there is one switch-case, not a controller change. Runtimes without an entry are silently skipped by auto-update (we currently have a couple of those — &lt;code&gt;hermesrs&lt;/code&gt; and &lt;code&gt;k8sops&lt;/code&gt; — that don't track a public OCI release cadence). The rest of the controller works in plain semver tags.&lt;/p&gt;

&lt;p&gt;The thing I'd point a junior K8s-controller author at, in this code, is the annotation-driven separation: &lt;em&gt;the controller doesn't do the rollout, it asks for the rollout&lt;/em&gt;. Once you internalize that, a lot of K8s controllers get smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look at next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/Prismer-AI/k8s4claw" rel="noopener noreferrer"&gt;k8s4claw repo&lt;/a&gt; if you want the full operator&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/controller/autoupdate_controller.go" rel="noopener noreferrer"&gt;&lt;code&gt;autoupdate_controller.go&lt;/code&gt;&lt;/a&gt; for the controller in one file&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/registry/resolver.go" rel="noopener noreferrer"&gt;&lt;code&gt;registry/resolver.go&lt;/code&gt;&lt;/a&gt; for the version picker&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/willamhou/building-an-ipc-bus-for-kubernetes-sidecars-wal-dlq-and-ring-buffer-backpressure-4b27"&gt;The IPC bus deep dive&lt;/a&gt; — Post 2 of this series, on how channel sidecars talk to the runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source, Apache-2.0. If you've built an auto-updater that handles canary deploys or signature verification, I'd genuinely like to read your code. Drop a link in the comments.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>opensource</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Building an IPC bus for Kubernetes sidecars: WAL, DLQ, and ring-buffer backpressure</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Thu, 23 Apr 2026 02:18:14 +0000</pubDate>
      <link>https://forem.com/willamhou/building-an-ipc-bus-for-kubernetes-sidecars-wal-dlq-and-ring-buffer-backpressure-4b27</link>
      <guid>https://forem.com/willamhou/building-an-ipc-bus-for-kubernetes-sidecars-wal-dlq-and-ring-buffer-backpressure-4b27</guid>
      <description>&lt;p&gt;If you put two sidecars in a pod and ask them to talk to each other over HTTP, sooner or later one of them crashes mid-request and you lose a message. If you do it enough times, you reinvent a message bus.&lt;/p&gt;

&lt;p&gt;This post is about the small in-pod message bus we ended up writing for &lt;a href="https://github.com/Prismer-AI/k8s4claw" rel="noopener noreferrer"&gt;k8s4claw&lt;/a&gt;, a Kubernetes operator for AI agent runtimes. The bus sits between channel sidecars (Slack, Discord, Webhook) and the agent runtime container. It has four wire protocols, a write-ahead log, a BoltDB-backed dead letter queue, and a ring buffer with backpressure. All of it is open source (&lt;a href="https://github.com/Prismer-AI/k8s4claw/tree/main/internal/ipcbus" rel="noopener noreferrer"&gt;internal/ipcbus/&lt;/a&gt;), around 2k lines of Go.&lt;/p&gt;

&lt;p&gt;This post is the design doc you actually want to read, not the one we had to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Claw&lt;/code&gt; pod looks like this when it has a Slack channel attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────┐
│  Pod                                         │
│                                              │
│  [channel-slack] ──UDS──► [ipc-bus] ──►┐     │
│                                        ▼     │
│                                  [runtime]   │
│                                              │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three containers. The channel sidecar reads from Slack. The runtime is the actual AI agent. The IPC bus is a &lt;a href="https://kubernetes.io/blog/2023/08/25/native-sidecar-containers/" rel="noopener noreferrer"&gt;native sidecar&lt;/a&gt; (init container with &lt;code&gt;restartPolicy: Always&lt;/code&gt;) that routes messages between them.&lt;/p&gt;

&lt;p&gt;The naive version of this is: let the two containers talk HTTP directly. The reality is that at least four things are going to go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The runtime will be overloaded when a Slack event arrives and we need somewhere to buffer it.&lt;/li&gt;
&lt;li&gt;The runtime will crash mid-response and we need to redeliver.&lt;/li&gt;
&lt;li&gt;A slow downstream (say, a user's laptop on 3G) will fall behind and we need to push back instead of dropping.&lt;/li&gt;
&lt;li&gt;Two different runtimes we support speak four different wire protocols. HTTP isn't enough.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So we wrote a bus. Let me walk through the four mechanisms that earn their keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 1 — length-prefix framing
&lt;/h2&gt;

&lt;p&gt;This isn't glamorous, but it's the first thing you get wrong in a message bus.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;Message&lt;/code&gt; is a JSON blob on the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt;          &lt;span class="n"&gt;MessageType&lt;/span&gt;     &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
    &lt;span class="n"&gt;Channel&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"channel,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;CorrelationID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"correlationId,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;ReplyTo&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"replyTo,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;Timestamp&lt;/span&gt;     &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;       &lt;span class="s"&gt;`json:"timestamp"`&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt;       &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt; &lt;span class="s"&gt;`json:"payload,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the wire it looks like &lt;code&gt;[4-byte big-endian length][JSON bytes]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MaxMessageSize&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
    &lt;span class="n"&gt;FrameHeaderSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WriteMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to marshal message: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MaxMessageSize&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"message size %d exceeds maximum %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;MaxMessageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FrameHeaderSize&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FrameHeaderSize&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why length-prefix instead of newline-delimited JSON? Because JSON payloads can contain newlines inside strings and you'd have to escape them on the wire. Length-prefix framing just works: a reader reads 4 bytes, gets the length, reads that many bytes, deserializes. No lookahead, no escape tables.&lt;/p&gt;

&lt;p&gt;The 16 MB cap is there to fail loudly rather than run out of memory on a malformed header. In practice our real messages are well under 64 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 2 — four bridge protocols behind one interface
&lt;/h2&gt;

&lt;p&gt;Different runtimes speak different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;Full-duplex, JSON-native, easy from Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NanoClaw&lt;/td&gt;
&lt;td&gt;UDS&lt;/td&gt;
&lt;td&gt;Lowest overhead for same-pod communication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZeroClaw&lt;/td&gt;
&lt;td&gt;SSE&lt;/td&gt;
&lt;td&gt;Already has an HTTP API, SSE for server-push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PicoClaw&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Minimal client, hand-rolled in 50 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bus abstracts them behind one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RuntimeBridge&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four methods. Adding a new protocol is one file (&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/ipcbus/bridge_tcp.go" rel="noopener noreferrer"&gt;example: TCP bridge&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TCPBridge&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;streamBridge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;TCPBridge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dialer&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DialContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;streamBridge&lt;/code&gt; is a shared base that implements &lt;code&gt;Send&lt;/code&gt;/&lt;code&gt;Receive&lt;/code&gt;/&lt;code&gt;Close&lt;/code&gt; on top of any &lt;code&gt;net.Conn&lt;/code&gt;. It handles &lt;code&gt;context.Context&lt;/code&gt; deadlines properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;streamBridge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"not connected"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Respect context deadline for the write.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deadline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetWriteDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetWriteDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;WriteMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtle bit is &lt;code&gt;Receive&lt;/code&gt;. &lt;code&gt;ReadMessage&lt;/code&gt; blocks on the socket. If the caller cancels the context, we want the read to unblock. So &lt;code&gt;Receive&lt;/code&gt; spawns a second goroutine whose only job is to watch the context and call &lt;code&gt;Close&lt;/code&gt; on the conn, which makes the blocked &lt;code&gt;ReadMessage&lt;/code&gt; return with an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SSE bridge is the odd one out because SSE is unidirectional (server → client, event-stream format) and we need bidirectional. So it uses an HTTP POST for send and an SSE &lt;code&gt;GET /events&lt;/code&gt; for receive, with exponential-backoff reconnect on the stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ... connect and read events ...&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mechanism 3 — Write-Ahead Log (WAL)
&lt;/h2&gt;

&lt;p&gt;This is the one that earns the bus the right to exist.&lt;/p&gt;

&lt;p&gt;When a message comes in from a channel sidecar, the router does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Append a WAL entry to disk (emptyDir-backed) with state &lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;bridge.Send(ctx, msg)&lt;/code&gt; to hand it off to the runtime bridge.&lt;/li&gt;
&lt;li&gt;Mark the WAL entry complete as soon as &lt;code&gt;Send&lt;/code&gt; returns success. If &lt;code&gt;Send&lt;/code&gt; fails, call &lt;code&gt;scheduleRetry&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We delivery-mark on &lt;em&gt;transport success&lt;/em&gt; (the bridge accepted the bytes), not on runtime ack. We considered a runtime-ack round-trip and decided against it: it doubles round-trips, forces every runtime to implement ack semantics, and our &lt;code&gt;Message.ID&lt;/code&gt; is already idempotency-safe so downstream retries aren't harmful. If a message leaves &lt;code&gt;bridge.Send&lt;/code&gt; OK but the runtime crashes before processing it, we lose that one message. Tradeoff: acceptable for a chat agent, &lt;em&gt;not&lt;/em&gt; acceptable for a payment system. Different design calls, different bus.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scheduleRetry&lt;/code&gt; increments &lt;code&gt;Attempts&lt;/code&gt; on the WAL entry. After &lt;code&gt;maxRetryAttempts = 5&lt;/code&gt;, the entry is marked &lt;code&gt;dlq&lt;/code&gt; and a copy is parked in the DLQ.&lt;/p&gt;

&lt;p&gt;The WAL is a JSON-lines file. Each line is a &lt;code&gt;WALEntry&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;WALEntry&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Channel&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"channel"`&lt;/span&gt;
    &lt;span class="n"&gt;State&lt;/span&gt;    &lt;span class="n"&gt;WALState&lt;/span&gt; &lt;span class="s"&gt;`json:"state"`&lt;/span&gt;       &lt;span class="c"&gt;// pending | complete | dlq&lt;/span&gt;
    &lt;span class="n"&gt;Attempts&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`json:"attempts"`&lt;/span&gt;
    &lt;span class="n"&gt;TS&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"ts"`&lt;/span&gt;
    &lt;span class="n"&gt;Msg&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="s"&gt;`json:"msg,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON-lines is nice because you can &lt;code&gt;cat wal.log | jq&lt;/code&gt; during an incident and see exactly what the bus was doing. It's also append-only, which means writes are O(1) and you never corrupt the middle of the file on a crash — at worst you have a half-written last line, which the recovery code handles.&lt;/p&gt;

&lt;p&gt;The interesting operation is compaction. The file grows without bound otherwise. Compaction rewrites the file keeping only &lt;code&gt;pending&lt;/code&gt; entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Compact&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ... write all pending entries to wal.log.tmp ...&lt;/span&gt;
    &lt;span class="c"&gt;// atomic rename&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmpPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;NeedsCompaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;compactionThreshold&lt;/span&gt;  &lt;span class="c"&gt;// 10 MB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't compact on every &lt;code&gt;Complete&lt;/code&gt; call — that would tank throughput. The &lt;code&gt;cmd/ipcbus&lt;/code&gt; binary runs a 60-second ticker that checks &lt;code&gt;NeedsCompaction()&lt;/code&gt; and rewrites the file when it grows past 10 MB. That's a coarse heuristic — it will compact even if most entries are still &lt;code&gt;pending&lt;/code&gt;, wasting some I/O — but it's simple and steady-state overhead is near zero. A smarter policy (also consider the &lt;code&gt;pending&lt;/code&gt; ratio, pre-commit) would be a reasonable first PR.&lt;/p&gt;

&lt;p&gt;The WAL does not fsync on every append. We batch. If a node hard-kills, we can lose the last few hundred milliseconds of messages. That's an acceptable tradeoff for a system where the upstream Slack delivery is already best-effort. If you care more about durability, &lt;code&gt;Flush()&lt;/code&gt; is exposed and you can call it from your own code, but we chose not to make it automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 4 — Dead Letter Queue (DLQ)
&lt;/h2&gt;

&lt;p&gt;After 5 delivery attempts, a message is "dead." We don't silently drop it; we move it to the DLQ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewDLQ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DLQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bolt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bolt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BoltDB is &lt;a href="https://github.com/etcd-io/bbolt" rel="noopener noreferrer"&gt;embedded KV storage with B+tree on-disk layout&lt;/a&gt;. It's fast, transactional, and single-file. Perfect for a sidecar that needs a few megabytes of dead messages, queryable by ID and age.&lt;/p&gt;

&lt;p&gt;Two eviction policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;maxSize&lt;/strong&gt; — a hard cap on entry count. When we're full, we evict the oldest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ttl&lt;/strong&gt; — entries older than the TTL are purged. &lt;code&gt;NewDLQ(path, maxSize, ttl)&lt;/code&gt; takes both as constructor args; the &lt;code&gt;cmd/ipcbus&lt;/code&gt; binary passes &lt;code&gt;maxSize=10000, ttl=24h&lt;/code&gt; and runs an hourly &lt;code&gt;PurgeExpired&lt;/code&gt; ticker. Library callers can pick their own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because the DLQ is the debugging surface for the bus. Something went wrong? &lt;code&gt;kubectl exec&lt;/code&gt; into the sidecar, open the BoltDB file, and look at the last N entries. We've caught a couple of real bugs this way that would have been invisible with "drop on failure."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DLQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PurgeExpired&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DLQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DLQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DLQEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deliberately no replay-from-DLQ. If something's dead, it's dead. We want human attention, not automatic retry that hides a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 5 — ring buffer with backpressure
&lt;/h2&gt;

&lt;p&gt;The remaining problem: what if a channel sidecar is producing faster than the runtime can consume?&lt;/p&gt;

&lt;p&gt;Naive answer: unbounded queue. Result: OOM-killed pod.&lt;/p&gt;

&lt;p&gt;Real answer: bounded ring buffer with high/low watermarks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewRingBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;highWatermark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lowWatermark&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;RingBuffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ... defaults to high=0.8, low=0.3 ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the buffer fills past 80%, the bus emits a &lt;code&gt;slow_down&lt;/code&gt; control message upstream. The channel sidecar sees it and stops pulling from Slack. When the buffer drains below 30%, the bus emits &lt;code&gt;resume&lt;/code&gt; and the sidecar starts pulling again.&lt;/p&gt;

&lt;p&gt;Why two watermarks? Because if you use one, you thrash. Right at the threshold, every push flips state. Two watermarks with a gap gives you hysteresis. Classic control-theory stuff, very little Go stuff.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;slow_down&lt;/code&gt; / &lt;code&gt;resume&lt;/code&gt; messages ride the same wire format as everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;TypeAck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeNack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeSlowDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeResume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;TypeShutdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeRegister&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeHeartbeat&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating control traffic as just another &lt;code&gt;MessageType&lt;/code&gt; means channel sidecars don't need a separate control channel. One TCP/UDS/WS connection carries both payloads and backpressure signals. Simpler, fewer failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shutdown
&lt;/h2&gt;

&lt;p&gt;Graceful shutdown is its own hazard. On SIGTERM the &lt;code&gt;cmd/ipcbus&lt;/code&gt; binary runs a local &lt;code&gt;shutdown()&lt;/code&gt; helper that does the bare minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendShutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c"&gt;// tell sidecars we're going away&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// fixed grace window&lt;/span&gt;
    &lt;span class="n"&gt;wal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                 &lt;span class="c"&gt;// flush WAL to disk&lt;/span&gt;
    &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;              &lt;span class="c"&gt;// close the runtime bridge&lt;/span&gt;
    &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                    &lt;span class="c"&gt;// stop the UDS server + background tickets&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No polling, no early exit if sidecars disconnect, no DLQ close (process-exit flushes BoltDB's mmap and that's enough). Whatever is still &lt;code&gt;pending&lt;/code&gt; in the WAL when we exit gets replayed on next startup — that's the whole point of the WAL.&lt;/p&gt;

&lt;p&gt;There's also a fancier &lt;code&gt;ShutdownOrchestrator&lt;/code&gt; in &lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/ipcbus/shutdown.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/ipcbus/shutdown.go&lt;/code&gt;&lt;/a&gt; that takes a &lt;code&gt;drainTimeout&lt;/code&gt; parameter and polls &lt;code&gt;router.ConnectedCount()&lt;/code&gt; every 100 ms to exit early, but the current binary doesn't wire it up. Good first PR: swap the local helper out for the orchestrator so the sleep becomes a real wait-for-drain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we didn't do (on purpose)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-pod clustering.&lt;/strong&gt; The bus is deliberately in-pod. If you want cross-pod messaging, use a real broker (NATS, Redis streams). Scoping this to one pod kept us sane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering guarantees across channels.&lt;/strong&gt; Within one channel, messages are ordered. Across channels, no promise. Most agent workloads don't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exactly-once.&lt;/strong&gt; At-least-once with idempotent consumers is simpler and good enough. The runtime is expected to deduplicate on &lt;code&gt;Message.ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protobuf on the wire.&lt;/strong&gt; JSON is ~2× larger but 10× easier to debug. Given our throughput (tens of messages per second per pod, not millions), JSON is the right call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;We aimed for &amp;gt;80% statement coverage on the ipcbus package, approximately. The non-obvious piece: most of the reliability features are hard to unit-test with mocks because they're about failure modes. So we have a lot of tests that spin up real local listeners (&lt;code&gt;net.Listen("tcp", "127.0.0.1:0")&lt;/code&gt;, &lt;code&gt;net.Listen("unix", t.TempDir()+"/sock")&lt;/code&gt;, &lt;code&gt;httptest.NewServer(...)&lt;/code&gt;) and exercise the bridges end-to-end.&lt;/p&gt;

&lt;p&gt;For example, the SSE bridge test spins up an &lt;code&gt;httptest&lt;/code&gt; server that handles both &lt;code&gt;GET /events&lt;/code&gt; (as an SSE stream) and &lt;code&gt;POST /messages&lt;/code&gt;, and checks that connecting, sending, and receiving all work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestSSEBridge_SendReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sseEchoServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;bridge&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewSSEBridge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ... connect, wait for SSE stream to establish, send, receive ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 70 tests total, &lt;code&gt;-race&lt;/code&gt; clean. Good enough for a sidecar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bought us
&lt;/h2&gt;

&lt;p&gt;A uniform contract for channel sidecars. You write one Slack sidecar, it works with every runtime. You write one Discord sidecar, same thing. Runtime authors pick a protocol that fits their stack; they don't think about durability, retries, or backpressure — the bus handles it.&lt;/p&gt;

&lt;p&gt;The runtime adapter for a new protocol is ~50 lines. The channel sidecar SDK (&lt;a href="https://github.com/Prismer-AI/k8s4claw/tree/main/sdk/channel" rel="noopener noreferrer"&gt;&lt;code&gt;sdk/channel/&lt;/code&gt;&lt;/a&gt;) hides the framing entirely; you call &lt;code&gt;client.Send(ctx, json.RawMessage(...))&lt;/code&gt; and move on.&lt;/p&gt;

&lt;p&gt;The whole ipcbus package is ~2k lines of Go. If you want to read one file to get the flavor, &lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/ipcbus/router.go" rel="noopener noreferrer"&gt;&lt;code&gt;router.go&lt;/code&gt;&lt;/a&gt; is where all five mechanisms meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look at next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/Prismer-AI/k8s4claw" rel="noopener noreferrer"&gt;k8s4claw repo&lt;/a&gt; if you want to use it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Prismer-AI/k8s4claw/tree/main/internal/ipcbus" rel="noopener noreferrer"&gt;&lt;code&gt;internal/ipcbus/&lt;/code&gt;&lt;/a&gt; if you want to read the code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/willamhou/k8s4claw-a-kubernetes-operator-for-managing-ai-agent-runtimes-3anm"&gt;The intro post&lt;/a&gt; if you want context on how this fits into the operator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source, Apache-2.0. Questions and PRs welcome. If you've built something similar and went in a different direction, I'd love to hear why in the comments.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>opensource</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>When Rust's Exhaustive Match Helps (And When It Doesn't): Notes from a Bare-Metal Hypervisor</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Wed, 22 Apr 2026 03:41:04 +0000</pubDate>
      <link>https://forem.com/willamhou/when-rusts-exhaustive-match-helps-and-when-it-doesnt-notes-from-a-bare-metal-hypervisor-4olh</link>
      <guid>https://forem.com/willamhou/when-rusts-exhaustive-match-helps-and-when-it-doesnt-notes-from-a-bare-metal-hypervisor-4olh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: This is about an experimental hypervisor project that only runs on QEMU virt — no real-hardware validation yet. The lessons apply to "Rust's tooling edges in systems programming," not production guidance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;10 weeks into writing an ARM64 bare-metal hypervisor, I assumed Rust's exhaustive &lt;code&gt;match&lt;/code&gt; would be the safety net when I extended my state machine. Two observations, from one week of commits: &lt;strong&gt;exhaustive match didn't help my state machine at all, but caught 6 errors the one time I extended my &lt;code&gt;Device&lt;/code&gt; enum.&lt;/strong&gt; This post is about why — and why the distinction is about cardinality, not typestate vs tag enums.&lt;/p&gt;




&lt;p&gt;I'm writing an ARM64 bare-metal hypervisor. Part of it is a thing called a &lt;strong&gt;Secure Partition (SP)&lt;/strong&gt; — a lightweight VM managed by the SPMC. Each SP has a lifecycle: Reset → Idle → Running → Blocked → Preempted. 5 states, 7 legal transitions.&lt;/p&gt;

&lt;p&gt;Two weeks ago I added a new transition: &lt;code&gt;Blocked → Preempted&lt;/code&gt;, for chain preemption between SPs. By the textbook, this is exactly the scenario where Rust's &lt;code&gt;enum + match&lt;/code&gt; should shine: add a state/transition, the compiler finds every site that needs updating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The compiler said nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about why I didn't use the "enum-with-fields" pattern you see in tutorials, why &lt;code&gt;match&lt;/code&gt; exhaustiveness didn't help on this state machine, and where it actually &lt;em&gt;did&lt;/em&gt; help.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Code
&lt;/h2&gt;

&lt;p&gt;No toy examples. Here's the actual &lt;code&gt;SpState&lt;/code&gt; from the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/sp_context.rs&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq)]&lt;/span&gt;
&lt;span class="nd"&gt;#[repr(u8)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;SpState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Idle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Preempted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classic &lt;strong&gt;tag-only enum&lt;/strong&gt; — &lt;code&gt;#[repr(u8)]&lt;/code&gt;, every variant is one byte, no payload. Why not the textbook &lt;code&gt;Running { entry_pc: u64 }&lt;/code&gt; / &lt;code&gt;Preempted { saved_ctx: VcpuContext }&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Because the state lives in an &lt;strong&gt;&lt;code&gt;AtomicU8&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The SPMC runs on multiple physical CPUs. Different CPUs inside TF-A's SPMD (Secure Partition Manager Dispatcher) can route requests to the same SP at once. Two CPUs racing to do &lt;code&gt;Idle → Running&lt;/code&gt; — one &lt;em&gt;must&lt;/em&gt; lose, or both will &lt;code&gt;ERET&lt;/code&gt; into the same SP and clobber register context.&lt;/p&gt;

&lt;p&gt;CAS drives the race:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;try_transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;SpState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt;&lt;span class="nf"&gt;.compare_exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// success: AcqRel publishes our context-save&lt;/span&gt;
        &lt;span class="n"&gt;new_state&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// failure: Acquire syncs the observed loser&lt;/span&gt;
        &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AcqRel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Acquire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(()),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corrupt SP state value"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint isn't memory layout — &lt;code&gt;#[repr(u8, C)]&lt;/code&gt; on a fields-carrying enum does give stable layout. The real constraint is &lt;strong&gt;size&lt;/strong&gt;: &lt;code&gt;AtomicU8&lt;/code&gt; wraps one byte, and any enum with a &lt;code&gt;u64&lt;/code&gt; payload is at least 8 bytes wide. Atomic &lt;code&gt;u64&lt;/code&gt; CAS is fine on aarch64, but that means every state change either serializes through a fat struct CAS or falls back to a lock. I wanted single-byte CAS in the fast path, so the payload lives elsewhere (in a separate &lt;code&gt;VcpuContext&lt;/code&gt; guarded by the state transition itself).&lt;/p&gt;

&lt;p&gt;Side note on &lt;code&gt;expect("corrupt SP state value")&lt;/code&gt;: it really does panic. In this project the panic handler halts the offending CPU and dumps state via UART — because if the &lt;code&gt;AtomicU8&lt;/code&gt; ever holds a value outside &lt;code&gt;0..=4&lt;/code&gt;, memory corruption has already happened and limping along is worse than stopping. That's a conscious choice for this binary, not a general bare-metal guideline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Exhaustive Match Didn't Help
&lt;/h2&gt;

&lt;p&gt;The legal-transition check lives in one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/sp_context.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;transition_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.state&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Blocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Blocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Blocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Preempted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ← the newly added line&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Preempted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Preempted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the final &lt;code&gt;_ =&amp;gt; false&lt;/code&gt;. This is &lt;strong&gt;not&lt;/strong&gt; an exhaustive match — the wildcard swallows every unlisted combination as "illegal."&lt;/p&gt;

&lt;p&gt;The commit that added &lt;code&gt;Blocked → Preempted&lt;/code&gt; was literally 1 line. The compiler reported nothing, because to the compiler, all 25 &lt;code&gt;(from, to)&lt;/code&gt; combinations are covered (7 explicit + &lt;code&gt;_&lt;/code&gt; fallback).&lt;/p&gt;

&lt;p&gt;I could have replaced &lt;code&gt;_ =&amp;gt; false&lt;/code&gt; with all 18 illegal combinations enumerated. I started to — "exhaustive is more Rust-y". Then I gave up halfway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This way...&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SpState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Blocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="c1"&gt;// ... 15 more lines of this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No new information, and every future state addition means maintaining an N² table. &lt;code&gt;_ =&amp;gt; false&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the documentation here: &lt;strong&gt;what's listed is legal; everything else isn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: For simple C-style enum + state-transition pairs, &lt;code&gt;match&lt;/code&gt; exhaustiveness doesn't save you. Bugs at this layer can only be caught by unit tests (my &lt;code&gt;test_sp_context.rs&lt;/code&gt; has 58 assertions covering every legal transition plus key illegal ones).&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Actually Saved Me
&lt;/h2&gt;

&lt;p&gt;The place where &lt;code&gt;match&lt;/code&gt; exhaustiveness actually saved me was device dispatch.&lt;/p&gt;

&lt;p&gt;My hypervisor uses a &lt;code&gt;Device&lt;/code&gt; enum to enumerate all virtual devices. Every time the guest touches MMIO, a &lt;code&gt;match&lt;/code&gt; dispatches to the right implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/devices/mod.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Device&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Uart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;pl011&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtualUart&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Gicd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;gic&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtualGicd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Gicr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;gic&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtualGicr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;VirtioBlk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;virtio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;mmio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtioMmioTransport&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;virtio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;blk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtioBlk&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;VirtioNet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;virtio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;mmio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtioMmioTransport&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;virtio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;net&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtioNet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Pl031&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;pl031&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VirtualPl031&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;strong&gt;is&lt;/strong&gt; a fields-carrying enum — each variant holds the state struct for its device. No &lt;code&gt;_&lt;/code&gt; fallback on matches against it, because every variant has its own handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;MmioDevice&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Device&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Uart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Gicd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Gicr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;VirtioBlk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;VirtioNet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Pl031&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// write, contains, is_ready, ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I added &lt;code&gt;Pl031&lt;/code&gt; (PL031 RTC) for Android boot, I only touched the enum definition. The compiler immediately fired &lt;strong&gt;6 errors&lt;/strong&gt; — every site that &lt;code&gt;match&lt;/code&gt;es against &lt;code&gt;Device&lt;/code&gt; was missing the &lt;code&gt;Pl031&lt;/code&gt; arm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[E0004]: non-exhaustive patterns: `&amp;amp;Device::Pl031(_)` not covered
  --&amp;gt; src/devices/mod.rs:51:15
error[E0004]: non-exhaustive patterns: `&amp;amp;mut Device::Pl031(_)` not covered
  --&amp;gt; src/devices/mod.rs:62:15
error[E0004]: non-exhaustive patterns: `&amp;amp;Device::Pl031(_)` not covered
  --&amp;gt; src/devices/mod.rs:73:15
// ... 6 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those were helper methods I'd written when adding &lt;code&gt;VirtioNet&lt;/code&gt; and &lt;strong&gt;completely forgotten about&lt;/strong&gt;. Had I used C &lt;code&gt;switch&lt;/code&gt; without &lt;code&gt;-Wswitch-enum&lt;/code&gt; (which Linux kernel and TF-A both enable by default), those two sites would silently fall into &lt;code&gt;default&lt;/code&gt; and return "unknown device." The guest would do any MMIO to the RTC, fail to find a device, and hang mid-boot with an error pointing somewhere completely unrelated.&lt;/p&gt;

&lt;p&gt;C with &lt;code&gt;-Wswitch-enum&lt;/code&gt; + &lt;code&gt;-Werror&lt;/code&gt; gives you the same check — the relevant difference is that Rust makes it a precondition for compiling instead of a build-system setting you can drop. Worth more in a solo project, less in a shop with a strict style guide.&lt;/p&gt;

&lt;p&gt;Either way, the compiler caught this bug instead of the guest doing so at boot time.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Exhaustive Match Actually Pays Off
&lt;/h2&gt;

&lt;p&gt;Reviewing this state-machine extension + &lt;code&gt;Device&lt;/code&gt; extension, here's my distilled rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhaustive match saves you&lt;/strong&gt;: &lt;strong&gt;fields-carrying enum + every variant has independent handler logic&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Device::{Uart, Gicd, ..., Pl031}&lt;/code&gt; — each device's &lt;code&gt;read/write&lt;/code&gt; is totally different&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MmioAccess::{Read { reg, size }, Write { reg, size, val }}&lt;/code&gt; — read vs write semantics differ&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExitReason::{HvcCall, SmcCall, DataAbort, WfiWfe, ...}&lt;/code&gt; — each exception class has its own handler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common trait: &lt;strong&gt;adding a variant potentially leaves gaps across the entire codebase&lt;/strong&gt;, and each gap's correct implementation is non-trivial (not just "error vs OK" binary output).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhaustive match doesn't help&lt;/strong&gt;: &lt;strong&gt;simple tag enum + cartesian-product check&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State machine &lt;code&gt;(from, to)&lt;/code&gt; transition table — N² explosion, &lt;code&gt;_ =&amp;gt; false&lt;/code&gt; is more readable&lt;/li&gt;
&lt;li&gt;Permission matrix &lt;code&gt;(user_role, action)&lt;/code&gt; — same&lt;/li&gt;
&lt;li&gt;Input sanity check &lt;code&gt;match(input) { valid_range =&amp;gt; ..., _ =&amp;gt; reject }&lt;/code&gt; — tautological&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These scenarios are "enumerate a small set of legal cases, reject everything else." &lt;code&gt;_ =&amp;gt; fallback&lt;/code&gt; loses no information — it's &lt;em&gt;more&lt;/em&gt; readable.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;#[repr(u8)]&lt;/code&gt; is everyday life in hypervisor/kernel/driver code. Don't apologize for the atomic trade-off.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time a "Rust state machine" tweet appears, someone in the replies recommends typestate. Typestate is genuinely powerful when transitions happen through owning APIs (&lt;code&gt;File::open → Handle&amp;lt;Open&amp;gt;&lt;/code&gt;), but it doesn't compose with shared mutable state across CPUs — the entire point of &lt;code&gt;AtomicU8&lt;/code&gt; is that multiple cores hold a reference to one byte. Typestate requires owning &lt;code&gt;self&lt;/code&gt; by value to consume the old state; a multi-CPU SPMC can't do that on the fast path. Not a rejection of typestate, just the wrong tool for this edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;_ =&amp;gt; fallback&lt;/code&gt; isn't a sin, but ask yourself every time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"If I add a new variant in the future, should this site force me to update it?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes → drop the &lt;code&gt;_&lt;/code&gt;, enumerate every variant&lt;/li&gt;
&lt;li&gt;No (illegal state-machine pair, MMIO unknown-offset) → &lt;code&gt;_ =&amp;gt; default&lt;/code&gt; is documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. State-machine correctness is never a gift from Rust. It's a gift from tests + documentation + code review.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;test_sp_context.rs&lt;/code&gt; has dedicated tests for every legal transition, a bunch of illegal ones, and CAS races. Rust didn't generate those; I wrote them. Rust saved me from some defensive code (no "sixth value" of &lt;code&gt;SpState&lt;/code&gt; — &lt;code&gt;try_from_u8&lt;/code&gt; rejects it), but whether the legal-transition table is &lt;em&gt;correct&lt;/em&gt;, Rust has no opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What really saves you is "fields-carrying enum + each variant has its own handler."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's Rust's signature strength. Find the places in your codebase that fit this pattern and get them right — it pays more than agonizing over whether the state machine should be typestate-ified.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;My hypervisor isn't a "zero-unwrap" project. The repo has about 6 &lt;code&gt;unwrap()&lt;/code&gt; calls (concentrated in test fixtures and boot-time paths that can't reasonably panic) and 45 &lt;code&gt;_ =&amp;gt; default&lt;/code&gt; fallback arms (mostly in MMIO register decode for unknown offsets).&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;unwrap()&lt;/code&gt; and &lt;code&gt;_ =&amp;gt;&lt;/code&gt; was a decision at the time, not laziness. Engineering beats slogans.&lt;/p&gt;

&lt;p&gt;Rust gives you a good weapon. It doesn't think for you. Whether the state-transition table is legal is in &lt;em&gt;your&lt;/em&gt; head, not the compiler's.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;: &lt;a href="https://github.com/willamhou/hypervisor" rel="noopener noreferrer"&gt;github.com/willamhou/hypervisor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blog&lt;/strong&gt;: &lt;a href="https://willamhou.github.io/hypervisor/" rel="noopener noreferrer"&gt;willamhou.github.io/hypervisor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is part 5 of the ARM64 Hypervisor development series. The Chinese version is the canonical source — see &lt;a href="https://github.com/willamhou/hypervisor/blob/main/docs/zhihu/part5-enum-state-machine.md" rel="noopener noreferrer"&gt;part5-enum-state-machine.md&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>embedded</category>
      <category>arm</category>
      <category>systems</category>
    </item>
    <item>
      <title>k8s4claw: A Kubernetes Operator for Managing AI Agent Runtimes</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Tue, 21 Apr 2026 05:08:42 +0000</pubDate>
      <link>https://forem.com/willamhou/k8s4claw-a-kubernetes-operator-for-managing-ai-agent-runtimes-3anm</link>
      <guid>https://forem.com/willamhou/k8s4claw-a-kubernetes-operator-for-managing-ai-agent-runtimes-3anm</guid>
      <description>&lt;p&gt;Every AI agent framework has its own deployment story. Claude-based assistants run one way, OpenAI agents another, security-focused runtimes yet another. If you run more than one on Kubernetes, you end up writing the same boilerplate over and over: secret management, persistent storage, graceful updates, inter-service messaging, observability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;k8s4claw&lt;/strong&gt; is an open-source Kubernetes operator that wraps all of this behind a single CRD. You describe &lt;em&gt;what&lt;/em&gt; the agent is, it handles &lt;em&gt;how&lt;/em&gt; it runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claw.prismer.ai/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Claw&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;research-agent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openclaw&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4"&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;secretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm-api-keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operator reconciles this into a StatefulSet, headless Service, ConfigMap, ServiceAccount, PodDisruptionBudget, and optionally NetworkPolicy and Ingress. When you add a channel (Slack, Discord, Webhook), it also wires up sidecars and a local message bus.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture, shows how to get it running locally, and explains the design decisions behind the IPC bus, the auto-update controller, and the runtime adapter system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;We had several agent runtimes in flight at once — different languages, different process models, different resource profiles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;TypeScript/Node.js&lt;/td&gt;
&lt;td&gt;Full-featured AI assistant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NanoClaw&lt;/td&gt;
&lt;td&gt;TypeScript/Node.js&lt;/td&gt;
&lt;td&gt;Lightweight personal assistant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZeroClaw&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;High-performance agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PicoClaw&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Ultra-minimal serverless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IronClaw&lt;/td&gt;
&lt;td&gt;Rust + WASM&lt;/td&gt;
&lt;td&gt;Security-focused agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HermesClaw&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Conversational with tool use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K8sOps&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Cluster self-healing (claw4k8s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each had its own Helm chart, sidecar layout, and update strategy. Adding a Slack channel meant editing several files. Rotating credentials meant touching every deployment. Rolling back a bad update was a manual process.&lt;/p&gt;

&lt;p&gt;We wanted one control plane for all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TB
    subgraph "Kubernetes Cluster"
        OP[k8s4claw Operator]

        subgraph "Claw Pod (with channels)"
            INIT["claw-init"]
            RT["Runtime Container"]
            IPC["IPC Bus Sidecar"]
            CH["Channel Sidecar"]
        end

        STS[StatefulSet]
        SVC[Service]
        CM[ConfigMap]
        PVC[(PVCs)]

        OP --&amp;gt;|manages| STS
        OP --&amp;gt;|manages| SVC
        OP --&amp;gt;|manages| CM
        STS -.-&amp;gt;|runs| RT
        STS -.-&amp;gt;|runs| IPC
        STS -.-&amp;gt;|runs| CH

        CH &amp;lt;--&amp;gt;|UDS| IPC
        IPC &amp;lt;--&amp;gt;|Bridge| RT
    end

    EXT["Slack / Discord / Webhook"]
    CH &amp;lt;--&amp;gt;|API| EXT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operator watches &lt;code&gt;Claw&lt;/code&gt; custom resources and reconciles a full stack of Kubernetes objects. A minimal agent (no channels, no persistence) gets just the runtime container plus &lt;code&gt;claw-init&lt;/code&gt;. If you declare any channels in &lt;code&gt;spec.channels&lt;/code&gt;, the operator also injects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;claw-init&lt;/strong&gt; — an init container that merges default runtime config with any user overrides before the runtime starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime container&lt;/strong&gt; — the actual AI agent binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPC Bus sidecar&lt;/strong&gt; (only when channels are present) — a WAL-backed message router that sits between the runtime and the channel sidecars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel sidecar(s)&lt;/strong&gt; — one per referenced &lt;code&gt;ClawChannel&lt;/code&gt; (Slack, Discord, Webhook today).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a second CRD, &lt;code&gt;ClawChannel&lt;/code&gt;, that describes how to connect to an external system. Channels are defined once and referenced by many Claws.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 1.28+ (or &lt;a href="https://kind.sigs.k8s.io/" rel="noopener noreferrer"&gt;kind&lt;/a&gt; for local development)&lt;/li&gt;
&lt;li&gt;Go 1.25+&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;controller-gen&lt;/code&gt; (&lt;code&gt;go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest&lt;/code&gt;) — needed by &lt;code&gt;make install&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Install and run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Prismer-AI/k8s4claw.git
&lt;span class="nb"&gt;cd &lt;/span&gt;k8s4claw

&lt;span class="c"&gt;# Install CRDs into the active cluster&lt;/span&gt;
make &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Run the operator locally against your current kubeconfig.&lt;/span&gt;
&lt;span class="c"&gt;# --disable-webhooks lets you skip cert-manager setup during local dev.&lt;/span&gt;
&lt;span class="c"&gt;# In-cluster deployments should leave webhooks enabled.&lt;/span&gt;
go run ./cmd/operator/ &lt;span class="nt"&gt;--disable-webhooks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create your first agent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic llm-api-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-xxx

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: claw.prismer.ai/v1alpha1
kind: Claw
metadata:
  name: my-agent
spec:
  runtime: openclaw
  config:
    model: "claude-sonnet-4"
  credentials:
    secretRef:
      name: llm-api-keys
  persistence:
    session:
      enabled: true
      size: 2Gi
      mountPath: /data/session
    workspace:
      enabled: true
      size: 10Gi
      mountPath: /workspace
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;kubectl get claw my-agent &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connect Slack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claw.prismer.ai/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClawChannel&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-slack&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bidirectional&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;secretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack-bot-token&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A0123456789"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference it from your &lt;code&gt;Claw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-slack&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bidirectional&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the next reconcile the operator injects a Slack sidecar, spins up the IPC bus sidecar, and wires them together. The runtime container does not need to know anything about Slack — it just talks to the bus.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive: The IPC Bus
&lt;/h2&gt;

&lt;p&gt;The IPC bus is the most interesting piece of k8s4claw. It is a Kubernetes &lt;a href="https://kubernetes.io/blog/2023/08/25/native-sidecar-containers/" rel="noopener noreferrer"&gt;native sidecar&lt;/a&gt; (an init container with &lt;code&gt;restartPolicy: Always&lt;/code&gt;) that routes JSON messages between channel sidecars and the agent runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel Sidecar ──UDS──► IPC Bus ──Bridge──► Runtime Container
                         │ WAL  │
                         │ DLQ  │
                         │ Ring │
                         └──────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why not just HTTP?
&lt;/h3&gt;

&lt;p&gt;We tried. The problem is reliability. When a Slack event arrives while the runtime is overloaded, you need somewhere to buffer it. If the runtime crashes mid-response, you need to redeliver. When a channel sidecar falls behind, you need backpressure instead of dropped messages.&lt;/p&gt;

&lt;p&gt;Three mechanisms do the work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write-Ahead Log (WAL)&lt;/strong&gt; — Every inbound message is appended to a WAL on &lt;code&gt;emptyDir&lt;/code&gt; before delivery. On restart, unacknowledged messages are replayed. Periodic compaction keeps the file bounded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dead Letter Queue (DLQ)&lt;/strong&gt; — Messages that exceed the retry limit land in a BoltDB-backed DLQ instead of being dropped silently. You can inspect them later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ring buffer with backpressure&lt;/strong&gt; — A fixed-size circular buffer with configurable high/low watermarks. Crossing the high watermark sends &lt;code&gt;slow_down&lt;/code&gt; upstream; draining to the low watermark sends &lt;code&gt;resume&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bridge protocols
&lt;/h3&gt;

&lt;p&gt;Different runtimes speak different wire protocols. The bus abstracts this behind a &lt;code&gt;RuntimeBridge&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Bridge&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;Full-duplex JSON over WS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NanoClaw&lt;/td&gt;
&lt;td&gt;UDS&lt;/td&gt;
&lt;td&gt;Length-prefix framed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZeroClaw&lt;/td&gt;
&lt;td&gt;SSE&lt;/td&gt;
&lt;td&gt;HTTP POST + Server-Sent Events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PicoClaw&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Length-prefix framed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the actual interface (&lt;a href="https://github.com/Prismer-AI/k8s4claw/blob/main/internal/ipcbus/bridge.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/ipcbus/bridge.go&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RuntimeBridge&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a new transport means implementing these four methods.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive: Auto-Update Controller
&lt;/h2&gt;

&lt;p&gt;The auto-update controller polls OCI registries on a cron schedule, filters new tags by a semver constraint, and performs health-verified rollouts with automatic rollback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;autoUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;versionConstraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^1.x"&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;healthTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;
    &lt;span class="na"&gt;maxRollbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt; — on each cron tick, list tags from the registry and filter by the semver constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initiate&lt;/strong&gt; — annotate the &lt;code&gt;Claw&lt;/code&gt; with the target image and transition into the &lt;code&gt;HealthCheck&lt;/code&gt; phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health check&lt;/strong&gt; — watch the StatefulSet readiness until all replicas are ready or the timeout fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success&lt;/strong&gt; — update status, clear the annotation, schedule the next cron tick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout&lt;/strong&gt; — roll back to the previous image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker&lt;/strong&gt; — after N consecutive rollbacks, stop trying and emit an event plus a Prometheus metric.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The state machine lives in annotations and status conditions, so it survives operator restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"claw.prismer.ai/update-phase"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"HealthCheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconcileHealthCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Version history
&lt;/h3&gt;

&lt;p&gt;Every attempt is recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;autoUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;currentVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.2.0"&lt;/span&gt;
    &lt;span class="na"&gt;versionHistory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.2.0"&lt;/span&gt;
        &lt;span class="na"&gt;appliedAt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-28T03:00:00Z"&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Healthy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.1.5"&lt;/span&gt;
        &lt;span class="na"&gt;appliedAt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-21T03:00:00Z"&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RolledBack&lt;/span&gt;
    &lt;span class="na"&gt;failedVersions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.1.5"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;circuitOpen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Runtime Adapter Pattern
&lt;/h2&gt;

&lt;p&gt;Each runtime is a Go struct implementing &lt;code&gt;RuntimeAdapter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RuntimeAdapter&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Pod shape&lt;/span&gt;
    &lt;span class="n"&gt;PodTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PodTemplateSpec&lt;/span&gt;
    &lt;span class="n"&gt;HealthProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Probe&lt;/span&gt;
    &lt;span class="n"&gt;ReadinessProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Probe&lt;/span&gt;
    &lt;span class="n"&gt;DefaultConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;RuntimeConfig&lt;/span&gt;
    &lt;span class="n"&gt;GracefulShutdownSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt;

    &lt;span class="c"&gt;// Spec validation&lt;/span&gt;
    &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClawSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorList&lt;/span&gt;
    &lt;span class="n"&gt;ValidateUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldSpec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newSpec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClawSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorList&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new adapter typically lives in a single file of ~100 lines. The shared &lt;code&gt;BuildPodTemplate&lt;/code&gt; helper handles init containers, volume mounts, security context, and environment variables, so the adapter only declares what is actually different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MyRuntimeAdapter&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MyRuntimeAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PodTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PodTemplateSpec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BuildPodTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;RuntimeSpec&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;"my-registry/my-runtime:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainerPort&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContainerPort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"100m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"256Mi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"500m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"512Mi"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// plus HealthProbe, ReadinessProbe, DefaultConfig, GracefulShutdownSeconds,&lt;/span&gt;
&lt;span class="c"&gt;// Validate, ValidateUpdate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation is per-runtime on purpose. OpenClaw and IronClaw require credentials because they call LLM APIs. ZeroClaw and PicoClaw permit credential-less operation. HermesClaw rejects &lt;code&gt;spec.channels&lt;/code&gt; because it brings its own gateway. NanoClaw currently has no update-time persistence checks. The point is each adapter owns its own rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Go SDK
&lt;/h2&gt;

&lt;p&gt;For programmatic access there is a Go SDK (&lt;a href="https://github.com/Prismer-AI/k8s4claw/tree/main/sdk" rel="noopener noreferrer"&gt;&lt;code&gt;sdk/&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/Prismer-AI/k8s4claw/sdk"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// uses the ambient kubeconfig by default&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClawSpec&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenClaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"MODEL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"claude-sonnet-4"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Block until the Claw reaches phase "Running" or ctx expires.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitForReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a channel SDK for writing custom sidecars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/Prismer-AI/k8s4claw/sdk/channel"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithChannelName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-channel"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;// or set CHANNEL_NAME env&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithSocketPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/var/run/claw/bus.sock"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithBufferSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// Send a message to the runtime.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`{"text":"Hello"}`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Receive returns a channel of *InboundMessage.&lt;/span&gt;
&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// handle msg&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;The repo has reasonable test coverage on the core packages. A recent local run looked roughly like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Coverage (approx.)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internal/webhook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internal/runtime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internal/registry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sdk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internal/controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sdk/channel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internal/ipcbus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Numbers move PR by PR. CI publishes a coverage report as an artifact and gates on a total-coverage threshold; there is no per-package floor enforced today. Treat the table as a snapshot, not a contract.&lt;/p&gt;

&lt;p&gt;The testing pyramid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; — pure functions, table-driven, &lt;code&gt;t.Parallel()&lt;/code&gt; everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake-client tests&lt;/strong&gt; — &lt;code&gt;fake.NewClientBuilder()&lt;/code&gt; for controller logic without a real cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;envtest integration tests&lt;/strong&gt; — real etcd + API server for webhook validation and reconcile loops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The auto-update controller uses dependency injection via &lt;code&gt;Clock&lt;/code&gt; and &lt;code&gt;TagLister&lt;/code&gt; interfaces so time-dependent and registry-dependent code is fully testable with no network calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Not Done Yet
&lt;/h2&gt;

&lt;p&gt;Worth being honest about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;custom&lt;/code&gt; runtime type&lt;/strong&gt; is present in the CRD enum but no adapter is registered. If you want a runtime that is not in the built-in list today, you fork and add an adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HermesClaw&lt;/strong&gt; does not yet integrate with the k8s4claw channel sidecars — it uses its own gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local operator runs&lt;/strong&gt; need &lt;code&gt;--disable-webhooks&lt;/code&gt; unless you've set up cert-manager or your own TLS. In-cluster deployments via the Helm chart handle this for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRD surface is larger than just &lt;code&gt;Claw&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;ClawChannel&lt;/code&gt;, &lt;code&gt;ClawSelfConfig&lt;/code&gt;, and related types are part of the contract. "Single CRD" is a simplification; "small, focused set of CRDs" is closer to the truth.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;k8s4claw is open source under Apache-2.0. The current open contribution target is &lt;a href="https://github.com/Prismer-AI/k8s4claw/issues/4" rel="noopener noreferrer"&gt;Issue #4: add snapshot and PDB envtest coverage&lt;/a&gt;. If you want to propose something else, open a new issue and we'll triage it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Prismer-AI/k8s4claw" rel="noopener noreferrer"&gt;github.com/Prismer-AI/k8s4claw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run AI agents on Kubernetes and you're tired of maintaining the plumbing around them, give it a try. Star the repo if it helps, and open an issue if something is off — both signals are useful.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Add Tamper-Evident Audit Trails to Your LangChain Agent</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Mon, 20 Apr 2026 02:16:33 +0000</pubDate>
      <link>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-langchain-agent-3onc</link>
      <guid>https://forem.com/willamhou/how-to-add-tamper-evident-audit-trails-to-your-langchain-agent-3onc</guid>
      <description>&lt;p&gt;Your LangChain agent calls tools. It searches the web, reads files, queries databases, calls APIs. But can you &lt;em&gt;prove&lt;/em&gt; what it did?&lt;/p&gt;

&lt;p&gt;Logs capture what happened. Cryptographic receipts prove it. The difference matters when an auditor, a customer, or a regulator asks "show me exactly what the agent did and prove it wasn't altered after the fact."&lt;/p&gt;

&lt;p&gt;This tutorial adds Ed25519-signed, hash-chained audit trails to a LangChain agent in under 5 minutes. No external service, no API keys, no infrastructure. Everything verifies offline with a public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;A LangChain agent where every tool call produces a signed receipt containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt;: which tool was called, with what parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt;: the agent's Ed25519 public key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt;: timestamp&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof&lt;/strong&gt;: Ed25519 signature over JCS-canonicalized (RFC 8785) payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain&lt;/strong&gt;: SHA-256 hash linking to the previous receipt (tamper-evident ordering)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anyone modifies a receipt after the fact, the signature breaks. If anyone deletes or reorders receipts, the hash chain breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth[langchain] langchain-openai langchain-community
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Create a signing identity
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;

&lt;span class="c1"&gt;# Creates an Ed25519 keypair, stored locally in ~/.signet/
# If the key already exists, just load it:
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-langchain-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-langchain-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Public key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No key server, no certificate authority. The private key stays on disk, the public key is what verifiers use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the signing callback
&lt;/h2&gt;

&lt;p&gt;Signet ships a LangChain callback handler that signs every tool call automatically. Two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth.langchain&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignetCallbackHandler&lt;/span&gt;

&lt;span class="n"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SignetCallbackHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handler signs the full tool lifecycle: &lt;code&gt;on_tool_start&lt;/code&gt; (what was called), &lt;code&gt;on_tool_end&lt;/code&gt; (what it returned, hashed), and &lt;code&gt;on_tool_error&lt;/code&gt; (what went wrong). If signing fails, the handler logs a warning and lets the agent continue. It never crashes your chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Wire it into your agent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DuckDuckGoSearchRun&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_react_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# Standard LangChain setup
&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DuckDuckGoSearchRun&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hwchase17/react&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create and run agent with signing callback
&lt;/span&gt;&lt;span class="n"&gt;agent_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;create_react_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent_executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the weather in Tokyo?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool call now produces a signed receipt. No code changes to the tools themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Inspect the receipts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receipts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Receipt #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Tool:       &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Params hash: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;params_hash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Signature:   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sig&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Timestamp:   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receipt #1
  Tool:       duckduckgo_search
  Params hash: sha256:a1b2c3...
  Signature:   ed25519:Mz4xNTk2NjQ0NDgw...
  Timestamp:   2026-04-19T10:30:00Z

Receipt #2
  Tool:       _tool_end
  Params hash: sha256:d4e5f6...
  Signature:   ed25519:Nk5yODk3MjE1Njg4...
  Timestamp:   2026-04-19T10:30:01Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the start/end pair: the first receipt captures the tool call, the second captures a hash of the output. Together they prove what was called &lt;em&gt;and&lt;/em&gt; what it returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify a receipt
&lt;/h2&gt;

&lt;p&gt;Anyone with the public key can verify, offline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;

&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receipts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valid: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tamper with any field and verification fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evil_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# tamper
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;
&lt;span class="n"&gt;tampered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tampered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Verify the audit chain
&lt;/h2&gt;

&lt;p&gt;The audit log is a hash-chained JSONL file. Each entry's hash covers the previous entry, so deleting or reordering receipts breaks the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_signet_dir&lt;/span&gt;

&lt;span class="n"&gt;signet_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;default_signet_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;chain_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signet_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chain intact: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chain_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entries: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chain_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without Signet&lt;/th&gt;
&lt;th&gt;With Signet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"The agent called web_search" (log entry)&lt;/td&gt;
&lt;td&gt;Ed25519 signature proving it, verifiable by anyone with the public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs can be edited after the fact&lt;/td&gt;
&lt;td&gt;Signature breaks if any field is modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ordering proof&lt;/td&gt;
&lt;td&gt;Hash chain breaks if receipts are deleted or reordered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust the operator's logs&lt;/td&gt;
&lt;td&gt;Verify independently, offline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When you need this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regulated industries&lt;/strong&gt;: EU AI Act Article 12 requires "automatic recording" of AI system activities. Signed receipts satisfy this with cryptographic proof, not just logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise deployments&lt;/strong&gt;: When the question is "can you prove what the agent did?", signed receipts are the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-to-agent&lt;/strong&gt;: When Agent B needs to verify what Agent A actually did before acting on its output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident response&lt;/strong&gt;: After something goes wrong, tamper-evident receipts let you reconstruct exactly what happened without trusting anyone's claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bilateral co-signing&lt;/strong&gt;: Have both the agent and the tool server sign each interaction independently. Neither party can fabricate receipts. See &lt;code&gt;signet proxy&lt;/code&gt; for MCP integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy attestation&lt;/strong&gt;: Evaluate YAML policy rules and include the decision (allow/deny/require_approval) inside the signed receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chains&lt;/strong&gt;: Prove that Agent A was authorized by Human B to perform a specific action with scoped constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are in &lt;code&gt;signet-auth&lt;/code&gt; today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Prismer-AI/signet&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Signet is open source (Apache 2.0). Rust core with Python and TypeScript bindings. No external service, no API keys.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>python</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>Claude Managed Agents Has Built-in Tracing. Here's What It Can't Do.</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Tue, 14 Apr 2026 09:52:23 +0000</pubDate>
      <link>https://forem.com/willamhou/claude-managed-agents-has-built-in-tracing-heres-what-it-cant-do-35g</link>
      <guid>https://forem.com/willamhou/claude-managed-agents-has-built-in-tracing-heres-what-it-cant-do-35g</guid>
      <description>&lt;h1&gt;
  
  
  Claude Managed Agents Has Built-in Tracing. Here's What It Can't Do.
&lt;/h1&gt;

&lt;p&gt;Anthropic shipped &lt;a href="https://claude.com/blog/claude-managed-agents" rel="noopener noreferrer"&gt;Claude Managed Agents&lt;/a&gt; last week. The pitch: production-grade agents with sandboxing, scoped permissions, and session tracing — built in, no setup required.&lt;/p&gt;

&lt;p&gt;The tracing feature specifically: "Session tracing, integration analytics, and troubleshooting guidance are built directly into the Claude Console, so you can inspect every tool call, decision, and failure mode."&lt;/p&gt;

&lt;p&gt;This is genuinely useful. If you're debugging a multi-step agent workflow, having every tool call logged in a console is miles better than parsing stderr.&lt;/p&gt;

&lt;p&gt;But there's a distinction worth making — one that matters in exactly the situations where it matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Anthropic Recorded It" vs. "You Can Prove It"
&lt;/h2&gt;

&lt;p&gt;Claude Managed Agents is cloud-hosted. The tracing data lives in Claude Console, on Anthropic's infrastructure.&lt;/p&gt;

&lt;p&gt;That means the audit trail is: &lt;strong&gt;Anthropic says this happened.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most debugging use cases, that's fine. You trust Anthropic. They trust you. The logs are accurate. Nobody is lying.&lt;/p&gt;

&lt;p&gt;But consider the situations where audit trails actually get pulled:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your agent made an unauthorized transfer.&lt;/strong&gt; The question isn't "what does the console say" — it's "can you prove, to a third party, that the agent executed this action with these parameters at this time, and that this record hasn't been modified?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A compliance audit.&lt;/strong&gt; SOC 2, HIPAA, GDPR. The auditor asks for evidence of agent actions on sensitive data. "Here are logs from Anthropic's console" is not the same as "here is a cryptographically signed chain of records that I hold and you can independently verify."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An incident investigation.&lt;/strong&gt; After a breach, forensic investigators need evidence that is tamper-evident and independently verifiable. If the evidence lives on the infrastructure that may have been compromised — or that a vendor controls — its integrity cannot be assumed.&lt;/p&gt;

&lt;p&gt;The distinction isn't about trust in Anthropic. It's about the difference between &lt;strong&gt;a record&lt;/strong&gt; and &lt;strong&gt;evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cryptographic Signing Adds
&lt;/h2&gt;

&lt;p&gt;A signed audit trail works differently.&lt;/p&gt;

&lt;p&gt;Each tool call generates a receipt: the action, the parameters, the timestamp, the agent identity — all hashed and signed with the agent's private Ed25519 key. Receipts chain together: each receipt includes the hash of the previous one. Modifying any record breaks the chain. Deleting a record is detectable.&lt;/p&gt;

&lt;p&gt;The key difference: &lt;strong&gt;you hold the proof, not a vendor.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurement-bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ops-team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketplace_purchase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPU-A100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# This receipt is a cryptographic artifact.
# You hold it. Anthropic doesn't.
# Any third party can verify it without contacting anyone.
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an auditor asks "prove this agent executed this action with these parameters," you hand them the receipt and the public key. They verify it offline. No Anthropic console access required. No vendor dependency in the evidence chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Gaps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Vendor-held vs. self-held evidence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Managed Agents tracing: logs live in Claude Console. Anthropic controls the data.&lt;/p&gt;

&lt;p&gt;Signed receipts: cryptographic artifacts you hold locally. No third party in the verification chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Log integrity vs. cryptographic integrity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Managed Agents: session logs. Accurate under normal conditions. But a log file — even a well-managed one — can be modified. There's no mechanism in a standard log that makes tampering detectable after the fact.&lt;/p&gt;

&lt;p&gt;Signed receipts: hash-chained. Tamper with any entry and the chain breaks. Detect deletions. Detect reordering. The integrity guarantee is mathematical, not administrative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Single-party vs. bilateral proof&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Managed Agents: Anthropic logs what happened on their infrastructure.&lt;/p&gt;

&lt;p&gt;Bilateral signing (Signet v0.4+): the agent signs the request, the server independently signs the response. One tamper-evident record, two signatures, two trust domains. Rewriting the chain requires compromising both keys on separate machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Managed Agents Does Well
&lt;/h2&gt;

&lt;p&gt;To be clear about what this is not: this is not a criticism of Managed Agents as a product.&lt;/p&gt;

&lt;p&gt;For developers building Claude-based agents who need to go to production quickly, Managed Agents is a compelling offer. Sandboxing, authentication, session persistence, scoped permissions, multi-agent coordination — real infrastructure problems, solved. The tracing in Console is useful for development and operational debugging.&lt;/p&gt;

&lt;p&gt;The gaps above only matter in specific contexts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulated industries (finance, healthcare, legal) where audit evidence must be third-party verifiable&lt;/li&gt;
&lt;li&gt;Incident response and forensics where evidence integrity must be demonstrable&lt;/li&gt;
&lt;li&gt;Enterprise compliance where "trust the vendor" isn't an accepted audit answer&lt;/li&gt;
&lt;li&gt;Cross-vendor or multi-agent workflows where a single vendor doesn't control the full chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For consumer applications, hobby projects, or internal tools where you trust Anthropic and compliance requirements are light: Managed Agents tracing is probably sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complementary Stack
&lt;/h2&gt;

&lt;p&gt;Managed Agents and signed audit trails aren't competitors. They operate at different layers.&lt;/p&gt;

&lt;p&gt;Managed Agents handles: infrastructure, sandboxing, session management, permission scoping, operational tracing.&lt;/p&gt;

&lt;p&gt;Signed receipts handle: cryptographic proof of what happened, independently verifiable by any third party, held by you, not a vendor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; works with Managed Agents. Claude Managed Agents uses MCP to connect to external tools — Signet's &lt;code&gt;@signet-auth/mcp&lt;/code&gt; intercepts at the MCP transport layer and signs every tool call before it executes. The two layers stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Managed Agents
  └── MCP tool calls
        └── Signet SigningTransport  ← signs here
              └── your tool server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Console shows you what happened. The signed receipts prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Claude Managed Agents ships a real, useful tracing feature. If you're using it, your debugging workflow just got better.&lt;/p&gt;

&lt;p&gt;But "Anthropic recorded it" and "you can prove it" are different claims. In the situations where audit trails matter most — compliance, incident response, regulated industries — the difference is significant.&lt;/p&gt;

&lt;p&gt;Signing is the layer that converts logs into evidence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; adds Ed25519 signing and tamper-evident audit chains to AI agent tool calls. Works with Claude Managed Agents, LangChain, CrewAI, AutoGen, and 7 other frameworks. Apache-2.0 + MIT.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Now on the official Claude Code plugin marketplace: &lt;code&gt;/plugin install signet@claude-plugins-official&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
      <category>claude</category>
    </item>
    <item>
      <title>AI Agents Can Move Money But Can't Produce Receipts</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Tue, 14 Apr 2026 03:12:41 +0000</pubDate>
      <link>https://forem.com/willamhou/ai-agents-can-move-money-but-cant-produce-receipts-3ong</link>
      <guid>https://forem.com/willamhou/ai-agents-can-move-money-but-cant-produce-receipts-3ong</guid>
      <description>&lt;h1&gt;
  
  
  AI Agents Can Move Money But Can't Produce Receipts
&lt;/h1&gt;

&lt;p&gt;In March 2026, security researchers disclosed &lt;a href="https://medium.com/effortless-programming/zombieclaw-the-ai-botnet-nobody-is-talking-about-04b0dbf5ed1b" rel="noopener noreferrer"&gt;ZombieClaw&lt;/a&gt; — a botnet recruiting compromised AI agent instances. Over 30,000 instances were &lt;a href="https://securityscorecard.com/blog/how-exposed-openclaw-deployments-turn-agentic-ai-into-an-attack-surface/" rel="noopener noreferrer"&gt;found exposed&lt;/a&gt; with default configurations. Reported losses reached &lt;a href="https://www.techbuzz.ai/articles/openclaw-s-ai-marketplace-infected-with-crypto-stealing-malware" rel="noopener noreferrer"&gt;up to $16 million&lt;/a&gt; in cryptocurrency. Hundreds of malicious skills were distributed through ClawHub (&lt;a href="https://www.koi.ai/blog/clawhavoc-341-malicious-clawedbot-skills-found-by-the-bot-they-were-targeting" rel="noopener noreferrer"&gt;341 initially identified by Koi&lt;/a&gt;, with &lt;a href="https://blog.virustotal.com/2026/02/from-automation-to-infection-how.html" rel="noopener noreferrer"&gt;more found by VirusTotal&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.kaspersky.com/blog/openclaw-vulnerabilities-exposed/55263/" rel="noopener noreferrer"&gt;Kaspersky found 512 vulnerabilities&lt;/a&gt;, eight critical. &lt;a href="https://businessinsights.bitdefender.com/technical-advisory-openclaw-exploitation-enterprise-networks" rel="noopener noreferrer"&gt;Bitdefender&lt;/a&gt;, VirusTotal, Sophos, and &lt;a href="https://www.oasis.security/blog/openclaw-vulnerability" rel="noopener noreferrer"&gt;Oasis Security&lt;/a&gt; all published analyses.&lt;/p&gt;

&lt;p&gt;But here's what nobody is talking about: &lt;strong&gt;after the attack, there is no cryptographic proof of what any compromised agent actually did.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No signed records. No tamper-evident logs. No way to distinguish "the agent executed &lt;code&gt;transfer_eth()&lt;/code&gt; because the user asked" from "the agent executed &lt;code&gt;transfer_eth()&lt;/code&gt; because a prompt injection rewrote its instructions."&lt;/p&gt;

&lt;p&gt;The text logs exist, sure. But text logs can be edited, deleted, or fabricated. When $16M is missing, "trust the logs" is not a forensic standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Forensics Problem
&lt;/h2&gt;

&lt;p&gt;When a traditional server gets compromised, incident response teams have tools: immutable audit logs, signed system events, chain-of-custody protocols. When an AI agent gets compromised, you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conversation history&lt;/strong&gt; — stored by the agent itself. The compromised agent can edit its own history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool call logs&lt;/strong&gt; — if they exist at all, they're unsigned text files. An attacker who controls the agent controls the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The agent did it"&lt;/strong&gt; — not enough for insurance claims, compliance reports, or criminal prosecution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZombieClaw exploited this gap perfectly. The attackers didn't just steal money — they operated in an environment where &lt;strong&gt;there is no verifiable evidence of what happened&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond ZombieClaw
&lt;/h2&gt;

&lt;p&gt;The AI agent security conversation focuses on prevention: sandboxing, permission systems, policy engines, skill auditing. These are important. But prevention has a 100% failure rate over time. Every system eventually gets breached.&lt;/p&gt;

&lt;p&gt;What happens after?&lt;/p&gt;

&lt;p&gt;Without cryptographic proof of agent actions, you can't answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which agent initiated the transaction?&lt;/li&gt;
&lt;li&gt;Were the parameters what the user actually approved?&lt;/li&gt;
&lt;li&gt;When exactly did the compromise begin?&lt;/li&gt;
&lt;li&gt;Was this agent's audit log tampered with after the fact?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SOC 2, HIPAA, and GDPR all require audit trails for actions on sensitive data. "The AI agent did it and we have no verifiable records" creates real gaps in compliance posture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Signed Audit Trail Would Have Changed
&lt;/h2&gt;

&lt;p&gt;If every tool call had been cryptographically signed at execution time, the ZombieClaw investigation would look different:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before compromise:&lt;/strong&gt; Signed receipts establish a baseline. Each agent has an Ed25519 identity. Every tool call is signed with the agent's key, timestamped, and chained into a tamper-evident log. The hash chain means you can't delete or reorder entries without breaking the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;During compromise:&lt;/strong&gt; The attacker takes control of the agent. If the attacker uses the agent's existing key, every malicious action is still signed — you have a record of what was executed and when. If the attacker generates a new key, the signing identity changes — the anomaly is visible in the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After compromise:&lt;/strong&gt; Forensics teams can verify the entire chain offline. They can see which actions were signed by the legitimate agent key vs. an unknown key. They can narrow down when the signing identity changed. They can verify that the log hasn't been modified after the fact.&lt;/p&gt;

&lt;p&gt;None of this is possible with unsigned text logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Doesn't Solve
&lt;/h2&gt;

&lt;p&gt;Signing is not prevention. A signed receipt that says "agent transferred 50 ETH to attacker's wallet" doesn't stop the transfer — it proves it happened.&lt;/p&gt;

&lt;p&gt;A signed audit trail doesn't solve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Malicious skills&lt;/strong&gt; — A signed record of a malicious skill executing is evidence, not a defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection&lt;/strong&gt; — The agent was tricked, not unauthorized. The signature is valid because the agent really did execute the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key compromise&lt;/strong&gt; — If the attacker steals the signing key, they can sign anything. Bilateral co-signing (where the server independently signs the receipt) mitigates this by requiring two keys from two trust domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User intent&lt;/strong&gt; — A signed receipt proves the agent executed the call, not that the user wanted it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full host compromise&lt;/strong&gt; — If the attacker owns the entire machine, they control the key and the log. Off-host anchoring (publishing chain hashes externally) is the mitigation, but it's not free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signing is the forensics layer. You still need sandboxing, permission systems, and skill auditing for prevention. But when prevention fails — and it will — you need evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap in Current Tools
&lt;/h2&gt;

&lt;p&gt;As of April 2026, most major AI agent frameworks have no cryptographic signing on tool call records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Typical audit mechanism&lt;/th&gt;
&lt;th&gt;Signed?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General-purpose agents&lt;/td&gt;
&lt;td&gt;OpenClaw, Hermes Agent&lt;/td&gt;
&lt;td&gt;Conversation logs, SQLite&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent OS&lt;/td&gt;
&lt;td&gt;OpenFang&lt;/td&gt;
&lt;td&gt;SHA-256 hash chain&lt;/td&gt;
&lt;td&gt;Hash only, no signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration frameworks&lt;/td&gt;
&lt;td&gt;LangChain, CrewAI&lt;/td&gt;
&lt;td&gt;Callbacks, event logs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenFang is the closest — they have a hash chain, which detects casual tampering. But without signatures, an attacker with database access can rewrite the entire chain and it still validates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can You Do Today
&lt;/h2&gt;

&lt;p&gt;If you're running AI agents in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sign every tool call.&lt;/strong&gt; Give each agent an Ed25519 identity and sign every action. &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; does this as a library — &lt;code&gt;pip install signet-auth&lt;/code&gt; or &lt;code&gt;npm install @signet-auth/core&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chain signed receipts.&lt;/strong&gt; Individual signatures are good. A hash-chained log of signed receipts is better — deletion and reordering become detectable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use bilateral signing when possible.&lt;/strong&gt; Agent signs the request, server signs the response. Now rewriting the chain requires compromising both keys on different machines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export chain hashes off-host.&lt;/strong&gt; Periodically publish the tip hash to an external system (git commit, append-only cloud storage, even a tweet). This anchors the chain against full-host compromise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat audit integrity as a security requirement, not a feature.&lt;/strong&gt; If your agent can move money, it needs signed receipts. Period.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Truth
&lt;/h2&gt;

&lt;p&gt;AI agents can move money, execute code, and access credentials. Most still can't produce a receipt.&lt;/p&gt;

&lt;p&gt;The next ZombieClaw is coming. The question is whether you'll have evidence when it happens.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; adds Ed25519 signing and tamper-evident audit logs to AI agent tool calls. Open source, Apache-2.0 + MIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Your MCP Server Has No Audit Trail — A Security Checklist</title>
      <dc:creator>willamhou</dc:creator>
      <pubDate>Mon, 13 Apr 2026 06:24:46 +0000</pubDate>
      <link>https://forem.com/willamhou/your-mcp-server-has-no-audit-trail-a-security-checklist-h1k</link>
      <guid>https://forem.com/willamhou/your-mcp-server-has-no-audit-trail-a-security-checklist-h1k</guid>
      <description>&lt;h1&gt;
  
  
  Your MCP Server Has No Audit Trail — A Security Checklist
&lt;/h1&gt;

&lt;p&gt;Last month, an AI agent mass-deleted a production environment. The team spent 3 days piecing together what happened — stderr logs, partial timestamps, no proof of which agent or what parameters. No audit trail.&lt;/p&gt;

&lt;p&gt;This isn't rare. Amazon Kiro deleted a prod environment. Replit's agent dropped a live database. Supabase MCP leaked tokens via prompt injection. In every case: &lt;strong&gt;zero cryptographic evidence of what happened.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MCP is becoming the standard for agent-tool communication. Claude Code, Cursor, Windsurf, and dozens of tools use it. But the MCP spec ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ No request signing&lt;/li&gt;
&lt;li&gt;❌ No audit log&lt;/li&gt;
&lt;li&gt;❌ No caller identity verification&lt;/li&gt;
&lt;li&gt;❌ No replay protection&lt;/li&gt;
&lt;li&gt;❌ No parameter integrity checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MCP server accepts any request from any process, trusts it completely, and keeps no verifiable record. Here's a practical checklist to fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Threat Model
&lt;/h2&gt;

&lt;p&gt;Before the checklist, understand what you're defending against:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parameter tampering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent sends &lt;code&gt;create_issue("fix bug")&lt;/code&gt;, something in the pipeline changes it to &lt;code&gt;delete_repo("production")&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Data loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Legitimate &lt;code&gt;deploy_to_prod&lt;/code&gt; captured and replayed 50 times&lt;/td&gt;
&lt;td&gt;Repeated side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Impersonation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rogue process sends requests claiming to be your trusted agent&lt;/td&gt;
&lt;td&gt;Unauthorized actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-server forwarding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Request intended for staging gets forwarded to production&lt;/td&gt;
&lt;td&gt;Wrong environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log tampering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text logs edited after an incident to cover tracks&lt;/td&gt;
&lt;td&gt;No incident response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compliance gap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SOC 2 / HIPAA / GDPR require audit trails; "the AI did it" is not sufficient&lt;/td&gt;
&lt;td&gt;Regulatory risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ 1. Use TLS for HTTP transports
&lt;/h3&gt;

&lt;p&gt;If your MCP server uses HTTP (SSE or Streamable HTTP), always terminate TLS. This protects data in transit but does &lt;strong&gt;not&lt;/strong&gt; protect against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compromised clients sending bad requests&lt;/li&gt;
&lt;li&gt;Replay attacks (TLS protects the pipe, not the message)&lt;/li&gt;
&lt;li&gt;Log tampering after the fact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For stdio transports (most local MCP servers), TLS doesn't apply — the attack surface is different (any local process can connect).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx example&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/mcp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Data in transit.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Doesn't cover:&lt;/strong&gt; Request integrity, identity, audit.&lt;/p&gt;


&lt;h3&gt;
  
  
  ✅ 2. Validate inputs at the boundary
&lt;/h3&gt;

&lt;p&gt;Every tool handler should validate its arguments. MCP passes arbitrary JSON — treat it like user input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// proceed...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Zod or similar for runtime validation. Never trust &lt;code&gt;args&lt;/code&gt; blindly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Malformed input, injection.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Doesn't cover:&lt;/strong&gt; Who sent it, whether it's a replay, audit trail.&lt;/p&gt;


&lt;h3&gt;
  
  
  ✅ 3. Add authentication (API keys or mTLS)
&lt;/h3&gt;

&lt;p&gt;For HTTP transports, require an API key or use mutual TLS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simple API key check&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// proceed...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For stdio, authentication is harder — any local process with access to the pipe can send requests. This is where cryptographic signing becomes necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Unauthorized callers (HTTP only).&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Doesn't cover:&lt;/strong&gt; Parameter integrity, replay, stdio auth, audit trail.&lt;/p&gt;


&lt;h3&gt;
  
  
  ✅ 4. Sign every request with cryptographic receipts
&lt;/h3&gt;

&lt;p&gt;This is the gap most MCP servers don't address. Signing binds a request to a specific agent identity and makes tampering detectable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;Signet&lt;/a&gt; adds Ed25519 signing to MCP. A signed receipt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_issue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:b878192..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp://github.local"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed25519:0CRkURt/tc6r..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deploy-bot"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-09T10:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rnd_dcd4e13579..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed25519:6KUohbnS..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tamper with any field → signature fails. Replay → nonce rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client side — sign every tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SigningTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@signet-auth/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StdioClientTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-mcp-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SigningTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Every tools/call now carries a signed receipt in params._meta._signet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receipt is injected into &lt;code&gt;_meta._signet&lt;/code&gt;. MCP servers ignore unknown fields by spec — &lt;strong&gt;zero server changes needed&lt;/strong&gt; to start signing. Works with stdio and HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server side — verify incoming signatures:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NonceCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@signet-auth/mcp-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonceCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NonceCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trustedKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ed25519:...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;// allowed agent keys&lt;/span&gt;
    &lt;span class="na"&gt;expectedTarget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp://my-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// anti-forwarding&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// 5-min freshness window&lt;/span&gt;
    &lt;span class="nx"&gt;nonceCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// replay protection&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Verified: signature valid, signer trusted, fresh, correct target&lt;/span&gt;
  &lt;span class="c1"&gt;// proceed with tool execution...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In ~50 microseconds, this checks: signature validity, signer trust, freshness, target binding, tool/params integrity, and nonce uniqueness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python (works with LangChain, CrewAI, AutoGen, or standalone):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;signet_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SigningAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devops-team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Identity, parameter integrity, replay, freshness, target binding.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Doesn't cover:&lt;/strong&gt; Preventing the action (signing is attestation, not policy).&lt;/p&gt;


&lt;h3&gt;
  
  
  ✅ 5. Keep a tamper-evident audit log
&lt;/h3&gt;

&lt;p&gt;Signing individual requests is good. Chaining them into a tamper-evident log is better. If someone deletes or reorders records, the chain breaks.&lt;/p&gt;

&lt;p&gt;Signet does this automatically — every signed receipt is appended to a SHA-256 hash-chained JSONL log at &lt;code&gt;~/.signet/audit/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record_1: { receipt, prev_hash: "sha256:0000...", record_hash: "sha256:abc1..." }
record_2: { receipt, prev_hash: "sha256:abc1...", record_hash: "sha256:def2..." }
record_3: { receipt, prev_hash: "sha256:def2...", record_hash: "sha256:ghi3..." }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query and verify from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signet audit &lt;span class="nt"&gt;--since&lt;/span&gt; 24h              &lt;span class="c"&gt;# what happened today&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--tool&lt;/span&gt; github &lt;span class="nt"&gt;--since&lt;/span&gt; 7d &lt;span class="c"&gt;# github calls this week&lt;/span&gt;
signet audit &lt;span class="nt"&gt;--verify&lt;/span&gt;                 &lt;span class="c"&gt;# verify all signatures&lt;/span&gt;
signet verify &lt;span class="nt"&gt;--chain&lt;/span&gt;                 &lt;span class="c"&gt;# check hash chain integrity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audit_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audit_verify_chain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Tamper detection, incident forensics, compliance audit.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Doesn't cover:&lt;/strong&gt; Tamper &lt;em&gt;proof&lt;/em&gt; (someone with disk access can delete the entire log; off-host anchoring is on the roadmap).&lt;/p&gt;


&lt;h3&gt;
  
  
  ✅ 6. Implement rate limiting and timeouts
&lt;/h3&gt;

&lt;p&gt;Even with signing, a compromised agent can flood your server. Add rate limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;_signet&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;callCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// per-agent limit&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rate limit exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// proceed...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And always set timeouts on tool execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  ✅ 7. Principle of least privilege
&lt;/h3&gt;

&lt;p&gt;Don't give your MCP server access to everything. Run it with minimal permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate API keys per tool (read-only key for &lt;code&gt;list_issues&lt;/code&gt;, write key for &lt;code&gt;create_issue&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Filesystem access scoped to specific directories&lt;/li&gt;
&lt;li&gt;Database user with only the required grants&lt;/li&gt;
&lt;li&gt;Network egress limited to required endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is independent of MCP — it's basic defense-in-depth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Practice&lt;/th&gt;
&lt;th&gt;Protects against&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;TLS&lt;/td&gt;
&lt;td&gt;Eavesdropping&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Input validation&lt;/td&gt;
&lt;td&gt;Injection, malformed data&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Unauthorized callers&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Request signing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tampering, replay, impersonation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3 lines&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Audit log&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Incident response, compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Automatic with signing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Denial of service&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Least privilege&lt;/td&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most MCP servers today implement 1-3 at best. Steps 4 and 5 — signing and audit — are the gap. They're also the hardest to bolt on after the fact, which is why starting with a library that handles both is worth the &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @signet-auth/core @signet-auth/mcp
&lt;span class="c"&gt;# or&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;signet-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Prismer-AI/signet" rel="noopener noreferrer"&gt;github.com/Prismer-AI/signet&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Apache-2.0 + MIT dual licensed. Open source, no SaaS, no phone-home.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your AI agent can delete a database, you should be able to prove it did.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
