<?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>DEV Community: Zira</title>
    <description>The latest articles on DEV Community by Zira (@zira125).</description>
    <link>https://dev.to/zira125</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821470%2Fe5f46063-a9b0-4a62-b149-0e284388c1ff.jpeg</url>
      <title>DEV Community: Zira</title>
      <link>https://dev.to/zira125</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zira125"/>
    <language>en</language>
    <item>
      <title>Your AI Agent State Store Needs a Schema Migration Plan</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Mon, 17 Aug 2026 15:37:01 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-state-store-needs-a-schema-migration-plan-29o7</link>
      <guid>https://dev.to/zira125/your-ai-agent-state-store-needs-a-schema-migration-plan-29o7</guid>
      <description>&lt;p&gt;An agent can survive a process restart and still lose the plot after a schema change.&lt;/p&gt;

&lt;p&gt;The dangerous migration is not the one that crashes immediately. It is the one that lets old workers read new state, or new workers silently reinterpret old state, then continue with a plausible but wrong plan.&lt;/p&gt;

&lt;p&gt;This article gives a small migration contract you can apply to SQLite, Postgres, or a document store.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Version the state, not only the database
&lt;/h2&gt;

&lt;p&gt;A database migration tells you that columns changed. It does not tell an agent how to interpret a saved run.&lt;/p&gt;

&lt;p&gt;Put an explicit schema version on every durable run, checkpoint, and pending effect:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action_state = {
  "run_id": "run_123",
  "schema_version": 3,
  "status": "WAITING_FOR_TOOL",
  "next_step": "send_report",
  "pending_effect_id": "effect_456",
  "updated_at": "2026-08-17T12:00:00Z"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Treat schema_version as part of the execution contract. A worker must reject an unsupported version instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use expand, migrate, contract
&lt;/h2&gt;

&lt;p&gt;Do not deploy a reader and writer that change meaning at the same time.&lt;/p&gt;

&lt;p&gt;Use three phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expand:&lt;/strong&gt; add fields without removing or changing old meanings. New workers can write both representations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate:&lt;/strong&gt; backfill existing records and dual-read while comparing old and new interpretations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract:&lt;/strong&gt; stop writing the old form only after the fleet and recovery tools understand the new form.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a field changing from a string to a structured value, keep the old field until the comparison window is complete:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def read_destination(row):
    new_value = row.get("destination_v2")
    old_value = row.get("destination")

    if new_value and old_value and normalize(new_value) != normalize(old_value):
        raise StateConflict(row["run_id"])

    return new_value or parse_legacy(old_value)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important behavior is the conflict, not the parser. A mismatch should stop automation and create a reviewable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Never migrate an in-flight effect by implication
&lt;/h2&gt;

&lt;p&gt;A saved agent run may be between intent and outcome. That record needs more than a new shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable effect_id and idempotency key&lt;/li&gt;
&lt;li&gt;old and new schema versions&lt;/li&gt;
&lt;li&gt;dispatch state: NOT_SENT, SENT, or UNKNOWN&lt;/li&gt;
&lt;li&gt;provider lookup information for reconciliation&lt;/li&gt;
&lt;li&gt;the policy and credential versions used at dispatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a migration sees UNKNOWN, it must preserve UNKNOWN. It must not mark the effect complete because the new schema has a default value.&lt;/p&gt;

&lt;p&gt;That distinction prevents a restart or migration from sending a duplicate email, browser mutation, or webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make rollback a read problem
&lt;/h2&gt;

&lt;p&gt;Application rollback is unsafe if the new writer has already emitted records that the old worker cannot parse. Before rollout, answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the previous binary read every record the new binary will write?&lt;/li&gt;
&lt;li&gt;Can a clean host restore the backup and replay the migration deterministically?&lt;/li&gt;
&lt;li&gt;What happens to a worker paused halfway through backfill?&lt;/li&gt;
&lt;li&gt;Can you identify records written by each version?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful deployment gate is a compatibility matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Writer&lt;/th&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;old&lt;/td&gt;
&lt;td&gt;old&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;old&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;old&lt;/td&gt;
&lt;td&gt;reject safely or pass by contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the last row is undefined, rollback is not a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test the migration like an agent failure
&lt;/h2&gt;

&lt;p&gt;Create a disposable copy of production-shaped state and inject these failures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kill a worker during dual-write.&lt;/li&gt;
&lt;li&gt;Pause a backfill after 10% of records.&lt;/li&gt;
&lt;li&gt;Change policy between read and effect dispatch.&lt;/li&gt;
&lt;li&gt;Restore a backup with mixed schema versions.&lt;/li&gt;
&lt;li&gt;Re-run the migration twice.&lt;/li&gt;
&lt;li&gt;Present an old worker with a new record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each case, assert invariants instead of only checking that the process exits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no duplicate effect for one idempotency key&lt;/li&gt;
&lt;li&gt;no record silently downgraded to a guessed default&lt;/li&gt;
&lt;li&gt;every UNKNOWN effect remains reconcilable&lt;/li&gt;
&lt;li&gt;migration is resumable and idempotent&lt;/li&gt;
&lt;li&gt;old state remains available until the contract phase&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A practical hosting check
&lt;/h2&gt;

&lt;p&gt;If your agent runs continuously, the state directory, migration lock, and backup schedule are part of the deployment surface. A managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=state-schema-migration" rel="noopener noreferrer"&gt;always-on OpenClaw hosting on Ampere&lt;/a&gt; can solve where the process runs, but it does not define your schema contract or make an unsafe migration reversible.&lt;/p&gt;

&lt;p&gt;Keep the database backup and the migration manifest together. Test a clean-host restore, then start one worker in read-only or dry-run mode before allowing effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a state change, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] every durable record has an explicit schema version&lt;/li&gt;
&lt;li&gt;[ ] old and new readers have a defined compatibility matrix&lt;/li&gt;
&lt;li&gt;[ ] expand, migrate, and contract are separate releases&lt;/li&gt;
&lt;li&gt;[ ] in-flight effects preserve UNKNOWN and idempotency keys&lt;/li&gt;
&lt;li&gt;[ ] backfill can pause, resume, and run twice safely&lt;/li&gt;
&lt;li&gt;[ ] rollback has been tested against records written by the new version&lt;/li&gt;
&lt;li&gt;[ ] a clean restore includes the migration manifest and lock state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model can produce a better plan after a migration. It cannot repair state that your runtime silently misinterpreted. Make the state contract executable first.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>testing</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your AI Agent Logs Are Not an Audit Trail Until You Test the Evidence</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Mon, 17 Aug 2026 11:36:23 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-logs-are-not-an-audit-trail-until-you-test-the-evidence-19ld</link>
      <guid>https://dev.to/zira125/your-ai-agent-logs-are-not-an-audit-trail-until-you-test-the-evidence-19ld</guid>
      <description>&lt;p&gt;An agent can print thousands of log lines and still leave you unable to answer the one question that matters after a failure: what did it actually do?&lt;/p&gt;

&lt;p&gt;A useful audit trail is not the same thing as verbose logging. Logs describe observations. An audit trail must let you prove the order of important decisions, detect missing evidence, and distinguish an attempted side effect from a confirmed one.&lt;/p&gt;

&lt;p&gt;This article shows a small evidence contract you can add around an agent runtime. It is deliberately boring: append-only events, stable IDs, a hash chain, redaction at write time, and tests that intentionally remove or reorder records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an evidence contract
&lt;/h2&gt;

&lt;p&gt;For every run, record an immutable run ID and a monotonically increasing sequence number. Wall-clock timestamps are useful for humans, but they are not safe ordering keys when workers have skewed clocks.&lt;/p&gt;

&lt;p&gt;A minimal event 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;"run_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;"run_01J..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TOOL_OUTCOME_CONFIRMED"&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;"github.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;"request_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rk_7f..."&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_digest"&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;"credential_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider_id:issue_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prev_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;"event_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="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;Keep the event vocabulary small. For a tool call, I normally need at least:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;INTENT_RECORDED: the runtime accepted a proposed action.&lt;/li&gt;
&lt;li&gt;DISPATCHED: a request crossed the provider boundary.&lt;/li&gt;
&lt;li&gt;OUTCOME_CONFIRMED: the provider returned a verifiable result.&lt;/li&gt;
&lt;li&gt;OUTCOME_UNKNOWN: the process lost certainty after dispatch.&lt;/li&gt;
&lt;li&gt;RECONCILED: a later lookup resolved UNKNOWN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not write raw prompts, cookies, bearer tokens, or full tool responses into this ledger. Redact before persistence, not in a dashboard query. A dashboard is too late if the original evidence already contains a credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a hash chain to catch silent gaps
&lt;/h2&gt;

&lt;p&gt;A hash chain does not make the log truthful. It makes tampering or omission detectable when the verifier has a trusted checkpoint.&lt;/p&gt;

&lt;p&gt;For each event, canonicalize the fields, then calculate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; event_hash = SHA256(canonical_event_without_event_hash + prev_hash)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store a signed or separately protected checkpoint at run completion and at regular intervals. The verifier should reject:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a sequence number that goes backwards or skips without an explicit gap event&lt;/li&gt;
&lt;li&gt;a prev_hash that does not match the prior record&lt;/li&gt;
&lt;li&gt;an event whose recomputed hash differs&lt;/li&gt;
&lt;li&gt;two events claiming the same sequence number&lt;/li&gt;
&lt;li&gt;a completion record without a terminal outcome for each dispatched effect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your storage is eventually consistent, do not pretend a missing record is proof of absence. Mark the range as INCOMPLETE and reconcile it from the source or replica that owns the checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate execution evidence from delivery evidence
&lt;/h2&gt;

&lt;p&gt;A runtime may successfully finish a task while failing to deliver the notification. Record those as separate state machines. OUTCOME_CONFIRMED for an API call does not prove that an email, webhook, or chat message was delivered.&lt;/p&gt;

&lt;p&gt;Use a stable request key for every externally visible effect. On retry, look up the request key and provider message ID before sending again. If the provider cannot answer, preserve UNKNOWN rather than guessing. This is how the audit trail prevents a recovery loop from becoming a duplicate-send loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add evidence tests, not only happy-path tests
&lt;/h2&gt;

