Skip to content

fix(scope): legacy and internal cache entries refused for every caller, kind-first authorization, monotone child provenance (Spec 105 PR B, FR-001/002) - #1282

Merged
Dumbris merged 11 commits into
mainfrom
105-b-cache
Sep 18, 2026
Merged

Dumbris merged 11 commits into
mainfrom
105-b-cache

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 16, 2026

Copy link
Copy Markdown
Member

Summary

Spec 105 (agent-token scope hardening), planning artifacts and PR A merged in #1279. This PR closes FR001-G1..G7 · roadmap scope-cache-legacy-invalidation · research D2, D5 and the new D16 (header-only read gate). Gap ids, evidence and test sketches: specs/105-agent-scope-hardening/gap-map.md §1; the refusal shapes it must satisfy: contracts/refusals.md.

What changed

  • Framed cache records: magic | fixed header (version, caller kind, flags, tier bits, expiry, size, producer authorization digest) | JSON body. Pre-frame values are legacy provenance: refused for every caller and invalidated on first redemption, without decoding (FR-002).
  • Header-only read gate (D16): the gated door decides in O(1) from the fixed header — administrator (FR-001 kind-first) / unrestricted agent / digest-equal reader admitted; everyone else refused without loading the producer snapshot. Refusals commit like a miss (bbolt write-count parity), are payload-size independent, and share status, body and timing class with a miss. Reserved flag/tier bits in a header are unrecognised provenance (refused on the same O(1) path).
  • Admitted readers never get the refusal shape: an admitted reader whose body is undecodable / header-disagreeing gets a distinct ErrEntryUnreadable (entry invalidated) — the refusal shape is decided on the header only (rule recorded in contracts/refusals.md).
  • Producer snapshots are content-addressed and unbounded; stats (TotalEntries/TotalSizeBytes) are recomputed exactly by the cleanup sweep, so legacy invalidation never has to size the payload.
  • Child pages carry monotone provenance (never broader than the parent); pinned REST dispatch and redemption are refused alike; CallerKindInternal entries are refused for every caller and kept.

Tests

Red-first per gap id (each confirmed failing on origin/main by assertion), pinned pre-105 tests inverted (never deleted), admin controls kept. Notable: manager_payload_independent_test.go (allocation/bytes-read constant across miss / scope-refused 4 MB entry with a 5k-server producer / cold cache / legacy 4 MB), manager_refusal_commit_test.go, manager_frame_integrity_test.go (exhaustive flag/tier byte sweep), manager_legacy_test.go (admin and scoped-reader malformed bodies), mcp_read_cache_scope_test.go.

Verification

  • go build both editions · go test -race on the touched packages, internal/server (CI skip regex), -tags server serveredition · -count=3 -shuffle=on on the PR packages · goldens byte-identical to main · gofmt/vet incl. GOOS=linux/GOOS=windows · ./scripts/test-api-e2e.sh 65/65.
  • golangci-lint v2 could not run locally (binary built with go1.25 vs the go 1.26 target) — CI covers it.
  • Cross-model review: 7 rounds, CLEAN at round 7 — rounds 1–4 codex gpt-5.6-sol, rounds 5–7 opencode github-copilot/gpt-5.6-sol; every finding verified by probe before fixing (verdict tables in the worktree's .review-tmp/critique-r1.md). The recurring theme was the timing class of refusals (work proportional to hidden snapshot/payload size), answered structurally by D16.

Follow-ups / Spec 105 gaps

See gap-map.md §8 (REST replay has no tool gate; UI active profile bounds no MCP session). Remaining Spec 105 PRs: D (#1283), E (#1284), H0 (#1285) in this batch, then C → F → G → H1.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 16, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 65bc97d
Status: ✅  Deploy successful!
Preview URL: https://6d32642b.mcpproxy-docs.pages.dev
Branch Preview URL: https://105-b-cache.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Sep 16, 2026

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 86.72316% with 47 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/cache/manager.go 83.46% 11 Missing and 10 partials ⚠️
internal/cache/models.go 82.40% 11 Missing and 8 partials ⚠️
internal/cache/authorization.go 95.60% 2 Missing and 2 partials ⚠️
internal/experiments/guesser.go 50.00% 0 Missing and 1 partial ⚠️
internal/runtime/runtime.go 50.00% 0 Missing and 1 partial ⚠️
internal/server/cache_authz.go 95.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 105-b-cache

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (21 MB)
  • smart-mcp-proxymcpproxy-goARNOHR.dockerbuild (0 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 35117731746 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

Comment thread internal/cache/models.go Fixed
Comment thread internal/cache/models.go Fixed
Dumbris added a commit that referenced this pull request Sep 16, 2026
A controller that reports success without a server config (the test
doubles do) was dereferenced in handleAddFromRegistry; chi's recoverer
turned the fault into a bare 500. Under Go 1.26 on windows/amd64 the
recovered hardware exception corrupts the Go heap (golang/go#81238), so
the internal/httpapi test binary died later with "found pointer to free
object" / "unexpected fault address" on unrelated PRs (#1282, #1285).

Return a JSON 500 instead, the way redactedRegistrySummary already
tolerates a nil entry, and pin it with a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b9312b4)
Dumbris added a commit that referenced this pull request Sep 16, 2026
A controller that reports success without a server config (the test
doubles do) was dereferenced in handleAddFromRegistry; chi's recoverer
turned the fault into a bare 500. Under Go 1.26 on windows/amd64 the
recovered hardware exception corrupts the Go heap (golang/go#81238), so
the internal/httpapi test binary died later with "found pointer to free
object" / "unexpected fault address" on unrelated PRs (#1282, #1285).

Return a JSON 500 instead, the way redactedRegistrySummary already
tolerates a nil entry, and pin it with a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b9312b4)
Dumbris added a commit that referenced this pull request Sep 16, 2026
A controller that reports success without a server config (the test
doubles do) was dereferenced in handleAddFromRegistry; chi's recoverer
turned the fault into a bare 500. Under Go 1.26 on windows/amd64 the
recovered hardware exception corrupts the Go heap (golang/go#81238), so
the internal/httpapi test binary died later with "found pointer to free
object" / "unexpected fault address" on unrelated PRs (#1282, #1285).

Return a JSON 500 instead, the way redactedRegistrySummary already
tolerates a nil entry, and pin it with a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b9312b4)
Dumbris added a commit that referenced this pull request Sep 16, 2026
A controller that reports success without a server config (the test
doubles do) was dereferenced in handleAddFromRegistry; chi's recoverer
turned the fault into a bare 500. Under Go 1.26 on windows/amd64 the
recovered hardware exception corrupts the Go heap (golang/go#81238), so
the internal/httpapi test binary died later with "found pointer to free
object" / "unexpected fault address" on unrelated PRs (#1282, #1285).

Return a JSON 500 instead, the way redactedRegistrySummary already
tolerates a nil entry, and pin it with a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b9312b4)
Dumbris added a commit that referenced this pull request Sep 16, 2026
…emp-dir race (#1288)

* fix(httpapi): stop the recovered nil-config panic in add-from-registry

A controller that reports success without a server config (the test
doubles do) was dereferenced in handleAddFromRegistry; chi's recoverer
turned the fault into a bare 500. Under Go 1.26 on windows/amd64 the
recovered hardware exception corrupts the Go heap (golang/go#81238), so
the internal/httpapi test binary died later with "found pointer to free
object" / "unexpected fault address" on unrelated PRs (#1282, #1285).

Return a JSON 500 instead, the way redactedRegistrySummary already
tolerates a nil entry, and pin it with a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(scanner): wait for the Pass-2 job before tearing down temp dirs

TestServiceStartScanDeepOnRunsSourceResolutionAndPass2 failed on ubuntu
CI with "TempDir RemoveAll cleanup: directory not empty": the Pass-2
goroutine writes tools.json into the server working dir after
ResolveFullSource returns and before it registers its engine job, so an
idle engine can mean "Pass 1 cleared, Pass 2 not started yet" and the
test's teardown raced the write. Wait for the Pass-2 job to reach a
terminal status in storage (its last write on every exit path) first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Dumbris and others added 11 commits September 16, 2026 18:47
… kind-first gate, monotone child provenance, non-disclosing read_cache (T021–T027)

Failing tests for FR-001/FR-002 gaps FR001-G1..G7, all asserting against
HEAD (bac8f6b) behaviour:

- G1 internal/cache/manager_legacy_test.go (new): nil-producer / version-0 /
  unknown-version / stamped-but-unversioned records refused for every caller
  kind (admin, admin_user, anonymous, agent, user), evicted on first refused
  redemption, absent after bbolt close+reopen, stats consistent with the
  bucket; per-key invalidation. Handler-level admin/anonymous/user refusal in
  internal/server/mcp_read_cache_scope_test.go (new).
- G2 internal/cache/manager_internal_test.go (new): "internal"-stamped
  registry/npm entries refused for every read_cache caller WITHOUT eviction
  (Peek/Get still serve). Writer stamps pinned in
  internal/runtime/registry_cache_stamp_test.go (real SearchRegistryServers
  against a loopback registry) and
  internal/experiments/guesser_cache_stamp_test.go (cacheInfo). Handler-level
  admin refusal + agent/user live≡absent parity + no eviction.
- G3 TestExpiredRecords extended: expired entry absent after the expiring
  Get, in-memory and persisted stats agree with the bucket (rollback bug).
- G4 recursive child carries the PARENT producer (MCP + REST): producing
  agent reads the child an administrator minted.
- G5 agent refusal collapses unauthorized/not-found/expired into one body
  ("cache key not found") on handleReadCache and CallToolDirect.
- G6 TestAuthorization_CallerKindFirst (D5): profile-bound / empty-profile /
  deleted-profile administrators redeem any snapshot; agents never redeem an
  administrator snapshot; deny-all guard applies to agent readers only.
- G7 internal/server/scope_cache_fixtures_test.go (new): upgrade fixture
  with a real restart on the same data dir (SC-004), fresh internal entry,
  recursive child on REST, spec's named profiled-admin → pinned-agent child
  fixture, pinned-token REST dispatch-vs-redemption body parity, and the
  held-call narrowing fixture (regression pin — passes on HEAD).

Not touched (T031, implementation phase): the pre-D5 cells in
TestAuthorization_CouldHaveProduced, TestGetRecordsAs_LegacyEntryWithoutProducer,
and the "not readable with this credential" assertions in
mcp_read_cache_authz_test.go / mcp_call_tool_direct_test.go still pin the
pre-feature behaviour and must be inverted alongside the implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t gate, monotone child provenance, non-disclosing read_cache (T028–T032)

FR-001/FR-002 implementation turning the T021–T027 red tests green:

- internal/cache: Record.Version (RecordVersion=1, stamped by every Store);
  HasCurrentProvenance; CallerKindInternal; ErrKeyNotFound/ErrKeyExpired/
  ErrLegacyProvenance/ErrInternalEntry (the last two errors.Is
  ErrUnauthorizedRead). getGuarded commits every path that deletes or records
  a stat (miss, expiry, legacy invalidation) by returning nil from the bbolt
  Update closure and surfacing the verdict outside, so the delete is durable
  and m.stats agrees with the bucket (G1, G3). On the gated door an entry with
  no producer, no version or an unrecognised version is refused for EVERY
  caller kind and evicted; an internal entry is refused without eviction
  (G2). ReadCacheResponse.Producer (json:"-") carries the paged entry's
  snapshot.
- CouldHaveProduced is ordered caller kind first (D5, G6): an administrator
  reader qualifies for any snapshot regardless of its own profile binding;
  a non-administrator never for an administrator snapshot; the deny-all
  guard, profile-set, pin, server-set and permission checks apply between
  agent snapshots only; internal producers match nobody.
  Unrestricted() renamed IsAdministrator().
- Writers: runtime registry search and the repository guesser stamp
  CallerKindInternal (G2, T029).
- handleReadCache: a re-truncated page is stamped with the PARENT entry's
  snapshot (childPageProducer), never the redeemer's (G4); readCacheRefusal
  collapses unauthorized/expired/internal/legacy into the not-found body for
  non-administrator callers (G5) while the activity record keeps the real
  reason and administrators get the reason; storage faults stay distinct.
  CallToolDirect inherits both through the shared handler.
- T031 inversions (never deleted): the four pre-D5 admin cells in
  TestAuthorization_CouldHaveProduced, TestGetRecordsAs_LegacyEntryWithoutProducer
  (every caller refused + invalidated), the "not readable with this
  credential" assertions in mcp_read_cache_authz_test.go (now body parity
  with the nonexistent-key answer plus an administrator liveness control),
  and TestCallToolDirect_ReadCachePagesStoredRecords (seeds a stamped record).
- Docs: agent-tokens.md and routing-modes.md describe kind-first ordering,
  monotone child provenance, the non-disclosing body, legacy and internal
  entries. tasks.md T021–T032 ticked; ROADMAP.md regenerated.

Verified: go test -race ./internal/cache/... ./internal/runtime/...
./internal/experiments/... ./internal/httpapi/...; full ./internal/server
with the CI skip regex; -tags server builds and serveredition suites;
goldens untouched; gofmt clean on touched files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng class), unknown kinds are legacy, stats never drift, expired internal entries kept

Critique 1 (security):
- (1) MUST-FIX: a scoped read_cache refusal returned its error from the
  bbolt Update closure, so the transaction rolled back with no disk write
  (~5 us) while a miss committed a stats write (~13 ms): a ~3000x timing
  oracle that told a narrow token whether a live entry (a broader token's
  page, or a guessable registry/guesser key) sat behind the key. Every
  refusal now takes the committing branch as a miss; verdicts leave the
  closure through `verdict`, and the closure errors only on storage faults.
  Pinned structurally via bbolt TxStats.Write parity with a miss.
- (2) "unrecognised provenance" was decided on Version only; an entry stamped
  with a caller kind this binary does not know was served to admin and
  anonymous readers. HasCurrentProvenance now requires a known kind.
- (3) stats were decremented before saveStats ran, so a failed write left
  the in-memory counters disagreeing with the rolled-back bucket. Manager.update
  snapshots and restores the stats on a failed closure; saveStats errors
  instead of nil-dereferencing a missing stats bucket.
- (6, with critique 2 #6) an undecodable record (e.g. a version that
  overflows uint8 after a downgrade) is unrecognised provenance on the gated
  door: refused for every caller with the legacy sentinel and invalidated,
  as cleanup already treats it. The ungated Get is unchanged.
- (4) documented: pre-upgrade registry/guesser entries carry no stamp and are
  invalidated once by the first probe; (5) anonymous body is spec-conformant
  (spec.md:37) and is now pinned as the SC-005 parity control.

Critique 2 (parity/tests):
- (4) the gated door evicted an EXPIRED internal entry (expiry ran before the
  internal check) although the registry reader serves it as Stale until
  cleanup; provenance class is now decided before expiry.
- (1) the FR-001 named fixture pins parent-stamping (ProfileScoped/Profile),
  (2) administrator reason bodies and the anonymous parity body are asserted
  (mutation M2 now bites), plus ErrLegacyProvenance/ErrInternalEntry sentinels
  at the cache level, (3) the upgrade fixture re-seeds before every leg,
  (5) REST legs assert the not-found substring, (7) appctx CacheManagerAdapter.Set
  stamps internal instead of writing a legacy entry, (8) handler comment
  reworded for kind-first, (9)+docs: committed expiry eviction, one-time
  invalidation of entries from any earlier release.

Verified: -race on internal/cache, runtime, experiments, httpapi, appctx,
internal/server (with the standard skip list) and the -tags server suites;
goldens byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…che redemption

Codex round 1 (both chunks, one defect): an agent reader whose token
carries no AllowedServers is deny-all on every dispatch gate
(auth.CanAccessServer, serverInScope), but cache.CouldHaveProduced let
coversServers([], []) succeed, so an empty-grant agent could redeem an
agent-produced entry whose snapshot also carried an empty grant — on the
MCP read_cache tool and on the /api/v1/tools/call read_cache branch
alike. An empty grant is reachable: token creation normalises it to ["*"],
but a server-edition rotation that narrows the grant persists nil.

The agent branch of the gated predicate now opens with the empty-grant
deny-all guard, alongside the existing empty-profile one: such a reader
could have produced no entry, its own identically-stamped one included.
Refusal body and timing class are the not-found ones already in place.

Tests: nine new cells in TestAuthorization_CallerKindFirst (nil and []
grants, own entry, another empty-grant agent's, unscoped agent, admin;
broad agent and administrators still read the empty-grant entry), and
fixture (f) TestScopeCacheFixture_EmptyGrantAgentIsDenyAllOnRedemption:
CanAccessServer + REST direct dispatch refused as premise, administrator
control reads the record, MCP and REST live key ≡ absent key with the
not-found body, no eviction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…failure stats, header-only refusals

Codex round 2 on Spec 105 PR B (FR-001/FR-002), three cache findings and
one server finding, all confirmed by probe before fixing:

1. A current-version agent PRODUCER snapshot with an empty server grant, or
   bounded to an empty effective profile, could have authorized no tool, yet
   containment against the empty set let any broader agent redeem it.
   CouldHaveProduced now rejects such producer snapshots for agent readers
   before containment; administrators still qualify for any snapshot.

2. Manager.update restored the in-memory stats only when the closure
   failed. A commit failure after a successful closure (disk full at fsync)
   left the record on disk with the counters already decremented. The
   snapshot is now taken before db.Update and restored on any error, under
   a mutex so it is taken under the same exclusion the transaction runs
   under; a dbUpdate seam lets a test fail the commit after the closure.

3. Refusals were payload-proportional: every existing key was fully
   JSON-decoded (multi-MB FullContent) before provenance, expiry or the
   guard ran, while a miss was not — a timing class the spec's
   non-disclosing refusal forbids. MarshalBinary now writes a small frame
   header (version, producer, expiry, size) in front of the record; the
   gated door decides every verdict short of admission on that header
   alone and decodes the record only after admission. UnmarshalBinary
   accepts framed and pre-frame bare-JSON values. Expired entries are no
   longer evicted by the gated read — refused like a miss and left to the
   cleanup sweep — so only the FR-002 invalidating refusals delete, and
   that delete is pinned as payload-independent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ader bound, pre-frame size accounting

Three confirmed findings on the framed cache record (Spec 105 FR-001/FR-002):

- UnmarshalBinary now requires the frame header to agree exactly with the
  body it fronts (Version, ExpiresAt, TotalSize, deep Producer). The gated
  read admits on the header alone, so a crafted a-only header in front of an
  administrator/broader/legacy body was served to the a-only reader, and a
  future-expiry header over an expired body was decoded and evicted after
  admission. A disagreeing value is undecodable: refused for every caller,
  invalidated (folding out the header's size), never served.
- maxRecordHeaderLen raised 64 KiB -> 1 MiB, sized so an authorization naming
  several thousand servers in both grant and profile fits, and MarshalBinary
  refuses a header over the bound BEFORE persisting. Previously a legitimate
  large snapshot was stored, then refused and deleted on its producer's own
  first redemption; now StoreAs returns errRecordHeaderOversize and the
  truncator serves the payload uncached.
- Invalidating a pre-frame bare-JSON record folded 0 into TotalSizeBytes;
  it now folds out the value's length (known without decoding, an upper
  bound on the content) and evict clamps the total at zero.

Red tests in manager_frame_integrity_test.go; verdict table appended to
.review-tmp/critique-r1.md under "Codex round 3".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…snapshots, exact pre-frame accounting

Codex round 4 (specs/105, PR B), three confirmed findings:

1. User-kind snapshots kept only the user id and redemption compared only
   the principal, so a server-edition user narrowed since producing an
   entry (grant, permission tier or profile) still redeemed it. Users are
   bounded by the same dispatch gates as agent tokens, so the snapshot now
   carries AllowedServers/Permissions/ProfilePin and the read gate applies
   the same containment on top of identity equality (necessary, never
   sufficient). Shared rule: IsScoped/DenyAll/kindVerdict/containedBy.

2. The 1 MiB frame-header bound refused legitimate snapshots (configuration
   bounds neither server count nor name length), and a live-key refusal
   JSON-decoded the whole header on every probe — a fleet-sized timing
   oracle a miss did not share. The frame header is now FIXED-SIZE (52
   bytes: version, kind code, deny-all bit, expiry, size, snapshot SHA-256);
   each distinct producer snapshot is stored once in a content-addressed
   `cache_snapshots` bucket, written in the same transaction as the record
   and pruned by cleanup; the gate decides kind, version, expiry and
   deny-all on the header and loads the snapshot — through a small LRU,
   O(1) after the first probe — only for same-kind containment. No size
   bound remains: a 5,000-server snapshot round-trips and is redeemable by
   its producer, warm and after a restart.

3. Pre-frame invalidation folded out the raw record length (escaped JSON)
   and clamped the aggregate at zero, understating unrelated live entries.
   The legacy path — one-shot, every caller — now decodes the bare-JSON
   value and folds out exactly len(FullContent); the clamp is gone.

Every finding was reproduced by a probe and pinned by a red test proven
against pre-fix HEAD. Docs: agent-tokens.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al-cache fixture

The loopback-registry fixture sets AllowPrivateRegistryFetch through the
runtime config, which flips the process-wide allow-policy; under the
shuffle lane TestBuildRegistrySourceEntry_RejectsSSRFLiteralIP then ran
after it and accepted a literal IP. Same LIFO cleanup as #1222.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…), no legacy decode

Codex round 5 (specs/105, PR B), one root behind both findings: any
refusal that must LOAD the producer snapshot (round 4's content-addressed
snapshot was O(1) only while its 128-entry LRU was warm — a restart or
129 distinct snapshots made the first same-kind refusal read and JSON-
decode a fleet-sized snapshot) or DECODE the legacy value (round 4's
one-shot preFramePayloadSize) does work proportional to hidden state,
which the spec's non-disclosing refusal forbids in the timing class.
Measured before the fix: cold scope refusal against a 5,000-server
producer 1,657,864 B vs a 58,104 B miss; legacy 4 MB refusal 8,500,992 B.

Structural answer, recorded as research.md D16: the gated door decides
in O(1) from the fixed 52-byte header alone — it admits (a) an
administrator reader (FR-001 kind first), (b) an unrestricted agent
(wildcard grant, no pin, no profile) whose tier set covers the
producer's tier bits, now carried in the header's formerly reserved
byte, or (c) a reader whose effective-authorization DIGEST (SHA-256 of
the canonical snapshot: kind, principal, sorted AllowedServers, sorted
Permissions, pin, ProfileScoped, sorted ProfileServers — the profile
name excluded) equals the producer digest in the header; every other
reader is refused without loading anything. Reader facts are computed
once before the transaction so a miss and a refusal do the same work;
CouldHaveProduced is the same `admits` over the same facts. A strictly
wider but bounded reader is now refused: FR-001 obliges refusing
non-supersets, not admitting supersets, so the cells that pinned its
admission are inverted with names stating the new contract (three
agent cells, three user cells, one server-level user cell). The
snapshots bucket stays, keyed by the digest, for administrator
diagnostics only; the LRU, loadSnapshot and producerView are gone.
Frame magic \x02 -> \x03 (never in a release).

Legacy (pre-frame / corrupt-frame) entries are refused and deleted
WITHOUT decoding: the entry count is folded out exactly, the size is
left to the existing cleanup sweep, which now recomputes TotalEntries
and TotalSizeBytes from the bucket it already walks, so the statistics
are eventually consistent while the refusal stays O(1).

Tests (red first): the allocation matrix probes every refusal through a
reopened manager and gains the cold 5,000-server producer and the
pre-frame 4 MB legacy cells (after: wide = small = miss = 25,112 B);
digest-equal, reordered-list, renamed-profile and unrestricted readers
admitted cold; strictly-wider readers refused; the snapshots bucket
proven diagnostics-only; the sweep restores exact stats after a legacy
invalidation and reconciles arbitrary drift. Docs: agent-tokens.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed provenance; admitted readers never get the refusal shape

Codex round 6, cache finding 1: decodeHeaderBytes accepted flag bits
outside recordFlagDenyAll and tier bits outside the emitted mask. With
the digest preserved, an unknown tier bit was admitted on digest
equality and caught only by the header/body agreement check after a
payload-sized decode — the timing class round 5 removed — and an
unknown flag bit was invisible to both the decoder and agreesWith, so
the record was served. The decoder now rejects any reserved bit as
errRecordFrameCorrupt, so the gate classifies such a frame as
unrecognised provenance on the existing O(1) refuse-and-invalidate
path before admission. permBitKnown names the emitted union.

Server finding 1: an admitted (digest-equal / wildcard / admin) reader
whose body proved undecodable or header-disagreeing received the
not-found refusal shape after the body decode. By contract the
non-disclosing definition covers callers the gate refuses, and this
reader was admitted on the header — but so that every response wearing
the refusal shape is one the header decided, the branch now yields the
admitted-class ErrEntryUnreadable (not an ErrUnauthorizedRead, not a
miss), still invalidated, rendered for every caller kind as "cache
entry is unreadable … and has been invalidated". contracts/refusals.md
states the rule.

Tests: TestGetRecordsAs_UnknownHeaderBitsAreUnrecognisedProvenance (a
4 MB entry with one header byte flipped, probed cold by its producer
and an administrator: legacy verdict, invalidated, within the miss
allocation budget; exhaustive decoder sweep), the scoped-reader
malformed-body case beside the administrator one in
manager_legacy_test.go, the admitted-body fixtures retargeted to
ErrEntryUnreadable, and
TestReadCache_AdmittedReaderOfUnreadableEntryGetsDistinctBody at the
handler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Dumbris
Dumbris marked this pull request as ready for review September 16, 2026 15:50
@Dumbris
Dumbris merged commit 46de803 into main Sep 18, 2026
56 checks passed
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.

3 participants