@haven_ai/signer 0.4.0-alpha.0 → 0.6.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,7 @@ It exposes four stdio MCP tools, all sign-only:
61
61
 
62
62
  | Tool | Does | Emits |
63
63
  |---|---|---|
64
- | `haven_sign` | Sign one payment. Preferred form is `{ payment_id }` alone — the signer fetches the exact payload itself. Signs an EIP-712 typed-data payload on the delegation rail (a redemption, or an erc7710 settlement child), or a bare `payload_hash` on a v1 context; for the EIP-3009 x402 bridge it also records the funding context and returns a binding | `{ signature }` or `{ signature, x402_binding }` |
64
+ | `haven_sign` | Sign one payment, or (#3329) one task-budget open/close. Preferred form is `{ payment_id }` alone for a payment, or `{ task_budget_id }` alone (mutually exclusive with `payment_id`) for a task-budget open/close — the signer fetches the exact payload itself either way. Signs only Haven-prepared payloads (#3272, #3281, #3329): a direct-payment `PackedUserOperation` from this signer's own delegate account whose only call redeems a delegation made to that account EITHER directly OR through a self-delegated task-budget child under it (the two-link `[task child, budget]` chain), or — against a Haven-signed context, which it records and binds — an EIP-3009 funding leg of that same shape paying this signer's own delegate EOA, or an erc7710 settlement child from this signer's own account; other typed data is refused (`TYPED_DATA_NOT_ALLOWED`) | `{ signature }`, `{ signature, x402_binding }`, or `{ signature, task_budget_id, purpose }` |
65
65
  | `haven_sign_x402` | One-shot x402: funding signature **and** the merchant header in a single local call (`haven_sign` + `haven_x402_sign_header`). `{ payment_id }` alone is the preferred call | `{ signature, x402_binding, payment_header, accepted }` |
66
66
  | `haven_x402_sign_header` | Build + sign the EIP-3009 merchant payment header, only when the fresh merchant `payment_required` matches the recorded `x402_binding` | `{ payment_header, accepted }` |
67
67
  | `haven_sign_sweep_delegate` | Sign a Haven-prepared gasless EIP-3009 sweep that recovers stranded funds from the delegate wallet back to your own account. Never broadcasts | `{ signature }` |
@@ -70,7 +70,8 @@ The `initialize` handshake advertises which binding versions this signer
70
70
  understands, under `capabilities.experimental['haven/signer-compatibility']`
71
71
  and in the MCP `instructions` string. Both are **derived** from
72
72
  `SUPPORTED_X402_EXPECTED_VERSIONS` / `SUPPORTED_SWEEP_BINDING_VERSIONS` in
73
- `src/core.ts` — the same constants the signing path enforces — so this README
73
+ `src/core.ts` and `SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS` in
74
+ `src/sign-context.ts` (#3271) — the same constants the signing path enforces — so this README
74
75
  deliberately does not restate the numbers. Read them from the handshake, or
75
76
  from those constants.
76
77
 
@@ -93,11 +94,108 @@ const { paymentHeader } = await signer.buildX402PaymentHeader(
93
94
  )
94
95
  ```
95
96
 
96
- The signer also exposes `signPaymentHash(hash)` (raw ECDSA over a legacy
97
- AllowanceModule funding/transfer hash) and `signX402FundingHash(hash, expected)`
98
- for v1 contexts, and `signSweepAuthorization(input)` for the gasless sweep. All
99
- six are methods on the object `createEdgeSigner` returns, not standalone
100
- exports.
97
+ The signer also exposes `signSweepAuthorization(input)` for the gasless sweep.
98
+ All four are methods on the object `createEdgeSigner` returns, not standalone
99
+ exports. There is NO raw-hash primitive: `signPaymentHash(hash)` (raw ECDSA over
100
+ the retired AllowanceModule rail's hash) was removed in #3169, and
101
+ `signX402FundingHash` (the expected-context v1 bare-hash path) in #3272 — the
102
+ signer supports expected-context versions 2 and 3 only. **Library boundary
103
+ (#3272):** `signDelegationTypedData` is a verbatim primitive for embedders —
104
+ it signs whatever typed data it is handed. The allowlist lives in the MCP tool
105
+ layer: `haven_sign` signs typed data only when it is a bound direct-payment
106
+ `PackedUserOperation` (below) or an x402 payload against a Haven-signed context,
107
+ refuses anything else with `TYPED_DATA_NOT_ALLOWED`, and answers a bare
108
+ `payload_hash` with `BARE_HASH_REFUSED`. An embedder that calls
109
+ `signDelegationTypedData` directly owns that check itself. The core's
110
+ `signX402FundingTypedData` is not a verbatim primitive: since #3281 it runs
111
+ the x402 shape checks itself (a guarded funding leg or a verified settlement
112
+ child only), whoever calls it.
113
+
114
+ **The unbound-branch allowlist (#3272).** Without an x402 context, `haven_sign`
115
+ signs typed data only when ALL of these hold; otherwise it refuses, with no
116
+ signature and no audit entry (`TYPED_DATA_NOT_ALLOWED`, or
117
+ `USEROP_BINDING_MISMATCH` when the #3271 binding check fails, or the #1476
118
+ refusal for an unbound `Delegation`): the primary type is
119
+ `PackedUserOperation`; it passes the #3271 binding check below; its chain has pinned delegation contracts (Base, Base Sepolia); its
120
+ sender is THIS signer's own delegate account (the counterfactual
121
+ HybridDeleGator for the delegate key, derived offline — `delegate-account.ts` in `@haven_ai/sdk`);
122
+ and its `callData` is a single `execute` to the DelegationManager calling
123
+ `redeemDelegations`, whose arguments are decoded too (`redemption-guard.ts` in `@haven_ai/sdk`):
124
+ exactly one of two chain shapes (#3329) — a single grant made to this signer's own account by a
125
+ different account, or a two-link `[task child, budget]` chain whose leaf is self-delegated by this
126
+ signer's own account — never an empty chain or any other multi-link shape,
127
+ `SingleDefault` mode, and canonical
128
+ encoding at every level. The argument check matters: an EMPTY permission
129
+ context makes the DelegationManager run the execution as the account itself,
130
+ which would reach `transferOwnership`. A delegate-wallet `TransferWithAuthorization` or `Permit`,
131
+ a UserOp that calls the account itself (`transferOwnership`, `updateSigners`,
132
+ `upgradeToAndCall`), a UserOp for another account, and arbitrary typed data are
133
+ all refused. The audit log records the EIP-712 digest actually signed, not the
134
+ caller's `payload_hash`. Since #3283 the check itself is `@haven_ai/sdk`'s
135
+ `assertBoundDirectPaymentUserOp` (with the redemption guard, the account
136
+ derivation and the settlement-child verifier), which this signer imports and
137
+ which `HavenClient.signForData` runs as well — one implementation.
138
+
139
+ **Direct payments (#3271).** A direct payment (`POST /payments`, surfaced as
140
+ `haven_send` / `haven_pay`) is signed as the account's EIP-712
141
+ `PackedUserOperation`, and Haven returns both that typed data and
142
+ `payload_hash` — the ERC-4337 v0.7 UserOperation hash of the same operation.
143
+ Before `haven_sign` signs one, it recomputes that hash from the typed data's
144
+ own domain, types and message and refuses (`USEROP_BINDING_MISMATCH`) unless
145
+ it equals `payload_hash` exactly, in the HybridDeleGator domain of the typed
146
+ data's own sender, against the v0.7 EntryPoint. This is a **corruption**
147
+ check: the caller supplies both values, so it proves they describe the same
148
+ operation, never that Haven prepared it — provenance is `payment_id` (below).
149
+ It runs whether the typed data arrived as a tool argument or by the
150
+ `payment_id` fetch; a fetched direct context that is not a
151
+ `PackedUserOperation` at all is refused by the same check. The x402 EIP-3009
152
+ bridge's funding leg is checked against the Haven-signed expected context AND,
153
+ since #3281, by this same binding check against the declared UserOp hash, the
154
+ direct-payment allowlist and a recipient pin (a transfer of the quoted amount
155
+ to this signer's own delegate EOA). A settlement child must be re-delegated
156
+ from this signer's own account, never a ROOT grant. Anything else on the x402
157
+ arm is `TYPED_DATA_NOT_ALLOWED`, however validly Haven's binding key declared
158
+ it. The check exists because this typed data is
159
+ multi-KB and can reach the signer through a language model relaying it by
160
+ hand: one corrupted character used to produce a valid-looking signature over
161
+ the wrong digest, surfacing only as an opaque `AA24 signature error` from the
162
+ bundler, after the signature was already produced.
163
+
164
+ ## Startup, CLI options and the consent screen (#3173)
165
+
166
+ **Startup cost.** The signer loads `@haven_ai/sdk/edge` — the ethers-free
167
+ subset of the SDK it actually calls (error classes, typed-next-step builder,
168
+ x402 message builders, viem-based key helpers) — never the SDK barrel, and it
169
+ loads the `x402` package only on the merchant-header leg, on first use. Measured
170
+ on the same machine (median of 5 cold runs, macOS, Node 22): `--help` 1.47 s →
171
+ 0.71 s; the consent refusal 1.55 s → 0.77 s; `import('@haven_ai/sdk')` 1135 ms
172
+ versus `import('@haven_ai/sdk/edge')` 385 ms, of which viem is ~330 ms and stays
173
+ (the signer signs typed data with it). A loader hook resolving every module at
174
+ startup finds zero packages named `ethers` or `x402` (the SDK barrel, as a
175
+ positive control, resolves two). Two tests keep it so: `sdk-edge-import.test.ts`
176
+ fails if any runtime file imports the barrel or `x402/schemes` statically, and
177
+ the SDK's `edge-imports.test.ts` fails if the subpath's import graph ever
178
+ reaches ethers, `x402` or the HTTP client.
179
+
180
+ **CLI.** `--credentials <path>` (alias `--credentials-path`), `--ack`,
181
+ `--help`/`-h`. Any other option is
182
+ refused with one stderr line naming `--help` and exit code 2 — before #3173 an
183
+ unknown flag was silently ignored, so `--ack-local-tools` (the connector's
184
+ flag, which the connector's doctor tells you to pass to the *connector*)
185
+ produced only the consent wall. `--help` lists every registered tool (pinned
186
+ against `toolSchemas`, so a fifth tool cannot drift out of the text) and names
187
+ `npx @haven_ai/connect --doctor`.
188
+
189
+ **Consent screen.** The first-launch block summarises each tool in one
190
+ human-sized line (the full agent-facing descriptions are what the runtime sees,
191
+ not what a person approves) and ends by naming the connector's doctor for the
192
+ connector-wired case, where the doctor shows this as a failed *Signer stdio
193
+ handshake* check (the connector's setup outcome reports it as
194
+ `local_signer_ack_required`) and the repair is the connector's
195
+ `--ack-local-tools`. The refusal an MCP host
196
+ relays ("Connection closed" plus this process's exit message) names the same
197
+ command. The consent hash covers identity, tool names and the surface version —
198
+ not the block's prose — so neither change re-prompts an acknowledged install.
101
199
 
102
200
  ## Orchestration
103
201
 
@@ -156,6 +254,10 @@ funding confirms — so relay it promptly.
156
254
 
157
255
  ## What the signer refuses to sign
158
256
 
257
+ (`USEROP_BINDING_MISMATCH`, the #3271 direct-payment refusal, is described
258
+ under [Two ways to use it](#two-ways-to-use-it) and in the
259
+ [refusal table](#sign-context-refusal-codes).)
260
+
159
261
  These are local, independent checks. They do not trust Haven's assertion about
160
262
  what a payload means; they re-derive it.
161
263
 
@@ -164,9 +266,9 @@ what a payload means; they re-derive it.
164
266
  `x402_binding_signer` in the credential file) so the signer can reject
165
267
  locally invented or tampered contexts before signing anything.
166
268
  - **Wrong signing mode.** The *context* selects the path, never the caller's
167
- arguments: a context that commits to a typed-data digest requires the typed
168
- data, one that does not requires the bare hash. A mismatch is refused rather
169
- than signed into an on-chain failure.
269
+ arguments: a context commits to a typed-data digest and requires exactly that
270
+ typed data. A context without one (the retired version 1) is refused as an
271
+ unsupported version since #3272 — there is no bare-hash signing path.
170
272
  - **Another agent's quote.** A context naming a `payer_delegate` that is not
171
273
  this signer's own delegate is refused.
172
274
  - **An unbound delegation payload.** Typed data with `primaryType: "Delegation"`
@@ -200,7 +302,8 @@ what a payload means; they re-derive it.
200
302
  ## Sign-context refusal codes
201
303
 
202
304
  `{ payment_id }` calls (`haven_sign` / `haven_sign_x402`) fetch the exact
203
- signing payload from Haven (`GET /x402/:id/sign-context`, see
305
+ signing payload from Haven (`GET /x402/:id/sign-context`, and for a direct
306
+ payment `GET /payments/:id/sign-context` — see the #3271 paragraph below and
204
307
  [Custody](#custody)). Every refusal on that fetch is a `HavenSignContextError`
205
308
  (#3001) — a `HavenSigningError` subclass, so `instanceof HavenSigningError`
206
309
  still holds everywhere it did before, but structured like the version-mismatch
@@ -213,24 +316,46 @@ refusal class — `fallback`, `retry_with_new_quote`, `http_status`,
213
316
  names); every other row carries `next_tool_omitted_reason` with the exact
214
317
  remedy. `message` is unchanged.
215
318
 
319
+ **#3271: `haven_sign` (never `haven_sign_x402`) has one escape from this
320
+ table.** When the x402 fetch answers `SIGN_CONTEXT_REFUSED` with
321
+ `http_status: 409` and `backend_error_code: 'sign_context_unavailable'` — this
322
+ `payment_id` names a direct payment, not an x402 intent — `haven_sign` fetches
323
+ `GET /payments/:id/sign-context` instead, same auth header, timeout and
324
+ refusal structuring. That second fetch's own refusals reuse the codes in the
325
+ table below with direct-payment remedies: no quote to re-run, and the
326
+ `typed_data_b64` relay from the `haven_send` / `haven_pay` result is the
327
+ fallback. A 409 `sign_context_unavailable` from the direct route too (an
328
+ x402 row the x402 route could not serve, or a direct row with no stored
329
+ signing payload) surfaces the x402 route's own refusal instead. `haven_sign_x402` never takes this branch: a direct payment
330
+ carries no x402 context to fund a merchant retry with, so it surfaces the
331
+ 409 unchanged.
332
+
216
333
  | `code` | When | `next_action` | `fallback` | extra |
217
334
  |---|---|---|---|---|
218
335
  | `SIGN_CONTEXT_TIMEOUT` | The fetch (or its body read) did not finish within `SIGN_CONTEXT_TIMEOUT_MS` | `stop_and_tell_user` | `typed_data_b64` | — |
219
336
  | `SIGN_CONTEXT_UNREACHABLE` | The fetch failed before any response (DNS, connection refused, TLS, …) | `stop_and_tell_user` | `typed_data_b64` | — |
220
- | `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` or `x402_expected` (a pre-#1263 backend) | `stop_and_tell_user` | `typed_data_b64` | — |
221
- | `SIGN_CONTEXT_REFUSED` (410 / `expired`) | The quote's window closed | `payment_window_expired` | — | `retry_with_new_quote: true`, `http_status`, `backend_error_code: 'expired'` |
222
- | `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404), `already_executed` / `not_signable` / `sign_context_unavailable` (409) | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
337
+ | `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` / `x402_expected` (a pre-#1263 backend), or — on the direct-payment fetch — an unsupported `direct_sign_context_version` or a `signature_scheme` other than `eip712_userop` | `stop_and_tell_user` | `typed_data_b64` | — |
338
+ | `SIGN_CONTEXT_REFUSED` (x402: 410 or `expired`; direct: `expired` only) | x402: the quote's window closed. Direct: the payment's window closed — call `haven_send` / `haven_pay` again with the same `idempotency_key` | `payment_window_expired` | — | x402 only: `retry_with_new_quote: true`; both: `http_status`, and `backend_error_code: 'expired'` when the backend sent one |
339
+ | `SIGN_CONTEXT_REFUSED` (404, direct fetch) | The `payment_id` is not this agent's, or the backend predates #3271 and has no direct route | `stop_and_tell_user` | `typed_data_b64` | `http_status`, `backend_error_code` |
340
+ | `SIGN_CONTEXT_REFUSED` (426 `client_outdated`, #3303) | This signer is below the minimum version the Haven deployment has set (only when one is set). Nothing is signed; the prepared payment is left unsigned. Update with `client_update.upgrade_command`, restart the agent runtime, then retry the same call | `stop_and_tell_user` | — | `http_status: 426`, `backend_error_code: 'client_outdated'`, `client_update` (the update command as data); carries `next_tool_omitted_reason`, not the status read |
341
+ | `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404, x402 fetch), `already_executed` / `not_signable` (409), a bare 410 retired-rail tombstone (direct fetch) — or `sign_context_unavailable` (409): always on `haven_sign_x402`; on `haven_sign` only when neither route can serve the row | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
342
+ | `USEROP_BINDING_MISMATCH` | A direct payment's `PackedUserOperation` typed data (from the direct fetch, or a tool argument) does not recompute to its own `payload_hash`, or a fetched direct context is not a `PackedUserOperation` — see [Two ways to use it](#two-ways-to-use-it) above | `stop_and_tell_user` | — | no `http_status` — this is a local recomputation, not a backend refusal |
343
+ | `TYPED_DATA_NOT_ALLOWED` | Typed data without an x402 context that is not a bound direct-payment `PackedUserOperation` (#3272): wrong chain, not this signer's own account, a call other than `execute` → DelegationManager → `redeemDelegations`, or a redemption whose delegations are empty, not made to this account, or in a non-default mode — see the allowlist paragraph above. Reachable by `payment_id` when Haven serves such a payload | `stop_and_tell_user` | — | no `http_status` — a local shape check, not a backend refusal |
223
344
 
224
345
  `fallback: 'typed_data_b64'` appears only where signing OTHER bytes is a
225
- remedy — a transport failure or a body this signer could not read. It is
226
- **not** in the default quote result since #1272: obtain it by re-running the
227
- SAME quote tool with the SAME `idempotency_key` plus
228
- `include_signing_payload: true`, then pass `typed_data_b64` (plus
229
- `payload_hash` / `x402_expected`) instead of `payment_id`. A backend REFUSAL
346
+ remedy — a transport failure or a body this signer could not read, plus one
347
+ backend refusal: a 404 from the direct-payment fetch (an older backend with
348
+ no direct route, #3271). For x402 it is **not** in the default quote result
349
+ since #1272: obtain it by re-running the SAME quote tool with the SAME
350
+ `idempotency_key` plus `include_signing_payload: true`, then pass
351
+ `typed_data_b64` (plus `payload_hash` / `x402_expected`) instead of
352
+ `payment_id`. A direct payment's `haven_send` / `haven_pay` result always
353
+ carries `payload_hash` + `typed_data_b64`. Any other backend REFUSAL
230
354
  carries no fallback: an expired, executed or unsignable intent cannot be
231
- rescued by re-signing its bytes — an expired one is re-quoted (the same
355
+ rescued by re-signing its bytes — an expired x402 one is re-quoted (the same
232
356
  `payment_window_expired` + `retry_with_new_quote` the signer emits for
233
- `PAYMENT_WINDOW_EXPIRED`), the rest stop. These codes are signer-local,
357
+ `PAYMENT_WINDOW_EXPIRED`), an expired direct one is re-sent with the same
358
+ `idempotency_key` (no `retry_with_new_quote`), the rest stop. These codes are signer-local,
234
359
  not part of `@haven_ai/sdk`'s `AgentPaymentFailureCode` taxonomy, since they
235
360
  describe a local fetch failure, not a payment-domain outcome, and never reach
236
361
  the backend's REST/OpenAPI surface — only this package's MCP tool responses.
@@ -241,19 +366,23 @@ The delegate key is read from `HAVEN_DELEGATE_KEY` or a `--credentials` file's
241
366
  `delegate_key` (with a permissive-file warning). It stays in this process, and
242
367
  is never transmitted.
243
368
 
244
- **The signer makes at most one kind of network call, on one path, and it is a
245
- read.** Since [#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the
246
- `{ payment_id }` form of `haven_sign` and `haven_sign_x402` performs an
247
- authenticated, read-only `GET /x402/:payment_id/sign-context` against Haven, so
248
- that agents never have to relay multi-KB EIP-712 payloads through a model's
249
- context window. **Only the Bearer API key goes out; the delegate key is never
250
- part of that request or its response.** Since #2985 that read is bounded:
251
- it aborts after `SIGN_CONTEXT_TIMEOUT_MS` (15 s) and reports a
252
- `HavenSignContextError` naming the timeout and the `typed_data_b64` fallback,
253
- so a hung backend cannot hang the signer — and the agent — past the funding
254
- window. Every refusal on this fetch (timeout, unreachable host, a non-ok
255
- backend response, a malformed body) is structured the same way, not just
256
- prose — see [Sign-context refusal codes](#sign-context-refusal-codes) below.
369
+ **The signer makes at most two kinds of network call, both reads.** Since
370
+ [#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the `{ payment_id }`
371
+ form of `haven_sign` and `haven_sign_x402` performs an authenticated,
372
+ read-only `GET /x402/:payment_id/sign-context` against Haven, so that agents
373
+ never have to relay multi-KB EIP-712 payloads through a model's context
374
+ window. Since #3271, `haven_sign` (never `haven_sign_x402`) falls back to a
375
+ second read — `GET /payments/:payment_id/sign-context` — only when that first
376
+ fetch answers the backend's 409 `sign_context_unavailable`, i.e. this
377
+ `payment_id` names a direct payment rather than an x402 intent. **Only the
378
+ Bearer API key goes out on either read; the delegate key is never part of
379
+ either request or response.** Since #2985 both reads are bounded: each aborts
380
+ after `SIGN_CONTEXT_TIMEOUT_MS` (15 s) and reports a `HavenSignContextError`
381
+ naming the timeout and the `typed_data_b64` fallback, so a hung backend cannot
382
+ hang the signer — and the agent — past the funding window. Every refusal on
383
+ either fetch (timeout, unreachable host, a non-ok backend response, a
384
+ malformed body) is structured the same way, not just prose — see
385
+ [Sign-context refusal codes](#sign-context-refusal-codes) below.
257
386
  Nothing else in the package reaches the
258
387
  network: `haven_x402_sign_header` and `haven_sign_sweep_delegate` never fetch,
259
388
  the library surface above (`createEdgeSigner` and its six signing methods, over
@@ -283,13 +412,47 @@ protected storage/runtime config.
283
412
 
284
413
  ## Local audit
285
414
 
286
- Every MCP signing operation appends a JSONL row locally. File-backed runs write
415
+ Every MCP signing operation appends a JSONL row locally, best-effort (see the
416
+ end of this section). File-backed runs write
287
417
  next to the credential as `<credential>.signer-audit.jsonl`; env-only runs use
288
418
  `~/.haven/signer-audit.jsonl`. Rows include timestamp, tool, payload hash and
289
419
  delegate address, plus the account address and chain id when the credential
290
420
  carries them. They never include the delegate key, the signature, or the x402
291
421
  payment header.
292
422
 
423
+ Since #3172 the sidecar is owner-only and bounded. It is created `0600` — the
424
+ mode the credential beside it is expected to have — and a sidecar found
425
+ readable beyond its owner (every release before #3172 created it with the
426
+ default mode, `0644` under the usual `umask 022`, or an operator loosened it
427
+ later) is tightened to `0600` in place on the next
428
+ append, with one stderr line per occurrence. If `chmod` is refused, the line
429
+ names the same `chmod 600` remedy the credential warning gives. If the path
430
+ is a symlink (or anything but a regular file) nothing is chmod-ed — that
431
+ would hit the target — but note that audit rows are still written through
432
+ the link to its target, so the line says to remove the link (`rm <path>`),
433
+ not to chmod it. The check runs before rotation, so a legacy file that
434
+ rotates carries `0600` into `.1`. When the live file reaches
435
+ `AUDIT_ROTATE_BYTES` (8 MiB, roughly 30 000 rows) it is renamed to
436
+ `<path>.1`, replacing the previous `.1`, and a fresh file starts — two
437
+ generations at most. The rotation decision is not atomic across processes:
438
+ two signer processes on one credential that both hit the bound can leave the
439
+ predecessor generation discarded (the current entry is never lost). A failed
440
+ audit write (disk full, read-only, two appends racing at the bound) is
441
+ reported on stderr and never fails the signing call that already produced
442
+ its signature — so the consent block's
443
+ "appended for every signing operation" is a best-effort promise since #3172,
444
+ kept unchanged in text (editing consent copy does NOT move the consent hash —
445
+ that covers identity, tool names and the surface version, as the consent
446
+ screen section above already says; #3279 rewords the block without
447
+ re-prompting anyone). The
448
+ credential check itself is unchanged in wording and still judges a symlinked
449
+ credential by its target. The
450
+ `payload_hash` argument itself is bounded on the tool schema to a 32-byte hash
451
+ (`0x` + 64 hex), so the audit field is never caller-controlled free text; the
452
+ two object arguments that do reach the file (`payment_required`,
453
+ `authorization`) were already hashed before being written, and
454
+ `x402_expected` is never written to the sidecar.
455
+
293
456
  ## Hot-wallet minimization
294
457
 
295
458
  This applies to the **EIP-3009 bridge only** — the erc7710 path above has no