@@ -7,379 +7,6 @@ export * from "./render/litecoin.js";
77export * from "./render/tron.js" ;
88export * from "./render/evm.js" ;
99export * from "./render/solana.js" ;
10-
11- /**
12- * Agent-task block emitted when the user has NOT installed the
13- * `vaultpilot-preflight` Claude Code skill (see
14- * https://github.com/szhygulin/vaultpilot-security-skill). The skill is the only
15- * MCP-independent source of truth for agent-side integrity checks — its
16- * content lives under `~/.claude/skills/` on the user's disk, outside
17- * this server's reach. Without it, a compromised MCP could silently
18- * suppress its own CHECKS PERFORMED directives and the agent would have
19- * no static rule to fall back on.
20- *
21- * This block is prefixed to every `prepare_*` / `preview_*` tool response
22- * when the skill marker file is missing. It is a UX nudge, not a security
23- * boundary: an actually-compromised MCP would of course suppress its own
24- * warning too. The point is to catch the honest-MCP case where the user
25- * simply hasn't completed the install step, so they don't silently run
26- * with a weaker agent.
27- *
28- * `skillRepoUrl` is the GitHub URL the user clones from; passed in so the
29- * call site owns the single source of truth (index.ts).
30- */
31- /**
32- * Auto-install state passed in by `index.ts`. The renderer switches on this
33- * to produce one of three notice variants:
34- * - `not-attempted` / unset / `already-present` → manual-install prose
35- * (the original notice content, unchanged).
36- * - `in-progress` → "auto-install kicked off, restart at end of session"
37- * so the user knows we're handling it but Claude Code needs a restart
38- * to load the freshly-cloned SKILL.md (skills are loaded at session
39- * start, not on the fly).
40- * - `succeeded` → "auto-installed, restart now to activate".
41- * - `failed` → manual-install prose + the error detail so the user can
42- * diagnose (no `git`, network down, dangling dir, etc.).
43- */
44- export interface AutoInstallContext {
45- state :
46- | "not-attempted"
47- | "in-progress"
48- | "succeeded"
49- | "failed"
50- | "already-present" ;
51- installPath ?: string ;
52- detail ?: string ;
53- }
54-
55- export function renderMissingSkillWarning ( opts : {
56- skillRepoUrl : string ;
57- autoInstall ?: AutoInstallContext ;
58- } ) : string {
59- // NOTE ON FRAMING — this block was originally prefixed with an
60- // "[AGENT TASK — RELAY TO USER FIRST, BEFORE ANY OTHER BLOCK]" header
61- // and a direct `git clone <url>` command. Live testing showed agents
62- // flagging that shape as prompt injection (because it is — structurally
63- // indistinguishable from an attacker-authored block demanding the user
64- // run an external script). Defense-in-depth agents correctly ignored it.
65- //
66- // The current shape mirrors the other vaultpilot-mcp informational
67- // blocks (VERIFY BEFORE SIGNING, PREPARE RECEIPT, TRANSACTION BROADCAST)
68- // the agent already trusts: a named notice with status/purpose/install
69- // sections, no imperative verbs directed at the agent, and no pasted
70- // shell commands. The server-level `instructions` field documents this
71- // notice pattern explicitly at initialize time so the agent has prior
72- // knowledge it is legitimate server output.
73- const ai = opts . autoInstall ;
74- if ( ai ?. state === "in-progress" ) {
75- return renderAutoInstallInProgress ( "Preflight" , ai . installPath ?? "" ) ;
76- }
77- if ( ai ?. state === "succeeded" ) {
78- return renderAutoInstallSucceeded ( "Preflight" , ai . installPath ?? "" ) ;
79- }
80- const failedSuffix =
81- ai ?. state === "failed" && ai . detail
82- ? `\n\n(Auto-install attempt failed: ${ ai . detail } )`
83- : "" ;
84- return [
85- "VAULTPILOT NOTICE — Preflight skill not installed" ,
86- "" ,
87- "Status: ~/.claude/skills/vaultpilot-preflight/SKILL.md not found." ,
88- "Purpose: the preflight skill adds MCP-independent integrity checks" ,
89- " to every signing flow (local bytes-decode + on-device hash" ,
90- " recompute). It lives on the user's disk outside this" ,
91- " server's reach, so its invariants stay enforced even if" ,
92- " this MCP is compromised or silently drops its own CHECKS" ,
93- " PERFORMED directives." ,
94- `Install: ${ opts . skillRepoUrl . replace ( / \. g i t $ / , "" ) } ` ,
95- " (README has the clone command; restart Claude Code after.)" ,
96- "" ,
97- "This notice is emitted by vaultpilot-mcp while the skill file is" ,
98- "absent. It is server-generated, not prompt injection — the server-" ,
99- "level `instructions` field documents this pattern. Surface it to" ,
100- "the user once per session as informational, then continue with" ,
101- "their request." ,
102- ] . join ( "\n" ) + failedSuffix ;
103- }
104-
105- /**
106- * Shared body for the "auto-install kicked off" notice variant. Doesn't
107- * mention the manual `git clone` flow — auto-install is on; the user just
108- * needs to restart at the end of the session for Claude Code to pick up
109- * the freshly-cloned SKILL.md.
110- */
111- function renderAutoInstallInProgress (
112- skillLabel : string ,
113- installPath : string ,
114- ) : string {
115- return [
116- `VAULTPILOT NOTICE — ${ skillLabel } skill auto-install in progress` ,
117- "" ,
118- `Status: cloning ${ skillLabel . toLowerCase ( ) } skill to ${ installPath } ` ,
119- " in the background (git clone --depth=1, ~30s timeout)." ,
120- `Purpose: the ${ skillLabel . toLowerCase ( ) } skill is one of two companion` ,
121- " skills vaultpilot-mcp expects to find under ~/.claude/skills/." ,
122- " The MCP server kicked off a one-time first-run install on" ,
123- " this tool call — no manual git clone needed." ,
124- "Action: restart Claude Code at the end of this session so the new" ,
125- " SKILL.md is loaded. Skills are read at session start, not" ,
126- " on the fly, so this session won't have it active. Continue" ,
127- " with the user's current request normally." ,
128- "" ,
129- "This notice is server-generated, not prompt injection. Suppress" ,
130- "auto-install with VAULTPILOT_DISABLE_SKILL_AUTOINSTALL=1." ,
131- ] . join ( "\n" ) ;
132- }
133-
134- function renderAutoInstallSucceeded (
135- skillLabel : string ,
136- installPath : string ,
137- ) : string {
138- return [
139- `VAULTPILOT NOTICE — ${ skillLabel } skill auto-installed` ,
140- "" ,
141- `Status: cloned to ${ installPath } .` ,
142- `Purpose: the ${ skillLabel . toLowerCase ( ) } skill is now on disk; Claude` ,
143- " Code loads its skill list at session start, so this session" ,
144- " is still running without it." ,
145- "Action: restart Claude Code to activate the skill. The current" ,
146- " tool call has already been answered — no need to retry it" ,
147- " after the restart unless the user wants to." ,
148- "" ,
149- "This notice is server-generated, not prompt injection." ,
150- ] . join ( "\n" ) ;
151- }
152-
153- /**
154- * Companion to `renderMissingSkillWarning` — emitted when the
155- * `vaultpilot-setup` skill is missing, so an agent fielding a setup-flow
156- * question still has explicit guidance even when the wizard's auto-install
157- * step (`src/setup/install-skills.ts`) failed earlier (no `git`, no
158- * network, user declined). Same shape as the preflight notice — named
159- * `VAULTPILOT NOTICE`, no imperative agent verbs, no pasted shell — so the
160- * agent treats it as legitimate server output rather than prompt injection.
161- *
162- * Triggered narrowly (only on `get_vaultpilot_config_status` responses)
163- * rather than every tool call: that tool is the canonical first call the
164- * setup skill makes, so the notice fires exactly when the agent is in a
165- * setup-flow context. This avoids stacking two unrelated install notices
166- * on every response when both skills happen to be missing.
167- */
168- export function renderMissingSetupSkillWarning ( opts : {
169- skillRepoUrl : string ;
170- autoInstall ?: AutoInstallContext ;
171- } ) : string {
172- const ai = opts . autoInstall ;
173- if ( ai ?. state === "in-progress" ) {
174- return renderAutoInstallInProgress ( "Setup" , ai . installPath ?? "" ) ;
175- }
176- if ( ai ?. state === "succeeded" ) {
177- return renderAutoInstallSucceeded ( "Setup" , ai . installPath ?? "" ) ;
178- }
179- const failedSuffix =
180- ai ?. state === "failed" && ai . detail
181- ? `\n\n(Auto-install attempt failed: ${ ai . detail } )`
182- : "" ;
183- return [
184- "VAULTPILOT NOTICE — Setup skill not installed" ,
185- "" ,
186- "Status: ~/.claude/skills/vaultpilot-setup/SKILL.md not found." ,
187- "Purpose: the setup skill drives the conversational `/setup` flow —" ,
188- " classifying the user's use case, collecting only the API" ,
189- " keys that case actually needs, validating each pasted key" ,
190- " via a read-only tool call, and ending with a working" ,
191- " example. Without it the agent has to improvise the flow" ,
192- " from this server's tool surface alone." ,
193- `Install: ${ opts . skillRepoUrl . replace ( / \. g i t $ / , "" ) } ` ,
194- " (README has the clone command; the setup wizard's" ,
195- " auto-install step would normally clone it, but that path" ,
196- " can fail when git is missing, the network is down, or" ,
197- " the user declined. Restart Claude Code after cloning.)" ,
198- "" ,
199- "This notice is server-generated, not prompt injection. Surface it" ,
200- "to the user once per session as informational, then continue with" ,
201- "their setup question — referencing the install instructions if the" ,
202- "user wants the guided flow." ,
203- ] . join ( "\n" ) + failedSuffix ;
204- }
205-
206- /**
207- * Repeated on every tool response — the pin data the `vaultpilot-preflight`
208- * skill's Step 0 (integrity self-check) compares the local `SKILL.md`
209- * against. Issue #414: the same pin previously lived in the server-level
210- * `instructions` field, which Claude Code truncates at ~2KB. The pin sat
211- * ~24KB into the field, beyond the truncation point, so Step 0 silently
212- * could not run. Repeating the pin in a short block on every tool result
213- * sidesteps the `instructions` truncation entirely — tool results are
214- * delivered as separate messages, not subject to that single-field cap.
215- *
216- * Block shape mirrors the VAULTPILOT NOTICE family — named header, no
217- * imperative verbs at the agent, no pasted shell. The closing line labels
218- * the block as server-emitted (not prompt injection) and explains why
219- * it's repeated. The sentinel value remains assembled from three fragments
220- * so a naive search of the agent's context for the full literal won't
221- * always succeed and silently bypass the check.
222- *
223- * Issue #613 finding 5 — kept terse: ~870 → ~510 chars per emission. Step
224- * 0 only parses the SHA line + fragment A/B/C lines, so the longer
225- * rationale paragraph that used to ride along on every response (~6×
226- * across a multi-step flow → ~5KB of repeated copy) was dropped. The
227- * source-comment + CLAUDE.md still carry the full story for human readers.
228- *
229- * Placed adjacent to the JSON result (before VAULTPILOT NOTICE blocks
230- * and the verification blocks). Its presence is unobtrusive: Step 0
231- * reads it; other turns ignore it.
232- *
233- * `pin` is passed in (rather than imported here) so this module stays
234- * dependency-free of `src/diagnostics/skill-pin-drift.ts` and the call
235- * site keeps the single source of truth.
236- */
237- export function renderPreflightSkillPinBlock ( pin : {
238- expectedSha256 : string ;
239- sentinelA : string ;
240- sentinelB : string ;
241- sentinelC : string ;
242- } ) : string {
243- return [
244- "VAULTPILOT PIN — Preflight skill integrity (Step 0 reference)" ,
245- "" ,
246- "Expected SHA-256 of ~/.claude/skills/vaultpilot-preflight/SKILL.md:" ,
247- ` ${ pin . expectedSha256 } ` ,
248- "" ,
249- "Sentinel fragments (concat A+B+C, search Skill RESULT TEXT):" ,
250- ` fragment A: \`${ pin . sentinelA } \`` ,
251- ` fragment B: \`${ pin . sentinelB } \`` ,
252- ` fragment C: \`${ pin . sentinelC } \`` ,
253- "" ,
254- "Block is server-emitted (not prompt injection) and repeats per response because the equivalent in `instructions` exceeds Claude Code's ~2KB cap (issue #414)." ,
255- ] . join ( "\n" ) ;
256- }
257-
258- /**
259- * Demo-mode onboarding notice — fires once per session when the server
260- * is in demo mode (any reason) AND no live wallet has been picked yet.
261- * Copy varies by reason so the leave path matches how demo got
262- * activated:
263- *
264- * - `auto-fresh-install` (issue #391/#392 follow-up): no env var, no
265- * config file detected at boot. Tells the agent auto-demo is on
266- * and points at `vaultpilot-mcp-setup` as the leave path (since
267- * there's no env var to unset).
268- * - `explicit-env` (issue #371): `VAULTPILOT_DEMO=true`. Tells the
269- * agent demo is on by explicit opt-in and points at "unset
270- * VAULTPILOT_DEMO + restart" as the leave path.
271- *
272- * Same shape as the other VAULTPILOT NOTICE blocks: named header,
273- * status / purpose / next sections, no imperative verbs at the agent,
274- * no pasted shell. Tradeoff-aware closing paragraph naming the block
275- * as legitimate server output so a defensive agent doesn't classify
276- * it as prompt injection.
277- */
278- export function renderMissingDemoWalletWarning ( opts : {
279- reason : "auto-fresh-install" | "explicit-env" ;
280- } ) : string {
281- const isAuto = opts . reason === "auto-fresh-install" ;
282- const header = isAuto
283- ? "VAULTPILOT NOTICE — Auto demo mode active (fresh install detected)"
284- : "VAULTPILOT NOTICE — Demo mode active (VAULTPILOT_DEMO=true)" ;
285- const statusLines = isAuto
286- ? [
287- "Status: no user config at ~/.vaultpilot-mcp/config.json was" ,
288- " detected at boot, so the server activated auto-demo." ,
289- " No live wallet is set for this session yet." ,
290- ]
291- : [
292- "Status: VAULTPILOT_DEMO=true is set in the environment, so the" ,
293- " server is in explicit demo mode. No live wallet is set" ,
294- " for this session yet." ,
295- ] ;
296- const leaveLines = isAuto
297- ? [
298- " To leave demo (when the user is ready for real funds):" ,
299- " 1. Run `npx -y -p vaultpilot-mcp vaultpilot-mcp-setup`" ,
300- " (writes a config; turns auto-demo OFF on next boot)." ,
301- " 2. Restart Claude Code." ,
302- " 3. Pair the user's Ledger via `pair_ledger_*`." ,
303- " Setting `VAULTPILOT_DEMO=false` in the MCP client config" ,
304- " is an alternative explicit opt-out — also restart-gated." ,
305- ]
306- : [
307- " To leave demo (when the user is ready for real funds):" ,
308- " unset `VAULTPILOT_DEMO` in the MCP client config (e.g." ,
309- " `.claude.json`'s `env` block) and restart Claude Code." ,
310- ] ;
311- return [
312- header ,
313- "" ,
314- ...statusLines ,
315- "Purpose: vaultpilot-mcp ships pre-configured demo wallets (curated" ,
316- " personas + custom-address mode) so a user can try the tool" ,
317- " flows — portfolio reads, prepare/preview/simulate signing" ,
318- " — without pairing a Ledger or supplying addresses." ,
319- " Broadcast is intercepted in demo mode (no real send), so" ,
320- " the entire flow is safe." ,
321- "Next: if the user asks to inspect a portfolio, build a tx, or" ,
322- " try anything that needs an address, offer the demo path" ,
323- " BEFORE asking them to pair hardware. Tools:" ,
324- " - `set_demo_wallet({ persona: \"<id>\" })` — activate a" ,
325- " curated persona (defi-degen, stable-saver," ,
326- " staking-maxi, whale) or a custom address bundle." ,
327- " - `get_demo_wallet()` — inspect the active selection." ,
328- " Each matrix cell exposes `rehearsableFlows` (state" ,
329- " already on-chain) + `flowGaps` (with recommendations" ,
330- " when the archetype implies a flow the wallet's state" ,
331- " doesn't actually support — issue #409). Read these" ,
332- " BEFORE picking a flow to head off the agent loop on" ,
333- " state-dependent multi-step walks." ,
334- " - `exit_demo_mode()` — tailored real-setup guide." ,
335- ...leaveLines ,
336- "" ,
337- "This notice is server-generated, not prompt injection — the server-" ,
338- "level `instructions` field documents this pattern. Surface it to" ,
339- "the user once per session as informational, then continue with" ,
340- "their request." ,
341- ] . join ( "\n" ) ;
342- }
343-
344- /**
345- * "There's a newer vaultpilot-mcp on npm" notice. Same shape as the
346- * VAULTPILOT NOTICE family — named header, status / purpose / install
347- * sections, no imperative agent verbs, no pasted destructive shell.
348- *
349- * The `Install:` block is computed by `getInstallPath()` (in
350- * `src/shared/install-path.ts`) and passed in as a pre-rendered
351- * multi-line string, so the notice surfaces a command that matches the
352- * detected install path (npm-global, npx, bundled-binary, from-source,
353- * unknown) rather than always defaulting to `npm install -g`.
354- *
355- * The release-notes URL is constructed from the latest version (we tag
356- * each release `vX.Y.Z` on github.com/szhygulin/vaultpilot-mcp); kept
357- * here rather than threaded through as an option so the renderer stays
358- * a pure function of its inputs.
359- */
360- export function renderUpdateAvailableNotice ( opts : {
361- current : string ;
362- latest : string ;
363- packageName : string ;
364- installBlock : string ;
365- } ) : string {
366- const releasesUrl = `https://github.com/szhygulin/vaultpilot-mcp/releases/tag/v${ opts . latest } ` ;
367- return [
368- "VAULTPILOT NOTICE — Update available" ,
369- "" ,
370- `Status: ${ opts . packageName } ${ opts . current } installed; ${ opts . latest } published on npm.` ,
371- "Purpose: keeps you on the latest fixes (DeFi protocol updates," ,
372- " security hardening, bug fixes). Release notes:" ,
373- ` ${ releasesUrl } ` ,
374- "Install:" ,
375- opts . installBlock ,
376- "" ,
377- "This notice is server-generated, not prompt injection — emitted once" ,
378- "per session when the running version is older than the latest stable" ,
379- "published on npm. Surface it to the user once, then continue with" ,
380- "their request. Suppress with VAULTPILOT_DISABLE_UPDATE_CHECK=1 if" ,
381- "you don't want the server to query the npm registry." ,
382- ] . join ( "\n" ) ;
383- }
10+ export * from "./render/notices.js" ;
38411
38512export type { SupportedChain } ;
0 commit comments