README, packages/signer/README.md, docs/VECTOR.md, and docs/architecture/non-custodial-split.md
updated for the family-3 (self-improvement) keyless migration - the last of the three families
this migration ever touched:
- README: security notice rewritten as the migration-complete statement (zero tools accept key
material, every signing operation happens on the local signer, enforced by the now-empty,
size-zero-pinned custody boundary allowlist); tool count 23 to 24; Self-Improvement Module
table replaced with browse/analyze plus the four build_* rows, the two-step proposal pair
marked as two transactions, agent-orchestrated; features and safety copy updated to
signer-owned spend limits everywhere; Configuration table's three dead spend-limit/audit-log
rows removed (nothing reads those env vars any more); Safety Layer box removed from the
architecture diagram entirely (box-drawing alignment verified programmatically, not by eye);
Testing section names tier-1 vs gated E2E vs LEGACY_FULL for all three keyless families
including their stake costs.
- packages/signer/README.md: pairing-status bullet and closing line rewritten - all four
families are keyless now, this signer is the single signing point, nothing is left open.
- docs/VECTOR.md: four new tool sections for the self-improvement build_* family (this doc never
documented self-improvement under any name before, so these are additions, not renames - same
situation as the registry family in the prior PR's docs task); Safety Controls section's
custodial-family list corrected to reflect that none remain.
- docs/architecture/non-custodial-split.md: family-3 landed marker on the PR-sequence table,
plus the four content-alignment fixes pre-assigned to this PR by
rehaul-handoff-2026-07-30-content-alignment.md (the "governance" to "self-improvement" renames
in the two-step-flow heading, the "last family" sentence, the family table label, and the
rollout row), plus the "Float -> bigint amount math" wording fix.
- .env.example: verified Task 5's edit already states the migration-complete scope correctly
with a pointer to packages/signer/README.md; no further change needed.
Content rules followed for all new copy in this commit: no "governance" (GOV_* env-var names
exempted, pre-existing deployed config keys), no em dashes, no "funds", research-themed
examples where new examples were needed. Caught and fixed one self-introduced violation before
committing: docs/VECTOR.md's four new tool sections initially copied the file's own pre-existing
em-dash-based parameter-bullet convention; replaced with colons in exactly those new lines,
verified zero em dashes remain in the added range.
Task 6 review findings, all verified before fixing:
- self-improvement-keyless.test.ts: the proposal_spend negative test's inner
timing bound (30_000ms) equaled the test's own timeout, making the
assertion's diagnostic message unreachable - the test runner's generic
timeout would always win first. Tightened to 15_000ms.
- self-improvement-e2e.test.ts: added a deadline-sanity pin next to the
"valid until" regex match (the signing window must be meaningfully in the
future, not just present and Z-suffixed) and strengthened the critique/
endorse browse assertions to require a non-zero section count, so a
zero-result response can no longer pass.
- run.test.ts: reworded a banner comment ("funds" -> "AP3X"), matching this
branch's content rule against "funds"/"pay"/"earn" vocabulary.
- self-improvement.ts: harmonized all four build_self_improvement_* tools'
agentDid parameter description to make the trap unmistakable - this field
is the trailing 64-hex asset-name segment of an agent's DID, NOT the full
did:vector:agent:... string (confirmed live in Task 6: passing the full
string throws a Lucid serialization error). Description-only change;
regenerated the snapshot and confirmed the delta touches exactly these
four entries, nothing else.
Also corrected two printed numbers in task-6-report.md's narrative (a
gitignored file, not part of this commit): the itemized fee subtotal was
stated as 49.444332, recomputes to 49.444305; the inferred register-fee
residue was stated as 0.321503, recomputes to 0.321530. Verified by hand
before editing. The report's table and top-line balance delta were already
correct.
Adds the three remaining integration layers for spec PR 8 (self-improvement
family keyless): a tier-1 suite exercising all four build_* tools against the
live testnet module with zero secrets (build-only, nothing signed or
submitted), a gated full lifecycle E2E that actually submits, and a fresh
self-improvement section in the legacy run.test.ts suite.
THE MONEY ASSERTION: the gated E2E's proposal-spend step confirmed on-chain
(tx 312ffdef8beebfb8ba94868b0bf289b5fa7b4f8f9f743eefbf6aaef7360d3246) - the
deployed module validator accepted a keyless-built, validated submit (script
spend + CIP-33 reference scripts + dual mint) for the first time ever. First
attempt succeeded; the documented rebuild-once-on-window-expiry recovery was
never triggered. Full lifecycle (register -> proposal lock -> proposal spend
-> critique -> endorse -> deregister), 9/9 tests, wallet reconciled to the
lovelace: 982.280960 -> 932.515125 AP3X (49.765835 one-way; the 10 AP3X
registration deposit round-tripped; 5 of 6 fees recovered exactly from
transaction history, the 6th closes the arithmetic within 0.001 AP3X).
Tier-1 (self-improvement-keyless.test.ts): 8/8 live, no secrets - browse and
analyze_metrics render real chain data, critique/endorse/proposal_lock build
unsigned CBOR at the exact expected AP3X amounts with datum round-trips,
proposal_spend's negative path (unknown lockTxHash) fails fast pointing at
vector_await_transaction, bogus changeAddress fails fast locally.
Legacy suite (run.test.ts): run.test.ts has never had a self-improvement
section in its history (checked full git log before writing anything, per
the dispatch's own fallback instruction) - added one fresh, following the
file's established conventions, with its own dedicated agent lifecycle
(Agent Network Tools' agent is already deregistered by the time this section
runs, so reusing its DID would reference a burned NFT). Default path
(register -> browse -> analyze -> critique -> endorse -> deregister, ~19-20
AP3X one-way) ran live, 23/23 pass. The full two-step proposal submission is
written behind LEGACY_FULL=1 (typechecked, not executed this run - the E2E
above already proves that exact path live; re-running it here would spend
~29 AP3X a second time for no new information), correctly skipped (1 skip).
24 tests total across all 6 describe blocks, 0 failures.
Found and fixed two issues before shipping, neither a src/ change: the
self-improvement family's agentDid wire param is the hex asset-name segment
only (not the full did:vector:agent:... string the registry family's
agent_id means) - pre-existing since before spec PR 8, confirmed against
commit 8162673; and a Markdown-bold-asterisk regex gap in my own
analyze_metrics assertion (same class of gotcha registry-keyless.test.ts
already documents). Content-rule self-review caught and fixed a literal
"governance" string in a code comment and seven new em dashes before commit.
Gates: typecheck 0 (3 packages), unit 392/392, smoke 27/27 (24 tools, 0
drift), auth 12/12, boundary 5/5 - all identical to the entering baseline
(test files only, no src/ changes). Full report with all tx hashes, explorer
links, and the wallet reconciliation at .superpowers/sdd/task-6-report.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus review round on the self-improvement-keyless capstone commit, 2
Important + 3 Minor, all residue (no custody defects):
- env.d.ts: delete the three dead VECTOR_SPEND_LIMIT_PER_TX/_DAILY/
VECTOR_AUDIT_LOG_PATH ambient declarations - safety.ts was their only
reader and it's deleted. The capstone custody PR should not leave
live-looking spend-limit declarations behind.
- boundary.test.ts: new mechanical pin asserting self-improvement.ts never
references localUPLCEval - the test-only eval escape hatch on
buildProposalSpend may never be passed from a tool handler. Proved
load-bearing: applied the mutation (a 4th { localUPLCEval: true } arg),
watched it fail by name, reverted, confirmed a byte-identical diff against
the mutation (git diff --stat showed only the unrelated storageUri
simplification below), re-ran green.
- rate-limit-gating.test.ts: reworded the now-false "self-improvement.ts is
the remaining compiler-blind case" comment - both tool-registration files
are typed now; the timing-based hermeticity rationale stands on its own.
- types.ts + gov-build.ts + self-improvement.ts: added optional storageUri
to VectorBuildStakeResult, set by buildCritique only; the critique tool
handler now renders r.storageUri directly instead of reconstructing
ipfs://${ipfsCid}, removing the duplicated URI-format knowledge. Result
types are not wire schemas - zero snapshot drift confirmed.
- registry-build.ts:303: "governance family" -> "self-improvement family".
Gate: typecheck 0 (3 packages) - unit 392/392 (391 + the new eval-escape-hatch
pin) - smoke 27/27, 24 tools, zero snapshot drift - auth 12/12 - boundary
5/5 (4 custody + the new pin). Custody grep re-confirmed empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites self-improvement.ts (1024 -> 556 lines): typed, no @ts-nocheck,
imports build logic from gov-build.ts (Tasks 3-4). Splits the old single-shot
vector_self_improvement_submit_proposal into two keyless build tools
(proposal_lock, proposal_spend) since the deployed module validator requires
a confirmed lock before the spend+mint step can reference it; renames
critique/endorse to vector_build_self_improvement_*. browse and
analyze_metrics stay behavior-identical read-only tools (render strings
verified byte-for-byte against the custodial originals).
Deletes safety.ts (its last importer). Removes SpendLimits/SpendStatus/
AuditEntry from shared types (grep-verified zero references first). Drops
the now-dead spend-limit/audit-log env vars. Empties the custody boundary
allowlist with a size-0 pin - every builder family is now keyless.
Tool count 23 -> 24. Snapshot delta is surgical: 3 removed, 4 added, 19 of
the other 20 byte-identical (analyze_metrics' description had to drop the
literal word "mnemonic" to satisfy the custody grep - the one disclosed
exception).
Headline: grep -rni "mnemonic|seed|bip39|fromSeed" packages/builder/src
returns zero lines. This was the last custodial family.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README.md Testing section claimed registry-keyless.test.ts (tier-1) builds "all five
agent-registry build_* tools" - false. Verified directly against the test source: it
successfully builds and CBOR-checks only register and message; update appears solely on
its non-owner-rejection path (no CBOR extracted); transfer and deregister don't appear in
the file at all - both are covered by the gated E2E suite and the legacy suite instead.
Reworded to state exactly what tier-1 covers and point the full lifecycle claim at the E2E
suite, where it's actually true.
docs/architecture/non-custodial-split.md:137 ("Builder — 24 tools, zero keys") is the
doc's original full-migration target table, easy to misread as current-state next to
README's "23" today. Added "at full migration" so it can't be read as stale.
Checked pr7-body.md for the same all-five-tier-1 claim per the reviewer's note - it does
not repeat it (the Tier-1 bullet only states the 6/6 pass count, no per-tool breakdown),
so no change needed there.
README, .env.example, packages/signer/README.md, and docs/architecture/non-custodial-split.md
updated for the family-2 keyless migration (agent-registry build_* tools, no mnemonic
parameters): security notice narrowed to name the Self-Improvement Module as the only
remaining custodial family; agent-registry tool table renamed to the five build_* rows;
features bullets, self-hosting copy, Configuration/spend-limit scope notes, and the
architecture diagram's Safety Layer footnote all updated to "self-improvement only"; signer
README's pairing-status bullet and closing line extended to cover wallet/tx/contract AND
agent-registry; architecture doc's PR-sequence table marks family 2 as landed.
docs/VECTOR.md gains five new tool sections (register/update/transfer/deregister/message) -
this doc never documented the registry family under any name before, so these are additions
rather than renames; keyless framing (changeAddress, no mnemonic, unsigned CBOR + 4-call flow)
matches the style of the existing wallet/tx/contract sections. Safety Controls section's
custodial-family list also corrected to self-improvement only.
Closes an Opus review round on Task 5's registry integration work.
Adds packages/builder/test/sign-helper.test.ts (top-level test/, picked up
by the unit glob) as the first hermetic gate that would fail if
signWithMnemonic's witness-preservation fix regressed. Before this, the
fix had zero coverage under any required gate: sign-helper.ts lives under
test/integration/, outside the unit glob; run.test.ts's
assertSuccessOrKnownError swallows the resulting "Failed to submit
transaction" as an indistinguishable known error; and the E2E's
hash-equality check can't catch it either, since dropping witnesses never
changes the transaction body hash - only the ledger tells the two apart,
and this test never talks to it. The new test builds a synthetic,
fully-offline script-bearing transaction directly via CML (one input/
output, a witness set carrying one PlutusV3 script, one map-form redeemer,
one datum), signs it with a public, well-known, never-funded BIP39 test
vector (computed from the installed bip39 package rather than transcribed
from memory - the first guess had the wrong checksum word and failed
loudly), and asserts every witness category plus the vkey witness survive
signing with the body hash unchanged. Proved it's load-bearing: reverted
the fix to the buggy from-scratch witness set, watched the test fail
naming the exact lost witness, reverted back (byte-identical, zero diff),
re-ran green.
registry-keyless.test.ts: the non-owner-update assertion drops its second
regex alternative (`Failed to build agent update`), which was a superset
match able to hide a real, unrelated build failure behind the same
"ownership was correctly refused" label. Now asserts the specific
`Ownership check failed` message only.
run.test.ts: buildSignSubmit now hard-asserts `Transaction Submitted` on
the submit leg when walletHasAda is true, instead of routing through the
tolerant assertSuccessOrKnownError - on a funded wallet a submit failure
is a real regression, not indistinguishable from an unfunded wallet, and
should fail the test rather than pass silently as a "known error". The
graceful path is kept for the unfunded case. Also documents, in a comment,
the known/accepted risk that a register-succeeds/deregister-fails run in
this file leaves the 10 AP3X test deposit locked on-chain (same shape as
the pre-keyless suite).
Hermetic only this round, per instruction: no live network calls, no
transaction submissions, no real secrets. Full account in
.superpowers/sdd/task-5-report.md's Addendum section.
Gates: typecheck 0, unit 351/351 (350 + 1 new), smoke 26/26, auth 12/12,
boundary 4/4 (confirms the new test's mnemonic constant sits outside the
custody scan by design, same as the rest of test/, and is labeled
TEST_ONLY_PUBLIC_MNEMONIC regardless per public-repo hygiene).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the three remaining integration layers for spec PR 7 (agent-registry
keyless): a tier-1 suite that exercises the five build_* tools against the
live testnet registry with zero secrets (build-only, nothing signed or
submitted), a gated full lifecycle E2E (register -> update -> deregister)
that actually submits, and a shared signWithMnemonic() helper extracted
from keyless-e2e.test.ts so registry-e2e.test.ts and run.test.ts's
registry section don't each re-inline the same CML signing sequence.
run.test.ts's "Agent Network Tools" section is rewritten: each of the five
old custodial calls is now build_* -> signWithMnemonic -> submit, via a
new buildSignSubmit() helper that reuses the file's existing
assertSuccessOrKnownError convention at both the build and submit legs.
Found and fixed one load-bearing bug while proving this live, not just in
review: the inline signing block moved verbatim per the brief's Step 1
built the signed tx's witness set from scratch, which is lossless only for
a script-less payment (the one case it had been proven against). The
registry family's build_* tools attach a minting policy and/or spending
validator, so their unsigned CBOR already carries scripts/redeemers that
the from-scratch witness set silently dropped - the ledger rejected the
first live E2E attempt with MissingScriptWitnessesUTXOW /
PPViewHashesDontMatch. Verified zero funds were lost (balance identical
before/after the rejected submission - a mempool-level rejection never
reaches a block) before fixing it by merging into the tx's existing
witness set via CML's add_all_witnesses instead of discarding it. Re-run
clean: all 3 E2E transactions confirmed on-chain, plus all 5 of the legacy
suite's registry operations submitted successfully too (independent
confirmation across both the mint and spend redeemer paths). Also widened
one tier-1 regex that didn't tolerate the live tool's Markdown-bold label
formatting.
Live evidence (testnet, funded wallet, 8 real transactions total across
the gated E2E and the legacy suite): balance 986.689510 -> 982.280960
AP3X, fully reconciled against transaction fees plus one message delivery
to an owner-derived enterprise address. All tx hashes, explorer links, and
the full reconciliation are in task-5-report.md. mnemonic.txt and the two
temporary evidence-capture scripts were removed and confirmed absent
before this commit.
Gates: typecheck 0, unit 350/350, smoke 26/26, auth 12/12, boundary 4/4 -
all re-verified after cleanup. E2E skip-path (VECTOR_E2E_SUBMIT unset)
exits 0 with tests 0 for both keyless-e2e.test.ts and registry-e2e.test.ts,
confirmed with mnemonic.txt genuinely absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites agent-network.ts as a typed, keyless module: the five custodial
signing tools (vector_register_agent, vector_update_agent,
vector_transfer_agent, vector_deregister_agent, vector_message_agent) are
replaced by unsigned-build equivalents (vector_build_register_agent,
vector_build_update_agent, vector_build_transfer_agent,
vector_build_deregister_agent, vector_build_message_agent) that take no
mnemonic - the 4-call flow is get_address -> build_* -> signer sign ->
submit -> confirm. The two read-only tools (vector_get_agent_profile,
vector_discover_agents) keep identical behavior, description, and schema,
now backed by registry-build.ts's typed helpers (Tasks 2-3) instead of
local duplicated logic.
@ts-nocheck is dropped - the file now typechecks clean under strict mode
(server.tool()'s generic inference did not explode; the TS2589 concern
that originally motivated @ts-nocheck is confirmed dead under zod/v4).
boundary.test.ts: CUSTODIAL_UNTIL_LATER_PR shrinks from
{agent-network.ts, self-improvement.ts} to {self-improvement.ts} - size
pin 2 -> 1, message updated for spec PR 8 (the last remaining family).
tool-inventory.test.ts: EXPECTED_TOOLS updated for the 5 renames,
alphabetical order preserved, count stays 23.
tool-schemas.snapshot.json regenerated; delta verified surgical - exactly
the 5 old names removed and 5 new names added, all other 18 entries
(11 vector.ts + 5 self-improvement + the 2 unchanged read-only agent
tools) byte-identical.
Also fixes a stale cross-reference this rename exposed:
self-improvement.ts's registry-NFT-not-found error told the caller to
"re-register with vector_register_agent", a tool name that no longer
exists post-rename. Updated to vector_build_register_agent. This is the
only reference to the old names left in packages/builder/src outside the
deferred integration test (test/integration/run.test.ts, explicitly
scoped to a later task per progress.md).
One addition beyond the task brief (Task 3 review, M-c context): the
build_deregister_agent troubleshooting copy gets a 4th tip pointing at
registry-build.ts's I-1 error (deregistration needs a plain AP3X-only
UTxO to clear the validator's returned-change floor).
Gates: typecheck 0, unit 350/350, smoke 26/26 (23 tools), auth+rate-limit
12/12, boundary 4/4.
Four items from the final whole-branch review, all docs-only:
1. Architecture diagram: the ASCII diagram routed every tool call through
Rate Limiter -> Safety Layer -> Lucid/Ogmios, which is false for the
keyless build_* path (safety.ts is never consulted there). Annotated the
Safety Layer box with a footnote marker instead of redrawing the diagram -
box alignment verified programmatically before and after (same column
positions for every vertical bar).
2. Configuration table: added one sentence stating VECTOR_SPEND_LIMIT_PER_TX/
DAILY govern only the still-custodial families, matching .env.example's
existing scope note.
3. Testing section: "builds every build_* tool's unsigned transaction"
overclaimed - keyless-build.test.ts covers 4 of 5 (verified directly
against the test source: vector_build_send_tokens is absent from it,
covered instead by unit tests and the legacy integration suite). Reworded
to name the gap explicitly.
4. packages/builder/test/integration/keyless-e2e.test.ts:9 (comment only):
"which lives on PR #7's branch" was stale now that #7 is merged and
packages/signer is in-tree. Reworded to name the in-tree package and be
precise about what the stand-in does and doesn't exercise (same CML
primitives, without decode/policy/audit).
#7 (signer) and #8 (builder hardening) merged into main this morning; this
branch is rebased onto the result and packages/signer is now in-tree. Three
sites still said pairing the signer with the hosted builder "does not yet
remove the custody exposure" (true when #7 was written, before this PR's
keyless build_* tools existed - now false for the wallet/tx/contract family):
- README.md's "Local signer" section: now states the 4-call non-custodial
flow (signer_get_address -> build_* -> signer_sign -> submit -> await) is
live today for that family, names the signer's four tools, and keeps the
agent-registry/self-improvement families explicitly scoped out.
- packages/signer/README.md's "Known limitations" bullet and "Running it"
closing line: same correction, with the specific now-keyless build_* tool
names and an accurate (not overclaiming) description of what the E2E test
exercises - it signs with the same CML primitives sign.ts uses, as a
stand-in for calling vector_signer_sign directly, not a call into the
signer package's own code.
Grepped case-insensitively across the repo for "does not yet remove" /
"custody exposure" / "signer half is ready" / "builder half is not yet"
before and after: exactly these 3 sites found, all corrected, zero stale
instances remain.
README, .env.example, and docs/architecture/non-custodial-split.md updated for
the family-1 keyless migration (build_* tools, no mnemonic parameters): custody
notice rewritten to describe the keyless boundary precisely, tool tables synced
to the 23-tool surface (4 renames, 4 changed in place, 2 removed), self-hosting
and testing copy updated, .env.example spend-limit scope annotated, and the
architecture doc's PR-sequence table marks family 1 as landed.
docs/VECTOR.md (flagged by Task 4 as referencing stale tool names) is also
updated: renamed/removed tool sections corrected, keyless changeAddress params
added, and the Safety Controls / Error Handling sections stop claiming
server-side spend limits and mnemonic requirements that no longer apply to the
tools this page documents. Edits confined to tool names and custody claims -
no restructuring, no wholesale ADA to AP3X pass beyond the sections touched.
Opus review round 2 on Task 6:
- agent-network.ts: seedUtxo -> oneShotUtxo (5 occurrences). This is
one-shot-minting-policy vocabulary that survives PR 7's keyless rewrite,
but the boundary test's FORBIDDEN regex is an unanchored case-insensitive
substring match on "seed" - left as-is it would have false-red PR 7's
guard the moment agent-network.ts drops off the custodial allowlist. Only
selectWallet.fromSeed (genuine key-material vocabulary, deleted by PR 7
anyway) still matches `seed` in this file. File is @ts-nocheck, so this
was verified mechanically, not just visually: line-match counts before
(seedUtxo=5) and after (seedUtxo=0, oneShotUtxo=5) line up exactly, and a
fresh `import()` of the module still loads error-free.
- keyless-build.test.ts: the first tier-1 test summed every output
(`paid >= 3_000_000n`), but a self-send's change output lands at the same
address as the payment, so the sum floor could never actually detect a
dropped or wrong payment amount - change alone clears it. Added an
exact-count check (outputs at exactly 3_000_000n lovelace, >=1) alongside
the existing sum/fee/unsigned assertions, matching the pattern already
used by the build_transaction test in the same file. Also added the
missing assertBuildNotSubmitted check on the dry-run test's own build
response.
- run.test.ts: deleted walletBalanceAda - write-only since the UTxO
Consolidation block (its only reader) was removed earlier in this task.
Re-verified full gate plus one live re-run of the tightened tier-1 suite
(build-only, no funds spent, no E2E resubmission).
"different seed UTxOs" is one-shot minting-policy vocabulary, not key
material, but the boundary test's FORBIDDEN regex matches "seed" as a
substring anywhere in scanned source. agent-network.ts is allowlisted as
custodial today so this line is invisible to the guard, but PR 7 takes it
keyless and drops the allowlist entry - at that point this comment would
false-red the guard for no reason. Reworded to "one-shot UTxOs", meaning
unchanged.
Addresses opus reviewer findings on the Task 5 commit (33c4b52).
Important:
- agent-network.ts (3 sites) and self-improvement.ts (2 sites): response
copy no longer tells a caller to run vector_get_spend_limits (deleted in
Task 5) after a safety-limit error. Replaced with
"Server spend limits for this tool are set via VECTOR_SPEND_LIMIT_PER_TX /
VECTOR_SPEND_LIMIT_DAILY" (matching each site's existing phrasing).
Nothing else in either file touched - both stay custodial until PRs 7/8.
Also reworded the two comments (of my own, from Task 5) that named the
deleted tool - tool-inventory.test.ts's header docblock and
rate-limit-gating.test.ts's probe-choice comment - so a final
`grep -rn vector_get_spend_limits packages/` is clean except
run.test.ts (Task 6's, left untouched as instructed).
- boundary.test.ts: added a third test pinning
CUSTODIAL_UNTIL_LATER_PR.size === 2, so PR 7/8 shrinking it is a visible,
reviewable number change rather than a silent list edit.
- boundary.test.ts: FORBIDDEN is no longer word-bounded
(`/(mnemonic|seed|bip39|fromSeed|walletFromSeed|selectWallet\.fromSeed)/i`,
\b dropped) so it catches `seedPhrase`, `WALLET_SEED`, `bip39x`-shaped
vocabulary, not just whole-word matches. Verified empirically (not just by
inspection) that this introduces zero new hits outside the two allowlisted
files - grepped `seed`/`bip39`/`mnemonic` case-insensitively across all of
packages/builder/src first, confirmed every hit was already confined to
agent-network.ts/self-improvement.ts, then reran the full boundary suite
to prove it: no benign collisions, nothing to report back before
committing. Added a fourth test pinning every FORBIDDEN alternative
against a fixture corpus (mnemonic/MNEMONIC/seedPhrase/WALLET_SEED/bip39/
fromSeed/walletFromSeed/selectWallet.fromSeed/seed all match;
"address-only wallet"/"unsigned transaction"/"changeAddress" don't) so
deleting an alternative from the regex fails a named test instead of
silently narrowing the guard.
Cheap folds:
- vector.ts: the five build_* tool DESCRIPTIONS (not response copy - that
already taught the confirm step as of Task 5) now end with "...then
broadcast with vector_submit_transaction and confirm with
vector_await_transaction." Regenerated the snapshot; verified structurally
that the delta is description-field-only on exactly those five entries
(inputSchema untouched, all 23 keys unchanged) - this is also what makes
the Task 5 addition-A docblock's "descriptions teach the full flow" claim
actually true end-to-end.
- tool-inventory.test.ts: each per-tool test now also asserts
'description' in schemaSnapshot[toolName] before the deepStrictEqual, so
a regen that silently drops a description (e.g. JSON.stringify eating an
undefined value) fails by naming the tool, not as an opaque diff.
- regen-tool-schemas.mjs: server entry point and snapshot output path are
now resolved from import.meta.dirname instead of hardcoded
repo-root-relative strings, so the script runs from any cwd. Verified by
actually invoking it from packages/builder (not the repo root) and
confirming it wrote to the one correct
packages/builder/test/smoke/tool-schemas.snapshot.json.
Full gate re-run after all of the above: typecheck 0 errors, test:unit
144/144 (140 baseline + 4 boundary tests, up from 2), test:smoke 26/26,
test:integration:auth 12/12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finishes the builder-side wallet/tx family's non-custodial split (spec PR 6).
Task 4 registered the keyless build_* tools; this task deletes the last
custodial surface in vector.ts and makes every remaining query tool keyless:
- Deleted initLucid, getWalletInfo, the vector_get_address and
vector_get_spend_limits tool registrations, and the now-unused safetyLayer
and Lucid imports. SafetyLayer/safety.ts itself stays (still used by
agent-network.ts and self-improvement.ts until PRs 7-8).
- vector_get_utxos, vector_get_transaction_history: address is now required,
mnemonic param and branch deleted, provider construction routed through
makeProvider().
- vector_dry_run: mnemonic param removed; the outputs-branch now builds via
lucidForAddress + buildMultiOutput (keyless, UNSIGNED) instead of signing
with a mnemonic-derived wallet.
- packages/shared/src/types.ts: removed the now-dead VectorToken,
VectorWalletInfo, VectorAdaTransactionResult, VectorTokenTransactionResult,
VectorBuildTransactionResult, VectorDeployContractResult and
VectorInteractContractResult interfaces (and their re-exports from
vector/index.ts). SpendLimits/SpendStatus/AuditEntry stay - safety.ts is
still live for the two custodial families.
- vector_build_transaction / vector_build_deploy_contract /
vector_build_interact_contract response copy now teaches the full
build -> sign -> submit -> confirm flow (they previously stopped at
submit), matching vector_build_send_apex/send_tokens.
- rate-limit-gating.test.ts: repointed its hermetic probe from the
now-deleted vector_get_spend_limits to vector_build_send_apex with an
invalid changeAddress - lucidForAddress rejects it in-process before any
provider call, so the suite stays hermetic.
- Added packages/builder/test/boundary.test.ts: a mechanical grep-based
custody boundary (design-spec success criterion 1). Verified failing before
the rewrite (vector.ts's mnemonic vocabulary) and passing after; also
verified the guard actually fails by injecting `// mnemonic` into build.ts
and watching it name the exact file:line before reverting.
- Tool surface is now 23 (25 - 2 deletions). tool-inventory.test.ts updated
accordingly.
Snapshot shape widened (this once, deliberately): tool-schemas.snapshot.json
entries are now { description, inputSchema } instead of a bare inputSchema,
so the regression guard also covers the security-teaching copy in tool
descriptions (the sign/submit/confirm flow callers learn from them), not just
JSON Schema shape. tool-inventory.test.ts's per-tool test now deep-compares
the whole entry and is relabeled "schema+description unchanged". This is why
the snapshot diff touches every entry, not just the 10 that actually changed.
Committed the regen script at packages/builder/scripts/regen-tool-schemas.mjs
(previously scratchpad-only) with this shape change applied.
Verified against the feat/builder-auth-and-submit baseline: the snapshot key
delta is exactly 4 renames (send_apex/send_tokens/deploy_contract/
interact_contract -> build_*), 4 changed-in-place (build_transaction,
dry_run, get_utxos, get_transaction_history), and 2 removals (get_address,
get_spend_limits) - confirmed with a structural JSON diff, since the uniform
shape-widening makes raw line-based git diff noisy (e.g. it shows
vector_message_agent's key line as touched even though its inputSchema is
byte-identical old vs new - a Myers-diff line-matching artifact, not real
agent-network.ts drift).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Move fixture-provider.test.ts up to test/ (test:unit's glob is
packages/*/test/*.test.ts, deliberately shallow, so a nested
test/fixtures/ file was silently never run). The fixture module itself
(fixture-provider.ts, the capture script, the JSON fixture) stays in
test/fixtures/; only the *.test.ts file moved, importing it via
./fixtures/fixture-provider.ts.
- Harden capture-protocol-params.mjs: refuse to write a fixture whose
costModels/coinsPerUtxoByte/keyDeposit could have come from
OgmiosProvider's own `??` fallback defaults instead of a genuine
response. The brief's sanity numbers (minFeeA 45, minFeeB 156253, ...)
are literally those same fallback literals, so matching them proves
nothing; costModels has no populated fallback, so a well-formed,
appropriately-sized cost model per Plutus version is the one signal a
schema-mismatched response cannot fake.
- packages/builder/tsconfig.json: include test/**/* so `npm run
typecheck` actually checks builder's test files (it previously covered
src/**/* only, unlike packages/shared). Needed
allowImportingTsExtensions for the now-in-scope relative .ts-suffixed
imports across every existing test file. Surfaced one real pre-existing
gap: auth.test.ts calls loadAuthConfig with deliberately minimal env
objects, but the exported signature demanded the full
env.d.ts-augmented NodeJS.ProcessEnv (7 required custom fields)
even though the function only ever reads MCP_AUTH_TOKENS. Narrowed the
parameter to Record<string, string | undefined> - the function's real
contract - which is a type-only change with a single production call
site (index.ts's parameterless loadAuthConfig()) that already relies on
the default.
Verified: test:unit 117/117 (was 113 - the 4 fixture tests were never
counted before), typecheck 0 errors, test:smoke 28/28, build unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "unusual identity strings" fixture embedded a literal 0x00 byte in a
string literal instead of the \x00 escape sequence. Consequence: the file
had no valid text encoding as far as git's binary-diff heuristic is
concerned, so the whole 5278-byte file rendered as "Binary files differ"
with zero textual diff - invisible to review, and would have been
invisible to any GitHub PR reviewer too. It also made the raw NUL
character indistinguishable from the adjacent ASCII-space case when
viewed directly.
Same runtime string (a genuine one-character NUL string, confirmed via
'\x00'.charCodeAt(0) === 0), no behavior change - the file is just
diffable text again.
The auth guidance was accurate but lived only in the Configuration
reference table, after both quickstart paths (npm start / docker run)
that a self-hoster would follow top to bottom. Following either to
completion on a public host reproduces the unauthenticated-server problem
this PR exists to fix, without ever seeing a warning at the point of
action.
Add one line after each quickstart's run command, pointing to
Configuration. The Docker line also notes that docker-compose.yml in this
repo already binds 127.0.0.1 by default, unlike the inline `docker run`
example.
vector_await_transaction's confirmation check only ever looked for an
unspent output at index 0 - a heuristic that can read a landed transaction
as unconfirmed, since these tools broadcast CBOR signed elsewhere with no
guarantee of that output existing or staying unspent. The check also wrapped
the lookup in .catch(() => []), collapsing genuine infrastructure failures
(Ogmios down, malformed response) into the same "not confirmed yet" result,
defeating pollUntilConfirmed's documented contract that a check rejection
should propagate.
OgmiosProvider.getKoiosTxStatus + koiosIndicatesConfirmed (shared/provider.ts)
now provide a direct, non-swallowing Koios tx_status check; poll.ts's new
buildConfirmationCheck picks it when VECTOR_KOIOS_URL is configured and
falls back to the outRef heuristic (also no longer swallowing errors)
otherwise - the same preference order OgmiosProvider.awaitTx already used,
without reusing awaitTx itself (still not caller-configurable, still
swallows every failure). awaitTx stays in place only because the Provider
interface requires it; nothing else calls it.
buildConfirmationCheck and koiosIndicatesConfirmed are unit tested via
dependency injection, the same pattern poll.test.ts already uses for
now/sleep, with no network access required.
Covers what Task 1 actually enforces: auth is only on when MCP_AUTH_TOKENS
is set (otherwise every caller is one anonymous, shared-bucket identity),
rate limits are per identity, and the token parser rejects empty tokens,
whitespace, duplicates, and all-blank values at startup rather than
degrading silently. Notes that vector_submit_transaction and
vector_await_transaction take no mnemonic and sign nothing. Updates the
tool count from 23 to 25 everywhere it appears in the README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The snapshot guard flagged both additions, which is what it is for. Delta
verified as exactly two ADDED entries with no existing schema changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the keyless half of the non-custodial flow: the signer returns
signed CBOR, and these broadcast it and wait for confirmation.
submitTx previously returned the first 64 hex characters of the submitted
CBOR when the response failed to parse - a string indistinguishable from a
real transaction hash. It now refuses via a new exported
parseSubmitResponse, since these tools are the first callers to actually
surface that value to an agent. Extracted to module scope and tested
directly (18 cases) rather than through submitTx itself, since provider.ts
imports fetch from cross-fetch rather than reading the global - stubbing it
would leave the real implementation live and hit a genuine DNS lookup.
vector_await_transaction's polling loop is extracted into vector/poll.ts
(pollUntilConfirmed) rather than left inline in the tool. Two reasons:
- The original loop slept a fixed 3s every iteration regardless of budget
remaining, so a short timeoutSeconds could overshoot its deadline by
close to 3x (e.g. timeoutSeconds=1 could block ~3s). The sleep is now
clamped to the time left before the deadline.
- Extracting it makes the loop unit-testable (11 cases) with an injected
fake clock, instead of only being reachable through a live network call.
Does not reuse the provider's own awaitTx: it hard-codes a 60-attempt
budget with no caller control and returns a bare boolean, collapsing
"still pending" and "gave up" into one signal - the tool needs a
configurable timeout and distinct wording for the two, since a pending tx
may still land and must not be reported as failed.
Judgement call on the brief's fixture: its final test asserted
parseSubmitResponse(CBOR.slice(0, 64)) throws. With the simple 64-hex-char
isTxHash check specified in the brief, 'ab'.repeat(32) legitimately
qualifies as a hash - it IS 64 valid hex characters - so that assertion was
changed to assert.equal instead of contorting the implementation to add
stricter validation the brief didn't ask for. The invariant that matters
still holds: parseSubmitResponse only ever receives the HTTP response body,
never the submitted tx, so this value can no longer be invented by our own
code from data it already held - only echoed back by a server, in which
case treating it like any other bare-text hash is correct.
Beyond the brief: added coverage for JSON responses in shapes the parser
doesn't recognize (array/number/null/boolean/nested object/unhandled field
name), the "id" field alias, a zero-budget/never-yielding poll, and that a
rejection from the confirmation check propagates instead of being read as
"not yet confirmed".
test:unit 96/96, typecheck clean. test:smoke fails as expected (23 -> 25,
naming vector_await_transaction and vector_submit_transaction as added) -
regenerating that snapshot is Task 5's job. test:integration:auth 12/12
unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tasks 2 and 3 verified their work with throwaway scripts against a real
spawned server that were never committed, so npm test protected none of it -
index.ts's auth gate had zero automated coverage, and the @ts-nocheck
agent-network.ts/self-improvement.ts rate-limit wiring had none beyond
listTools().
Adds two hermetic integration suites (real spawned server, localhost only,
no wallet/chain calls):
- auth-gating.test.ts: both HTTP routes' 401/403 gating, the auth-before-
session-lookup ordering, cross-identity rejection, token-leak scanning,
and that the server survives all of it (the reviewer's severe finding -
a missing early `return` after a 401 falls through into a second
writeHead() and crashes the whole process via an unhandled rejection).
- rate-limit-gating.test.ts: per-identity isolation and that exhaustion on
a vector.ts tool also rate-limits agent-network.ts and self-improvement.ts
tools for that identity, through the real registered tools rather than
limiterFor() in isolation.
Extends test/setup.ts additively (spawnServer/stopRawServer/RawServerHandle)
so these tests can set MCP_AUTH_TOKENS and drive per-request Authorization
headers by hand; startServer()/stopServer()/callTool() are unchanged and the
existing run.test.ts still fails for exactly its pre-existing reason.
Wires a new test:integration:auth script into CI's build-and-test job.
All five mutations from the review were applied to a working-tree copy,
built, and reverted per-mutation to prove the new tests actually fail:
- removing either route's early `return` after a 401: caught (cascading
ECONNREFUSED once the process crashes; a dedicated liveness test targets
this directly)
- the cross-identity check's undefined-lookup branch: not independently
catchable - transports/sessionIdentities are always populated and
cleared together, synchronously, so that state is unreachable via HTTP;
documented in the report, verified a same-region operator-flip mutation
IS caught cleanly
- module-scope `identity` reference in self-improvement.ts: crashes the
server at startup (ReferenceError at import time), caught via the
before() hook
- checkRateLimit restored to module scope in agent-network.ts: caught
cleanly, isError:true / "rateLimiter is not defined" instead of the
well-formed rate-limit text
No file under any src/ changed. test:unit 67/67, test:smoke 26/26 (23
tools, 0 drift), typecheck clean, new suite 12/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The limiter was a module-level singleton, so one busy caller throttled every
other caller on the box and a shared daily budget was trivially exhausted.
Buckets are now keyed by authenticated identity, and keyed centrally so
reconnecting does not hand back a clean bucket.
POST /messages is gated too, and cross-checked against the session's own
identity - otherwise a valid token for one caller could drive another
caller's session by guessing its id.
All four loadAuthConfig throw sites could be made to interpolate the secret
that triggered them with the suite staying fully green - the one constraint
this module states twice in its own header had no regression protection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hosted deployments set MCP_AUTH_TOKENS; localhost self-hosters are unaffected.
The resolved identity doubles as the rate-limit bucket key. Rejection reasons
never echo the supplied token, and a bare token's log label is a hash prefix.
Hardened beyond the brief after an adversarial pass (20 additional tests):
- A malformed MCP_AUTH_TOKENS that parses to zero usable identities (e.g.
",,," from a broken template) now throws instead of silently resolving to
`enabled: false` - the exact "operator believes auth is on, server is
actually open" failure shape this module exists to prevent. An explicitly
empty/unset value still disables cleanly, unchanged.
- A token containing embedded whitespace is now rejected at config-load
time: resolveIdentity's \S+ match can never span it, so without this check
the token would be configured yet permanently unpresentable.
- Documented and pinned two non-fixable format limitations: only the first
colon in an entry separates label from token (a label can't itself contain
one), and a literal comma inside a label/token silently fragments the
entry since the delimiter has no escape mechanism.
- Pinned correct existing behaviour: exact case-sensitive token match, no
prefix/superstring matching, Map-backed storage is immune to
__proto__/constructor prototype pollution, one label may legitimately
cover multiple tokens (rotation), empty-string header treated as missing.
- Map.get token lookup is not constant-time; judged acceptable and
documented inline rather than swapped for crypto.timingSafeEqual - see
the comment above the lookup in auth.ts for the reasoning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>