&lt;p&gt;The test suite should mutate the ledger and verify that the verifier fails closed. Here is a compact test matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fault injection&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delete event 42&lt;/td&gt;
&lt;td&gt;INCOMPLETE or chain failure, never VERIFIED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap events 41 and 42&lt;/td&gt;
&lt;td&gt;sequence or hash failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change policy digest&lt;/td&gt;
&lt;td&gt;policy mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insert a fake confirmation&lt;/td&gt;
&lt;td&gt;unknown request key or signature failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove the final checkpoint&lt;/td&gt;
&lt;td&gt;run remains UNSEALED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace a credential version&lt;/td&gt;
&lt;td&gt;authorization mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Truncate after DISPATCHED&lt;/td&gt;
&lt;td&gt;effect remains UNKNOWN and enters reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run these tests against the actual serialization and storage path. A unit test over an in-memory object proves very little if production code serializes JSON differently, strips fields, or writes records out of order.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expose to operators
&lt;/h2&gt;

&lt;p&gt;Give operators a run timeline with four separate indicators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;evidence completeness: are all expected sequence ranges present?&lt;/li&gt;
&lt;li&gt;integrity: does the hash chain verify to a trusted checkpoint?&lt;/li&gt;
&lt;li&gt;authorization: do policy and credential versions match the dispatch decision?&lt;/li&gt;
&lt;li&gt;effect certainty: which actions are confirmed, unknown, or only intended?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not collapse these into a single green check. A run can have an intact ledger and an UNKNOWN external effect. It can also have a confirmed effect but incomplete logs. Those states require different recovery actions.&lt;/p&gt;

&lt;p&gt;For always-on OpenClaw or browser-agent deployments, the hosting choice is secondary to this contract. A managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=audit-evidence" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can reduce the amount of infrastructure you operate, but it does not replace redaction, scoped credentials, durable evidence, or reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical acceptance checklist
&lt;/h2&gt;

&lt;p&gt;Before calling your agent observable, prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every run and external effect has a stable ID&lt;/li&gt;
&lt;li&gt;ordering uses a sequence or logical clock, not timestamps alone&lt;/li&gt;
&lt;li&gt;sensitive fields are redacted before persistence&lt;/li&gt;
&lt;li&gt;the verifier detects deletion, mutation, duplication, and reordering&lt;/li&gt;
&lt;li&gt;checkpoints are protected separately from the event store&lt;/li&gt;
&lt;li&gt;execution completion and outbound delivery have separate evidence&lt;/li&gt;
&lt;li&gt;UNKNOWN is a durable state with a reconciliation path&lt;/li&gt;
&lt;li&gt;a clean rebuild can verify the ledger without trusting the crashed process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not more logs. The goal is a bounded answer to: what was intended, what crossed a boundary, what was confirmed, and what still needs reconciliation? Developers building agents that survive restarts, retries, and partial outages will get more value from that answer than from another wall of debug output.&lt;/p&gt;

</description>
      <category>security</category>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Agent Audit Trail Is Not Evidence Until You Can Verify It</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:35:26 +0000</pubDate>
      <link>https://dev.to/zira125/your-agent-audit-trail-is-not-evidence-until-you-can-verify-it-423n</link>
      <guid>https://dev.to/zira125/your-agent-audit-trail-is-not-evidence-until-you-can-verify-it-423n</guid>
      <description>&lt;p&gt;An agent log can tell you what the process claims happened. It does not prove that the record is complete, ordered, or unchanged.&lt;/p&gt;

&lt;p&gt;That distinction matters after a failed deployment, a suspicious tool call, or a customer asking who approved an external side effect. A timestamped JSON file is useful for debugging. It is weak evidence if a worker can overwrite it, two processes can emit the same sequence number, or a restart can silently drop the events between decision and dispatch.&lt;/p&gt;

&lt;p&gt;The practical fix is not “log more.” Give the audit stream a small verification contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the contract before choosing a storage engine
&lt;/h2&gt;

&lt;p&gt;For each event, require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a run ID and tenant ID&lt;/li&gt;
&lt;li&gt;a monotonic sequence allocated by one authority&lt;/li&gt;
&lt;li&gt;an event type and schema version&lt;/li&gt;
&lt;li&gt;the actor, tool, resource, and policy decision&lt;/li&gt;
&lt;li&gt;a server-side timestamp plus the previous event hash&lt;/li&gt;
&lt;li&gt;a stable event ID for deduplication&lt;/li&gt;
&lt;li&gt;a signature or MAC over the canonical event bytes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wall-clock time is for humans. Sequence and hash links are for ordering and integrity. Do not use a model-generated timestamp or event order as the source of truth.&lt;/p&gt;

&lt;p&gt;A minimal canonical record looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event = {
  "run_id": "run_7f2",
  "seq": 42,
  "event_id": "evt_42",
  "type": "TOOL_DISPATCHED",
  "tool": "github.create_issue",
  "resource": "repo:acme/api",
  "policy": "allow:ticket-bot",
  "occurred_at": "2026-08-17T08:00:00Z",
  "prev_hash": "sha256:..."
}

canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
event["hash"] = sha256(canonical.encode()).hexdigest()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Store the hash after the event is accepted, not before. In production, sign or MAC the canonical bytes with a key unavailable to the agent process. Hashing detects accidental changes; a separate key boundary helps detect who could have rewritten the stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the verifier independent of the agent
&lt;/h2&gt;

&lt;p&gt;The agent should emit events, but it should not be the only component that decides whether its own history is valid. Run a small verifier as a separate job or service that checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;sequence numbers are strictly increasing within a run&lt;/li&gt;
&lt;li&gt;every event points to the previous accepted hash&lt;/li&gt;
&lt;li&gt;event IDs are unique&lt;/li&gt;
&lt;li&gt;required transitions are legal&lt;/li&gt;
&lt;li&gt;signatures or MACs validate&lt;/li&gt;
&lt;li&gt;checkpoints agree with the preceding segment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, a tool call should normally have a transition like:&lt;/p&gt;

&lt;p&gt;PROPOSED -&amp;gt; POLICY_ALLOWED -&amp;gt; DISPATCHED -&amp;gt; OUTCOME_CONFIRMED&lt;/p&gt;

&lt;p&gt;A timeout is not confirmation. If the process dies after dispatch, record OUTCOME_UNKNOWN and reconcile with the provider using the stable event ID. Never “repair” the stream by inventing a success event just to make the state machine look tidy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoints make long histories practical
&lt;/h2&gt;

&lt;p&gt;A hash chain lets you detect a changed or missing event, but verifying millions of events on every query is expensive. Add signed checkpoints every N events or at each durable state transition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run ID&lt;/li&gt;
&lt;li&gt;last sequence&lt;/li&gt;
&lt;li&gt;last event hash&lt;/li&gt;
&lt;li&gt;verifier version&lt;/li&gt;
&lt;li&gt;checkpoint signature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep checkpoints in storage with a different write path from the live event stream. A checkpoint is not a backup and does not prove that the provider received an outbound request. It only gives you an independently verifiable boundary for the local history.&lt;/p&gt;

&lt;p&gt;For an always-on OpenClaw or browser-agent deployment, this is one reason to treat the runtime’s durable state and its audit evidence as separate restore items. A managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=verifiable-agent-audit-trail" rel="noopener noreferrer"&gt;managed always-on agent hosting on Ampere&lt;/a&gt; can help with the hosting layer, but it does not make an audit trail trustworthy automatically. You still need scoped credentials, an append-only policy, a verifier, and a tested restore path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible failure-injection test
&lt;/h2&gt;

&lt;p&gt;Do not declare the audit trail complete because the happy path looks good. Run these tests against a disposable environment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injection&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drop event 17&lt;/td&gt;
&lt;td&gt;verifier reports a sequence or hash gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reorder events 20 and 21&lt;/td&gt;
&lt;td&gt;verifier rejects the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate event ID&lt;/td&gt;
&lt;td&gt;verifier reports a duplicate, not two tool calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutate the tool resource&lt;/td&gt;
&lt;td&gt;signature or MAC validation fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kill the worker after DISPATCHED&lt;/td&gt;
&lt;td&gt;run becomes OUTCOME_UNKNOWN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore an older checkpoint&lt;/td&gt;
&lt;td&gt;verifier reports rollback or fork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotate the signing key&lt;/td&gt;
&lt;td&gt;old events remain verifiable; new events use the new key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important assertion is not merely “an alert fired.” Capture the exact invalid range, run ID, last trusted checkpoint, and whether any external side effect needs reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to monitor in production
&lt;/h2&gt;

&lt;p&gt;Track evidence health separately from agent health:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audit_events_accepted_total&lt;/li&gt;
&lt;li&gt;audit_verification_failures_total&lt;/li&gt;
&lt;li&gt;audit_unknown_outcomes&lt;/li&gt;
&lt;li&gt;time since last verified checkpoint&lt;/li&gt;
&lt;li&gt;event-ingest lag&lt;/li&gt;
&lt;li&gt;duplicate event IDs&lt;/li&gt;
&lt;li&gt;runs with a missing terminal transition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A green process health check can coexist with a broken audit stream. Alert on verification lag and unknown outcomes even when the worker is still responding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;Logs explain. An audit trail constrains and proves.&lt;/p&gt;

&lt;p&gt;If you cannot identify the last trusted event, detect a missing or reordered record, distinguish an unknown external outcome from a confirmed one, and verify the evidence after restoring it, you have observability but not reliable history.&lt;/p&gt;

&lt;p&gt;Build the verifier and the failure-injection test before you need the evidence. That is the part that survives a restart, a compromised worker, and an uncomfortable question about what the agent actually did.&lt;/p&gt;

</description>
      <category>testing</category>
    </item>
    <item>
      <title>Your Agent Logs Need a Secret Boundary, Not Just Redaction</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 18:05:38 +0000</pubDate>
      <link>https://dev.to/zira125/your-agent-logs-need-a-secret-boundary-not-just-redaction-804</link>
      <guid>https://dev.to/zira125/your-agent-logs-need-a-secret-boundary-not-just-redaction-804</guid>
      <description>&lt;p&gt;An AI agent can have correct tool authorization and still leak a credential through its observability stack.&lt;/p&gt;

