@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 +198 -35
- package/dist/cli.cjs +1516 -1055
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1681 -1220
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +963 -520
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -128
- package/dist/index.d.ts +216 -128
- package/dist/index.js +886 -451
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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`
|
|
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 `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
168
|
-
data
|
|
169
|
-
|
|
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`,
|
|
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`
|
|
221
|
-
| `SIGN_CONTEXT_REFUSED` (410
|
|
222
|
-
| `SIGN_CONTEXT_REFUSED` (
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
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`),
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|