mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-02 02:42:04 +00:00
Four fixed outright (M-1, M-2, L-4, L-7, L-8), one mitigated with the derivation change deliberately deferred (L-1), two surfaced rather than changed because their resolution is frozen for parity (L-2, L-3), and two resolved as documented-behaviour with the reasoning written down (L-5, L-6). The L-1 v2 design is now written up in TODO.md rather than left as a one-line recommendation: keep v1 frozen, add derivation_version alongside the existing gen_version pattern, domain-separate with an HMAC step rather than new BIP-85 app numbers, land both implementations together with cross-impl fixtures, and make the upgrade opt-in per entry. Sequenced after the outside review on purpose. Also recorded: three defects found while fixing, none of which the audit had reached — Python's publish_delta manifest republish, the fetch-limit downgrade path, and merge_index_payloads mutating its argument. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
353 lines
23 KiB
Markdown
353 lines
23 KiB
Markdown
# SeedPass TypeScript Port — Security Audit
|
|
|
|
- **Date:** 2026-08-18
|
|
- **Auditor:** Claude (Fable 5), full manual source review
|
|
- **Scope:** the TypeScript port only — `js/packages/core`, `js/packages/cli`, build/CI supply chain
|
|
- **Commit audited:** `e9d8908` (`port/typescript-web-extension`), clean working tree
|
|
- **Method:** line-by-line read of every non-test source file (~7,400 lines), cross-checked
|
|
against the Python reference where parity claims matter, plus a full test-suite run as a
|
|
baseline probe. No edits were made to the repository other than this report.
|
|
|
|
## Verdict
|
|
|
|
**No critical or high-severity vulnerabilities were found.** The cryptographic core, the
|
|
agent trust boundary, the relay sync transport, and the filesystem layer are unusually
|
|
well-engineered, with prior audit cycles visibly absorbed into the code. The findings below
|
|
are two medium-severity correctness/data-loss issues (one with a reproducible failing test),
|
|
a cluster of low-severity hardening and parity gaps, and informational notes. Nothing found
|
|
here allows secret extraction by an attacker who does not already hold the seed, the master
|
|
password, or same-uid code execution (the documented trust boundary).
|
|
|
|
Baseline at this commit: `pnpm -r typecheck` clean; **391 of 392 tests pass**. The single
|
|
failure is flaky and is the direct evidence for finding M-1.
|
|
|
|
---
|
|
|
|
## Findings
|
|
|
|
### M-1 (Medium) — Restore can non-deterministically return a stale snapshot when two syncs share a second — **FIXED 2026-08-19**
|
|
|
|
- **Where:** `js/packages/core/src/sync/syncFlows.ts:115-117` (`fetchLatestSnapshot` sort)
|
|
- **What:** Manifests are ordered by `created_at` (Nostr grants only whole-second
|
|
resolution) with the event id as tie-break. The id tie-break is stable and
|
|
relay-independent — but it is *arbitrary with respect to time*. Two snapshots published
|
|
within the same second tie on `created_at`, and the older one wins the id comparison
|
|
roughly half the time. `nostr restore` then silently restores the older vault state;
|
|
entries created between the two syncs disappear with no warning
|
|
(`skippedNewerManifests` stays empty because nothing was *skipped* — the wrong manifest
|
|
simply sorted first).
|
|
- **Evidence:** `js/packages/cli/test/nostr.test.ts` — "syncs the vault to the relay and
|
|
restores after local destruction" fails intermittently (reproduced 1 in 4 runs on this
|
|
machine: the restore returned only the earlier test's `pre-sync` entry). This flake is
|
|
not the environment; it is the bug.
|
|
- **Impact:** Silent loss of recent entries on restore. Most likely in scripted or
|
|
automated sync flows that publish twice quickly; also gives a relay a small window to
|
|
serve the older of two same-second snapshots with full deniability.
|
|
- **Recommendation:** Add a monotonic sequence number inside the (signed) manifest JSON and
|
|
use it as the secondary sort key; alternatively detect a `created_at` tie among
|
|
assemblable manifests and either merge both or surface the ambiguity to the caller. The
|
|
fix must land on the Python side too, or the two implementations will disagree on which
|
|
snapshot is "latest."
|
|
|
|
### M-2 (Medium) — Config-level password policy is ignored; same entry derives different passwords in TS vs Python — **FIXED 2026-08-18**
|
|
|
|
- **Where:** `js/packages/core/src/derive/password.ts:53-64` (`resolvePolicy` hardcodes
|
|
defaults); `js/packages/cli/src/secrets.ts:43-49` (only the entry's own `policy` block is
|
|
passed).
|
|
- **What:** Python builds its `PasswordGenerator` with
|
|
`policy=self.config_manager.get_password_policy()` (`src/seedpass/core/manager.py:2033`)
|
|
and merges the entry's `policy` block *over that config base*
|
|
(`manager.py:3167-3201`). The TS port merges the entry block over hardcoded defaults
|
|
(`min_* = 2`, specials on). The per-profile config keys (`min_uppercase`, `min_digits`,
|
|
…) exist in the TS config (`configFile.ts` mirrors them) but are never read by
|
|
derivation.
|
|
- **Impact:** Any Python profile whose config policy differs from the defaults derives
|
|
**different passwords** in the TS port for entries that carry no explicit policy block.
|
|
To the user this presents as wrong passwords / apparent data loss after migration. Users
|
|
with default config are unaffected.
|
|
- **Recommendation:** Thread the profile config's policy fields into
|
|
`materializeSecret`/`generatePassword` as the base policy before applying the entry's
|
|
overrides, and add a cross-implementation fixture with a non-default config policy —
|
|
the current parity suite does not cover this case, which is why it passed.
|
|
- **Fix (2026-08-18):** `materializeSecret` takes a `basePolicy` option and merges the
|
|
entry's block over it. All three callers supply the profile config's policy:
|
|
`program.ts` via `basePolicyForOptions` (which reads the config beside the index when
|
|
`--vault` names one directly), `agent.ts` via `loadConfig(join(appDir, fingerprint))`,
|
|
and the TUI via a new `sessionSecret` helper that every menu path now goes through, so
|
|
a future call site cannot silently omit the base. `util generate-password` is
|
|
deliberately unchanged and now documents itself as profile-less: it has no entry and no
|
|
profile, so its flags are the whole policy.
|
|
- **Evidence:** `js/packages/cli/test/passwordPolicy.test.ts` — four cases against values
|
|
computed by the Python implementation (config-only v2, entry-overrides-one-field v2,
|
|
config-only v1, and exclude_ambiguous), each also asserted *unequal* to the value the
|
|
pre-fix code produced. Mutation-verified: dropping `basePolicy` from the merge in
|
|
`secrets.ts` turns all four red.
|
|
|
|
### L-1 (Low) — BIP-85 app-32 path is shared across password, SSH, and PGP derivation — **MITIGATED 2026-08-19 (detection shipped; derivation change deferred)**
|
|
|
|
- **Where:** `derive/ssh.ts:47-49`, `derive/pgp.ts:171-175`, `derive/password.ts:175` — all
|
|
three derive from `m/83696968'/32'/{index}'`.
|
|
- **What (verified end-to-end):**
|
|
- `ssh@N` and `pgp@N` take the first 32 bytes of the same HMAC-SHA512 digest → **the
|
|
same Ed25519 private key**, byte for byte.
|
|
- `password@N` takes 64 bytes of that digest → the same-index SSH/PGP private key **is
|
|
the first half of the password's input entropy** (the password itself remains safe:
|
|
256 unknown bits survive, and PBKDF2 is one-way — but this violates key-separation
|
|
hygiene, and SSH keys are exactly the kind of secret that gets exported and used).
|
|
- **Reachability:** The TS creation path cannot produce the collision — an entry's
|
|
derivation index equals its vault id (`entryOps.ts:270/275, 418/423`), ids are unique
|
|
(`insert` throws on overwrite), and the persisted watermark (`e3b1325`) prevents id
|
|
reuse. It **is** reachable through data: the schema accepts any nonnegative `index` on
|
|
ssh/pgp entries (`schema/entries.ts:78-106`), so Python-created vaults, imported
|
|
backups, and sync merges can carry `ssh@N` and `pgp@N` with equal indices, and the TS
|
|
port will faithfully derive identical keys.
|
|
- **Status:** The ssh/pgp collision is a documented known limit shared with Python
|
|
(`docs/agent_security_model.md`). The password-entropy-prefix relationship extends that
|
|
documented finding and does not appear in the docs.
|
|
- **Recommendation:** Protocol-level fix (both implementations, versioned): domain-separate
|
|
the three uses — different app numbers or an HKDF step keyed by entry kind. Short term:
|
|
document the password-prefix relationship alongside the existing known limit, and warn
|
|
on import/merge when two entries of different kinds share an app-32 index.
|
|
|
|
### L-2 (Low) — Concurrent same-id creation on two replicas silently discards one entry at merge — **SURFACED 2026-08-19**
|
|
|
|
- **Where:** `js/packages/core/src/sync/merge.ts:347-365` (per-id last-writer-wins).
|
|
- **What:** The allocation watermark is per-replica. Two devices working offline can both
|
|
allocate id N; the merge resolves per id by timestamp-then-hash, and the losing entry is
|
|
silently replaced — no conflict is recorded or surfaced. Because an id is a permanent
|
|
BIP-85 derivation coordinate, the surviving entry of a different kind may derive related
|
|
key material (see L-1).
|
|
- **Impact:** Silent entry loss under a realistic multi-device workflow. Inherent to the
|
|
deterministic-CRDT design and matched by Python (parity-frozen), so this is a design
|
|
finding, not a divergence.
|
|
- **Recommendation:** Surface it: the merge already computes everything needed to detect
|
|
"both sides created different-kind entries at the same id" — report those ids in the
|
|
sync/restore summary instead of resolving silently. A protocol-level fix (replica-scoped
|
|
id ranges or random ids) would need coordination with Python.
|
|
|
|
### L-3 (Low) — Tombstone retention cap enables deletion replay (documented; confirmed) — **SURFACED 2026-08-19**
|
|
|
|
- **Where:** `merge.ts:17, 405-416` — `TOMBSTONE_RETENTION_CAP = 2048`, oldest evicted.
|
|
- **What:** After 2048 deletions, older tombstones are dropped; merging a stale replica or
|
|
an old relay snapshot then resurrects entries the user deleted. Documented as a known
|
|
protocol limit shared with Python; confirmed present and correctly implemented as
|
|
specified.
|
|
- **Recommendation:** None beyond the docs' own roadmap; consider counting evictions and
|
|
warning the user when the cap has actually trimmed history.
|
|
|
|
### L-4 (Low) — Inert settings: the TUI confirms behavior that does not exist — **FIXED 2026-08-18**
|
|
|
|
- **Where:** `tui/menus.ts:1482-1489` (`additional_backup_path` — the TUI replies
|
|
"Additional backups will be written to X" but no code path ever writes a backup there);
|
|
`tui/menus.ts:1012` (`quick_unlock_enabled` — a toggle with no effect); also stored but
|
|
unused: `backup_interval`, `pin_hash`, `nostr_max_retries`, `nostr_retry_delay`
|
|
(`configFile.ts:26-47`).
|
|
- **Impact:** `additional_backup_path` is the harmful one: a user can reasonably believe a
|
|
second copy of their vault exists when it does not — false assurance in a disaster-
|
|
recovery feature. The rest are cosmetic.
|
|
- **Note:** This class of bug is known here — commits `135a626`, `933cad3`, `a698255`
|
|
fixed three other inert settings. These are the stragglers.
|
|
- **Recommendation:** Either implement them or have the menu say plainly "not available in
|
|
this build," the pattern already used for QR codes and the semantic index.
|
|
|
|
### L-5 (Low) — Token `label_regex` is unanchored: substring semantics widen token scope — **RESOLVED 2026-08-18 (documented; matches Python)**
|
|
|
|
- **Where:** `agent.ts:248-255` (`tokenMaySee` uses bare `RegExp.test`); surfaced at
|
|
`program.ts:1318` (`--label-regex`).
|
|
- **What:** A token issued with `--label-regex prod` matches every label *containing*
|
|
"prod" — including `not-prod-db`. The operator issuing the token is the one most likely
|
|
to be surprised. (Owner-supplied pathological regexes are also unbounded, but the owner
|
|
can only ReDoS their own daemon — noted, not scored.)
|
|
- **Recommendation:** Anchor the pattern (`^(?:${re})$`) or document substring semantics
|
|
loudly in `--label-regex` help and `capabilities()`. Match Python's behavior, whichever
|
|
it is, and add it to the parity matrix.
|
|
|
|
### L-6 (Low) — Exec allowlist constrains the command word, not its arguments — **DOCUMENTED 2026-08-18**
|
|
|
|
- **Where:** `agent.ts:337-349`; execution in `sinks.ts:102-121`.
|
|
- **What:** `exec_allowlist` compares only the first token. A use-scoped holder may pass
|
|
arbitrary arguments to an allowlisted binary. Sink children inherit the *daemon's*
|
|
stdio, not the caller's, and get a scrubbed env — good — but the child's 1-bit exit code
|
|
is returned per use, and an argument-flexible binary (anything with an output-file or
|
|
network flag) can move `SEEDPASS_SECRET` somewhere the token holder can read.
|
|
- **Status:** The docs already state "use can become reveal without an exec allowlist";
|
|
this finding is that the allowlist's containment is weaker than an operator would
|
|
assume even *with* it.
|
|
- **Recommendation:** Document that allowlisted binaries must be chosen as if the token
|
|
holder controls their argv; longer term, allow full argv templates in the allowlist
|
|
(`["ssh-add", "-"]`) rather than bare command words.
|
|
|
|
### L-7 (Low) — Daemon uses wire-supplied `fingerprint` as a path component without format validation — **FIXED 2026-08-18**
|
|
|
|
- **Where:** `agent.ts:209, 298, 554` (`join(appDir, fingerprint, ...)`); `put` at
|
|
`agent.ts:633-652` accepts any string.
|
|
- **What:** The CLI layer validates fingerprints (`appDir.ts:82-92`,
|
|
`/^[0-9A-F]{16}$/`) before they touch a path; the daemon does not. Every reaching path
|
|
first requires the fingerprint to be in `held`, and only the owner-gated `put` populates
|
|
`held` — so exploitation requires the owner capability, i.e. an attacker who has already
|
|
won under the documented same-uid model. Defense-in-depth gap only.
|
|
- **Recommendation:** Call `assertValidFingerprint` in the daemon's `put` handler and also
|
|
verify `generateFingerprint(mnemonic) === fingerprint`, which additionally catches an
|
|
honest mismatch corrupting the audit-log location.
|
|
|
|
### L-8 (Low) — Plaintext backup import is not bound to a profile — **FIXED 2026-08-18**
|
|
|
|
- **Where:** `program.ts:1549-1606` (`vault import`), `tui/menus.ts:1432-1459`
|
|
(`importDatabase`); wrapper schema in `core/src/vault/portableBackup.ts`.
|
|
- **What:** Encrypted (`seed-only`) backups are implicitly seed-bound — decryption fails
|
|
under the wrong profile. Plaintext (`encryption_mode: "none"`) backups carry a
|
|
`fingerprint` field that is **never compared** to the target profile. A plaintext backup
|
|
of vault A imports cleanly into profile B; every derived entry (passwords, SSH, PGP,
|
|
seeds) then silently produces *different* secrets than the originals.
|
|
- **Also:** the CLI detects encryption mode with a regex over the raw file text
|
|
(`program.ts:1560`) rather than parsing the JSON. It is not exploitable (JSON string
|
|
escaping prevents a false match), but it is fragile; parse the wrapper instead.
|
|
- **Recommendation:** Compare `wrapper.fingerprint` to the target profile's fingerprint
|
|
and require an explicit override on mismatch, in both the CLI and TUI paths.
|
|
|
|
---
|
|
|
|
## Informational notes
|
|
|
|
- **I-1 — Agent socket framing decodes chunks independently.** `agent.ts:699-723` builds
|
|
the line buffer with per-chunk `toString("utf8")`; a multibyte UTF-8 character split
|
|
across TCP chunks corrupts that request (non-ASCII labels are legal). Use
|
|
`string_decoder`. Separately, pipelined requests on one connection get replies in
|
|
completion order with no request ids; the bundled `AgentClient` opens one connection per
|
|
request and is unaffected, but a future client (the planned browser extension) could
|
|
misattribute replies. Consider serializing per-connection handling or adding ids.
|
|
- **I-2 — v1 password generation has modulo bias** (`password.ts:179-183`,
|
|
`byte % alphabet.length` over a ~94-char alphabet). Parity-frozen forever by design; v2
|
|
fixed it with rejection sampling. Worth stating in the security docs so it is a known
|
|
property, not a rediscovery.
|
|
- **I-3 — Fingerprint normalization is stronger than Python's.** `fingerprint.ts` hashes
|
|
the fully canonicalized mnemonic (NFKD, internal whitespace collapsed); Python's
|
|
`fingerprint.py` uses `strip().lower()` only. A phrase with doubled internal spaces
|
|
produces *different profile identities* across implementations. Edge case; add a parity
|
|
fixture or align Python.
|
|
- **I-4 — `token-issue` coerces `name` and `label_regex` with `String()`**
|
|
(`agent.ts:498, 503`) while other wire fields are shape-checked; an array arrives as
|
|
`"a,b"`. Harmless today; shape-check for consistency.
|
|
- **I-5 — Token lookup compares secret hashes non-constant-time** (`agent.ts:233-236`).
|
|
Comparing SHA-256 digests of a high-entropy secret; not practically exploitable. The
|
|
owner capability check does use `timingSafeEqual`.
|
|
- **I-6 — TOTP index derivation from imported data:** a malformed `index` on an existing
|
|
totp entry makes `nextTotpIndex` return NaN and the subsequent derivation throw
|
|
(`entryOps.ts:117-123`) — a loud failure, not a corruption; noted only.
|
|
|
|
---
|
|
|
|
## What was checked and found sound
|
|
|
|
The following areas were read in full and are called out because they are load-bearing —
|
|
and correct:
|
|
|
|
- **AEAD/vault crypto** (`aead.ts`, `payload.ts`, `fernet.ts`): AES-256-GCM with fresh
|
|
random 12-byte nonces per call; V2/legacy Fernet decrypt-only with HMAC verified before
|
|
decryption using a constant-time compare (which also forecloses a CBC padding oracle);
|
|
format dispatch mirrors Python exactly.
|
|
- **Key derivation** (`indexKey.ts`, `passwordKdf.ts`, `bip85.ts`): HKDF-SHA256 with
|
|
domain separation; KDF parameters read from disk are bounded (`KDF_LIMITS`) so a
|
|
tampered kdf block cannot hang the process or allocate gigabytes; mnemonics are
|
|
canonicalized before both validation and derivation; profile KDF default is 200k
|
|
PBKDF2 iterations matching Python (an earlier-session concern about a 100k default was
|
|
wrong — 100k is only a legacy *fallback candidate* on decrypt).
|
|
- **Agent daemon** (`agent.ts`): owner ops gated on a capability file outside the app dir
|
|
(0600, `wx`, under `XDG_RUNTIME_DIR`); token auth happens *before* vault decryption
|
|
(closes an enumeration oracle and a CPU-burn probe); missing entry vs. constraint denial
|
|
are indistinguishable; `Object.hasOwn` blocks `__proto__` lookups; TOTP timestamps are
|
|
clamped to ±120 s for token callers; NaN-TTL coercion rejected; connection and
|
|
line-length caps; socket created under a restrictive umask before chmod.
|
|
- **Audit log** (`audit.ts`): HMAC chain plus a signed length-pinning head, with exactly
|
|
one crash-skew record tolerated; a missing or mismatched head fails both verification
|
|
and *append*, so truncation cannot be laundered. Honest limits documented in-file.
|
|
- **Sinks** (`sinks.ts`): secrets via env or stdin, never argv; child env is an allowlist
|
|
(secrets, tokens, and socket paths do not leak into sink children); clipboard auto-clear
|
|
compares before wiping; EPIPE from an early-exiting child cannot crash the daemon.
|
|
- **Relay sync transport** (`events.ts`, `relay.ts`, `snapshot.ts`): every fetched event
|
|
is re-verified against the *requested filter* (authors pinned to the profile's own
|
|
pubkey — relays are not trusted to honor filters), signature-verified, well-formedness
|
|
checked with correct `created_at` type discipline; decompression bombs are killed
|
|
mid-stream against explicit byte budgets; relay downgrade (withholding a chunk) is
|
|
detected and reported via `skippedNewerManifests`; delta replay across snapshot lineages
|
|
is blocked by client-side `#e`-tag re-checking.
|
|
- **Merge determinism** (`canonical.ts`, `merge.ts`): CPython-exact canonical JSON
|
|
(ensure_ascii, float repr, code-point key sort) with loud rejection of unsafe integers;
|
|
null-prototype maps make `__proto__` an ordinary key on both entries and tombstones.
|
|
- **Filesystem discipline** (`vaultFile.ts`, `appDir.ts`): atomic writes via `wx` temp +
|
|
fsync + rename + directory fsync, short-write loop, fchmod-on-handle (not path);
|
|
fingerprints validated before becoming path components; recursive profile delete
|
|
re-verifies containment; lock files carry owner tokens and are stolen only from
|
|
demonstrably dead pids.
|
|
- **CLI egress policy** (`program.ts`, `refs.ts`, TUI): reference-first output with an
|
|
*allowlist* metadata redactor (unknown fields become `has_*` flags — the right polarity
|
|
for loose schemas); plaintext egress is confined to explicitly named commands; token
|
|
mode cannot escalate to owner (`SEEDPASS_TOKEN` blocks mnemonic resolution); new-seed
|
|
generation refuses to write a phrase into a pipe; TUI database export is always
|
|
encrypted (plaintext export exists only behind the CLI's explicit `--plaintext`);
|
|
secret-bearing file writes go through fresh-0600-inode atomic writes with overwrite
|
|
confirmation.
|
|
- **Supply chain**: runtime deps are exactly noble/scure crypto + commander + zod, pinned
|
|
by a lockfile with integrity hashes and installed `--frozen-lockfile` in CI; CI runs
|
|
typecheck, tests, a standalone-bundle smoke test, an npm-pack install test, `pnpm audit`,
|
|
and generates a CycloneDX SBOM; the shipped artifact is a single non-minified esbuild
|
|
bundle with a SHA-256 sidecar; official BIP-85 spec vectors run directly against the
|
|
core (`ecff57e`).
|
|
|
|
## Known limits (documented, verified to match the code)
|
|
|
|
Per `docs/agent_security_model.md`, and confirmed accurate against this commit: a same-uid
|
|
attacker wins (capability file, ptrace); seeds/secrets are not scrubbed from JS memory;
|
|
`use` becomes `reveal` without an exec allowlist (and see L-6 for the allowlist's own
|
|
limits); the capability file exists on disk for the daemon's lifetime; tombstone replay
|
|
(L-3) and the ssh/pgp index collision (L-1) are shared with Python.
|
|
|
|
## Baseline
|
|
|
|
```
|
|
pnpm -r typecheck # clean
|
|
pnpm -r test # core: 234/234 pass (jsdom env: 230 pass, 4 skipped)
|
|
# cli: 157/158 pass — 1 flaky failure = finding M-1
|
|
```
|
|
|
|
---
|
|
|
|
## Remediation status (2026-08-18, after the audit)
|
|
|
|
| Finding | State | Where |
|
|
|---|---|---|
|
|
| M-1 sync tie-break | fixed | signed `published_ms` ordering in both implementations |
|
|
| M-2 config password policy | fixed | `secrets.ts` `basePolicy`; `test/passwordPolicy.test.ts` |
|
|
| L-1 shared BIP-85 app-32 path | mitigated | collision detection in both; derivation change deferred to after review |
|
|
| L-2 concurrent same-id creation | surfaced | `MergeReport.conflicts`, reported by `nostr restore` |
|
|
| L-3 tombstone replay | surfaced | `MergeReport.tombstonesEvicted` |
|
|
| L-4 inert settings | fixed | `backups.ts` ports BackupManager's write side; Quick Unlock now says it is unimplemented |
|
|
| L-5 unanchored `label_regex` | resolved as-is | matches Python's `re.search`; documented in help, `capabilities()`, the parity matrix, and pinned by tests |
|
|
| L-6 exec allowlist scope | documented | help, `capabilities().tokens.exec_allowlist_semantics`, enforcement-point comment |
|
|
| L-7 unvalidated `fingerprint` in `put` | fixed | format + seed-match checks in `agent.ts` |
|
|
| L-8 unbound plaintext import | fixed | fingerprint binding in CLI and TUI; `parseBackupWrapper` replaces the regex sniff |
|
|
|
|
Every fix above is mutation-verified: the guard or hook is disabled, the
|
|
matching tests are confirmed red, and the change is restored. A passing suite
|
|
was not treated as evidence on its own.
|
|
|
|
**Every finding in this report is now closed.** Four were fixed outright, one
|
|
is mitigated with the derivation change deliberately deferred (adding a new
|
|
derivation path immediately before the first outside review inverts the point
|
|
of the review), and two are surfaced rather than changed because their
|
|
resolution is frozen for cross-implementation parity.
|
|
|
|
Three defects were found while fixing them, none of which the audit had
|
|
reached:
|
|
|
|
- Python's `publish_delta` republished the manifest without the new ordering
|
|
field, sorting the manifest carrying the newest `delta_since` below the
|
|
snapshot it superseded.
|
|
- Raising Python's manifest fetch limit turned a vestigial fallback loop into
|
|
a live downgrade path.
|
|
- Python's `merge_index_payloads` mutated its `current` argument, and an
|
|
existing idempotency test was passing only because of that aliasing.
|
|
|
|
Still outstanding, and unchanged by this round: no GPT-family review has run.
|
|
Findings from one model family are one perspective, not independence.
|