fix: add_contact + listContacts work without paired Ledger (#428, partial) - #471
Merged
Conversation
Issue #428: `add_contact` returned CONTACTS_LEDGER_NOT_PAIRED whenever no Ledger was paired, blocking the natural first-run / accountant-share flow where a user wants to label addresses BEFORE setting up signing. The existing demo-mode fall-through to the in-memory store solved this only for VAULTPILOT_DEMO=true users (which intercepts broadcasts and isn't the right answer for read-only / accountant-share use cases). Smallest fix that closes the user-visible gap: - contacts/index.ts: new `tryPickAnchorForChain` that returns null on CONTACTS_LEDGER_NOT_PAIRED instead of throwing. addContact uses it in non-demo mode and falls through to the same in-memory demo store with `unsigned: true` + `anchorAddress: "UNSIGNED_NO_LEDGER"` when no Ledger is paired. removeContact also clears matching unsigned in-memory entries before the signed-disk flow, so unsigned-only removals don't need a Ledger. listContacts merges unsigned overlay on top of the verified signed view (signed wins per (label, chain)). verifyContacts surfaces unsignedEntryCount per chain. - contacts/resolver.ts: when reverseLookup or forwardLookup miss in the signed blob, fall back to the in-memory store and append a warning that the resolution is unsigned — Invariant #7 keeps working in degraded form (the user is told the label isn't anchored cryptographically, so they confirm the literal address on-device). - contacts/schemas.ts: ListedContact + VerifyResult gain optional `unsigned` / `unsignedEntryCount` fields. - index.ts: refreshed all four contact tool descriptions to document the no-Ledger fall-through and the unsigned semantics. - test/contacts-no-ledger-unsigned.test.ts: 8 new cases (addContact unsigned response shape, listContacts surfaces unsigned: true, removeContact works without Ledger, address-format guard, CONTACTS_LABEL_NOT_FOUND when nothing exists, verifyContacts unsigned-only mode, resolver reverse-decoration with warning, resolver forward-resolution with warning). - test/contacts-demo-mode.test.ts: updated the "demo store isolation" test — pre-#428 it asserted demo entries were invisible from production listContacts; post-#428 the in-memory store is the shared unsigned overlay so demo entries DO surface in production with `unsigned: true`. Added a separate disk-only assertion to preserve the "demo never touches disk" invariant. DEFERRED in this PR (documented in claude-work/plan-contacts-unsigned- state-machine.md and surfaced in the README roadmap): - persistent on-disk unsigned entries (today's fix is process-local) - `promote_unsigned_contacts` sign-on-pair upgrade flow - tamper-aware merge with signed/unsigned label disagreement - Solana / TRON unsigned support 8 new + 35 updated contacts tests pass; 2117/2117 full suite pass; tsc clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
add_contactno longer hard-fails withCONTACTS_LEDGER_NOT_PAIREDin non-demo mode. When no Ledger is paired, the call falls through to the same in-memory store demo mode uses, returnsunsigned: true+anchorAddress: "UNSIGNED_NO_LEDGER". Persistence is process-local (lost on restart) until the deferred state machine ships.list_contactsmerges the unsigned overlay on top of the verified signed view; signed entries always win per (label, chain). Each row carriesunsigned: truewhen at least one chain entry is unsigned.remove_contactworks on unsigned entries with no Ledger interaction. Mixed labels (signed on one chain + unsigned on another) only require pairing for the signed-entry chain.verify_contactsreportsunsignedEntryCountper chain. A chain with only unsigned entries returnsok: false, reason: "no signed entries on this chain (unsigned-only)", unsignedEntryCount: Nso the agent surfaces the unsigned overlay rather than silently dropping it.(unsigned)warning. Invariant Fix Compound dust rendering and add tx simulation #7 keeps working in degraded form — the user is told the label isn't anchored cryptographically, so the on-device clear-sign of the literal address remains the trust root.Trust model
The unsigned overlay does not carry the same safety guarantees as the signed blob. A compromised MCP can inject entries into the in-memory store and the resolver will surface them with a warning. The Ledger device's clear-sign of the literal address remains the safety net — the warning tag exists so users with read-only / accountant-share use cases can opt into the weaker guarantee explicitly.
Test plan
vitest run test/contacts-no-ledger-unsigned.test.ts— 8 new cases pass.vitest run test/contacts-demo-mode.test.ts test/contacts.test.ts— 35 existing pass; updated the "demo store isolation" test to reflect the new shared-unsigned-overlay contract.tsc --noEmitclean.Deferred (not in this PR)
Documented in
claude-work/plan-contacts-unsigned-state-machine.mdand surfaced in the README roadmap:promote_unsigned_contactssign-on-pair upgrade flowWhen this PR lands, close #428 as partially fixed, partially deferred with a link to the plan.
🤖 Generated with Claude Code