Commit Graph
9 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 5 7f6edf10cc fix(agent): close two ways a local caller could subvert the session daemon
Both were found by the 2026-08-17 self-review and confirmed by speaking to
the socket directly. The daemon is the enforcement point for the whole token
model and treats every caller as untrusted, so neither fix leans on the CLI's
own validation.

1. A `put` reported as FAILED could leave the seed resident and unexpiring.
   State was committed to `held` before the audit append was awaited. A
   non-numeric ttl made `expiresAt` NaN; the audit write then threw on the
   non-finite value, so the caller saw ok:false while the seed stayed in
   memory forever (`NaN <= now` is always false) with no record it was ever
   unlocked. Now ttl is validated as a positive integer at the boundary, and
   an audit failure rolls the seed back out — ok:false means genuinely locked.

2. A `use`-scoped token could crash the whole agent. `stdinSink` wrote to the
   child's stdin with no error listener, so a command that exits before
   draining raised an unhandled EPIPE; the agent runs sinks in-process and
   installs no uncaughtException handler, so it exited and dropped every held
   seed. The least-privileged principal could take down the enforcement point
   by naming a command that exits early. stdin errors are now swallowed and
   the child's exit code carries the result, as feedClipboardTool already did.

Also hardens token-issue: scopes/kinds are shape-checked before the array
methods run against them (a bare-string kinds turned an exact match into a
substring match), and ttl/uses go through the same positive-integer boundary
so a token can no longer be minted with a NaN, never-expiring lifetime.

Three regression tests speak the wire protocol directly. Verified as negative
controls: with the fixes stashed, the stdin test reproduces the EPIPE uncaught
exception at sinks.ts:143 and two of the three fail; with them restored all
131 CLI tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-17 22:15:52 -04:00
thePR0M3TH3ANandClaude Opus 5 089d6cdb96 docs: record the TypeScript port's open work and self-review findings
Adds a TypeScript-port section to TODO.md covering three things the branch
was carrying only in conversation: the open blockers, ten findings from a
read-and-probe review of the branch, and the milestones nobody has started.

The findings matter more than their count. Every one was found by reading
the code or speaking to the agent socket directly, and all 128 CLI tests
passed before and after each was confirmed, so none of them is a
regression -- they are places the suite never looked. Two are worth calling
out here: a put the agent reports as FAILED can leave the parent seed
resident and unexpiring with no audit record, and a use-scoped token
holder can kill the agent with a stdin sink that exits early, dropping
every held seed. Both live in the daemon, which is the enforcement point
for the whole token model.

Three others are settings that do nothing: clipboard_clear_delay,
kdf_iterations and inactivity_timeout are all stored, displayed and
ignored. Python enforces the first and third. A control that silently
does nothing is worse than an absent one, because it stops people asking.

Also records the decision still owed on the api surface, which is
currently excluded from cutover and has no TypeScript equivalent beyond
the session agent.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-17 21:37:55 -04:00
thePR0M3TH3ANandClaude Opus 5 3192c9f306 Offer a 24-word master seed, and put the entropy draw beyond reach of any handler
L2: generate_bip85_seed and create_profile_from_generated_seed now take
words_num, accepting 12 or 24 and rejecting anything else with a clear
message. The default stays 12 -- every existing profile uses it, 128 bits is
entirely adequate, and moving the default would change nothing about
existing vaults while surprising anyone creating a new one. 24 is offered
because the master seed protects the whole vault and was the one secret
shorter than the seeds derived beneath it, which already default to 24.

L3: fixed structurally rather than by narrowing exception types.
os.urandom(32) now sits OUTSIDE the try block, so no handler can stand
between the entropy draw and the caller -- an entropy failure is unreachable
by anything that could substitute a value. Also removes a redundant
`except Exception` whose body was identical to the `Bip85Error` branch above
it.

The structural form is stronger than an exception list, and the test says so
precisely: it asserts the OSError reaching the caller is the SAME OBJECT
os.urandom threw, which holds only if nothing caught and repackaged it.
Listing exception types would still leave a handler in the path for someone
to widen later.

Adds five tests: the identity assertion above, 12- and 24-word generation,
the default staying 12, and rejection of 0/11/18/25/-12.

Determinism gate green (89 tests, up from 84). The seed/profile regression
subset is 143 passed with one failure --
test_profile_export_import_round_trip -- confirmed against the pre-change
baseline as pre-existing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:43:24 -04:00
thePR0M3TH3ANandClaude Opus 5 6ac40d2e15 TODO: the versioning constraint now describes what shipped
The intro still said entries carry no algorithm version, which stopped being
true when gen_version landed. Restates the rule in its permanent form -- never
alter the output of a version already in the wild -- rather than as a
description of a gap that is now closed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:35:06 -04:00
thePR0M3TH3ANandClaude Opus 5 7047a5dfed Add an SBOM, and fix the torch randomness nits (L4, L5)
L5: dependency-audit.yml now generates a CycloneDX SBOM from
requirements.lock and uploads it. pip-audit answers "are there known CVEs
today"; an SBOM answers "which exact versions were in this build", which is
the question that arises when something is disclosed later against a release
already in the wild.

