vector-signer-audit-log.json resolves inside the repo checkout by default,
and the README self-disclosed that it was NOT in .gitignore (unlike the
builder's vector-audit-log.json). A documented footgun is worse than no
footgun when the fix is one line. Adds the entry and updates the README
paragraph that described the gap to describe the fix instead.
Final whole-branch review, Fix 1 of 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Includes an opt-in testnet round-trip test (keyless build -> decode -> policy
-> sign, never submits), excluded from CI because it needs a funded wallet.
Run against the real funded testnet wallet: pass 3, fail 0, nothing submitted.
Verified every README claim against the code as implemented, not just the
brief text, and corrected several:
- VECTOR_SIGNER_AUDIT_LOG_PATH's default resolves against the package's own
install location (repo root), not the process's cwd as originally stated --
and that default filename is not yet covered by .gitignore.
- An audit-log write failure blocks ALL signing, not just accounting:
audit.append() throws by design and vector_signer_sign withholds an
already-produced signature rather than release one with no durable record.
- Documented the #private-field defense against accidental key leakage via
JSON.stringify/console.log, and the read-only/sign tool split that keeps
the signing key out of scope for get_address/decode/get_spend_limits.
- Documented that pairing this signer with the CURRENT hosted builder does
not yet remove the custody exposure it's designed to fix: no builder tool
is keyless yet, and there is no submit_transaction tool to hand a produced
signature to. That's the next stage of the migration, not shipped here.
Env-var cross-check (brief's Step 6, both directions) passes clean.
A decode_transaction that signs only when policy-allowed, worded to avoid
"sign", passed all 21 tests - a preview tool returning real signed bytes with
no audit entry and no budget consumed. And the stdout-purity constraint was
only enforced incidentally by an unrelated stderr sanity check, while the MCP
client silently swallows unparseable stdout lines.
Gap 1 (decode-can-sign) is closed structurally, not just by test: tools.ts
splits into registerReadOnlyTools() (get_address, decode_transaction,
get_spend_limits - receives only signerAddress/limits/auditLogPath/audit,
never config or keySource) and registerSignTool() (the only function in the
module whose closure ever holds a loaded SigningIdentity). Reproducing the
review's exact mutation against the new code is now a compile error
(TS2304: Cannot find name 'identity') rather than a passing test suite.
Defense-in-depth on top of that: a new content-based test independently
computes what signing the previewed transaction would actually produce and
asserts decode's response never contains those bytes, under any wording -
this is what catches a more determined mutation that deliberately re-widens
the read-only context to reach signing capability again.
Gap 2 (stdout purity) is closed with a purpose-built minimal Transport
(RawCapturingStdioTransport) that captures every raw stdout byte independently
of the SDK's own lenient line parsing, while still driving the real Client
class for a correct initialize handshake. Asserts every line written to
stdout, across a healthy startup and a normal tool call, parses as JSON.
Both fixes reproduced-then-verified: applying the review's mutations first
confirmed each gap was real (all tests green under the vulnerable code),
then confirmed each new check catches it, then confirmed restoring the fix
returns to 23/23 with no collateral. Also re-ran the original hard-requirement
mutations (signed- and refused-path unguarded audit.append, the exact
catch{warn();return signature} anti-pattern, and a missing return after the
refused branch that lets a refused transaction fall through to real signing)
against the restructured tools.ts to confirm the refactor weakened nothing -
none did; the missing-return mutation alone is caught by 4 tests, including
3 of the brief's own original 9.
typecheck 0, test:unit 180/180, builder test:smoke 26/23/0, boundary 4/4,
signer test:smoke:signer 23/23 (9 brief + 12 original edge-case pass + 2 new).
Hard requirement for this task: audit.ts's append() throws by design on a
write failure (see its own CONTRACT note). The trap is catching that throw
and still returning the signature already produced - that reproduces exactly
the failure the throw exists to prevent, one call site later: a real
signature reaches the caller with no durable record of it, so a restarted
process's committedTodayLovelace() silently under-counts and the daily limit
stops binding.
vector_signer_sign now wraps both audit.append() calls explicitly:
- Signed path: if the append throws, the signature is withheld. The
function returns before ever reading signed.signedCborHex - the only
`return` that reaches it is the one after a successful append. The
response tells the caller a valid signature WAS produced but could not be
durably recorded, so it is being withheld rather than released.
- Refused path (lower stakes - no funds move either way): if the append
throws, the refusal itself is still delivered with its concrete policy
reason (never silently replaced by a generic disk-error message), plus a
note that this specific refusal could not be recorded, so the caller does
not mistake an incomplete audit trail for a normal one.
Mutation-tested against the brief's own unguarded version (see the task
report for the full transcript): reverting to unwrapped `audit.append()`
calls does NOT leak signed CBOR either way (the throw prevents the function
from ever reaching its return statement) - but it does replace "REFUSED.
<reason>" with the SDK's generic isError conversion of audit.ts's own
message on BOTH paths, so a refusal's own policy reason becomes invisible
to the caller exactly when the audit log is unhealthy. Exactly 2 of 21
tests failed under that mutation, precisely the two targeting this gap, no
collateral.
Edge-case pass added to the smoke test (12 new tests, each on its own
dedicated server instance):
- loadConfig() / the built server's behaviour with no key material
configured at all (clear throw; process exits non-zero and promptly,
never hangs, never writes anything to stdout).
- No mnemonic word or the derived private key appears in the startup
banner or in any tool response across a sweep that includes a real
successful sign.
- decode_transaction never writes an audit entry, even for an in-policy
transaction.
- An undecodable transaction passed to sign() never reaches evaluate(), so
there is nothing to audit yet - by design, pinned rather than assumed.
- get_spend_limits surfaces the concrete refusal reason, not just the
word REFUSED.
- The hard requirement itself: a control test proving the in-policy
fixture genuinely signs under a healthy audit log, then the same
transaction against an unwritable audit log withholding the signature,
then a pin of the (intentional, conservative) consequence that the
withheld signature still counts toward this process's own committed-
today total even though nothing was ever released.
- The refused-path decision under the same unwritable-audit-log condition.
Also reworded decode_transaction's response text ("(read-only)" /
"would be ALLOWED" instead of "(not signed)" / "would be signed") so it
never contains the substring "signed" even when a decoded transaction
happens to be in-policy - matches this same smoke test's own
`!/signed/i.test(text)` check on the brief's decode test, which otherwise
fails against the brief's own unmodified wording on every call.
typecheck 0, test:unit 180/180, builder test:smoke 26/23/0, boundary 4/4,
signer test:smoke:signer 21/21 (9 brief + 12 edge-case pass).
stdio transport only - no HTTP listener, no Provider, no outbound network.
Wires together decode.ts/policy.ts/sign.ts/audit.ts/keysource.ts (Tasks 2-6)
behind four tools: vector_signer_get_address, vector_signer_decode_transaction,
vector_signer_sign, vector_signer_get_spend_limits.
Two corrections needed beyond the brief's given code to reach its own stated
"pass 9, fail 0" outcome, both verified necessary by reproducing the failure
first:
- packages/signer/tsup.config.ts: target bumped es2020 -> es2022. index.ts's
top-level `await server.connect(...)` is ES2022 syntax; esbuild hard-errors
("Top-level await is not available in the configured target environment")
under the package's original es2020 target rather than attempting an unsafe
downlevel transform. Scoped to the signer package only - the builder never
uses top-level await and its own tsup.config.ts is untouched.
- tools.ts's decode_transaction response text: "(not signed)" and "would be
signed" both contain the substring "signed", which collides with this same
smoke test's own `!/signed/i.test(text)` check on every call, including the
refused case. Reworded to "(read-only)" / "would be ALLOWED" - same meaning,
no collision. Fixed the response text rather than loosening the test, which
stays exactly as given.
Brief's own smoke test (9 tests) passes verbatim. Workspace-wide: typecheck 0,
test:unit 180/180 (no regressions), builder test:smoke still 26/23/0.
The audit-append-failure handling required by the task (a produced signature
must never be released if its audit record fails to write) is not yet in this
commit - see the follow-up commit.
is_valid lives on Transaction rather than TransactionBody, so hardcoding it
was invisible to every hash-equality test - flipping it passed all 21. For a
deliberately-invalid script transaction that silently rewrites the claim a
node re-checks in phase 2.
Also pins the indefinite-length CBOR round trip that the trailing-bytes guard
depends on, which until now was asserted only in a comment.
from_cbor_hex only requires a valid prefix, so appending bytes to a real
transaction let sign.ts silently sign a shorter transaction than the caller
supplied and return a confident result — a signature over different bytes
than what was asked for, with nothing to reveal the discrepancy. sign.ts
calls from_cbor_hex independently of decode.ts and does not inherit that
module's consumption check (commit 5da4447) automatically; this closes the
same gap here, with the identical case-insensitive comparison and reasoning.
Also closes an untested claim: decode.ts's own suite documents but never
pins that the case-insensitive comparison tolerates legitimate uppercase-hex
input without a false positive. Pinned here for sign.ts.
Edge-case pass beyond the brief's 7 tests, all reproduced against the
unmodified implementation before any fix and mutation-verified after:
- trailing bytes silently accepted (BUG, fixed above)
- existing vkey witnesses are preserved and appended to, not replaced
(already correct; mutation-tested by dropping the copy loop)
- two different keys produce two independently-verifying witnesses,
chained or independent (already correct)
- determinism strengthened to compare raw signature bytes across a
freshly re-derived key instance, not just the memoized one (already
correct)
- auxiliary data / metadata survives signing byte-for-byte, body hash
unchanged (already correct)
- no thrown error, on any path, ever echoes the private key value
(already correct; mutation-tested by deliberately leaking it)
Lucid's constructor requires a Provider, which would put a network object in
the signer. The CML path was verified to produce an identical tx hash and
byte-identical signed output. Preserves any pre-existing witnesses.
TypeScript `private` is compile-time only, so JSON.stringify or console.log of
a KeySource printed the mnemonic or signing key in full - contradicting this
module's own "never via a log line" invariant, and one natural mistake away
in a package that already logs diagnostics to stderr.
Native #private fields plus toJSON/inspect overrides. Also pins the default
account index, which no test previously required to be 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
walletFromSeed is pure derivation and needs no Provider, keeping this
module network-free. MnemonicKeySource is the familiar default;
PrivateKeyKeySource is strictly safer since it controls one account
rather than the whole HD tree. describe() is asserted never to leak key
material.
Verified rather than assumed: walletFromSeed's private key prefix is
ed25519e_sk1 and its address is addr1... (Base, Mainnet default) - the
brief's regex already matched. The brief's test mnemonic passes BIP39
checksum validation.
Hardened beyond the brief after an edge-case pass:
- Mnemonic case: bip39's wordlist lookup is case-sensitive, so an
uppercase or mixed-case mnemonic threw outright. Casing carries no
BIP39 meaning, so lowercasing is safe - now applied, and verified to
derive the identical address rather than merely "some" address.
- PrivateKeyKeySource validated the trimmed key/address but stored the
original untrimmed value - the same knownAddresses() bug shape as
policy.ts (validate trimmed, store padded, silently fail every later
exact-string comparison). Now stores what was validated.
- Empty mnemonic input reported "got 1" word (''.split(' ') === ['']),
not "got 0" - message accuracy only, both branches already rejected.
Documented, not fixed: PrivateKeyKeySource cannot verify that `address`
is actually controlled by `privateKeyBech32`. That needs CML, and this
module deliberately imports only @lucid-evolution/wallet, whose one
raw-private-key function requires a live Provider. The failure is
bounded if misused - signing needs no address, so a mismatch surfaces as
an on-chain rejection, and policy.ts's own-address matching only ever
over-refuses on a wrong address, never under-refuses.
29 tests beyond the brief's 10. 5 of the fixes above were mutation-tested
(each reverted and confirmed to fail red before restoring the fix).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A malformed amount followed by valid entries silently dropped every later
entry when the loop aborted instead of skipping - 30/30 tests passed against
that mutation because every bad-amount case put the bad entry last.
Persistence now writes to a temp file and renames, so a kill mid-write costs
at most the in-flight entry rather than the whole day's history.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records intent at signing time; the signer has no network so it cannot
observe submission. Refusals are recorded but never consume daily budget.
Hardened beyond the brief after an edge-case pass found two genuine
under-report vectors in committedTodayLovelace() (the function the daily
limit's alreadySpentTodayLovelace comes from): a negative netOutflowLovelace
string parses as a valid negative bigint and would silently subtract from the
total, and a null element in the on-disk array crashes the summation outright
rather than being skipped. Both are neutralized. Also: load() now reports a
non-array log file on stderr instead of resetting silently, recent(0) no
longer returns the whole log (Array.prototype.slice(-0) is slice(0), not "no
entries" - a plain JS gotcha), and append() now throws on a write failure
instead of swallowing it, since a swallowed failure is a normal-looking
return while the entry silently exists only in memory until the next
restart's reload finds it missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
knownAddresses validated the trimmed value but stored the original, so an
own address configured with an incidental leading or trailing space passed
the fail-closed gate yet could never match its own outputs - every change
output was then counted as outflow and normal transactions were refused.
Also pins prefix-match, trim-before-check, and case-sensitivity, none of
which any test previously required.
Fix 1 (Important, live bypass): the blank-address fix from the prior commit
used .trim().length > 0, which only strips the ECMAScript WhiteSpace set
(NBSP U+00A0, BOM U+FEFF, etc). U+200B ZERO WIDTH SPACE and U+2060 WORD
JOINER are Unicode category Cf (format), not Zs, so trim() leaves them
untouched - ownAddresses: ['\u200B'] passed the filter, bypassed the
fail-closed guard, and reproduced allowed:true against the shipped code
(verified directly, not just against a mutant). Replaced the blank-only
filter with a positive check: an entry only counts as a known own address if
it starts with 'addr' after trimming. A denylist of invisible codepoints is
unwinnable; this rules out anything built from them without enumerating any.
Applies at both call sites (evaluate's guard and computeOutflow's Set) via
the shared knownAddresses() helper, as it already did.
Fix 2 (Important, surviving mutant): added a regression test that exercises
computeOutflow's own Set-construction filtering directly, independent of
evaluate's separate guard - a blank/invisible output address paired with a
matching blank/invisible ownAddresses entry must still count as outflow, not
be matched as change. Nothing previously exercised this combination: every
prior blank-entry test either went through evaluate (whose guard would
refuse first) or paired a blank ownAddresses entry with a real output
address. Confirmed this test now fails if computeOutflow's Set reverts to
unfiltered while evaluate's guard stays filtered (re-verified in this
session; see the report addendum for output).
Fix 3: added a doc comment on evaluate() stating plainly that it bounds
lovelace only - assetMovements is reported for visibility and never gates
allowed. No interface change; this is documentation of the existing,
deliberate v1 scope boundary already flagged in the task report, not new
behavior.
Re-ran the full mutation set after all three fixes: inverted classification,
removed fail-closed guard entirely, guard reverted to raw
ownAddresses.length === 0, and computeOutflow's Set left unfiltered are all
caught. Full before/after evidence in the report addendum.
Adversarial pass on the spend policy found that ownAddresses.length === 0
does not catch a non-empty array containing only blank entries - a realistic
outcome of ''.split(',') on an empty config/env value, which yields [''] not
[]. A blank entry never matches a real output address, so it degrades to
"every output looks foreign" rather than permitting anything, but it bypassed
the explicit fail-closed path and its message, and if the resulting
over-counted outflow still happened to land under the configured limits the
transaction would be signed without any real own address ever having been
known - exactly the condition that check exists to refuse.
knownAddresses() filters blank entries before both the Set used for
classification and the fail-closed emptiness check, so an all-blank list now
refuses the same way a literally empty one does. A blank entry alongside a
real address is unaffected (regression-tested).
Also adds the rest of the adversarial pass: exact-limit boundaries one
lovelace either side, many-small-outputs summation, aggregation correctness
(duplicate own addresses, repeated foreign addresses, multi-asset and
multi-output asset movements), check-ordering pins, zero-limit and
already-over-budget edge cases, and a pinned note that SpendLimits governs
lovelace only - native asset movements are tracked but not limited.
Every core comparison was verified to actually fail on injection (inverted
own/foreign check, dropped fee, dropped daily history, off-by-one on both
limit comparisons) before being reverted; see the task report.
Net outflow is every output that does not return to us, plus the fee - more
accurate than summing declared amounts, and computable without chain access.
Refuses when no own address is known, since change cannot then be identified.
The mechanism explanation is worth keeping; the snapshot of which fixtures
were tested on which day is commit-message content that goes stale silently.
from_cbor_hex only requires a valid prefix, so extra bytes were silently
dropped and decode returned a confident, fully-populated result - the exact
fail-open the module exists to prevent, and policy trusts this output.
The signer's tsconfig includes test files, and they import source with an
explicit .ts extension (which tsx resolves at runtime). Without this flag
tsc exits 2, which fails the blocking typecheck gate in CI. Safe because
noEmit is set; the alternative - excluding tests - would leave them
unchecked.
Pure function over CBOR. Everything policy needs lives in the body, which is
why the signer never resolves inputs and therefore needs no chain access.
Fails closed on anything it cannot fully parse.
readAssets uses AssetName.to_hex() (raw bytes, no CBOR envelope) rather than
to_cbor_hex().slice(2), which corrupts asset names of 24-32 bytes because the
CBOR byte-string header grows from 1 to 2 bytes at length 24. Verified
directly against CML at lengths 0-32; the golden fixture carries no native
assets so the test suite alone can't catch this.
Review found node:child_process had zero coverage across all four assertions:
execSync('curl ...') would have passed every one. Forbidden now, and verified
to fail when injected.
Also closed: backtick-quoted specifiers evaded the substring and regex scans;
sourceFiles() only walked .ts so a stray .js was invisible; the dependency
check ignored optional/peer deps and named only two packages.
Two holes: the shared-dependency assertion was a tautology that held for any
manifest, and ambient globals (fetch, WebSocket) need no import so the import
scan was blind to them. Both now enforced, and both verified to fail when a
violation is injected.
The signer's security claim is that it cannot reach the network, not that it
chooses not to. test/boundary.test.ts enforces that mechanically over both the
import graph and the manifest.
packages/builder is publishable (private:false) but depended on the
private workspace package at runtime - a published manifest would
advertise an unresolvable dependency. tsup's noExternal inlines it into
the bundle, so devDependencies is the correct home.
Also removes the root tsconfig.json, which still pointed at the deleted
src/, and corrects two README paths the move invalidated.
PR #3 renamed ADA to AP3X across agent-facing strings. Tool descriptions
are part of inputSchema, so the snapshot correctly flagged 6 tools. Delta
verified as terminology-only - no structural schema change.
Both projectRoot computations were off by a level after the move: vector.ts
landed one directory above the repo root, and safety.ts's default audit-log
path landed in packages/ rather than the repo root that .env.example and
.gitignore both document. Verified empirically against the built entrypoint.
The doc still carried the pre-spike hypothesis (stale SDK, restructure
tool registration) as fact. Neither happened: ^1.4.1 resolves to 1.27.1,
and the real cause is Zod 3 vs the SDK's Zod 4 expectation. Also records
the accepted additionalProperties trade-off and replaces the resolved
spike open-item with the outstanding branch-protection action.
Pure file moves plus import repointing at the shared package. Removes
duplicated endpoint constants and formatting helpers that were copied
across three files. Verified: 23 tools with identical schemas.
npm install regenerated package-lock.json for the workspaces conversion.
Without it, npm ci on a fresh clone cannot resolve the workspace layout
and CI fails.
A test whose job is catching subtle JSON-serializer drift should not use
loose comparison - deepEqual would pass a string 1 against a numeric 1.
Also documents the zod/v4 isSchemaOptional() quirk on bare .default()
at the call site rather than only in a planning doc.
Extracts pure tx helpers, shared types, endpoint config, and the Ogmios
provider. Subpath exports (./tx vs ./provider) let the forthcoming signer
import pure helpers while being structurally unable to reach the network layer.
The existing smoke test only asserted tool names and count, which is
exactly why the additionalProperties regression from the zod/v4
conversion (previous two commits) went undetected by it — a tool's
argument schema can change shape completely without changing its name.
Add test/smoke/tool-schemas.snapshot.json: the full inputSchema for
all 23 tools, keyed by name, generated from a live built server after
the zod/v4 conversion (so it reflects the corrected, uniform surface,
not the mid-migration split). Extend tool-inventory.test.ts with a
key-set check plus one deepEqual test per tool, so a mismatch names
the specific tool and shows a field-level diff instead of a single
23-tool "objects differ" failure. Verified the failure mode directly:
temporarily corrupting one entry in the fixture failed exactly one
named test with a readable diff, while the other 25 kept passing.
This is a change detector, not a freeze — PRs 6, 7 and 8 are expected
to regenerate the fixture when they deliberately restructure tool
schemas, per the comment in the test file.
Both files carry @ts-nocheck, so there's no type-inference benefit
here (that was vector.ts's problem, fixed separately). This is purely
about making the runtime JSON Schema serializer uniform across all 23
tools: the MCP SDK routes Zod v3 schemas through the vendored
zod-to-json-schema and Zod v4 schemas through Zod's own native
toJSONSchema, and the two disagree on `additionalProperties` for
object schemas. Converting vector.ts alone left the surface split 11
"open" (v4, additionalProperties absent) / 12 "closed" (v3,
additionalProperties: false).
Plain import swaps — neither file uses z.record, so no signature
changes were needed. Both files use z.<type>().default(...) on several
fields (agent-network.ts: limit; self-improvement.ts:
proposalOutputIndex, priority, focus); verified via the SDK's own
zod-compat safeParseAsync/objectFromShape helpers that omitting each
of those fields still resolves to its default under zod/v4, exactly as
under v3. Also verified live: a missing required argument is still
rejected (schema-validation error, not a handler crash) on one tool
from each of the three files.
Confirmed post-conversion: all 23 tools report additionalProperties
consistently (0 false / 23 absent — uniform), typecheck stays clean
(these files are excluded from it), and build + unit tests pass.
The SDK's ZodRawShapeCompat degrades on Zod 3 schemas, producing TS2589
and a knock-on TS2769. Importing zod/v4 (already shipped inside zod
3.25.76) clears all 8 type errors with no dependency change. Tool
names and count (23) are unchanged.
This does change the advertised JSON Schema, though. The SDK routes
Zod v3 schemas through the vendored zod-to-json-schema converter, but
Zod v4 schemas through Zod's own native toJSONSchema (see
server/zod-json-schema-compat.js). The two disagree on object schemas:
v4's converter does not emit `additionalProperties: false` where v3's
did. Confirmed live: vector_get_balance (now v4) reports
additionalProperties undefined, while vector_get_agent_profile (still
v3 at this commit) reports additionalProperties: false. Runtime
enforcement is unaffected in both cases — a missing required argument
is still rejected, and extra keys were already stripped under v3 — so
this is a wire-schema difference only, not a validation gap. The two
sibling files (agent-network.ts, self-improvement.ts) still import
plain "zod" (v3) at this commit, so the 23-tool surface is currently
split 11 "open" / 12 "closed" on additionalProperties; a follow-up
commit converts them to zod/v4 to make the surface uniform again and
adds a full-schema snapshot test so this class of regression cannot
land silently in the future.
CI runs test:smoke on every PR but the README only listed test:unit and
test:integration, so a contributor following the README wouldn't know to
run it locally.
This doc ships into a public repo (PR 1's own remit). Strip process
notes that read as an internal artifact rather than architecture:
the Decisions-table row naming who implements/reviews each PR, the
two "needs a funded testnet wallet from Filip" notes (also invites
"so it's untested?" from outside readers), and the instruction to
hold a line on PR #2, which already merged. Renumbered the Open
items list after dropping the wallet entry. The named-reviewer phrase
in section 10 falls in the same category and is genericized for the
same reason, though it wasn't separately called out.
Section 13 (residual risks) and the §12 typecheck findings are
untouched — those are load-bearing technical content, not planning
chatter.
- permissions: contents: read — this workflow only builds and tests,
it never writes to the repo; matches the convention deploy.yml
already sets.
- timeout-minutes: 15 on the job so a hung step doesn't burn runner
time indefinitely.
- concurrency group keyed on the ref so a new push cancels the
in-flight run for the same branch/PR instead of queuing behind it.
Action versions are left unpinned (still @v4) — SHA-pinning only this
workflow while deploy.yml stays on @v4 would be an inconsistency to
clean up in its own pass, not here.
Every PR in the non-custodial-split series is supposed to preserve
`tools/list` returning 23 tools with identical schemas, but nothing
enforced it — PRs 2, 6, 7 and 8 will restructure and rename tool
families, and a snapshot only means something if it's taken before
that starts. Boot the built server, call listTools(), and assert both
the count and the exact sorted name list against a checked-in
baseline captured from a live run on this branch. Failures report
which names were added/removed rather than a bare array-diff.
Reuses startServer()/stopServer() from test/setup.ts; does not call
getMnemonic(), so it needs no wallet. Wired into CI as a new step
after the unit tests. Also raises the server-start timeout in
setup.ts from 15s to 60s — the Cardano WASM import makes cold starts
slow enough that 15s was flaky on the smoke test's first CI run.
test:smoke is intentionally separate from test:unit (which does not
build) since startServer() spawns build/index.js.
The Testing section still told contributors to write a real mnemonic
to mnemonic.txt and run `npm test`, then claimed the result was full
agent-lifecycle coverage against Vector testnet. Since test:unit is
now the default `test` script (five wallet-free CBOR assertions), that
instruction produced a false green and silently skipped the coverage
it advertised. Document test:unit and test:integration separately and
drop the stale VECTOR_MNEMONIC env-var alternative, which setup.ts
never reads.
The repo is public and the referenced fixes are still several PRs out.
Removes the internal audit pointer, the current auth-state confirmation,
the submission failure-mode detail, and the dependency CVE specifics.
Architecture, rationale, and PR sequence are unchanged.
PR #2 shipped 'never stored server-side', which is true but materially
incomplete - the mnemonic is a tool-call parameter, so it transits the MCP
client, the model provider, and hosted process memory. Replaced with an
accurate notice pointing at the architecture doc.