&lt;p&gt;The usual advice is “redact secrets from logs.” That is necessary, but it is not a complete boundary. Redaction happens after a value has already entered a logging path. By then, the value may have been copied into an exception, a debug field, a trace attribute, a retry payload, a browser snapshot, or a queue record.&lt;/p&gt;

&lt;p&gt;A safer design makes secret material unloggable by default. The event schema carries references and classifications, not raw credentials. The logger rejects forbidden fields before serialization, and tests deliberately try to smuggle secrets through every event shape.&lt;/p&gt;

&lt;p&gt;This article shows a small pattern you can adapt to an agent runtime, MCP server, browser worker, or OpenClaw deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an event contract
&lt;/h2&gt;

&lt;p&gt;Do not let every tool call emit an arbitrary JSON blob. Define the fields that an event is allowed to contain:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action, run_id, tool_name, resource, decision, phase, outcome, request_id, error_code, duration_ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then define fields that are never accepted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;access tokens and API keys&lt;/li&gt;
&lt;li&gt;cookies and browser storage&lt;/li&gt;
&lt;li&gt;authorization headers&lt;/li&gt;
&lt;li&gt;full request and response bodies&lt;/li&gt;
&lt;li&gt;prompts containing user secrets&lt;/li&gt;
&lt;li&gt;downloaded files and screenshots unless explicitly classified&lt;/li&gt;
&lt;li&gt;arbitrary tool arguments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important distinction is between a value that identifies a secret and the secret itself. A log can record &lt;em&gt;credential_ref=github-write-lease-7&lt;/em&gt; and &lt;em&gt;credential_version=18&lt;/em&gt; without recording the token bytes.&lt;/p&gt;

&lt;p&gt;A useful event shape looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action: tool.dispatch
run_id: run_01J...
tool_name: github.create_issue
resource: repo:acme/widgets
decision: allow
phase: dispatched
credential_ref: github-write-lease-7
credential_version: 18
request_id: req_01J...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The reference still helps you investigate authorization and replay behavior. It does not give a log reader the credential needed to repeat the action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject before serialization
&lt;/h2&gt;

&lt;p&gt;A redaction function should be the last line of defense, not the main design. Put a schema gate before the logger serializes an event.&lt;/p&gt;

&lt;p&gt;Here is a minimal Python example. It is intentionally small enough to run as a unit-test fixture:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event = {
    "action": "tool.dispatch",
    "run_id": "run-123",
    "tool_name": "github.create_issue",
    "credential_ref": "github-write-lease-7",
    "credential_version": 18,
    "request_id": "req-456",
    "args": {"title": "rotate the key", "authorization": "Bearer SECRET"},
}

FORBIDDEN_KEYS = {
    "authorization", "access_token", "api_key", "cookie",
    "set_cookie", "private_key", "password", "secret"
}

def validate_event(event):
    def walk(value, path=""):
        if isinstance(value, dict):
            for key, child in value.items():
                if key.lower().replace("-", "_") in FORBIDDEN_KEYS:
                    raise ValueError(f"forbidden field at {path}/{key}")
                walk(child, f"{path}/{key}")
        elif isinstance(value, list):
            for index, child in enumerate(value):
                walk(child, f"{path}/{index}")
        elif isinstance(value, str) and value.startswith("Bearer "):
            raise ValueError(f"credential-like value at {path}")
    walk(event)
    return event

validate_event(event)  # fails closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In production, use a typed schema rather than relying only on key names. Key-name checks miss secrets embedded in free-form error strings or nested tool output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the observability planes
&lt;/h2&gt;

&lt;p&gt;Agent systems commonly mix at least four kinds of data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Control events:&lt;/strong&gt; state transitions, policy decisions, leases, and request IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic data:&lt;/strong&gt; stack traces, timing, provider errors, and retry context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User content:&lt;/strong&gt; prompts, documents, screenshots, and tool results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret material:&lt;/strong&gt; credentials, cookies, signing keys, and session artifacts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the first category should be broadly searchable. Diagnostic data needs controlled access and bounded retention. User content should be opt-in, encrypted, and associated with a data owner. Secret material should not enter the event pipeline at all.&lt;/p&gt;

&lt;p&gt;This is also where hosting choices matter. If you run an always-on browser or OpenClaw worker, choose a runtime where logs, durable state, backups, and credentials can be inspected as separate surfaces. &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-log-boundary" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can be one option to evaluate for that deployment problem, but hosting does not remove prompt-injection risk, credential risk, or the need for a logging boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the paths developers forget
&lt;/h2&gt;

&lt;p&gt;A happy-path test that logs a normal tool call proves very little. Add a leak matrix that injects a canary secret into each path:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Canary&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tool arguments&lt;/td&gt;
&lt;td&gt;CANARY_ARG_123&lt;/td&gt;
&lt;td&gt;event rejected or value replaced before serialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider error&lt;/td&gt;
&lt;td&gt;CANARY_ERR_456&lt;/td&gt;
&lt;td&gt;error code retained, raw message excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry payload&lt;/td&gt;
&lt;td&gt;CANARY_RETRY_789&lt;/td&gt;
&lt;td&gt;payload reference retained, body omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;browser snapshot&lt;/td&gt;
&lt;td&gt;CANARY_COOKIE_111&lt;/td&gt;
&lt;td&gt;snapshot not exported to broad logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;child-tool output&lt;/td&gt;
&lt;td&gt;CANARY_CHILD_222&lt;/td&gt;
&lt;td&gt;parent event contains a digest or reference only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crash dump&lt;/td&gt;
&lt;td&gt;CANARY_CRASH_333&lt;/td&gt;
&lt;td&gt;dump access-controlled and scrubbed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The test should inspect the serialized event bytes, not just the in-memory object. A serializer, exception formatter, or tracing exporter can reintroduce data after your validation step.&lt;/p&gt;

&lt;p&gt;A simple shell check for a JSON-lines sink might look like this:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if grep -R -nE 'CANARY_(ARG|ERR|RETRY|COOKIE|CHILD|CRASH)_[0-9]+' ./test-output; then&lt;br&gt;
  echo "secret canary reached an observability sink" &amp;gt;&amp;amp;2&lt;br&gt;
  exit 1&lt;br&gt;
fi&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Keep investigation possible without keeping secrets&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Removing every useful detail is not a solution. Investigators still need to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which run attempted the action?&lt;/li&gt;
&lt;li&gt;Which policy version made the decision?&lt;/li&gt;
&lt;li&gt;Which credential reference and version were selected?&lt;/li&gt;
&lt;li&gt;Did the worker dispatch the request or only plan it?&lt;/li&gt;
&lt;li&gt;Was the outcome confirmed, rejected, or left UNKNOWN?&lt;/li&gt;
&lt;li&gt;Can the event be correlated with a provider-side request ID?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use stable identifiers, hashes, classifications, and timestamps for those questions. Do not store raw arguments merely because they make a future investigation convenient.&lt;/p&gt;

&lt;p&gt;This fits with a broader reliability rule: an event should describe what the control plane knows, not pretend to know the content of an effect it cannot safely retain. If a tool timed out after dispatch, record outcome=UNKNOWN and the reconciliation key. Do not copy the request body into a debug field just to make the incident easier to read.&lt;/p&gt;

&lt;p&gt;For related runtime patterns, see the &lt;a href="https://dev.to/zira125/your-ai-agent-needs-a-credential-lease-not-a-permanent-api-key-1mnm"&gt;credential lease design&lt;/a&gt;, the &lt;a href="https://dev.to/zira125/your-ai-agent-needs-a-side-effect-class-not-just-max-attempts-14of"&gt;side-effect classification model&lt;/a&gt;, and the &lt;a href="https://dev.to/zira125/your-agent-deploy-is-not-safe-until-in-flight-tools-quiesce-25pl"&gt;in-flight tool drain protocol&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rollout checklist
&lt;/h2&gt;

&lt;p&gt;Before enabling verbose agent observability in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define an allowlist event schema&lt;/li&gt;
&lt;li&gt;reject forbidden fields before serialization&lt;/li&gt;
&lt;li&gt;prohibit raw tool arguments by default&lt;/li&gt;
&lt;li&gt;give user content a separate store and access policy&lt;/li&gt;
&lt;li&gt;keep credentials out of logs, traces, crash dumps, and snapshots&lt;/li&gt;
&lt;li&gt;add canary secrets to tool, error, retry, browser, child-tool, and crash paths&lt;/li&gt;
&lt;li&gt;inspect serialized bytes at every exporter and sink&lt;/li&gt;
&lt;li&gt;record references, versions, hashes, and provider IDs instead of secret values&lt;/li&gt;
&lt;li&gt;define retention and deletion for each data class&lt;/li&gt;
&lt;li&gt;test the logging boundary again after changing a tool or exporter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redaction is still useful for legacy paths and defense in depth. It should not be the contract your agent depends on. The stronger contract is simpler: secret material never becomes a normal event field, and a failing boundary stops the event before it leaves the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Logging Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/specs/otel/logs/data-model/" rel="noopener noreferrer"&gt;OpenTelemetry Logs Data Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nist.gov/itl/ai-risk-management-framework" rel="noopener noreferrer"&gt;NIST AI Risk Management Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>security</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Agent Deploy Is Not Safe Until In-Flight Tools Quiesce</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:04:25 +0000</pubDate>
      <link>https://dev.to/zira125/your-agent-deploy-is-not-safe-until-in-flight-tools-quiesce-25pl</link>
      <guid>https://dev.to/zira125/your-agent-deploy-is-not-safe-until-in-flight-tools-quiesce-25pl</guid>
      <description>&lt;p&gt;A rolling deploy can be perfectly healthy at the process level and still corrupt an agent run.&lt;/p&gt;

&lt;p&gt;The dangerous window is not startup. It is the tool call that was accepted by the old worker while the deploy moved traffic to the new one. If the old worker is killed without a protocol, the replacement has to guess whether the side effect happened.&lt;/p&gt;

&lt;p&gt;That is why graceful shutdown for an agent needs more than SIGTERM and a health check. It needs a &lt;strong&gt;drain contract&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The drain contract
&lt;/h2&gt;

&lt;p&gt;Give each worker an explicit lifecycle:&lt;/p&gt;

&lt;p&gt;ACCEPTING -&amp;gt; DRAINING -&amp;gt; QUIESCED -&amp;gt; STOPPED&lt;/p&gt;

&lt;p&gt;A worker in ACCEPTING may claim new runs. A worker in DRAINING rejects new claims but finishes work already inside a safe boundary. QUIESCED means there are no owned operations left, or every remaining operation has moved to a durable reconciliation queue. Only then should the process exit.&lt;/p&gt;

&lt;p&gt;The important detail is that “no active HTTP requests” is not the same as “no active agent work.” A tool call can outlive the request that started it, and a browser or provider can still be processing a side effect after the worker disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate run draining from effect draining
&lt;/h2&gt;

&lt;p&gt;Track these states independently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;Safe-to-stop condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run execution&lt;/td&gt;
&lt;td&gt;No step is executing, or its checkpoint is durable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool dispatch&lt;/td&gt;
&lt;td&gt;No call is between intent and provider outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser session&lt;/td&gt;
&lt;td&gt;No mutation is in flight, or the outcome is reconciled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound delivery&lt;/td&gt;
&lt;td&gt;Every message has a provider ID or an UNKNOWN record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not make shutdown wait forever for an external provider. Set a bounded drain deadline. When the deadline expires, persist the operation as UNKNOWN, release the worker lease, and let a reconciler query the provider or apply a documented manual check.&lt;/p&gt;

&lt;p&gt;This is the same reason a &lt;a href="https://dev.to/zira125/your-ai-agent-needs-a-run-lease-not-just-a-timeout-26jg"&gt;run lease needs fencing rather than just a timeout&lt;/a&gt;: a timeout detects waiting, but it does not prove that an old worker has stopped acting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal shutdown sequence
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Flip the worker state to DRAINING in durable storage.&lt;/li&gt;
&lt;li&gt;Stop accepting new queue claims. Enforce this at the claim transaction, not only in the router.&lt;/li&gt;
&lt;li&gt;Wait for active steps to reach a checkpoint or a terminal outcome.&lt;/li&gt;
&lt;li&gt;For each in-flight effect, write or update an operation record with its stable idempotency key.&lt;/li&gt;
&lt;li&gt;At the deadline, mark unresolved operations UNKNOWN and enqueue reconciliation.&lt;/li&gt;
&lt;li&gt;Publish QUIESCED only after the worker owns no claims or leases.&lt;/li&gt;
&lt;li&gt;Terminate the process and verify that no stale fencing token is accepted by downstream tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful invariant is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A worker may exit only when it cannot start new work and every old operation is either terminal or durably UNKNOWN.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Test the failure, not just the happy path
&lt;/h2&gt;

&lt;p&gt;A deploy test should deliberately pause the worker at each boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after the queue claim but before the checkpoint&lt;/li&gt;
&lt;li&gt;after intent is recorded but before provider dispatch&lt;/li&gt;
&lt;li&gt;after provider dispatch but before the response is stored&lt;/li&gt;
&lt;li&gt;after the browser mutation but before the result is acknowledged&lt;/li&gt;
&lt;li&gt;after the drain deadline but before the old process exits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every pause, kill the old worker, start a replacement, and check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was the old claim fenced?&lt;/li&gt;
&lt;li&gt;Did the replacement resume from a durable checkpoint?&lt;/li&gt;
&lt;li&gt;Did an ambiguous effect become UNKNOWN instead of being blindly retried?&lt;/li&gt;
&lt;li&gt;Can reconciliation find the provider-side result?&lt;/li&gt;
&lt;li&gt;Is the same idempotency key used on every retry?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Log drain_started_at, drain_deadline_at, quiesced_at, unknown_count, and stale_claim_rejections. These signals tell you whether deploys are actually reducing risk or merely shortening process downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting does not create the contract
&lt;/h2&gt;

&lt;p&gt;An always-on runtime can make worker lifecycle management easier to operate, but it does not define your drain semantics. If you need a managed place to run an OpenClaw or browser-automation worker, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-drain-protocol" rel="noopener noreferrer"&gt;managed always-on agent hosting on Ampere&lt;/a&gt; is one option to evaluate. Keep the drain ledger, fencing checks, credential boundaries, and reconciliation logic in your application.&lt;/p&gt;

&lt;p&gt;The practical goal is not zero interruption. It is a deploy where every interruption has a known state, every stale worker is rejected, and no operator has to guess whether a tool call happened.&lt;/p&gt;

&lt;p&gt;If you build agent runtimes, follow for concrete failure-injection drills and control-plane patterns that survive the demo.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Your AI Agent Needs a Side-Effect Class, Not Just Max Attempts</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:05:01 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-needs-a-side-effect-class-not-just-max-attempts-14of</link>
      <guid>https://dev.to/zira125/your-ai-agent-needs-a-side-effect-class-not-just-max-attempts-14of</guid>
      <description>&lt;p&gt;An agent retry loop is not a reliability policy.&lt;/p&gt;

&lt;p&gt;It is only a counter.&lt;/p&gt;

&lt;p&gt;The dangerous question is not “how many times should this tool call run?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the previous attempt may have changed the world, what evidence lets us safely try again?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A model can retry a failed HTTP request, a shell command, a browser click, or an MCP call. Those actions do not share the same failure semantics. Treating them as interchangeable is how an agent sends duplicate emails, creates duplicate tickets, charges twice, or repeats a deployment after the first one already succeeded.&lt;/p&gt;

&lt;p&gt;This article shows a small policy that makes retryability explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify the side effect before dispatch
&lt;/h2&gt;

&lt;p&gt;Give every tool operation a side-effect class:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Default after timeout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PURE&lt;/td&gt;
&lt;td&gt;No external mutation. Safe to repeat.&lt;/td&gt;
&lt;td&gt;Retry automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDEMPOTENT&lt;/td&gt;
&lt;td&gt;Repeating with the same key converges to one result.&lt;/td&gt;
&lt;td&gt;Retry with the same key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPLAYABLE&lt;/td&gt;
&lt;td&gt;Repeat is safe only after checking a durable operation record.&lt;/td&gt;
&lt;td&gt;Reconcile first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNKNOWN&lt;/td&gt;
&lt;td&gt;The effect may have happened, but the caller cannot prove it.&lt;/td&gt;
&lt;td&gt;Stop and reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NON_RETRYABLE&lt;/td&gt;
&lt;td&gt;Repeating can create an unacceptable new effect.&lt;/td&gt;
&lt;td&gt;Require approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not infer this class from the tool name. A method called create_issue might be replayable if the provider supports an idempotency key, or non-retryable if it does not. A GET can still be unsafe if it triggers a workflow behind a badly designed endpoint.&lt;/p&gt;

&lt;p&gt;Store the classification in the tool contract, not in the prompt:&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;"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_deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IDEMPOTENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reconcile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider_lookup_by_key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_required"&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 executor should reject a dispatch that has no effect policy. “The model probably knows this is safe” is not a control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate attempt count from effect evidence
&lt;/h2&gt;

&lt;p&gt;A retry budget answers “how much work may we spend?” It does not answer “did the world change?” Keep both in durable state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation_id: op_01J...
attempt: 2
side_effect: IDEMPOTENT
idempotency_key: deploy:repo-a:commit-91c2
phase: DISPATCHED
provider_id: null
last_error: connection reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful phases are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PLANNED: the agent proposed an operation.&lt;/li&gt;
&lt;li&gt;AUTHORIZED: policy and credentials passed a fresh check.&lt;/li&gt;
&lt;li&gt;DISPATCHED: the request left your boundary.&lt;/li&gt;
&lt;li&gt;CONFIRMED: the provider returned a durable result.&lt;/li&gt;
&lt;li&gt;UNKNOWN: the process lost the response or timed out after dispatch.&lt;/li&gt;
&lt;li&gt;RECONCILED: a lookup proved the final state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A process restart must not turn DISPATCHED into NOT_STARTED. If you already persist run ownership, use the same fencing approach described in &lt;a href="https://dev.to/zira125/your-ai-agent-needs-a-run-lease-not-just-a-timeout-26jg"&gt;run leases for AI agents&lt;/a&gt; so an old worker cannot resume an operation while the recovery worker is reconciling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the retry gate boring and deterministic
&lt;/h2&gt;

&lt;p&gt;A retry decision should be explainable without asking the model:&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;def&lt;/span&gt; &lt;span class="nf"&gt;retry_decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONFIRMED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STOP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECONCILE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PURE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEMPOTENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRY_SAME_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REPLAYABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECONCILE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is that the idempotency key remains stable. Never generate a new key merely because the worker restarted. A new key converts a retry into a new operation.&lt;/p&gt;

&lt;p&gt;For providers without idempotency support, create your own operation record before dispatch and include a unique client token where the provider allows it. If neither is possible, classify the operation as NON_RETRYABLE or UNKNOWN after a lost response. Do not “try once more just in case.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure window, not only the happy path
&lt;/h2&gt;

&lt;p&gt;A useful harness kills the worker at each boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;before the request is built&lt;/li&gt;
&lt;li&gt;after authorization but before dispatch&lt;/li&gt;
&lt;li&gt;immediately after dispatch&lt;/li&gt;
&lt;li&gt;after the provider commits but before the response is read&lt;/li&gt;
&lt;li&gt;after the response is stored but before the agent continues&lt;/li&gt;
&lt;li&gt;during reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each injected crash, assert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the operation has one stable identity&lt;/li&gt;
&lt;li&gt;a stale worker cannot dispatch after recovery takes ownership&lt;/li&gt;
&lt;li&gt;the retry gate chooses RECONCILE for ambiguous outcomes&lt;/li&gt;
&lt;li&gt;the provider is queried by the original key or client token&lt;/li&gt;
&lt;li&gt;no duplicate side effect is accepted as success&lt;/li&gt;
&lt;li&gt;the final audit record explains why the operation was retried, stopped, or approved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A compact metric set helps expose policy drift: unknown_outcomes, reconciliations, duplicate_rejections, approval_interruptions, and retry_by_effect_class. A falling retry count is not automatically good. It may mean the agent is hiding failures or classifying everything as UNKNOWN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where hosting fits
&lt;/h2&gt;

