Mutation testing found `token.expires_at <= now` could become `<` with every
test still passing. The suite checked well before and well after expiry, which
cannot tell the two apart — so a token was free to be usable for one more
request than it claimed, which is the wrong direction for a credential.
The daemon now takes an injectable clock, so the boundary can be asserted
exactly rather than raced against wall time: at T-1 the token works, at
exactly T it is refused. Every expiry decision AND every expiry assignment
goes through that clock — a token issued against real time while checked
against test time would never expire, which would have made the test
meaningless. The TOTP timestamp-skew window uses it too, since a clock that
moved one but not the other is a difference no production path should have
either.
Also pins the owner capability: an empty or absent capability is refused, and
so is one of the right length but wrong content — the latter proving the
constant-time comparison is what rejects it rather than the length check.
One survivor is now recorded as genuinely unkillable rather than chased.
`!presented || !this.capability` cannot change behaviour when flipped to `&&`,
because the length comparison below rejects an empty string either way. The
harness learned a `mutation-equivalent:` marker that must carry a REASON, so
future survivor lists mean "look at this" instead of "this again" — and the
judgement stays re-checkable rather than merely trusted.
The harness also gained `--offset`, so a long target is measured in
foreground-sized slices. That is a safety property, not a convenience: this
script writes broken security code into the working tree, and it must never
run unattended alongside commits again.
agent slice 1 (mutants 1-6): 100% of viable mutants caught.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
Commit cf18ab8 included a one-line change to agent.ts that was never intended:
-if (generateFingerprint(mnemonic) !== fingerprint) {
+if (generateFingerprint(mnemonic) === fingerprint) {
That inverts the daemon's `put` check, so it would refuse every CORRECT
fingerprint and accept every wrong one — the exact L-7 hole this branch closed
two days ago, reopened backwards.
Cause: scripts/mutation_test.py deliberately writes broken security code into
the working tree one mutant at a time, and I ran it in the background and then
committed with `git add -A` from another shell while a mutant was live. The
script restored the file correctly when it finished; the mistake was entirely
mine, in committing during the window.
Reverted. The check is back, typecheck is clean, and all 630 tests pass.
The real fix is structural, because "remember not to commit during a mutation
run" is not a control. The script now:
- takes a lock file at start and refuses to run if one exists, so a
previous run that died with a mutant applied is loud rather than silent;
- releases it on success, on failure, and on Ctrl-C;
- VERIFIES after each target that the file it restored actually matches the
original, and aborts loudly if not — a mutant left behind is a broken
security check sitting in the tree waiting to be committed;
- prints a warning at start that the tree contains deliberately broken code.
The lock file is gitignored, so its presence never itself becomes a commit.
Worth stating plainly: this was found by reading `git diff` after a test
failure looked wrong, not by any check. A CI run on that commit would have
caught it — agentSecurity.test.ts fails on that mutation — which is an
argument for the CI wiring done earlier today, not against it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
Approvals scored 58%. Five survivors, all genuine:
- `ttlSeconds` and `uses` validation was unguarded by any test. An approval
issued with ttl 0 is expired the instant it exists; one with uses 0 can
never be consumed. Either would look like a granted authorization to
whoever asked for it and silently authorize nothing — the confusing
failure rather than the safe one.
- The expiry boundary (`<=` vs `<`) was unpinned. It now asserts that an
approval expiring exactly now is refused, and that one millisecond earlier
it still works, so the boundary is where it claims to be rather than
approximately there. Python uses `<=`, so this is parity as well as the
safer direction.
Approvals now 100%.
Sinks scored 67%. The survivor turned `&&` into `||` in the command-spec
parser, which makes a multi-element spec whose FIRST element contains
whitespace get tokenized — silently discarding every element after it. Losing
an argument changes which command runs, and dropping a `--dry-run` or a target
path is exactly the kind of silent change that matters for a sink being handed
a secret. Sinks now 100%.
Separately, the full suite caught a flaky test of my own: the recovery-split
tamper check rewrote the payload's tail and, depending on the payload's
padding, sometimes reproduced the original string exactly — asserting nothing.
It only failed when a randomized share happened to end the wrong way. Now it
flips a character and asserts the result actually differs before expecting the
throw, plus a second case that tampers at four positions rather than trusting
one. Five consecutive runs are clean.
The agent's mapping was also wrong: agent.ts holds the high-risk session, whose
tests live in highRisk.test.ts, so leaving that suite out scored the agent at
38% and blamed the code for a gap that was really a gap in the harness.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
Differential fuzzing can only reach code both implementations have. The
session agent, the API's HTTP layer, high-risk session handling and the sinks
exist only in TypeScript — and that is where the security properties live. For
those the question "would the suite notice if this broke?" has no oracle, so
it has to be answered by breaking things deliberately.
scripts/mutation_test.py does that: it flips comparison and boolean operators
and forces guards to always-allow, but only on lines that decide whether
something is permitted, expired, within a limit, or authentic. Mutating a log
line would produce a survivor that means nothing. Every mutant is typechecked
first, so a mutant that does not compile is reported as invalid rather than
counted as caught, and the original file is restored even on interrupt.
The first run on the API server found two survivors, and they were different
in kind:
**A real gap.** Disabling the rate-limit check entirely left every test
passing — nothing exercised it. Now tested: the limit refuses the request past
its budget, and unauthenticated requests consume budget too, since otherwise
the limit is bypassed by simply not sending a token.
**An equivalent mutant, which pointed at a real hole anyway.** The route-level
`requiresPassword` flag turned out to be fully redundant with the
`requirePassword` call in each handler, so disabling it changed nothing. The
redundancy is worth keeping, but nothing ENFORCED the pairing — so a future
route could set one and forget the other with the suite silent either way.
Two structural tests now pin it.
Writing the second of those tests immediately found a security regression I
had introduced: **`GET /api/v1/totp` returned live TOTP codes on a bearer
token alone.** Python gates that endpoint on the master password
(`_require_password` in api.py) and this port did not, so a leaked token
yielded working second factors for every TOTP entry in the vault. Now gated,
and tested for.
Server mutation score: 88% of viable mutants caught, the single survivor being
the equivalent one above.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN