Exactly-Once Web Effects for Agentic Browsers: Idempotent Actions, Write‑Ahead Intents, and Safe Retries for Auto‑Agent AI Browser Agents
Agentic browsers—AI-driven automation layers built on Playwright, Puppeteer, WebDriver, and headless shells—are arriving in production workflows. They are placing orders, filing tickets, reconciling ledgers, approving pull requests, and orchestrating SaaS. That’s great for productivity, but it also introduces a known systems problem into the web client/server model: how to guarantee an effect happens exactly once when your client can crash, retry, lose context, or rerun a script under a different network path?
Today’s web stacks implicitly assume a human is in the loop: when a user sees a spinner and hits refresh, you rely on back-end deduping (if any) and social friction to avoid duplicate purchases. AI agents won’t give you that safety margin; they will retry programmatically, in parallel, across sessions. If your endpoints aren’t replay-aware, you’ll get duplicate writes, inconsistent state, and potential security exposure when adversaries replay captured requests.
This article proposes a web-side design for exactly-once effects tailored to agentic browsers. We’ll walk through idempotent action design, write-ahead intents, replay-aware write fences, CSRF coordination, transactional snapshots, and safe retries—plus concrete headers, algorithms, and code.
Opinion: For the next generation of auto-agents, websites should treat an AI browser as a partially reliable, at-least-once delivery client. If you care about safety, move idempotency to the server boundary and make it first-class.
The Problem in Real Terms: Failure Modes Everywhere
AI browser agents fail in human-invisible ways:
- Network-level: TLS session resumption and 0-RTT “early data” may replay POSTs; mobile proxies or enterprise gateways retry; request races when a page is reloaded mid-submit.
- Agent runtime: The agent times out on a selector; retries the whole step; runs the task twice in parallel because a worker was rescheduled.
- UI-driven variability: Shadow DOM changes, slow SPA state hydration, reissued CSRF tokens, or accidental double-click equivalents.
- Cloud scaling: Multiple browser instances acting on the same account or session for throughput.
These translate into classes of risk:
- Duplicate side effects (two orders, two payments, two approvals)
- Lost-at-least-once side effects (client thinks it failed; server already committed)
- Cross-transaction skew (agent read state that is stale and wrote something conditional on a false assumption)
- Replay injection (adversary reuses captured requests)
Exactly-once effects require making the server replay-aware and the client retry-safe without leaking secrets or creating new attack surfaces.
What “Exactly-Once” Means on the Web
On the open web, you never get literal exactly-once delivery. What you can build are exactly-once externally observable effects per actor and intent within a defined scope and time window. That means:
- Idempotency: You can repeat the same logically equivalent request and get the same outcome. The resource transitions at most once.
- Deduplication across retries: Replays map to the same effect identity.
- Preconditions: Writes commit only if they were derived from a known snapshot or meet invariants.
- Safe detection: The server can distinguish a legitimate retry from an attack or a new request.
HTTP gives you useful primitives:
- Method idempotence semantics (RFC 9110): GET/HEAD/OPTIONS/TRACE are idempotent; PUT/DELETE are idempotent in spirit but not widely used for dynamic mutations; POST is not idempotent by default.
- Conditional requests: ETag + If-Match/If-None-Match for optimistic concurrency.
- Status codes: 409 Conflict, 412 Precondition Failed, 425 Too Early (for TLS 0-RTT replay mitigation), 422 Unprocessable Content.
Industry practice adds more:
- Idempotency-Key header (a de facto pattern popularized by payment APIs; also an IETF work-in-progress draft)
- Request canonicalization and hashing to bind “what” is being done to “who” is doing it
- Server-side intent registries that fence writes and record outcomes
Five Pillars of Agent-Safe Web Effects
- Idempotent Actions: Stable effect identity derived from intent and actor, not from transport or timing.
- Write-Ahead Intents: A prepare/commit model or a one-shot request with a recorded intent that the server can reconcile.
- Replay-Aware Write Fences: Server-side guards that detect replays, enforce preconditions, and either return the prior result or reject mismatches.
- CSRF Coordination: Bind CSRF tokens to the request intent and session; use Fetch Metadata and SameSite to reduce ambient authority misuse.
- Transactional Snapshots: Include a read snapshot token (ETag, LSN, vector clock) in writes to maintain consistency.
We’ll assemble these into concrete patterns and code.
Pattern A: Idempotency Keys + Intent Hashing
Idempotency keys alone are not enough. They deduplicate, but if an attacker replays your key with different parameters, you can get mismatched effects. The fix is to bind a canonical hash of the request to the key.
Recommended headers (names are examples, not standards):
- Idempotency-Key: a UUIDv4 or ULID generated per logical action
- Intent-SHA256: base64url-encoded SHA-256 digest of a canonicalized representation of the intended write
- Intent-Expires-At: absolute timestamp or TTL window
- Actor-Scope: identifier for the principal (user_id, account_id). If you use session cookies, the server will derive actor anyway; this header is helpful for APIs.
Client-side canonicalization (TypeScript example):
tsimport { createHash, randomUUID } from 'node:crypto'; function canonicalize(payload: Record<string, any>): string { // 1) stable JSON stringify: sort keys recursively and remove volatile fields function sortKeys(obj: any): any { if (Array.isArray(obj)) return obj.map(sortKeys); if (obj && typeof obj === 'object') { const out: Record<string, any> = {}; Object.keys(obj).sort().forEach(k => { if (['csrf', 'nonce', 'timestamp'].includes(k)) return; // exclude volatile out[k] = sortKeys(obj[k]); }); return out; } return obj; } return JSON.stringify(sortKeys(payload)); } function sha256Base64url(s: string): string { return createHash('sha256').update(s).digest('base64url'); } async function postWithIdempotency(url: string, body: Record<string, any>) { const key = randomUUID(); const canonical = canonicalize(body); const intent = sha256Base64url(canonical); const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key, 'Intent-SHA256': intent, 'Intent-Expires-At': new Date(Date.now() + 15 * 60_000).toISOString(), 'X-Agent-Run-Id': randomUUID(), }, body: JSON.stringify(body), credentials: 'include', }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }
Server-side fence (Node/Express + Postgres):
ts// Table to record idempotent outcomes // CREATE TABLE intent_dedup ( // actor_id TEXT NOT NULL, // idempotency_key TEXT NOT NULL, // intent_sha256 TEXT NOT NULL, // status INT NOT NULL, // response JSONB NOT NULL, // created_at TIMESTAMPTZ NOT NULL DEFAULT now(), // PRIMARY KEY (actor_id, idempotency_key) // ); // CREATE INDEX ON intent_dedup (actor_id, intent_sha256); app.post('/api/transfer', requireAuth, async (req, res) => { const actorId = req.user.id; // derived from session const key = req.get('Idempotency-Key'); const intent = req.get('Intent-SHA256'); const expiresAt = req.get('Intent-Expires-At'); if (!key || !intent) return res.status(400).json({ error: 'missing headers' }); if (expiresAt && new Date(expiresAt) < new Date()) { return res.status(400).json({ error: 'intent expired' }); } // Enforce canonical match const canonical = canonicalize(req.body); const computed = sha256Base64url(canonical); if (computed !== intent) return res.status(422).json({ error: 'intent mismatch' }); // Fast-path: check dedup table const existing = await db.oneOrNone( 'SELECT status, response, intent_sha256 FROM intent_dedup WHERE actor_id=$1 AND idempotency_key=$2', [actorId, key] ); if (existing) { if (existing.intent_sha256 !== intent) { return res.status(409).json({ error: 'idempotency key replay with different intent' }); } return res.status(existing.status).json(existing.response); } // Execute business logic in a transaction try { const result = await db.tx(async t => { // TODO: perform validation, balance checks, etc. const transfer = await t.one( 'INSERT INTO transfers (actor_id, amount, to_account) VALUES ($1,$2,$3) RETURNING *', [actorId, req.body.amount, req.body.to] ); // outbox pattern for side-effects (e.g., webhooks/email) await t.none('INSERT INTO outbox(event_type, payload) VALUES ($1,$2)', ['transfer.created', transfer]); return { status: 201, response: { transfer_id: transfer.id } }; }); await db.none( 'INSERT INTO intent_dedup(actor_id, idempotency_key, intent_sha256, status, response) VALUES ($1,$2,$3,$4,$5)', [actorId, key, intent, result.status, result.response] ); return res.status(result.status).json(result.response); } catch (e) { // In case of unknown outcome, you may record a transient failure with TTL return res.status(500).json({ error: 'server error' }); } });
Key points:
- The server binds the key to a digest of the canonical request body. Replaying the key with new parameters fails safely.
- Duplicate retries return the original result deterministically.
- The dedup record lives in durable storage, not process memory.
- Use a TTL or soft-delete to bound storage—keep intents for at least as long as clients might retry (often 24–72 hours), longer for money or inventory.
Pattern B: Replay-Aware Write Fences
A write fence is a policy that governs whether a write can proceed given its metadata and context. Done well, it neutralizes both accidental retries and adversarial replays.
Elements of a good fence:
- Dedup check: (actor_id, idempotency_key) uniqueness with stored intent digest
- Precondition check: If-Match snapshot token, business invariants (balance >= amount), rate limit windows
- Replay horizon: Do not accept early-data (0-RTT) replays; respond 425 Too Early when appropriate
- Double-submit detection: If a semantically identical request arrives concurrently, treat it as a retry; if semantically different, return 409
You can encode fence results explicitly:
- 201 Created + Location: /transfers/{id} on first success
- 200 OK with the same body for a legitimate retry
- 409 Conflict with a structured error code when intent mismatch occurs
- 425 Too Early when TLS 0-RTT early data is in use (this is more of an origin/CDN config and server TLS stack setting)
On CDNs and reverse proxies, disable 0-RTT for write paths or configure the origin to send 425 so user agents (including your agent) can retry after the handshake completes.
Pattern C: CSRF Coordination for Agents
CSRF defenses are abundant but often uncoordinated with idempotency. A robust agent-aware approach:
- SameSite cookies: Set session cookies with SameSite=Lax or Strict when possible. For cross-site flows, consider SameSite=None; Secure, and compensate with tokens.
- Fetch Metadata: Check Sec-Fetch-Site, Sec-Fetch-Mode, and Origin headers. Human browsers send strong signals; an agent should too.
- Double submit cookie: Issue a csrf cookie and require a matching token in a header or body.
- Token binding to intent: Bind the CSRF token to the Intent-SHA256 and session. This reduces token reuse for a different write.
Example CSRF token format:
- CSRF-Token = HMAC(server_secret, session_id || intent_sha256 || expiry)
Server validates:
- The session matches; intent digest matches; token unexpired; Fetch Metadata indicates same-site or expected cross-site; Origin header acceptable.
Example bind in code:
ts// Issue CSRF on GET form render app.get('/transfer/new', requireAuth, (req, res) => { const sessionId = req.session.id; // e.g., from cookie-based session res.json({ sessionId }); }); // Agent computes Intent-SHA256 and asks the server for a CSRF token bound to that intent app.post('/csrf/issue', requireAuth, (req, res) => { const { intent_sha256, expires_at } = req.body; const token = hmac(serverSecret, `${req.session.id}|${intent_sha256}|${expires_at}`); res.json({ csrf_token: token, expires_at }); }); // On submit, server recomputes and compares app.post('/transfer', requireAuth, (req, res) => { const intent = req.get('Intent-SHA256'); const token = req.get('X-CSRF-Token'); const expiresAt = req.get('Intent-Expires-At'); const expected = hmac(serverSecret, `${req.session.id}|${intent}|${expiresAt}`); if (token !== expected) return res.status(403).json({ error: 'csrf' }); // proceed with idempotency fence });
If you can’t bind CSRF to intent (legacy apps), at least use both CSRF and idempotency keys—the intersection significantly reduces effective replay surfaces. And check Origin on state-changing POSTs.
Pattern D: Transactional Snapshots with ETags and LSNs
Agents often read, compute, then write. If the read is stale, the write might be unsafe. Use transactional snapshots that the server recognizes.
Two common approaches:
- Resource ETags: The GET /cart returns ETag: "abc123". The agent includes If-Match: "abc123" on POST /checkout. If the cart changed, the server rejects with 412 Precondition Failed.
- Database commit LSN or version: The server returns a snapshot token like X-Snapshot: lsn:2024-12-01T12:34:56Z or version: 42. Writes include Precondition-Snapshot: version: 42. If the current version > 42 and the write is not commutative, reject with 409.
Example:
httpGET /cart HTTP/1.1 Accept: application/json HTTP/1.1 200 OK ETag: "W/\"cart-v17\"" Content-Type: application/json { "items": [ ... ] } --- POST /checkout HTTP/1.1 Content-Type: application/json Idempotency-Key: 5c2e... Intent-SHA256: 3Q2+... If-Match: "W/\"cart-v17\"" { "payment_method": "pm_123" }
If the cart changes to v18 between read and write, respond with 412. The agent then re-reads and recomputes. This reduces Heisenbugs where the agent assumes state that is no longer valid.
For multi-resource workflows, expose a session-level snapshot marker (e.g., a read timestamp). In databases that support it (Postgres with REPEATABLE READ, Spanner with timestamps), you can tie the agent’s GETs to a consistent snapshot and reference that in the write.
Pattern E: Exactly-Once Submit via Prepare/Commit (Write-Ahead Intents)
When the business effect is high-risk (payments, inventory, compliance approvals), a two-phase pattern is justified:
- Prepare intent
- POST /intents with the operation payload
- Server validates, reserves capacity, and returns intent_id with a CSRF token bound to intent
- No irreversible side-effects yet
- Commit
- POST /intents/{id}/commit with Idempotency-Key + Intent-SHA256 matching the prepared intent
- Server performs the irreversible effect once
This model is robust for flaky networks and allows safe resume:
httpPOST /intents HTTP/1.1 Content-Type: application/json { "to": "acct_abc", "amount": "25.00", "currency": "USD" } HTTP/1.1 201 Created Location: /intents/it_42 Content-Type: application/json { "intent_id": "it_42", "intent_sha256": "3Q2+...", "csrf": "t:...", "expires_at": "2025-12-24T12:00:00Z" } --- POST /intents/it_42/commit HTTP/1.1 Idempotency-Key: 3f30... Intent-SHA256: 3Q2+... X-CSRF-Token: t:... {} HTTP/1.1 201 Created Location: /transfers/tr_123 { "transfer_id": "tr_123", "status": "posted" }
The commit endpoint enforces:
- intent_id exists
- intent_sha256 matches prepared payload
- not already committed (dedup)
- CSRF token bound to intent
On retry (due to timeouts or agent crash), the same commit returns the same transfer_id.
Pattern F: Safe Retry Algorithms for Agents
Agent-side retries must be conservative and signal-rich.
- Budgeted exponential backoff with jitter: e.g., 250ms base, full jitter, max 8 tries, 30s cap
- Distinguish transport errors (retry) from application errors (don’t retry)
- Treat 409/412 as non-retryable until you refresh the snapshot or intent
- Respect 425 Too Early by retrying after handshake completion (your HTTP stack should handle this)
- Treat 5xx as retryable with backoff—but only with idempotency in place
- On network timeout after sending a request, always retry only with the same Idempotency-Key
Example retry loop snippet:
tsasync function retryWithIdempotency(fn: () => Promise<Response>) { const start = Date.now(); let attempt = 0; while (true) { try { const res = await fn(); if ([409, 412].includes(res.status)) throw new Error('precondition'); return res; // 2xx or other handled statuses } catch (e: any) { attempt++; const elapsed = Date.now() - start; if (e.message === 'precondition') throw e; // refresh state if (attempt > 8 || elapsed > 30_000) throw e; const sleep = Math.min(30_000, Math.random() * (2 ** attempt) * 250); await new Promise(r => setTimeout(r, sleep)); } } }
Always log and propagate trace IDs: X-Request-Id, X-Agent-Run-Id, and Idempotency-Key—so operators can reconcile what happened.
Agent–Origin Contract: Make It Explicit
Websites can advertise agent-safe features via headers:
- Accepts-Idempotency-Key: true
- Accepts-Intent-Hash: sha256
- Requires-CSRF-Bound-Intent: true
- Supports-Snapshot: etag
And respond with metadata:
- Idempotency-Key-Consumed: true/false
- Snapshot: etag: "W/"v17""
- Intent-Status: prepared|committed|replayed|mismatch
These are not standards, but even private agreements across cooperating services drastically reduce friction for agent authors.
Handling Side-Effects and Outboxes
Exactly-once within your origin does not automatically dedupe downstream side-effects (emails, webhooks). Use the outbox and consumer idempotency patterns:
- Outbox: Write domain events to an outbox table within the same transaction as the domain change; a background dispatcher delivers them. If the request is retried, the outbox already contains the event; delivery is deduped by downstream consumer idempotency keys.
- Consumer idempotency: Downstream services accept Idempotency-Key and maintain their own dedup table keyed by (producer, key).
This turns the whole graph into a composition of exactly-once boundaries.
Data Modeling for Idempotency
Design your persistence with business keys:
- Unique constraints that reflect logical uniqueness of the effect. Example: transfers(unique_business_key) where key = H(user_id, to, amount, currency, client_reference)
- The dedup table stores a pointer to the created resource ID. Replays return that ID.
- Keep request payload, headers, timestamps, and response for audit.
Postgres sketch:
sqlCREATE TABLE transfer_effects ( actor_id TEXT NOT NULL, business_key TEXT NOT NULL, transfer_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY(actor_id, business_key) ); CREATE TABLE idempotency_log ( actor_id TEXT NOT NULL, idempotency_key TEXT NOT NULL, intent_sha256 TEXT NOT NULL, effect_ref TEXT NOT NULL, response JSONB NOT NULL, status INT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY(actor_id, idempotency_key) );
Maintain both: a business-key guard (semantic dedup) and an idempotency-key guard (transport dedup). They serve different purposes and catch different classes of duplicate.
Threat Modeling: Replay and Injection
Adversarial considerations:
- Captured request replay: An attacker who steals an Idempotency-Key could attempt to replay with altered payload. Intent hashing and key binding protects you.
- Session fixation: If session cookies are misconfigured, an agent’s session can be hijacked. Use Secure, HttpOnly, SameSite, short TTL, and rotate on privilege elevation.
- CSRF bypass: Ensure you check Origin and Fetch Metadata, not just custom headers.
- Timing and side channels: Intent hashes should not reveal PII. Use keyed digests (HMAC) if the digest might be logged or shared across boundaries.
Safer intent digest:
tsfunction intentDigestHmac(payloadCanonical: string, actorId: string) { return createHmac('sha256', serverSecret).update(actorId + '|' + payloadCanonical).digest('base64url'); }
This binds the digest to the actor and prevents cross-actor correlation.
Transactional Snapshots: Beyond ETags
For multi-entity workflows (e.g., allocate inventory, then create shipping label), a stronger snapshot helps:
- Return a read timestamp or vector (X-Snapshot: ts=2025-12-24T12:34:56.789Z)
- Guarantee that reads with that token are served at that timestamp (if your DB supports time travel or MVCC with stable snapshots)
- On write, require Precondition-Snapshot: ts=... and reject if current time > ts and the mutation is not commutative or safe under skew
Some systems expose this as a “read consistency token”. If your stack can’t, approximate with ETag on a composite aggregate (e.g., user profile version + cart version + balance version) returned by a coordination endpoint.
Replay Windows, TTLs, and Storage Pressure
You must pick a retention window for dedup records. Consider:
- Agent retry budgets: If agents retry over hours, keep dedup at least that long
- Payment standards: Payment providers often keep idempotency references for days
- Space/Throughput: Use Redis or a write-optimized store for hot dedup checks; journal to Postgres for audit
- Eviction Rules: Evict only records in a terminal state; consider keeping a compact index (actor_id, intent_sha256 -> effect_id)
Hybrid approach:
- Cache (Redis) for fast dedup within 24–72 hours
- Durable journal (SQL) for long-term audit and rare replays
Multi-Step Forms: Step Tokens and Finalization
Traditional web forms spread state across multiple POSTs. For agents, promote a formal step token:
- Each step response returns Step-Token: st_xxx, bound to the accumulated model and snapshot
- The final submit includes Idempotency-Key, Intent-SHA256 of the entire accumulated model, and Step-Token, enabling the server to validate that the agent didn’t skip steps or mix incompatible states
Example server behavior:
- Step token encodes model_hash and step_number
- Final submit checks step >= required, model_hash matches Intent-SHA256, snapshot is valid
This improves safety in brittle SPAs where silent client-side mutations are common.
Browser-Side Support: Service Worker as Agent OS
For agents embedded in a browser environment, a service worker can offer:
- Automatic Idempotency-Key generation and persistence across reloads per logical action
- Request canonicalization and digest computation
- Automatic retries with backoff on network errors
- Storage of last-known responses keyed by (actor, idempotency_key)
This encapsulates best practices away from per-script boilerplate and works for both human UI and agent actions.
Interoperability with Standards
- HTTP Semantics (RFC 9110): Respect idempotence of methods; use conditional requests
- Early Data (RFC 8470): Avoid 0-RTT for writes or handle 425 Too Early
- De facto Idempotency-Key header: Several APIs (e.g., payment providers) use this; an IETF draft exists, but do not assume universal support
- HTTP Message Signatures: If you need non-repudiation, sign requests (be careful with browser constraints)
Don’t wait for a formal standard to implement server-side idempotency: the risk exists today, and patterns are well understood.
Testing the Hard Cases
Build a replay harness:
- Capture live requests with headers and bodies
- Re-send them concurrently, out-of-order, and with modified fields to validate fences
- Kill and restart the server between prepare and commit
- Simulate timeouts between write and response emission (client sees error, server commits)
- Run property-based tests that generate permutations of retries and concurrency
SLOs and observability:
- Track “mismatch attempts” (409 due to intent mismatch)
- Track “replayed requests served” (successful idempotent replays)
- Audit bounce rate on 412 preconditions
- Emit structured logs with actor_id, idempotency_key, intent_sha256, effect_id
Real-World Example: Checkout with Inventory Reservation
Flow:
- Agent reads product page -> receives ETag: "prod-v101" and X-Stock-Version: 775
- Agent initiates intent: reserves item
- Server creates reservation (expires in 10 minutes), returns intent_id + intent_sha256
- Agent commits checkout with payment method and shipping address
- Server posts order once, emits outbox events
Key safety:
- Reservation ensures no double decrement from retries
- Intent binds raw parameters; changing address or quantity invalidates the intent
- If-Match on cart prevents stale additions
- Dedup ensures only one order is created per intent, even under network chaos
Limitations and Truths
- Exactly-once delivery is impossible to guarantee end-to-end across arbitrary networks; what we provide is exactly-once effect per defined scope with bounded windows.
- Non-transactional externalities (sending emails, printing labels) require their own idempotency and reconciliation loops.
- Binding CSRF to intent improves safety but complicates UX if humans are in the loop; consider dual-mode tokens.
- Canonicalization must be stable across versions; changing field names breaks hashes—version your canonical format.
A Minimal Checklist for Agent-Safe Idempotency
- For every POST that causes side effects:
- Require Idempotency-Key, Intent-SHA256; store and fence
- Bind to actor and TTL
- Return the same response on legitimate retry
- Bind CSRF tokens to intent when possible; always check Origin and Fetch Metadata
- Provide snapshot tokens (ETag/If-Match) for read-then-write workflows
- Offer prepare/commit for high-risk operations
- Use outbox and consumer idempotency for side-effects
- Disable 0-RTT for write paths or respond with 425 Too Early
- Implement safe retry guidance in client libraries or service workers
- Instrument with structured logs and metrics for replays and mismatches
Putting It All Together: A Compact End-to-End Example
Client (agent) flow:
tsconst body = { to: 'acct_abc', amount: '25.00', currency: 'USD' }; const canonical = canonicalize(body); const intent = sha256Base64url(canonical); const key = crypto.randomUUID(); const csrf = await fetch('/csrf/issue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intent_sha256: intent, expires_at: expiryIso }), credentials: 'include', }).then(r => r.json()); await retryWithIdempotency(async () => fetch('/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key, 'Intent-SHA256': intent, 'Intent-Expires-At': expiryIso, 'X-CSRF-Token': csrf.csrf_token, 'X-Agent-Run-Id': runId, }, body: JSON.stringify(body), credentials: 'include', }) );
Server key behaviors:
- Reject if Idempotency-Key missing
- Reject if Intent-SHA256 doesn’t match canonical body
- On first commit, perform effect and store outcome in intent_dedup
- On retry with same key and digest, return stored outcome
- On retry with same key but different digest, return 409
- Enforce CSRF bound to intent
This is the smallest useful contract for safe, exactly-once web effects.
Conclusion
Agentic browsers convert the messy outer world of the web into an automated substrate—one where retries, races, and replays are normal, not exceptional. If you do nothing, you’ll experience duplicate writes, inconsistent state, and security exposure. The fix is not magic; it’s disciplined systems design at the web boundary:
- Introduce stable intent identity (hashing + idempotency keys)
- Record write-ahead intents and fence replays
- Bind CSRF to the intent and verify Fetch Metadata
- Tie writes to transactional snapshots with ETag/If-Match or version tokens
- Offer prepare/commit for high-stakes operations
- Adopt outbox and consumer idempotency downstream
- Test with aggressive replay harnesses and measure outcomes
This design aligns classic distributed systems techniques with the realities of browsers and HTTP. It’s implementable today with standard stacks and pays off immediately in lower incident rates and higher agent reliability. As auto-agents become ubiquitous, sites that advertise and enforce these semantics will be the ones agents can trust—and the ones operators can sleep on.