&lt;p&gt;If this worker must stay available for scheduled OpenClaw jobs or browser automation, a managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=side-effect-retry-policy" rel="noopener noreferrer"&gt;always-on OpenClaw hosting on Ampere&lt;/a&gt; can reduce the operational work of keeping the process online. It does not decide whether a side effect is safe, preserve your operation ledger, or eliminate credential and prompt-injection risk. Those controls belong in the application and its recovery tests.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: retry computation freely, retry mutations only with evidence. Once an agent records the effect class, stable operation identity, and reconciliation path before dispatch, “retry” becomes a controlled state transition instead of a hopeful loop.&lt;/p&gt;

&lt;p&gt;If you build agent runtimes, what side-effect class is hardest to verify in your system?&lt;/p&gt;

</description>
      <category>automation</category>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your AI Agent Needs a Run Lease, Not Just a Timeout</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:04:14 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-needs-a-run-lease-not-just-a-timeout-26jg</link>
      <guid>https://dev.to/zira125/your-ai-agent-needs-a-run-lease-not-just-a-timeout-26jg</guid>
      <description>&lt;p&gt;An agent worker can be alive, connected, and still be the wrong process to execute a run.&lt;/p&gt;

&lt;p&gt;That happens when a worker pauses during a model call, loses its network connection, or gets frozen by a host restart. The scheduler notices the timeout and starts a replacement. Then the old worker wakes up and continues with the same authority.&lt;/p&gt;

&lt;p&gt;Now two workers believe they own one run.&lt;/p&gt;

&lt;p&gt;A timeout detects suspicion. It does not transfer ownership.&lt;/p&gt;

&lt;p&gt;The missing primitive is a &lt;strong&gt;run lease&lt;/strong&gt;: a short-lived, renewable ownership record that every side-effecting step must present and that a replacement worker can fence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lease contract
&lt;/h2&gt;

