Commit Graph
6 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 5 73ac93a9f1 Deliver the oversize refusal before hanging up, and pin a POSIX-only test
Two left from the platform run, one real.

THE DAEMON HUNG UP WITHOUT SAYING WHY, ON macOS ONLY

The line-length cap did `socket.write(refusal)` and then `socket.destroy()`.
destroy() tears the socket down immediately and discards whatever is still
buffered — on Linux the write usually reaches the kernel first and the peer
sees "request too large", on macOS it does not and the peer sees an
unexplained close. The refusal itself worked on both; only the reason was
lost, and only on one platform, which is why nothing noticed until this suite
ran there.

Now `end(payload, cb)` and destroy from the callback, so the bytes are
flushed before teardown, plus a `refused` flag so a peer still pushing the
rest of its megabyte is ignored rather than re-buffered.

The test that caught it was itself half the problem: it treated its own EPIPE
as fatal, when a client's broken pipe is the expected consequence of a server
correctly hanging up mid-write. Fixed in the previous commit; this one fixes
what that then exposed.

AND ONE TEST ASSERTING POSIX BEHAVIOUR WITHOUT SAYING SO

`honours backslash escapes outside single quotes` called parseCommandSpec with
no platform flag, so it read process.platform and asserted POSIX semantics on
Windows — where backslash is a path separator and deliberately is not an
escape. It now passes `{ windows: false }`, because that is the branch it is
about.

Windows went 15 failures to 1 to 0; macOS 2 to 1 to 0.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 14:47:24 -04:00
thePR0M3TH3ANandClaude Opus 5 e2e9aa851e Fix what three platforms found: two shipped bugs, four unportable tests
The platform workflow's first run produced seventeen failures on Windows and
macOS, where the JavaScript suite had never executed. Two were real defects
in shipped code.

A CONTAINMENT CHECK THAT REFUSED EVERYTHING ON WINDOWS

`resolveWithinProfile` compared `target.startsWith(root + "/")` with a
hardcoded forward slash. On Windows `resolve()` returns
`C:\profile\file.txt` and the comparison asked for `C:\profile/`, so it
matched nothing: document export and import refused every path, including
legitimate ones. It failed CLOSED, so nothing leaked — the feature simply did
not work. Now uses `relative()`, which needs no separator of our choosing.

A COMMAND PARSER THAT ATE WINDOWS PATHS

`parseCommandSpec` treated backslash as a POSIX escape unconditionally, so
`--exec "C:\Program Files\tool.exe"` was spawned as the command `C:Program`
with an argument `Filestool.exe`. Every Windows path handed to --exec or
--stdin-to was mangled before the spawn. This is the SECOND instance of this
exact bug today — the Python TUI palette had it too — which is what a whole
class looks like rather than a one-off.

`windows` is an explicit parameter, not a `process.platform` read, so both
branches are tested from either platform. That parameter is the actual fix
for the class: the reason these survived is that the Windows branch could
only ever run on Windows.

THE REST WERE TESTS, NOT CODE

- `/bin/true` does not exist on macOS (it is /usr/bin/true), so a spawn failed
  with ENOENT and the refusal looked like the daemon's crash-safety fix having
  regressed. Now uses process.execPath, which exists everywhere by definition.
- The oversized-request test treated its own EPIPE as fatal. The daemon caps
  the line, answers and hangs up; on macOS that hangup lands while the client
  is still pushing the remaining megabyte. Correct server behaviour was
  failing the test on one platform and passing on the other.
- Three sink tests wrote `#!/bin/sh` files. Shebangs and /bin/sh do not exist
  on Windows. Now Node scripts run by the interpreter already running the
  tests.
- Eight `0600` assertions and two clipboard tests are guarded by documented
  constants in test/helpers/platform.ts. Windows has no POSIX mode bits and
  the runner has no clipboard.

The skips state a gap rather than closing it, and TODO.md now records that the
Windows permissions question grew from two Python tests to ten across both
languages — including the vault index and the semantic index, which hold
secrets. With TypeScript becoming the only implementation that is a shipping
question, not a test-suite one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 14:39:52 -04:00
thePR0M3TH3ANandClaude Opus 5 e1ea9c2b48 Teach the mutation harness two blind spots it had, and close what they found
The harness only mutated block-form guards (`if (cond) {`), so every guard
written as a one-liner was invisible to it — which turned out to be most of
the load-bearing ones. src/highRisk.ts generated ZERO mutants and printed as
a clean sweep. Three changes to the tool:

- Mutate single-statement guards too (`if (cond) return ...;`). This alone
  surfaced 27 previously unreachable mutants, including the entire token
  validation chain: revoked, expired, exhausted, scope-not-granted.
- Report a target that generates no mutants as NOT MEASURED. Zero killed and
  zero survived prints exactly like a clean sweep, and it is the one result
  that must never be read as a pass.
- Add `kind`, `mnemonic` and `seed` to the security hints. A token's `kinds`
  list is half of what it may reach, and leaving the word out hid the shape
  check that stops a string `kinds` becoming a substring match.

Also adds `--kind` so a new mutation rule can be swept across every target
without re-judging the mutants already settled.

What the new mutants found, all of it previously unasserted:

- A token's `kinds` restriction was never tested. This file's own header
  claimed kinds and labels were both covered; only labels were. Removing the
  check let a token scoped to one kind read every other kind's secrets.
- `token-issue` would mint a token for a profile the agent is not holding.
  The seed being resident is the only proof the caller had authority over
  that vault.
- `token-issue`'s array shape checks for `scopes` and `kinds`. A string
  `kinds` turns tokenMaySee's exact match into String.includes.
- An empty high-risk factor. Refused in both implementations, tested in
  neither; it would wrap the partition key under nothing while
  `factorConfigured` still answered true.
- POST /api/v1/fingerprint/select accepting a profile that does not exist —
  the caller's string becomes ctx.fingerprint, which later requests join onto
  a path. AppDir repeats the check, so the guard's real value is answering
  404 instead of an unhandled 500.
- The API's requiresPassword flag is redundant for every route today, so
  disabling it changed nothing. Its purpose is the route that does not exist
  yet, whose author sets the flag and forgets the call — so the test now
  registers exactly that route and confirms the server layer stops it before
  the handler runs.
- sinks.ts drops SEEDPASS_-prefixed variables from a child's environment.
  Nothing on the allowlist starts with SEEDPASS_, so the backstop cannot fire
  against the real list. sinkEnv now takes the allowlist as a defaulted
  parameter purely so that future-edit case can be exercised.

All six targets now sweep to zero survivors.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 18:19:15 -04:00
thePR0M3TH3ANandClaude Opus 5 cf18ab8091 test: close the gaps mutation testing found in approvals, sinks, and a flaky test
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
2026-08-19 13:41:55 -04:00
thePR0M3TH3ANandClaude Opus 5 135a6268ce feat(tui): actually clear the clipboard after the configured delay
clipboard_clear_delay has been in the default config (45s), settable, and
read by nothing: clipboardSink copied and returned. That made TUI Secret
Mode -- whose entire purpose is to route secrets off the screen -- strictly
less safe than Python's, because it parked them on a session-wide clipboard
indefinitely.

clipboardSink now takes an optional clearAfterSeconds. When set, it schedules
a wipe after the delay, but only if the clipboard still holds the value it
put there, so a value the user copied in the meantime is left alone -- the
same guard Python's copy_to_clipboard(text, timeout) applies via a read-back.
Where the clipboard cannot be read (no paste tool), it clears unconditionally,
because leaving a secret parked is the worse outcome. The timer is unref'd:
it fires while the long-running TUI is up but never holds a process open, so
the one-shot CLI and the agent are unchanged (they pass no delay).

Both TUI clipboard paths -- Secret Mode reveals and the entry 'C' action --
now route through the profile's clipboard_clear_delay.

Tests use a fake wl-copy/wl-paste pair on PATH: the secret is wiped after the
delay, and a value copied over it in the meantime survives.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-17 22:22:12 -04:00
thePR0M3TH3ANandClaude Fable 5 b0e2a309a4 feat: generate a master seed, and fix three bugs found by using the CLI
Driving the CLI from an empty app directory turned up a gap that no test
covered: there was no way to create a vault. Every path assumed you already
had a seed and told you to export SEEDPASS_MNEMONIC, so a new user could not
get started at all. Python has generate_bip85_seed; the port did not.

fingerprint create fills that in with the same construction Python uses --
32 bytes of OS entropy through BIP-85 app 39 index 0. The entropy call sits
outside any try/catch on purpose, carried over from Python's entropy audit:
nothing may catch a CSPRNG failure and substitute a value.

Where the phrase goes is a deliberate choice rather than a default. This is
the one secret in SeedPass with no other copy, so:

  - the destination is validated BEFORE generating, and delivery happens
    BEFORE the profile is created -- a failure at either point leaves
    nothing behind rather than a vault whose seed nobody has;
  - --out writes 0600 with O_EXCL, so an existing file is never clobbered;
  - with no destination and a non-TTY stdout it refuses outright, instead of
    writing the only copy of a seed into a pipe -- a log, a transcript, an
    agent's context.

Also fixed, all found the same way:

  - use --exec always exited 0, so chaining a deploy command after it with
    && reported success even when the deploy failed. The sink child's exit
    status is now the CLI's own.
  - the sink command spec split on whitespace and ignored quotes, so
    --exec 'sh -c "..."' -- the most common form there is -- handed sh the
    fragments of the script and died on an unterminated string.
  - --tags work,dev silently stored a single tag named "work,dev" that no
    search would ever match. Commas and spaces both separate now.

Two normal states reported raw errno: a stopped agent said
'connect ENOENT /.../agent.sock', and an occupied --out path said EEXIST.

The capability map claimed ssh/pgp were unported. Both work and are at
parity with Python; automation branches on that list, so it now describes
this build.

Verified against Python on a TS-generated seed: same fingerprint, same
password, same TOTP code. 320 JS tests and 36/36 cross-impl checks pass.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-15 14:35:39 -04:00