Skip to content

feat(scope): curated-CORE default - EVM-family flip + scope-friction observability (#733) - #830

Merged
szhygulin merged 1 commit into
mainfrom
feat/733-curated-core-evm-flip-v2
Jul 26, 2026
Merged

feat(scope): curated-CORE default - EVM-family flip + scope-friction observability (#733)#830
szhygulin merged 1 commit into
mainfrom
feat/733-curated-core-evm-flip-v2

Conversation

@graciangabriel8

Copy link
Copy Markdown
Contributor

Closes #733

What changed and why

A default, unconfigured install registered all 189 tools every turn because both scope levers defaulted to accept-all (parseFamilies(undefined) → all five families, parseProtocols(undefined)null). This flips the defaults to the curated-CORE set (ARCHITECTURE.md §6 R7, PROD option 2 on #721):

  • src/config/scope.ts
    • parseFamilies(undefined) now returns {evm} (new DEFAULT_FAMILIES, kept separate from ALL_FAMILIES).
    • parseProtocols(undefined) now returns the empty set; null survives only as the explicit accept-all sentinel. isProtocolEnabled is unchanged apart from a comment — null || has() still reads correctly under the new meaning.
    • Both env vars now accept a first-class all token as the escape hatch back to the pre-flip surface. Before this PR VAULTPILOT_CHAIN_FAMILIES=all "worked" only by accident (it parsed as a typo and hit the all-families fallback); with a narrow default that escape hatch is load-bearing, so it is explicit now.
    • The typo fallback (all tokens unrecognised → all families) is deliberately unchanged: unset means "user said nothing" (→ curated default), a typo means "user said something we couldn't read" (→ don't boot them into a stripped surface).
    • New getScopedOutFamilies() / getScopedOutProtocols() / getScopeEnableHint().
  • src/modules/diagnostics/index.tsget_vaultpilot_config_status's scope block gains scopedOutFamilies, scopedOutProtocols, enableHint.
  • src/index.ts — one startup stderr line enumerating active vs scoped-out families/protocols + the enable hint, emitted only when something is actually scoped out.

Friction measure (PROD-required, ships with the flip). The clean flip un-registers scoped-out tools, so the host never sees them and there is no in-band per-call refusal to count. Per the issue, the signal is the config-status scope block + the startup line, with request_capability as the demand signal. The heavier register-but-refuse counter is not built here — the issue says not to without PROD/ARCH confirmation.

Measured result: 71 tools registered by default (45 chain-agnostic core + 26 EVM-family), down from 189. R7's ≤72 bound holds with one tool of headroom.

Falsifier test

test/scope-tool-registration.test.ts (new). It walks every registerTool(server, "<name>", …) call site in src/index.ts (same parse strategy as test/presign-annotation-guard-735.test.ts; main() is skipped under vitest so real SDK registrations can't be counted) and applies the production isToolEnabled predicate to each name. Every call site is unconditional at top level inside main(), so "accepted by isToolEnabled" is exactly "registered".

  • Falsifier: with both env vars unset it asserts <= 72 registered tools plus the exact 71-name pinned set. On unfixed code the default surface is all 189 — both assertions go red.
  • Also asserts (derived from getToolScope, not from the pinned list, so pasting a new snapshot can't launder a regression): nothing kept carries a non-EVM family or any protocol tag, and no chain-agnostic core tool was dropped (the fund-safety direction — send_transaction, contacts, Ledger verifiers must never fall out).
  • Parity: VAULTPILOT_CHAIN_FAMILIES=all + VAULTPILOT_PROTOCOLS=all re-registers all 189.

test/scope.test.ts gains the same falsifier at the isToolEnabled level, explicit-config parity tests, and coverage of the scoped-out accessors + hint. test/diagnostics-config-status.test.ts gains the config-status wiring assertions.

⚠️ Tests were NOT run locally

There is no Node/npm on the authoring machine, so nothing here was executed — no build, no lint, no test run, and no claim of a green run. CI (Build & Test on Node 20 + 22) is the authoritative check. Tests were written by matching the existing files' framework, imports and mocking patterns (vi.resetModules() + vi.stubEnv + dynamic import, since scope.ts reads its env vars once at module eval).

Existing tests updated (behavior deliberately changed)

  1. test/scope.test.ts"default (no env vars) — every tool registered""default (no env vars) — EVM family + core only, no other family, no protocol". It asserted exactly the behavior this PR changes; it is now the falsifier for the new one, not a weakened assertion.
  2. test/scope.test.ts"VAULTPILOT_PROTOCOLS unset → null (all protocols allowed)""… → empty set (protocols are opt-in)", with a new companion test pinning all → the null sentinel.
  3. test/scope.test.ts — two family-axis tests (CHAIN_FAMILIES=evm, the alias test) now also stub VAULTPILOT_PROTOCOLS=all. Their subject is the family axis; without the stub the new protocol default would gate prepare_aave_supply and the tests would be asserting the wrong thing. No assertion was relaxed.

No other existing assertion was touched. The #712 co-scoping guard and the #726 sunswap tag test are untouched and still green by construction (sunswap is {family: tron}, so it is correctly scoped out by default).

Blast radius

  • Changes the default tool surface for every existing unconfigured install on upgrade: Solana/TRON/BTC/LTC tools and all 13 protocol tools disappear until the user opts in. This is the intended PROD-adjudicated behavior, but it is user-visible and not backwards compatible for anyone who never set the env vars. The startup line + config-status hint exist precisely to make that discoverable.
  • get_vaultpilot_config_status response shape gains three fields (additive). scope.protocols changes from null to [] for a default install.
  • No getToolScope tag changes, no handler/signing-path logic changes.

Fund-safety / security note — NOT self-certified

This touches the registration gate in front of signing-class tools (prepare_*, preview_*, send_transaction), so it is fund-safety-adjacent. It is not self-certified and needs maintainer security review. Specifically worth an independent look:

Residual concerns

  • ARCHITECTURE.md goes stale on merge. §6's evidence paragraph ("a default, unconfigured install registers all 189 tools … parseFamilies(undefined) returns all five families (L73), parseProtocols(undefined) returns null (L87)") describes the pre-flip state, as does the R7 row's framing. I deliberately did not edit it — spec/architecture docs are the doc owner's (ARCH) surface, not something an implementation PR rewrites to match itself. Routing it as a finding: ARCH should refresh §6 and R7's status once this lands (note docs(arch): maintenance — R7/R8 settled, §8 issue numbers, review nits, cache-race reconcile #740 is already touching that doc).
  • R7's ratification caveat. ARCHITECTURE.md R7 records that the ≤72 value is PROD-adjudicated and "flagged for user veto at spec review — PROD intake: adjudicate ARCHITECTURE.md §2 driving numbers R1–R13 into SPEC.md #721 stays OPEN pending the user's spec-edit ack." This PR implements the adjudicated direction; if that veto is still live, the merge decision inherits it.
  • scripts/bench-tools.mjs (measurement: p95 latency bench for get/prepare/preview_send (resolves R1-R3 from UNMEASURED) #719) now benches the narrow default surface unless the operator sets VAULTPILOT_CHAIN_FAMILIES=all VAULTPILOT_PROTOCOLS=all. The R8 byte budget is not gated in this unit, but whoever runs that bench needs to know which surface they are measuring.
  • Exact-set pin is intentionally brittle. Any newly added tool changes DEFAULT_TOOL_SURFACE and fails the test. That is the point (scope.ts's own doc comment asks for it — a new tool that matches no rule silently lands in the always-on core bucket), but it is a maintenance cost worth an explicit ack.
  • Whitespace-only VAULTPILOT_PROTOCOLS=" , " now yields the empty set instead of null. Consistent with the flip; noting it since it is a behavior change on an explicitly-set (if degenerate) value.
  • Branch-name collision (needs maintainer/orchestrator cleanup). An orphan branch feat/733-curated-core-evm-flip already existed on the remote (pushed earlier today under the same account, no PR attached) — apparently an interrupted earlier attempt at this same issue. I did not force-push or touch it; this PR is from feat/733-curated-core-evm-flip-v2. The orphan branch should be deleted by whoever owns it.

…observability (#733)

An unconfigured install registered all 189 tools every turn because both
scope levers defaulted to accept-all. Flip the defaults to the curated CORE
set (ARCHITECTURE.md §6 R7): unset VAULTPILOT_CHAIN_FAMILIES now means {evm},
unset VAULTPILOT_PROTOCOLS now means the empty set — 71 tools registered by
default, everything else opt-in.

- parseFamilies(undefined) -> {evm}; parseProtocols(undefined) -> empty set.
- Both env vars accept a first-class `all` token as the escape hatch back to
  the pre-flip surface (previously `all` only worked via the typo fallback).
- Explicit values keep their prior behavior, typo fallback included.
- Friction observability ships with the flip: getScopedOutFamilies /
  getScopedOutProtocols / getScopeEnableHint feed a new scope block on
  get_vaultpilot_config_status plus one startup stderr line.
@szhygulin
szhygulin marked this pull request as ready for review July 26, 2026 08:07
@szhygulin
szhygulin merged commit 50f82e5 into main Jul 26, 2026
3 checks passed
@szhygulin
szhygulin deleted the feat/733-curated-core-evm-flip-v2 branch July 26, 2026 08:07
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(scope): curated-CORE default — EVM-family flip (≤72 tools, R7) + scope-friction observability

2 participants