Skip to content

docs(specs): 101-tpa-db — versioned signed offline TPA signature database - #1028

Open
Dumbris wants to merge 14 commits into
mainfrom
101-tpa-db
Open

docs(specs): 101-tpa-db — versioned signed offline TPA signature database#1028
Dumbris wants to merge 14 commits into
mainfrom
101-tpa-db

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 23, 2026

Copy link
Copy Markdown
Member

Speckit stage-1 spec for the roadmap P1 tpa-db epic: Ed25519-signed, sequence-versioned bundle format with anti-downgrade + explicit rollback, verify-before-parse loader extending the 086 fail-closed pipeline, seed corpus strategy (TPA-2026-0001 class and siblings, provenance/license + eval sample pairs), publication channel riding the spec-087 refresh lifecycle, and a post-activation informational re-scan.

Everything is gated on the existing scan-eval recall/FP CI bar (recall >= 0.90, hard-negative FP <= 0.05) — blocked at merge AND at activation.

Spec-only PR: no plan, no tasks, no code.

Cross-model review

Five rounds with opencode (gpt-5.6-sol), each finding verified against the actual code before being addressed. 17 findings across the five rounds (8/6/1/1/1), all verified genuine — none rejected as false positives. Two further gaps were found locally rather than by the reviewer: the telemetry anonymity-whitelist collision and the Spec 087 FR-014 supersession. The trust model changed substantially as a result — the four that mattered most:

The eval gate was measuring nothing. cmd/scan-eval/gate.go's gateChecks() registers only the four built-in detect.Checks; scanner.BundleCheck — which production does append in internal/security/scanner/inprocess.go — is absent, and scan-eval never loads a bundle at all. So FR-016's "merge-blocking on corpus change" and SC-005 would both have passed vacuously, with the database entirely unscored. New FR-016a requires the harness to load a candidate through the production loader and map each new campaign category (gatedCategory() only enforces a category whose check id is registered); SC-005 gains a negative control.

The anti-downgrade watermark was bypassable, then weaponizable, then absent at first use. sequence lives in the manifest, so with unsigned drops accepted by default it was attacker-chosen — one unsigned drop claiming a huge sequence could permanently lock out every legitimate signed release. Fixing that by making the watermark monotonic then made key-compromise recovery impossible: a compromised key signs at the maximum sequence and no surviving key can ever outbid it. That needed a binary-release-only trust epoch — which was itself a replay window until the epoch was bound inside the signed manifest with exact-equality matching. And all of it still did nothing on a fresh install, where an absent watermark accepts any old validly-signed bundle, so watermarks are now seeded from the embedded bundle's own sequence.

Rollback silently undid itself. Resetting the watermark to the rolled-back sequence meant the next refresh cycle re-activated the exact release the operator had just rejected. Rollback is now a pinned exception over a monotonic watermark, with the rejected artifact deny-listed.

Telemetry as specified could not ship. internal/telemetry/anonymity.go hard-whitelists the tpa_scanner sub-object to four non-negative integer counters, so the proposed string/bool bundle fields would have been rejected pre-transmit. FR-021 now requires typed whitelist entries, an enum-bounded source, and publisher-issued version values only — a self-built corpus version string would itself be a fleet identifier.

Two rounds also caught regressions introduced by the previous round's fixes (an over-broad degradation rule that would have shipped every fresh install with scan-mode auto-approval silently disabled, and a pipeline ordering that required checking a manifest field before parsing the manifest).

The 5th-round fix — making the operator-authority baseline explicit and config-resident — has not itself been re-reviewed, per the repo's per-PR review-round cap.

Two [NEEDS CLARIFICATION] markers remain by design: artifact hosting location, and whether require_signed_bundle ever flips default-on.

Follow-up, deliberately not done here to keep this spec-only: roadmap.yaml's tpa-db epic has no spec: field pointing at specs/101-tpa-db (editing it would require regenerating ROADMAP.md for the roadmap-up-to-date gate).

…database