Verified against the real tool rather than written from memory -- the first
flag spelling (--output-format / --outfile) does not exist in cyclonedx-py
and would have failed in CI. The working form is
`--of json --output-reproducible -o <file>`, which emits CycloneDX 1.6 with
96 components and records [email protected], [email protected],
[email protected], [email protected], [email protected], [email protected] and
[email protected].0 at exact versions.

L4: torch/services/memory/index.js built its temp filename from Date.now()
plus Math.random() before an atomic rename -- predictable enough to
pre-create or symlink between the write and the rename. Now randomBytes(8).

Also renames the Math.random-derived value in relay-health.mjs from `nonce`
to `probeSuffix` and draws it from randomBytes(6). It is a public probe tag
used to correlate a publish with its readback, not a cryptographic nonce,
and nothing was wrong with it -- but the name invited someone to copy the
pattern somewhere it would matter.

torch has no test/ directory in the repo, so there is no suite to run for
these two files; both were syntax-checked and confirmed to import cleanly.
The SeedPass determinism gate is green (84 tests).

Not done, and recorded as a decision in TODO.md instead: torch/_backups/
holds 5240 tracked files across six dated snapshots. Untracking them is a
large deletion and not something to do as a side effect of an entropy audit.
They currently duplicate every security-relevant grep hit, and bare
`node --test` in torch/ discovers them and fails 18 tests from stale
snapshot code.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:34:45 -04:00
thePR0M3TH3ANandClaude Opus 5 2aed95a687 Version password generation; freeze v1, add v2 without the class quota
Passwords are never stored -- they are re-derived on demand from
(seed, index, length, policy) -- and entries carried no algorithm version.
Any change to generate_password therefore changed every existing password in
every vault, unrecoverably. This adds the version field that makes such
changes possible at all, then makes three.

Safety net first (M4). test_entropy_integrity.py freezes 18 v1 vectors
spanning every policy and both sides of the 32-byte stream-wrap boundary,
and asserts the RNG fails closed: os.urandom raising must abort seed
generation, short reads must abort, AES-GCM nonces must be fresh. Each guard
was mutation-verified rather than assumed -- removing _balance_distribution,
perturbing the stream wrap, or altering the character mapping each turns all
18 vectors red, and adding a hash-of-timestamp fallback to
generate_bip85_seed turns the fail-closed test red.

generate_password now takes gen_version, defaulting to 1. Existing entries
carry no field and keep deriving v1 byte-identically; the argument is only
forwarded when non-legacy, so all 13 call sites across CLI, API and both
TUIs are untouched. New password entries are stamped gen_version 2. An
unreadable version raises rather than guessing which algorithm produced a
password.

v2 fixes three findings:
- M1: drops the forced equal-quarters class quota. 1 -> 82 distinct class
  compositions at length 16 over 200 samples; per-character entropy
  6.42 -> 6.53 bits against a 6.555 ceiling.
- M2: rejection sampling instead of byte % len(alphabet). Chi-square 96.1
  against df=93 (p<0.001 critical value ~147).
- M3: an unbounded HMAC-expanded stream instead of cycling a 32-byte key,
  which was a repeating pad for passwords over 32 characters. Lag-32 repeat
  rate now sits within one standard error of chance.

Dropping the quota removed an accidental backstop: v1's minimum enforcement
overwrites uniformly random positions, so raising one class minimum could
destroy another, and only the quota's near-total rewrite hid it. v2 chooses
donor positions solely from classes that currently have a surplus, and
rejects a policy whose minima cannot fit the requested length.

Also removes a dead HKDF derivation whose result was discarded while the
docstrings named it as the algorithm in use (L1); PBKDF2 was and remains the
real one. Verified as a negative control -- the vectors stayed green.

Six test doubles are updated to the real generate_password signature; they
began receiving gen_version once new entries carried it.

Existing entries stay on v1 by design. An opt-in per-entry upgrade, with a
confirmation making the consequence unmissable, is still to come.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 16:27:23 -04:00
thePR0M3TH3AN 949d87dd4f docs: add supply-chain verification and operational runbooks 2026-03-02 09:14:00 -05:00
thePR0M3TH3AN 88c1974ac6 Harden TUI recovery paths and add release integrity gates 2026-02-25 19:18:54 -05:00
thePR0M3TH3AN 3ea37865cb Add landing install copy UX and document remaining security/test TODOs 2026-02-23 16:33:57 -05:00