&lt;p&gt;Store one lease per run, not one global worker heartbeat:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type RunLease = {
  runId: string
  ownerId: string
  fencingToken: number
  expiresAt: string
  lastRenewedAt: string
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important field is &lt;strong&gt;fencingToken&lt;/strong&gt;. It increases every time ownership changes. A worker with token 7 must not be able to perform a side effect after token 8 has been issued to a replacement.&lt;/p&gt;

&lt;p&gt;A lease should answer four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who owns this run now?&lt;/li&gt;
&lt;li&gt;When does that ownership expire?&lt;/li&gt;
&lt;li&gt;Which token proves the current ownership generation?&lt;/li&gt;
&lt;li&gt;What happens when the answer is unknown?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not let a model response, in-memory boolean, or process ID answer those questions. They disappear or become ambiguous during failover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renew before you execute
&lt;/h2&gt;

&lt;p&gt;A worker should renew the lease, then validate it again immediately before every irreversible operation:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def execute_step(run_id, owner_id, token, action):
    lease = store.read_lease(run_id)

    if lease.owner_id != owner_id:
        raise LostLease('owner changed')
    if lease.fencing_token != token:
        raise LostLease('fencing token changed')
    if lease.expires_at &amp;lt;= utc_now():
        raise LostLease('lease expired')

    return side_effect_store.apply(
        action,
        run_id=run_id,
        fencing_token=token,
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The side-effect store must enforce the token too. Checking only in the worker leaves a race between the check and the write. The database transaction, job queue, browser-session broker, or API gateway that accepts the action needs to reject stale tokens.&lt;/p&gt;

&lt;p&gt;That is the fence. A stale worker may still be running, but it no longer has authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate liveness from ownership
&lt;/h2&gt;

&lt;p&gt;A process heartbeat answers: “Is this process responding?”&lt;/p&gt;

&lt;p&gt;A run lease answers: “Is this process still authorized to mutate this run?”&lt;/p&gt;

&lt;p&gt;They are related but not interchangeable. A process can pass its heartbeat while its lease is expired. A busy worker can miss a heartbeat while still holding a valid lease. Your scheduler needs separate states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PROCESS_ALIVE&lt;/li&gt;
&lt;li&gt;LEASE_VALID&lt;/li&gt;
&lt;li&gt;LEASE_LOST&lt;/li&gt;
&lt;li&gt;OUTCOME_UNKNOWN&lt;/li&gt;
&lt;li&gt;RECONCILIATION_REQUIRED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a lease is lost during a tool call, do not blindly retry the tool. The provider may have accepted the request even if the worker never received the response. Record OUTCOME_UNKNOWN, query the provider with a stable idempotency key where possible, and only then decide whether to retry, compensate, or ask for approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal failure-injection test
&lt;/h2&gt;

&lt;p&gt;You can test the dangerous race without a large distributed system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start worker A with lease token 1.&lt;/li&gt;
&lt;li&gt;Pause A immediately before a side effect.&lt;/li&gt;
&lt;li&gt;Let the lease expire.&lt;/li&gt;
&lt;li&gt;Start worker B and assign token 2.&lt;/li&gt;
&lt;li&gt;Let B perform the side effect.&lt;/li&gt;
&lt;li&gt;Resume A and make it attempt the same side effect.&lt;/li&gt;
&lt;li&gt;Verify that the side-effect store rejects token 1.&lt;/li&gt;
&lt;li&gt;Verify that the run has one final ownership record and one reconciliation record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat the test with a delayed network response, a process restart, and a duplicate request. The expected result is not “the old worker stopped.” You cannot reliably guarantee that. The expected result is “the old worker could not mutate state after fencing.”&lt;/p&gt;

&lt;p&gt;Useful evidence includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lease acquisition and renewal timestamps&lt;/li&gt;
&lt;li&gt;owner and fencing token on every side effect&lt;/li&gt;
&lt;li&gt;rejection count for stale tokens&lt;/li&gt;
&lt;li&gt;time spent in OUTCOME_UNKNOWN&lt;/li&gt;
&lt;li&gt;reconciliation decisions and their operator or policy source&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where hosting fits
&lt;/h2&gt;

&lt;p&gt;An always-on agent runtime makes lease renewal and durable state easier to operate, but it does not create the safety property for you. If you run OpenClaw or another worker on managed infrastructure, keep the lease store and side-effect boundary explicit, test restart behavior, and verify what survives a rebuild.&lt;/p&gt;

&lt;p&gt;For teams that do not want to maintain the base always-on host, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=run-lease-fencing" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one option to evaluate. The important question is still architectural: can your worker prove current ownership, and can the downstream system reject stale ownership?&lt;/p&gt;

&lt;h2&gt;
  
  
  Run-lease checklist
&lt;/h2&gt;

&lt;p&gt;Before calling an agent workflow failover-safe, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ownership is stored durably per run&lt;/li&gt;
&lt;li&gt;ownership changes issue a monotonically increasing fencing token&lt;/li&gt;
&lt;li&gt;every irreversible step carries that token&lt;/li&gt;
&lt;li&gt;the downstream side-effect boundary rejects stale tokens atomically&lt;/li&gt;
&lt;li&gt;lease loss becomes an explicit state, not a generic retry&lt;/li&gt;
&lt;li&gt;ambiguous tool outcomes enter reconciliation&lt;/li&gt;
&lt;li&gt;restart and duplicate-request races are failure-injected regularly&lt;/li&gt;
&lt;li&gt;rebuild documentation explains how leases, state, and credentials are restored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeout tells you that a worker may be gone. A lease plus a fencing token tells every other component whether that worker is still allowed to act. That distinction is what keeps a restart from becoming a duplicate side effect.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Your AI Agent Needs a Cost Circuit Breaker, Not a Monthly Budget</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 02:04:55 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-needs-a-cost-circuit-breaker-not-a-monthly-budget-2654</link>
      <guid>https://dev.to/zira125/your-ai-agent-needs-a-cost-circuit-breaker-not-a-monthly-budget-2654</guid>
      <description>&lt;p&gt;A monthly spend limit is useful for accounting. It is a poor safety control for an AI agent.&lt;/p&gt;

&lt;p&gt;An agent can burn through a budget long before the invoice arrives: a retry loop, a tool that returns oversized context, a fallback model that is more expensive than the primary model, or several workers all repeating the same request. By the time someone notices the dashboard, the expensive behavior has already happened.&lt;/p&gt;

&lt;p&gt;The control I want instead is a &lt;strong&gt;cost circuit breaker&lt;/strong&gt;: a small state machine that reserves estimated spend before work starts, records actual usage, and changes what the agent is allowed to do as the budget is consumed.&lt;/p&gt;

&lt;p&gt;This is not a claim that a circuit breaker makes model usage cheap. It makes overspend a bounded and observable failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate four kinds of budget
&lt;/h2&gt;

&lt;p&gt;Do not put every limit into one 'max_tokens' field. Track at least these dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run budget:&lt;/strong&gt; the maximum estimated cost for one user request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant or project budget:&lt;/strong&gt; the amount that a workload may reserve in a time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry budget:&lt;/strong&gt; the number and cost of retries allowed for one operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider budget:&lt;/strong&gt; a cap for each model/provider so fallback cannot silently consume the whole allowance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A request can be under its token limit and still violate a provider or retry budget. The decision should be made against all four.&lt;/p&gt;

&lt;p&gt;A minimal reservation record can look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type BudgetReservation struct {
    RunID          string
    OperationID    string
    Provider       string
    EstimatedCents int64
    ActualCents    int64
    RetryIndex     int
    State          string // RESERVED, SETTLED, RELEASED, UNKNOWN
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important part is not the language. It is that the reservation has a stable OperationID and a durable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reserve before dispatch, settle after the response
&lt;/h2&gt;

&lt;p&gt;The dangerous order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call the model.&lt;/li&gt;
&lt;li&gt;Read the usage fields.&lt;/li&gt;
&lt;li&gt;Decide whether the call was affordable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order has already spent the money. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Estimate the worst-case input and output cost.&lt;/li&gt;
&lt;li&gt;Atomically reserve that amount against the applicable budgets.&lt;/li&gt;
&lt;li&gt;Dispatch the request with the reservation ID.&lt;/li&gt;
&lt;li&gt;Settle the reservation using provider usage data.&lt;/li&gt;
&lt;li&gt;Release unused capacity, or mark the reservation UNKNOWN if the outcome is ambiguous.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reserve operation needs to be atomic. Two workers must not both observe the same remaining balance and both receive permission.&lt;/p&gt;

&lt;p&gt;A simple SQL shape is enough to make the invariant explicit:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE budget_windows
SET reserved_cents = reserved_cents + :estimate
WHERE budget_key = :key
  AND reserved_cents + settled_cents + :estimate &amp;lt;= limit_cents;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the affected-row count is zero, reject or downgrade the operation. Do not enqueue it and hope a later worker notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Make the breaker change behavior
&lt;/h2&gt;

&lt;p&gt;A useful breaker has more than open and closed states. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NORMAL:&lt;/strong&gt; full tool and model policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;THROTTLED:&lt;/strong&gt; lower concurrency and shorter output ceilings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEGRADED:&lt;/strong&gt; allow read-only tools and a cheaper approved model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OPEN:&lt;/strong&gt; reject new work, but allow reconciliation of in-flight requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNKNOWN:&lt;/strong&gt; stop automatic retries until provider usage and delivery state are reconciled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The transition should be based on reserved plus settled spend, not only settled spend. Otherwise a burst of concurrent requests can oversubscribe the remaining budget.&lt;/p&gt;

&lt;p&gt;Example policy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Remaining allowance&lt;/th&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;More than 40%&lt;/td&gt;
&lt;td&gt;Normal execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15% to 40%&lt;/td&gt;
&lt;td&gt;Reduce concurrency and cap output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1% to 15%&lt;/td&gt;
&lt;td&gt;Read-only or low-cost model only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0% or negative&lt;/td&gt;
&lt;td&gt;Open for new work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The percentages are examples, not universal defaults. Tune them from your workload and record why a transition occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat retries as new reservations
&lt;/h2&gt;

&lt;p&gt;A retry is not free just because the original request failed from the application’s point of view. The provider may have processed it, and a timeout may leave the outcome unknown.&lt;/p&gt;

&lt;p&gt;Give every attempt a stable operation identity plus an attempt number:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_482 / summarize_invoice / attempt_2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before retrying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;look up the provider request ID if one exists;&lt;/li&gt;
&lt;li&gt;reconcile usage for the previous attempt;&lt;/li&gt;
&lt;li&gt;check whether the tool side effect completed;&lt;/li&gt;
&lt;li&gt;reserve the retry cost separately;&lt;/li&gt;
&lt;li&gt;stop when the retry budget is exhausted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important for always-on agents. A restart loop can turn one logical task into hundreds of billable attempts. If you run OpenClaw or another agent continuously, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cost-circuit-breaker" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can be one hosting option to evaluate for the runtime, but hosting does not remove model-cost, retry, or credential risk. The breaker still belongs in the agent control plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do not let fallback bypass the breaker
&lt;/h2&gt;

&lt;p&gt;Fallback logic often has an accidental escape hatch:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def call_with_fallback(request):
    try:
        return call('cheap-model', request)
    except TimeoutError:
        return call('premium-model', request)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Both calls need the same budget checks. Better:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def dispatch(request, provider):
    estimate = price(provider, request.input_tokens, request.max_output_tokens)
    reservation = reserve_all(provider, estimate, request.run_id)
    if not reservation:
        raise BudgetOpen('no capacity for this provider')
    try:
        response = call(provider, request, reservation.id)
        settle(reservation.id, usage=response.usage)
        return response
    except TimeoutError:
        mark_unknown(reservation.id)
        raise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Fallback then becomes a policy decision made after reconciliation, not an exception handler that can spend outside the guardrail.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Test the failure paths deliberately
&lt;/h2&gt;

&lt;p&gt;A cost control that only works on successful responses is not a control. Inject these cases in staging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two workers reserve the final available cents concurrently;&lt;/li&gt;
&lt;li&gt;the provider times out after accepting the request;&lt;/li&gt;
&lt;li&gt;usage data arrives late or is missing;&lt;/li&gt;
&lt;li&gt;a retry is attempted after the breaker opens;&lt;/li&gt;
&lt;li&gt;a premium fallback is requested while the provider budget is exhausted;&lt;/li&gt;
&lt;li&gt;a process crashes after reservation but before settlement;&lt;/li&gt;
&lt;li&gt;a process crashes after settlement but before releasing the unused estimate;&lt;/li&gt;
&lt;li&gt;the clock moves across a budget-window boundary;&lt;/li&gt;
&lt;li&gt;a duplicate message is delivered to two workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every case, assert an invariant: the ledger is reconcilable, no reservation is silently lost, no automatic retry bypasses the breaker, and the final charge is attributable to one run and attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Measure the control, not just the invoice
&lt;/h2&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estimated versus actual cost by provider;&lt;/li&gt;
&lt;li&gt;reserved capacity that remains UNKNOWN;&lt;/li&gt;
&lt;li&gt;retry cost per operation;&lt;/li&gt;
&lt;li&gt;fallback rate and fallback cost;&lt;/li&gt;
&lt;li&gt;time spent in each breaker state;&lt;/li&gt;
&lt;li&gt;rejected work and its reason;&lt;/li&gt;
&lt;li&gt;maximum concurrent reserved spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful question is not only “How much did this month cost?” It is “Which state transition prevented the next unbounded retry or fallback?”&lt;/p&gt;

&lt;p&gt;A monthly budget tells finance what happened. A cost circuit breaker gives the runtime a chance to stop, degrade, or reconcile before a small failure becomes a large bill.&lt;/p&gt;

&lt;p&gt;If you build agents, follow for practical control-plane patterns around state, permissions, recovery, and deployment rather than model demos alone.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your MCP Server Needs a Capability Budget, Not Just Auth</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 13 Aug 2026 22:02:56 +0000</pubDate>
      <link>https://dev.to/zira125/your-mcp-server-needs-a-capability-budget-not-just-auth-32p6</link>
      <guid>https://dev.to/zira125/your-mcp-server-needs-a-capability-budget-not-just-auth-32p6</guid>
      <description>&lt;p&gt;Most MCP security checklists stop at “is this caller authenticated?” That is necessary, but it does not answer the operational question: what is this tool allowed to do during this run?&lt;/p&gt;

&lt;p&gt;A useful boundary is a capability budget: a short-lived, explicit contract for each tool invocation. It should constrain the action, target, resource, quantity, and expiry. The model can request a tool, but the runtime decides whether the request fits the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the budget next to the tool
&lt;/h2&gt;

&lt;p&gt;Start with a deliberately boring schema:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type CapabilityBudget = {
  runId: string;
  tool: string;
  actions: string[];
  resources: string[];
  maxCalls: number;
  maxBytes?: number;
  expiresAt: string;
  approval: "none" | "human";
  policyVersion: string;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For example, a code-review run might receive:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "runId": "run_8f2",
  "tool": "github.create_comment",
  "actions": ["comment"],
  "resources": ["repo:acme/api:pull:481"],
  "maxCalls": 1,
  "expiresAt": "2026-08-14T03:00:00Z",
  "approval": "human",
  "policyVersion": "p17"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important part is what is absent: no repository-wide write permission, no issue creation, and no unbounded retry allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Enforce it at dispatch time
&lt;/h2&gt;

&lt;p&gt;Do not check the budget only when the agent plans the call. The queue, worker, and tool adapter should all treat the budget as untrusted input and revalidate it.&lt;/p&gt;

&lt;p&gt;A dispatch decision can be reduced to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the current run and budget.&lt;/li&gt;
&lt;li&gt;Verify the budget is unexpired.&lt;/li&gt;
&lt;li&gt;Verify the requested action and resource match exactly.&lt;/li&gt;
&lt;li&gt;Atomically reserve one call and any byte quota.&lt;/li&gt;
&lt;li&gt;Recheck the current policy and credential version.&lt;/li&gt;
&lt;li&gt;Dispatch with the reservation ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If steps 4 and 5 cannot be made durable, a worker crash can turn one approved call into several attempts. That is a reliability bug as well as a security bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate planning from spending
&lt;/h2&gt;

&lt;p&gt;Let the model propose a sequence, but make the runtime spend from the budget one reservation at a time. A plan such as “inspect, patch, test, notify” should not silently inherit write capability from the first step.&lt;/p&gt;

&lt;p&gt;A minimal ledger makes this visible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;reservation&lt;/th&gt;
&lt;th&gt;tool&lt;/th&gt;
&lt;th&gt;requested&lt;/th&gt;
&lt;th&gt;decision&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;r1&lt;/td&gt;
&lt;td&gt;repo.read&lt;/td&gt;
&lt;td&gt;1 call&lt;/td&gt;
&lt;td&gt;allowed&lt;/td&gt;
&lt;td&gt;confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;r2&lt;/td&gt;
&lt;td&gt;repo.write&lt;/td&gt;
&lt;td&gt;1 call&lt;/td&gt;
&lt;td&gt;approval required&lt;/td&gt;
&lt;td&gt;pending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;r3&lt;/td&gt;
&lt;td&gt;slack.send&lt;/td&gt;
&lt;td&gt;1 call&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;not dispatched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep pending and unknown distinct. Pending means no dispatch has been recorded. Unknown means dispatch may have happened but confirmation was lost. Only the latter requires provider lookup or an idempotency-key reconciliation before retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test the failure modes
&lt;/h2&gt;

&lt;p&gt;A capability budget is only real if it survives interruptions. Inject at least these cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The worker pauses after reservation but before dispatch.&lt;/li&gt;
&lt;li&gt;The provider times out after accepting the request.&lt;/li&gt;
&lt;li&gt;The policy changes while a job is queued.&lt;/li&gt;
&lt;li&gt;The model changes the resource identifier between planning and dispatch.&lt;/li&gt;
&lt;li&gt;Two workers race for the last allowed call.&lt;/li&gt;
&lt;li&gt;A retry arrives after the budget expires.&lt;/li&gt;
&lt;li&gt;A child tool asks for a broader capability than its parent run owns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every case, record the expected ledger state, whether a provider lookup is required, and whether a human must reapprove. A green test suite that never forces an unknown outcome is testing the happy path, not the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make the hosting boundary explicit
&lt;/h2&gt;

&lt;p&gt;If this runtime must stay available for queued work or browser-assisted tasks, hosting is part of the control plane. A managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp-capability-budget" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can be evaluated as one deployment option, but it does not replace capability checks, prompt-injection defenses, credential scoping, or reconciliation logic.&lt;/p&gt;

&lt;p&gt;The questions to verify are practical: where durable run state lives, how workers restart, how credentials are mounted, how logs are retained, and how you rebuild the same policy version on a clean host. Treat the hosting choice as an availability and recovery decision, not as an authorization decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact acceptance checklist
&lt;/h2&gt;

&lt;p&gt;Before calling an MCP integration production-ready, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every tool call has a run-bound, expiring capability budget.&lt;/li&gt;
&lt;li&gt;Action and resource are matched at dispatch, not only during planning.&lt;/li&gt;
&lt;li&gt;Call and byte limits are reserved atomically.&lt;/li&gt;
&lt;li&gt;Policy and credential versions are rechecked before execution.&lt;/li&gt;
&lt;li&gt;Parent and child tools cannot expand one another’s authority.&lt;/li&gt;
&lt;li&gt;Timeouts produce unknown, not an automatic retry.&lt;/li&gt;
&lt;li&gt;Provider IDs and idempotency keys support reconciliation.&lt;/li&gt;
&lt;li&gt;Expiry, revocation, and clean-host restore are tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Authentication answers “who is asking?” A capability budget adds “what may happen, where, how often, and until when?” That is the boundary worth reviewing.&lt;/p&gt;

</description>
      <category>security</category>
      <category>mcp</category>
      <category>agents</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI Agent Is Restarting in a Loop. Stop Treating It Like a Health Check</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:03:21 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-is-restarting-in-a-loop-stop-treating-it-like-a-health-check-18h6</link>
      <guid>https://dev.to/zira125/your-ai-agent-is-restarting-in-a-loop-stop-treating-it-like-a-health-check-18h6</guid>
      <description>&lt;p&gt;A process that is alive is not necessarily an agent that is healthy.&lt;/p&gt;

&lt;p&gt;A supervisor can report “running” while the worker is crashing before it registers, restoring a corrupt state file, losing its credential lease, or replaying the same outbound action after every restart. Restarting harder does not fix those failure domains. It can make them worse by hiding the first useful error and multiplying side effects.&lt;/p&gt;

&lt;p&gt;This article gives you a small restart-loop protocol: classify the failure, preserve the evidence, quarantine unsafe work, and only then decide whether to restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate process liveness from agent readiness
&lt;/h2&gt;

&lt;p&gt;Use at least three states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PROCESS_UP&lt;/strong&gt;: the process accepts a local health request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;READY&lt;/strong&gt;: configuration, durable state, credentials, and dependencies passed startup checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EXECUTING&lt;/strong&gt;: the agent is allowed to claim new work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use a single &lt;code&gt;/healthz&lt;/code&gt; endpoint for all three. A process can be up while its state database is locked or its credential lease has expired.&lt;/p&gt;

&lt;p&gt;A useful readiness response should identify the failed domain without exposing secrets:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain              status    evidence
runtime             ok        pid=1842, build=2026.08.13
state               fail      sqlite_busy_after=3s
credentials         unknown   lease_version=41
outbound_delivery   paused    pending=2, unknown=1
admission           blocked   reason=readiness_failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important part is the last line. A failed readiness check should stop new work, not necessarily kill the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Preserve the first failure, not just the last restart
&lt;/h2&gt;

&lt;p&gt;Supervisors often produce a misleading story:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start worker&lt;/li&gt;
&lt;li&gt;worker fails during recovery&lt;/li&gt;
&lt;li&gt;supervisor restarts worker&lt;/li&gt;
&lt;li&gt;logs rotate or the same error is buried&lt;/li&gt;
&lt;li&gt;repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Record a &lt;strong&gt;restart episode&lt;/strong&gt; before attempting another start. Give it an ID and attach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;boot ID and process ID&lt;/li&gt;
&lt;li&gt;config hash and state-schema version&lt;/li&gt;
&lt;li&gt;last durable run position&lt;/li&gt;
&lt;li&gt;credential version, never the credential value&lt;/li&gt;
&lt;li&gt;dependency readiness results&lt;/li&gt;
&lt;li&gt;the first exception and its timestamp&lt;/li&gt;
&lt;li&gt;whether a tool call or delivery was UNKNOWN at shutdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A compact event model is enough:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auto_restart_episode(episode_id, boot_id, config_hash, state_position, readiness_failures, first_error, unknown_side_effects)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the first error is not durable, you do not have a restart policy. You have a loop with a counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add a restart budget and a quarantine state
&lt;/h2&gt;

&lt;p&gt;A bounded restart budget prevents a broken worker from consuming all CPU, API quota, or queue capacity. For example:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAX_RESTARTS = 5
WINDOW = 10 minutes
recent = episodes within WINDOW
if len(recent) &amp;gt;= MAX_RESTARTS: return QUARANTINED
else: delay = min(300, 2 ** len(recent))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The exact numbers are workload-specific. The invariant is not: after repeated failure, the worker must stop claiming new work and require a human or an automated rollback decision.&lt;/p&gt;

&lt;p&gt;Quarantine should be explicit and observable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no new admissions&lt;/li&gt;
&lt;li&gt;leases expire or are returned safely&lt;/li&gt;
&lt;li&gt;in-flight work becomes UNKNOWN unless its outcome is confirmed&lt;/li&gt;
&lt;li&gt;outbound delivery retries pause&lt;/li&gt;
&lt;li&gt;a diagnostic bundle is retained&lt;/li&gt;
&lt;li&gt;recovery can start from a known-good configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not automatically delete the state directory as a “fix.” That may erase the only evidence needed to distinguish corrupted state from a bad deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test the four common restart-loop causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bad configuration
&lt;/h3&gt;

&lt;p&gt;Deploy a config with an invalid tool or model endpoint. Verify that the worker enters &lt;code&gt;NOT_READY&lt;/code&gt;, records the config hash, and does not claim work.&lt;/p&gt;

&lt;h3&gt;
  
  
  State incompatibility
&lt;/h3&gt;

&lt;p&gt;Restore a state snapshot from an older schema. Verify that migration is explicit, reversible, and logged. A migration that partially writes before crashing needs a recovery position, not another blind restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expired credentials
&lt;/h3&gt;

&lt;p&gt;Revoke or expire the credential lease while the worker is stopped. On boot, it should report &lt;code&gt;CREDENTIALS_NOT_READY&lt;/code&gt;, avoid tool calls, and request a fresh scoped lease.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unknown side effect
&lt;/h3&gt;

&lt;p&gt;Kill the process after dispatch but before the provider response. On restart, the run must remain &lt;code&gt;UNKNOWN&lt;/code&gt; until the provider is queried or an operator makes a documented reconciliation decision. It must not blindly replay the action.&lt;/p&gt;

&lt;p&gt;A useful failure matrix looks like this:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failure                  expected startup state    allowed action&lt;br&gt;
bad config                NOT_READY                 diagnostic only&lt;br&gt;
state migration error    QUARANTINED               rollback or repair&lt;br&gt;
expired credential       NOT_READY                 lease renewal&lt;br&gt;
unknown tool outcome     READY, admission paused    reconcile first&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  

&lt;ol&gt;
&lt;li&gt;Make recovery evidence part of the deployment
&lt;/li&gt;
&lt;/ol&gt;
&lt;/h2&gt;


&lt;p&gt;Before putting an always-on agent on a VPS or managed runtime, prove that you can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which code and config produced this boot?&lt;/li&gt;
&lt;li&gt;Which state was durable before the crash?&lt;/li&gt;
&lt;li&gt;Which work was executing, queued, delivered, or UNKNOWN?&lt;/li&gt;
&lt;li&gt;Which credentials survived, and what is their blast radius?&lt;/li&gt;
&lt;li&gt;Can I rebuild the runtime without copying an opaque machine image?&lt;/li&gt;
&lt;li&gt;Can I pause admissions without taking down diagnostics?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a managed place to run an always-on OpenClaw or browser workload, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=restart-loop-recovery" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one option to evaluate. It does not remove prompt-injection, credential, or application-level recovery risk; you still need the state, admission, and reconciliation checks above.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;A restart is an action, not a diagnosis.&lt;/p&gt;

&lt;p&gt;First preserve the failure episode. Then classify process liveness, readiness, execution state, delivery state, and credential state separately. Quarantine after a bounded number of attempts. Reconcile UNKNOWN side effects before replaying anything.&lt;/p&gt;

&lt;p&gt;That turns “the container keeps restarting” from a vague uptime problem into a testable recovery contract.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your AI Agent Needs a Credential Lease, Not a Permanent API Key</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:17:58 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-needs-a-credential-lease-not-a-permanent-api-key-1mnm</link>
      <guid>https://dev.to/zira125/your-ai-agent-needs-a-credential-lease-not-a-permanent-api-key-1mnm</guid>
      <description>&lt;p&gt;Long-running agents turn a small credential mistake into a large incident.&lt;/p&gt;

&lt;p&gt;A worker that can run overnight, spawn tools, or retry after a restart should not receive a permanent API key and keep it until the process dies. Treat access as a lease with an owner, scope, expiry, and revocation state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lease contract
&lt;/h2&gt;

&lt;p&gt;Store these fields beside the run, not only in environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lease_id&lt;/li&gt;
&lt;li&gt;run_id&lt;/li&gt;
&lt;li&gt;principal&lt;/li&gt;
&lt;li&gt;allowed_actions&lt;/li&gt;
&lt;li&gt;allowed_resources&lt;/li&gt;
&lt;li&gt;issued_at&lt;/li&gt;
&lt;li&gt;expires_at&lt;/li&gt;
&lt;li&gt;revoked_at&lt;/li&gt;
&lt;li&gt;credential_version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tool call is allowed only when all of these checks pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The lease exists and is unexpired.&lt;/li&gt;
&lt;li&gt;The run is still authorized to use it.&lt;/li&gt;
&lt;li&gt;The requested action and resource match the lease.&lt;/li&gt;
&lt;li&gt;The credential version is current.&lt;/li&gt;
&lt;li&gt;The worker has not entered a quarantine state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model can suggest a tool call. It must not decide whether the lease is valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recheck at dispatch time
&lt;/h2&gt;

&lt;p&gt;Checking access when a run starts is not enough. Queues, retries, and browser sessions create time gaps. Recheck immediately before dispatch:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def authorize(call, lease, policy, now):
    if lease.revoked_at is not None:
        return 'DENY_REVOKED'
    if now &amp;gt;= lease.expires_at:
        return 'DENY_EXPIRED'
    if lease.credential_version != policy.current_version(lease.principal):
        return 'DENY_STALE_VERSION'
    if call.action not in lease.allowed_actions:
        return 'DENY_ACTION'
    if call.resource not in lease.allowed_resources:
        return 'DENY_RESOURCE'
    return 'ALLOW'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Do not silently refresh a denied lease. Create a new authorization decision with a new lease ID, and record why the old one failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make revocation observable
&lt;/h2&gt;

&lt;p&gt;A useful event trail has one row per decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LEASE_ISSUED&lt;/li&gt;
&lt;li&gt;CALL_PROPOSED&lt;/li&gt;
&lt;li&gt;CALL_ALLOWED or CALL_DENIED&lt;/li&gt;
&lt;li&gt;CREDENTIAL_PRESENTED&lt;/li&gt;
&lt;li&gt;OUTCOME_CONFIRMED, OUTCOME_FAILED, or OUTCOME_UNKNOWN&lt;/li&gt;
&lt;li&gt;LEASE_REVOKED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never log the secret itself. Log the lease ID, credential version, provider request ID when available, and a hash of the resource identifier if the raw value is sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure test that matters
&lt;/h2&gt;

&lt;p&gt;Run this test against a staging provider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a run with a 10-minute lease.&lt;/li&gt;
&lt;li&gt;Queue a tool call but pause the worker before dispatch.&lt;/li&gt;
&lt;li&gt;Revoke the lease and rotate the credential.&lt;/li&gt;
&lt;li&gt;Resume the worker.&lt;/li&gt;
&lt;li&gt;Assert that dispatch is denied with DENY_REVOKED or DENY_STALE_VERSION.&lt;/li&gt;
&lt;li&gt;Repeat with the provider accepting the request but timing out the response.&lt;/li&gt;
&lt;li&gt;Mark the result UNKNOWN and reconcile using the provider request ID before retrying.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This catches the dangerous gap between the agent being allowed earlier and the agent being allowed now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting does not remove the boundary
&lt;/h2&gt;

&lt;p&gt;If you run an always-on OpenClaw or browser worker on a managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=credential-lease" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt;, the lease layer still belongs in your application. Hosting can simplify where a process runs; it does not decide which tenant, tool, resource, or credential version a worker may use.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every lease has an expiry and explicit scope.&lt;/li&gt;
&lt;li&gt;Dispatch rechecks policy after queueing and after retries.&lt;/li&gt;
&lt;li&gt;Revocation reaches workers, browser sessions, and child tools.&lt;/li&gt;
&lt;li&gt;Credential rotation invalidates old versions.&lt;/li&gt;
&lt;li&gt;UNKNOWN outcomes are reconciled before retry.&lt;/li&gt;
&lt;li&gt;Logs prove decisions without exposing secrets.&lt;/li&gt;
&lt;li&gt;A staging test demonstrates that a paused worker cannot use a revoked lease.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make agents incapable of acting. It is to make every action attributable, bounded, revocable, and recoverable.&lt;/p&gt;

</description>
      <category>automation</category>
    </item>
    <item>
      <title>Your Agent Container Is Not Isolated Until You Test the Mounts</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:19:03 +0000</pubDate>
      <link>https://dev.to/zira125/your-agent-container-is-not-isolated-until-you-test-the-mounts-4a6k</link>
      <guid>https://dev.to/zira125/your-agent-container-is-not-isolated-until-you-test-the-mounts-4a6k</guid>
      <description>&lt;p&gt;A container can make an AI agent easier to deploy without making it safe to operate.&lt;/p&gt;

&lt;p&gt;The failure is usually not the image. It is the boundary around the image: bind mounts, Docker socket access, shared networks, environment variables, and persistent volumes. If an agent can reach the host filesystem or reuse a broad credential after a rebuild, running in Docker is only a packaging choice.&lt;/p&gt;

&lt;p&gt;This article gives a repeatable mount-and-credential audit for OpenClaw-style agent runtimes and other tool-using workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a threat model
&lt;/h2&gt;

&lt;p&gt;Write down what the agent needs and what it must never reach.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Safer boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read workspace files&lt;/td&gt;
&lt;td&gt;Read-only mount, or a dedicated worktree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write artifacts&lt;/td&gt;
&lt;td&gt;One output directory, not the home directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Call a browser&lt;/td&gt;
&lt;td&gt;Separate browser service and disposable profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send messages&lt;/td&gt;
&lt;td&gt;Narrow, revocable delivery credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control Docker&lt;/td&gt;
&lt;td&gt;Never mount the Docker socket into the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read host secrets&lt;/td&gt;
&lt;td&gt;No host home, SSH, or cloud metadata mount&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The useful question is not whether the container starts. It is what a prompt-injected tool call can reach when the model makes the wrong request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defaults that deserve an explicit review
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A whole-project bind mount when the directory contains secrets or other repositories.&lt;/li&gt;
&lt;li&gt;/var/run/docker.sock mounted into the agent.&lt;/li&gt;
&lt;li&gt;host networking, which removes useful network separation.&lt;/li&gt;
&lt;li&gt;Passing every host environment variable through an env file.&lt;/li&gt;
&lt;li&gt;Reusing a long-lived browser profile across unrelated tasks.&lt;/li&gt;
&lt;li&gt;One opaque writable volume for both durable state and disposable cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns are not automatically wrong in every development environment. They are risky because they make the effective permission set larger than the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A smaller Compose baseline
&lt;/h2&gt;

&lt;p&gt;Start with a non-root UID, a read-only root filesystem, dropped capabilities, and a temporary directory for scratch space:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  agent:
    image: example/agent:latest
    user: "10001:10001"
    read_only: true
    cap_drop: ["ALL"]
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=256m
    volumes:
      - ./agent-workspace:/workspace:rw
      - agent-state:/var/lib/agent:rw
    environment:
      AGENT_WORKSPACE: /workspace
      DELIVERY_TOKEN_FILE: /run/secrets/delivery_token
    secrets:
      - delivery_token

  secrets:
    delivery_token:
      file: ./secrets/delivery_token

  volumes:
    agent-state:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is not a complete security policy. It is a boundary that can be inspected. Add outbound access through a small egress proxy or separate delivery worker when the agent does not need general network access.&lt;/p&gt;

&lt;p&gt;For an OpenClaw deployment that must stay available, a managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=docker-mount-isolation" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can reduce the amount of host maintenance you own. It does not remove the need to review mounts, credentials, browser profiles, or recovery behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundary from inside the container
&lt;/h2&gt;

&lt;p&gt;Do not stop at reading YAML. Run tests against the running process and record the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check identity and capabilities
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec agent id
docker compose exec agent sh -c 'grep CapEff /proc/1/status'
docker inspect agent --format '{{json .HostConfig.CapDrop}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The process should not run as root and should not retain unnecessary Linux capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enumerate mounts
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker inspect agent --format '{{range .Mounts}}{{println .Source "-&amp;gt;" .Destination "rw=" .RW}}{{end}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Fail the deployment if a source is broader than the task requires, if a sensitive host path appears, or if a cache is accidentally writable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Probe for host-control paths
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec agent sh -c 'test ! -S /var/run/docker.sock &amp;amp;&amp;amp; test ! -d /root/.ssh &amp;amp;&amp;amp; test ! -f /proc/1/root/etc/shadow'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A failed probe is not proof of compromise. It is proof that the boundary needs review.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test credential replacement
&lt;/h3&gt;

&lt;p&gt;Create a short-lived test credential with a unique identifier. Revoke it, restart the agent, and confirm that the old credential is rejected, the agent cannot read the host shell environment to find a replacement, logs record the denial without printing the secret, and a clean rebuild does not silently restore the revoked credential.&lt;/p&gt;

&lt;p&gt;A credential file should have an owner, scope, expiry, and revocation procedure. Being in a secret store is not a procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence needs two inventories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rebuildable:&lt;/strong&gt; image layers, package caches, temporary browser data, exported logs, and generated files that can be recreated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable:&lt;/strong&gt; agent identity, pending work, idempotency keys, policy version, provider cursors, approved destinations, and encrypted credential references.&lt;/p&gt;

&lt;p&gt;If both categories share one opaque volume, operators restore too much or too little. A clean-room restore should prove that durable state returns while stale browser cookies, abandoned locks, and revoked tokens do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure-injection checklist
&lt;/h2&gt;

&lt;p&gt;Run this before calling the deployment recoverable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete the container while a tool call is running.&lt;/li&gt;
&lt;li&gt;Remove the workspace mount and verify the agent fails closed.&lt;/li&gt;
&lt;li&gt;Revoke the delivery credential during a retry.&lt;/li&gt;
&lt;li&gt;Restore the state volume into a new container with a different image digest.&lt;/li&gt;
&lt;li&gt;Start two workers against the same state volume.&lt;/li&gt;
&lt;li&gt;Interrupt the process after it records intent but before the external side effect.&lt;/li&gt;
&lt;li&gt;Interrupt it after the provider accepts the request but before the response is stored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every case, record confirmed, not started, or unknown. Unknown is a first-class state. Retrying it blindly is how a recovery script sends duplicate messages or repeats a destructive tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deployment gate for CI
&lt;/h2&gt;

&lt;p&gt;A lightweight gate can catch regressions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no Docker socket mount;&lt;/li&gt;
&lt;li&gt;no host network mode;&lt;/li&gt;
&lt;li&gt;no sensitive host-path mounts;&lt;/li&gt;
&lt;li&gt;non-root user;&lt;/li&gt;
&lt;li&gt;dropped capabilities;&lt;/li&gt;
&lt;li&gt;explicit writable mounts;&lt;/li&gt;
&lt;li&gt;secrets passed by file or workload identity, not broad environment inheritance;&lt;/li&gt;
&lt;li&gt;state and cache volumes named separately;&lt;/li&gt;
&lt;li&gt;image digest recorded;&lt;/li&gt;
&lt;li&gt;restore and revocation tests attached to the release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to claim perfect isolation. It is to make the effective boundary visible, narrow, and testable.&lt;/p&gt;

&lt;p&gt;If you build or host AI agents, follow for practical failure tests around state, identity, delivery, and recovery. The model is only one component. The container boundary decides what happens when the model, tool, or operator is wrong.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