Spec-only (speckit stage 1) for the roadmap P1 tpa-db epic: signed,
sequence-versioned bundle format with anti-downgrade + rollback; loader
verify-before-parse extending the 086 fail-closed pipeline; seed corpus
strategy (>=25 signatures, provenance/license, eval sample pairs); the
publication channel riding the spec-087 refresh lifecycle; post-activation
informational re-scan; freshness telemetry. Eval-gated on the existing
scan-eval recall/FP CI bar.
@Dumbris Dumbris changed the title spec: 101-tpa-db — versioned signed offline TPA signature database docs(specs): 101-tpa-db — versioned signed offline TPA signature database Aug 23, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: afff3cb
Status: ✅  Deploy successful!
Preview URL: https://23aefa1b.mcpproxy-docs.pages.dev
Branch Preview URL: https://101-tpa-db.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 101-tpa-db

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (29 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (23 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 32717860187 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

Dumbris added 13 commits August 23, 2026 21:18
Cross-model review (opencode/gpt-5.6-sol) plus local verification against the
shipped loader and eval harness found ten genuine gaps in the stage-1 spec.
All were confirmed against the code before being addressed.

## Changes

Trust model / anti-downgrade
- FR-009: the sequence floor is now a property of SIGNED artifacts only.
  `sequence` lives in the manifest, so an unsigned drop's sequence is
  attacker-chosen; an unsigned candidate could previously claim a huge
  sequence and permanently lock out every legitimate signed release.
  Unsigned candidates no longer advance the floor, and a signature ratchet
  stops signing from being stripped off an install that already ran signed.
- FR-010: rollback no longer lowers the floor. Resetting the floor to the
  rolled-back sequence meant the next refresh could immediately re-activate
  the exact release the operator just rejected. Rollback is now a pinned
  exception over a monotonic high-watermark, with the rejected
  (sequence, fingerprint) deny-listed.
- FR-009a: the embedded-default availability fallback is stated as the single
  explicit exemption to SC-002 (it previously contradicted it outright) and
  must serve as DEGRADED coverage, so deleting the external bundle cannot be
  used to obtain scan-mode auto-approvals under a weaker corpus.
- FR-007: an active unsigned/unverified bundle likewise marks coverage
  degraded. `inprocess.go` derives `coverageOK` from bundle *presence*, so
  without this an attacker-supplied unsigned corpus keeps auto-approving.
- FR-003: key ids must be canonical public-key fingerprints, duplicates
  rejected, and manifest id / sidecar id / verifying-key id must all match.
- FR-006/FR-008: corrected an impossible pipeline order — the spec required
  checking the manifest-resident `sequence` before parsing the manifest.
  Verification is what precedes parsing; it runs over the raw bytes that are
  then parsed from memory, closing a verify/parse swap race.
- FR-011a: activation must be crash-consistent. Spec 087 FR-010's atomicity is
  in-memory only; a crash between activation and floor persistence reopens the
  replay window FR-009 exists to close.

Eval gate (the gate was measuring nothing)
- FR-016a/SC-005/US2: `cmd/scan-eval/gate.go`'s `gateChecks()` registers only
  the four built-in detect checks — `scanner.BundleCheck` is absent and
  scan-eval never loads a bundle, so FR-016's "merge-blocking on corpus
  change" and SC-005 would both have passed vacuously. The harness must load
  a candidate through the production loader, register the bundle check, and
  map each new campaign category (`gatedCategory()` enforces a category only
  when its mapped check id is registered). SC-005 gains a negative control.

Telemetry
- FR-021: the schema-v8 anonymity backstop hard-whitelists `tpa_scanner` to
  four integer counters, so string/bool bundle fields would be rejected before
  transmit. Requires typed whitelist entries, an enum-bounded `source`, and
  publisher-issued `bundle_version` values only (a self-built corpus version
  string would itself be a fleet identifier).

Consistency
- FR-007: states explicitly that this spec supersedes Spec 087 FR-014's
  precondition, since embedding publisher keys means a fetch is no longer
  refused merely because the operator configured no key of their own.

Related #1028
…01-tpa-db

Round-2 cross-model review (opencode/gpt-5.6-sol) on the round-1 revisions.
Six findings, all verified genuine; two of them were regressions the round-1
fixes introduced.

## Changes

Watermark could be weaponized into a permanent update lockout
- FR-005a (new): the trust set carries a monotonic trust EPOCH that only a
  binary release can advance, and anti-downgrade is scoped
  (authority, epoch, sequence). Without it, key-compromise recovery was
  impossible rather than merely painful: an attacker holding a compromised
  publisher key signs at the maximum representable sequence, and since
  round-1 made the watermark monotonic, no surviving key could ever outbid
  it — a one-shot permanent denial of updates. FR-005 and the key-rotation
  edge case now route compromise recovery through the epoch bump.
- FR-004a (new): anti-downgrade state is tracked per SIGNING AUTHORITY, not
  in one global namespace. An operator with a self-built signed corpus (an
  explicitly supported persona) would otherwise raise the single global
  watermark past the publisher's release line and permanently block official
  updates. All publisher keys group into one authority so per-authority
  watermarks stay compatible with key rotation.
- SC-002a (new): both lockout scenarios must be proven by fixture.

Regressions introduced by the round-1 fixes
- FR-007/FR-009a: round-1 made "any unsigned active bundle" degrade coverage,
  but the embedded default has no sidecar — so every fresh install would have
  shipped with scan-mode auto-approval silently disabled. Degradation now
  applies to unsigned EXTERNAL candidates only; the embedded default is
  binary-trusted and degrades solely when serving BELOW a retained signed
  watermark.
- FR-008: round-1 put the rule-count ceiling before signature verification and
  parsing, which is impossible — rule count is not knowable from raw bytes.
  The two ceilings are now split: byte size before verification, rule count
  after parse.

Consistency
- FR-011a: the crash-consistent transaction now explicitly includes the
  watermark fingerprint and the FR-010 deny-list. Omitting them let a crash
  preserve a rollback while losing the state that rejects the rolled-away
  release, silently undoing the rollback on the next refresh.
- Key Entities: "Sequence Floor" still said the floor is "lowered only by
  explicit rollback", directly contradicting revised FR-010. Renamed to
  Sequence Watermark, defined as monotonic, with rollback as a pinned
  exception. Terminology normalized from floor to watermark throughout so the
  two do not read as separate mechanisms.
- Added a threat-model boundary note: an attacker with config write is out of
  scope (they can already disable quarantine), which is why the ratchet keys
  on install state and require_signed_bundle is config-resident.

Related #1028
Round-3 cross-model review (opencode/gpt-5.6-sol) found one CRITICAL defect,
verified genuine: the trust epoch introduced in round 2 was itself a replay
window.

## The defect

FR-005a made the epoch a property of the binary's trust set only; FR-002's
manifest carried sequence and key id but no epoch. After a compromise
recovery bumped the epoch and re-baselined the watermark, an OLD, weaker
bundle legitimately signed by a still-trusted surviving key would be
reinterpreted as belonging to the NEW epoch, clear the fresh (near-zero)
watermark, and activate with signature_verified=true and full coverage —
reinstating precisely the blind spots the recovery release was published to
close. SC-002a tested recovery but not this replay.

## Changes

- FR-002: the trust epoch is now carried inside the SIGNED manifest, so it is
  cryptographically bound to the bundle and cannot be reinterpreted.
- FR-005a: epochs are per-authority, and a candidate is accepted only when its
  manifest epoch EQUALS the current binary-trusted epoch of the verifying
  authority — never merely less-than-or-equal. A below-epoch bundle is refused
  as stale-epoch replay even when its signature verifies against a still
  trusted key. Documents the accepted consequence: a binary on the old epoch
  stops receiving database updates until updated, which is correct because it
  still trusts the compromised key, and must surface as a stale-epoch degraded
  state rather than failing silently.
- FR-008: epoch match added to the pipeline, before the anti-downgrade stage.
- SC-002a: new fixture (a2) — a pre-bump bundle signed by the surviving key is
  refused after the bump rather than clearing the re-baselined watermark.
- FR-020: stale-epoch replay added to the surfaced rejection reasons; bundle
  status also reports signing authority and trust epoch.

Also, from a self-audit of the round-2 per-authority model:
- FR-009a: the embedded default degrades when below a watermark retained by
  ANY authority (previously ambiguous which authority was meant).
- FR-004a: choosing an operator-signed corpus over a higher-sequence publisher
  one is a deliberate operator decision, not a downgrade and not degraded
  coverage — but the active authority must be visible in bundle status.

Related #1028
…e (101-tpa-db)

Round-4 cross-model review (opencode/gpt-5.6-sol) found one HIGH defect,
verified genuine: the anti-downgrade guarantee did not exist at first use.

## The defect

Every anti-downgrade mechanism in the spec binds only once persisted state
exists. On a fresh install — or after any data-directory reset — there is no
watermark, so an attacker who can write the bundle path drops an OLD but
genuinely publisher-signed, current-epoch bundle (sequence 5 while the
shipping release is at 100). Signature verifies, epoch matches, nothing to
compare the sequence against: it activates as a fully signature-verified
bundle at FULL coverage, silently reinstating every blind spot fixed between
5 and 100. The FR-009 ratchet and the FR-010 pin/deny-list do not help, since
both also require pre-existing state. Confirmed no new contradiction was
introduced by the round-3 epoch fix.

## Changes

- FR-009b (new): watermarks are SEEDED from a trusted baseline rather than
  starting empty. The publisher authority seeds from the sequence of the
  bundle embedded in the running binary — a candidate older than what the
  binary already ships is strictly worse than the fallback and is refused —
  and the baseline is re-applied on every startup, so wiping the data
  directory cannot lower it below the binary's own corpus. The operator
  authority, which has no binary-trusted baseline, gets a minimum sequence
  configurable alongside the operator key. This also makes the FR-005a epoch
  re-baseline safe: a bumped epoch starts at the embedded sequence, not zero.
- SC-002 / US1 Independent Test: fresh-install and post-wipe downgrade are now
  explicitly in scope, with fixture (h) covering the old-but-validly-signed
  bundle on an install with no persisted state.
- Edge case "Anti-downgrade state lost": corrected — a wipe resets the
  watermark to the seeded baseline, not to zero. What a wipe genuinely loses
  is the ratchet state and any pin/deny-list.
- Key Entities: Sequence Watermark is documented as never empty.

Related #1028
…e (101-tpa-db)

Round-5 cross-model review (opencode/gpt-5.6-sol) found one HIGH defect,
verified genuine. It confirmed FR-009b closes the publisher-side replay
without contradicting FR-005a, FR-009, FR-009a, or FR-010, but caught that
the operator authority was left with the same hole.

## The defect

FR-009b gave the operator authority a minimum sequence "defaulting to zero,
with the residual exposure being the already-out-of-scope config-write
attacker". That parenthetical was wrong. With a zero default, no config write
is needed: after a data-directory wipe (or on a fresh install), a bundle-path
attacker — squarely inside the threat model — can replay any older genuinely
operator-signed bundle at full coverage. The operator authority therefore had
no post-wipe anti-downgrade at all by default, while the spec read as though
it did.

## Changes

- FR-009b: the operator baseline must be declared explicitly in
  mcp_config.json alongside the operator key, not defaulted to zero and not
  stored in the data directory — a baseline held in resettable state gives no
  protection exactly when it is needed. If an operator key is configured
  without a baseline, operator-authority anti-downgrade is treated as
  unsupported and coverage is marked degraded while an operator-signed bundle
  is active, instead of presenting it as fully trusted. The data-directory
  watermark may only raise the effective minimum above the configured
  baseline, never lower it. Epoch re-baseline now starts at the authority's
  baseline rather than zero.
- Edge case "Anti-downgrade state lost": states plainly that the operator-side
  guarantee survives a wipe only with a config-resident baseline.
- SC-002a: fixture (c) covers the operator authority after a wipe, both with
  and without a declared baseline.

Note: per the repo's per-PR cross-model review cap, this is the fifth round.
This fix has NOT itself been re-reviewed.

Related #1028
…ntrol (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 1 on specs/101-tpa-db/spec.md.
All five findings verified against the current code before changing anything.

## Changes
- FR-004b (new): the active signing authority is a config-resident declaration,
  default publisher; a candidate signed by a non-active authority is refused
  ("authority not active"). Per-authority watermarks alone let a bundle-path
  attacker — in the threat model — swap a publisher bundle at sequence 100 for
  any operator-signed artifact clearing the operator baseline, silently
  narrowing coverage while still reporting signature_verified=true at full
  coverage, because the comparison never crosses namespaces.
- FR-009a: the embedded default's sequence is compared ONLY against the
  publisher watermark (same namespace); the "any authority" rule made an
  operator corpus at sequence 900 permanently degrade a current publisher
  fallback at 100. The intent behind it is restated directly as condition (b):
  a previously-active external bundle of the active authority having gone
  missing.
- FR-016a: corrected the claim about unmapped eval categories — they are
  excluded from OverallRecall so their MISSES cannot fail the gate, but their
  false positives still count, since FPRate is computed over the whole
  hard-negative set regardless of category mapping (gate.go:193-197, 287-289).
- FR-016b (new) + SC-005 + US2 independent test: the anti-vacuity negative
  control is made deterministic. Aggregate recall over 25+ signatures absorbs a
  single removal within the 0.10 threshold headroom, and built-in checks
  (phrase.injection especially) often flag the dropped signature's sample
  anyway, so "remove a gating signature and the gate must go red" could pass
  vacuously. A designated control signature with no built-in or sibling
  overlap, an asserted gate-breach exit code, and a CI assertion of the
  no-overlap property replace it.
- SC-001: tampering is defined over the fields the detached signature actually
  covers (bundle bytes, signature value, sidecar identity fields, mismatched
  pair) instead of "any byte of ... sidecar", which no detached scheme can
  guarantee against whitespace/key-order edits.
- SC-002a gains fixture (b2) and US1's independent test gains case (i) for the
  authority-not-active refusal; FR-020, the threat-model edge case, Key
  Entities, and the Constitution Check note the new config-resident field.

## Testing
- Docs-only change; no Go sources touched.
- Every codebase claim re-verified before editing: cmd/scan-eval/gate.go
  (gateChecks/categoryCheck/OverallRecall/FPRate, no --bundle flag today),
  internal/security/scanner/inprocess.go (coverageOK), and
  internal/telemetry/anonymity.go (schema-v8 tpa_scanner whitelist).
…allback (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 2 on specs/101-tpa-db/spec.md.
All three findings verified against the document and gate.go before changing anything.

## Changes
- FR-010: the rollback target must be identified by content fingerprint, not by
  pathname alone. Rollback is the one path that deliberately bypasses the
  watermark, so naming its target by an attacker-writable path lets a
  bundle-path attacker choose what the bypass lands on — substituting a
  genuinely publisher-signed, current-epoch bundle at sequence 5 for the
  sequence-90 artifact the operator intended, which then gets PINNED. The
  operator now supplies the expected fingerprint, the path is only a hint, a
  mismatch is refused, and a rollback target must sit at or above the FR-009b
  seeded baseline (the embedded default excepted, since it is that baseline).
- FR-009a condition (b): narrowed to a missing OPERATOR-authority bundle. As
  written it degraded coverage whenever any previously-activated external
  bundle went missing, including the ordinary case of a publisher external file
  removed after a binary upgrade shipped a strictly newer embedded corpus —
  where condition (a) already answers the question on the same axis and says
  full coverage.
- US2 independent test: the negative control now runs over the FR-016b control
  scope instead of re-running the full-corpus command. One miss out of a
  25-signature corpus leaves aggregate recall near 0.96, above the 0.90
  threshold, so the full-corpus form contradicted FR-016b and would have proven
  nothing.
- US1 independent test, SC-002, FR-020: assertions and rejection reason for the
  rollback-target mismatch.

## Testing
- Docs-only change; no Go sources touched.
- Recall/FP semantics re-checked in cmd/scan-eval/gate.go (OverallRecall is
  gatedDetected/gatedMalicious aggregated across gated categories).
…control (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 3 on specs/101-tpa-db/spec.md.
All three findings verified against the document and gate.go before changing anything.

## Changes
- FR-011 + FR-008 + edge cases: the sidecar gets its own small byte cap and a
  bounded read, applied before it is parsed, on both the file-drop and fetch
  paths. The sidecar is the one artifact that must be read before any signature
  is verified — its algorithm id, key id, and signature value are verification's
  own inputs — so FR-006's verify-before-parse protection does not cover it, and
  a bundle-path attacker could drop a multi-gigabyte sidecar next to an
  otherwise valid bundle and stall or exhaust the refresh path. The pipeline
  order and the "two ceilings" note are updated accordingly.
- FR-010: the rollback floor is now its OWN authority's FR-009b baseline, not
  the publisher one in both cases. As written it compared an operator-signed
  rollback target at sequence 9 against the publisher embedded corpus at 100 and
  would have refused every legitimate operator rollback, contradicting the
  per-authority isolation of FR-004a.
- FR-016b: "restrict the control to its category" is not by itself provably
  failing — decide() breaches only on OverallRecall < minRecall, so a control
  category with ten gated malicious samples lands on exactly 0.90 after one miss
  and stays green on the boundary. The canonical construction is now a control
  corpus whose gated malicious set is the control sample alone; any other scope
  must compute and assert the recall drop rather than assume it.

## Testing
- Docs-only change; no Go sources touched.
- Boundary semantics re-checked in cmd/scan-eval/gate.go decide() (strict `<`
  against min-recall) and the per-authority baseline rules in FR-009b.
…tric (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 4 on specs/101-tpa-db/spec.md.
Findings verified against the document before changing anything; the telemetry
finding was accepted in substance but not in its proposed remedy (see below).

## Changes
- FR-011 + Edge Cases: a byte cap bounds bytes, not time. Local candidate
  artifacts (bundle and sidecar) must be REGULAR files — a FIFO, device, socket,
  or symlink resolving to one is refused outright rather than read — and every
  candidate read, local or fetched, runs under an enforceable I/O deadline whose
  expiry releases Spec 087's single-flight slot. Otherwise the bundle-path
  attacker leaves a FIFO with no writer and pins the refresh path indefinitely
  without ever approaching the cap.
- FR-010: an authority with no declared baseline — possible only for the
  operator authority, which FR-009b explicitly permits — has no rollback floor
  to enforce, and none is invented. The rollback proceeds and the install is
  marked degraded while that bundle is active, which is already FR-009b's
  treatment. Refusing the rollback would break FR-010 for a configuration
  FR-009b allows; assuming a zero floor would claim a guarantee that does not
  exist.
- SC-006: the freshness metric is scoped to publisher-issued corpora and its
  derivation is stated — age comes from a server-side join of the reported
  version/sequence to the publisher's own release dates. Installs on an
  operator-built corpus (the `other` bucket) are not ageable and form their own
  bucket rather than being folded into the ratio.

## Notes on the rejected remedy
The reviewer's suggested fix for SC-006 was to report `generated_at`/age in the
heartbeat. That is exactly the near-unique value FR-021's schema-v8 backstop
exists to keep out of the payload, and it would buy nothing the version/sequence
join does not already give, so the derivation was specified instead.

## Testing
- Docs-only change; no Go sources touched.
…uences (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 5 on specs/101-tpa-db/spec.md.
Round 5 reported no remaining adversary-reachable security hole; both findings
were internal contradictions, verified against the document before editing.

## Changes
- FR-009b, the Sequence Watermark entity, and SC-002: the baseline-less operator
  authority is now named as an explicit exemption everywhere the document made a
  universal claim. FR-009b permits an operator authority with no declared
  baseline (marking the install degraded), while the same requirement opened with
  "every watermark MUST be SEEDED", the entity said "Never empty", and SC-002
  called FR-009a the "sole exemption" — an implementation could not satisfy all
  four at once. There are exactly two exemptions and both are degraded-coverage
  states.
- FR-021: `sequence` is reported only when `bundle_version` resolves to a
  publisher-issued value; in the `other` bucket it is omitted. Bucketing the
  version string while transmitting a raw operator-chosen sequence defeats the
  purpose of the bucket, since an operator-built corpus can number itself with a
  unix timestamp or private build number and be just as fleet-identifying.
  SC-006 already scopes freshness to publisher corpora, so no analysis is lost.

## Testing
- Docs-only change; no Go sources touched.
- Round 5 is the per-PR cross-model review cap (CLAUDE.md escalation trigger 4);
  these two fixes are not themselves re-reviewed.
…exemption count (101-tpa-db)

Cross-model review (opencode / gpt-5.6-sol) round 6 on specs/101-tpa-db/spec.md.
All five findings verified against the document and the cited code before editing.

- Edge Cases (threat model): the boundary is now explicit about the data
  directory — the bundle-path attacker may also be able to delete/reset the
  persisted security state (equivalent to the wipe), against which only the
  binary/config-anchored guarantees survive (seeded publisher baseline,
  config-resident operator baseline, require_signed_bundle, active-authority
  declaration); ratchet and pin/deny-list are best-effort. Chosen-value
  corruption of the state store is out of scope (that attacker is already past
  every mcpproxy guarantee).
- Edge Cases + FR-009a: two stale 'single exemption to SC-002' statements now
  name both exemptions, matching SC-002's 'exactly two'.
- US1 acceptance 3 + SC-002: 'lower sequence' is qualified everywhere as a
  same-authority, same-epoch comparison; a deliberate FR-004b authority switch
  is by definition not a downgrade and needs no exemption.
- FR-010 + FR-005a: pin/deny-list entries are keyed (authority, epoch,
  sequence, fingerprint); an authority switch suspends (not deletes) the
  non-active authority's entries; an epoch bump retires prior-epoch entries so
  stale rollback state cannot block the key-compromise recovery bundle.
- FR-021: 'resolves to a publisher-issued value' is a provenance test (embedded
  default or publisher-authority-verified), never a claimed-version-string
  match — an operator corpus can copy a publisher version string verbatim; the
  backstop also rejects sequence-present-with-version-other cross-field.

- Docs-only change; no Go sources touched.
- Round 6 of the raised (10-round) cross-model review cap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants