Uh oh!
There was an error while loading. Please reload this page.
docs(adr): ADR-0127 - no authorization answer is cached without a declared invalidation contract and a TTL bound - #12519
Draft
claude[bot] wants to merge 1 commit into
Draft
Conversation
…TL bound (ADR-0127) Converts the accepted cross-request caching design into a decision record. The durable rule: no authorization answer is cached without a declared invalidation contract and a TTL bound -- both halves, always. D1 requires both halves. D2 fixes "declared invalidation contract" to mean an engine middleware seam, never a call-site list, because a forgotten call site fails as silent over-permission. D3 makes coarse object-level invalidation the baseline and gates keyed invalidation behind a measurement. D4 makes the TTL the correctness contract and the cross-node channel a latency narrowing only, with a non-optional boot-time posture statement. D5 requires entries to expire at min(ttl, nextBoundary), because validity windows see no write at their boundary and write-invalidation is blind there by construction. D6 sets the staleness posture: grants off by default, 0 a real path, knobs in deployment config. D7 keeps the session of record uncached. D8 names the two bypass call sites and the one object that must not be watched. D9 fixes how the pins are written. Records a decision only; no implementation ships here. Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 26, 2026
This was referenced Aug 26, 2026
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11969
Converts the accepted cross-request caching design into
docs/adr/0127-authorization-cache-invalidation-contract-and-ttl-bound.md. One new file; no code, nopackages/spec, nocontent/docs/releases/**.docs/adr/**is governed. This PR stays a draft: not flipped ready, not enqueued, no auto-merge armed. The maintainer's merge is the review record. An open draft here is the finished state of the automated half.Provenance — both rulings
admin_full_accessbecomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663 condition, ruled met: #11663 comment 5404675670 — 2026-08-25Premise check — the "#10348-C" token resolved
The card's surviving gate was written as "#10348-C", a token that does not resolve from outside: #10348 has no A/B/C legs, and "leg C" in this design's vocabulary is the caching leg C (#11966), which is unbuilt.
The dispatch pointed at "§10.1" of the design.⚠️ There is no §10.1 — §10 is "Proposed implementation split", a flat list of six items. The premise list lives in §6, "Dependency disclosure", restated in §9. It names exactly two premises, and the design defines the token itself in §4 (leg A/A1):
⇒ "-C" is Option C of #10348, a sub-part of that card — not leg C (#11966). §6's two premises are #10348-C and #11663; #11966 appears only as item 1 of the implementation split, i.e. something this design proposes to build, never something it depends on.
Both premises are down:
auth-manager.tsnow carries zero platform-admin judges, and the derivation sits atpackages/core/src/security/resolve-authz-context.tswith the remaining sites (admin-standing-surface.ts,last-admin-guard.ts,explain-engine.ts) consuming it rather than re-deriving it. That is the shape §6's "if it lands" branch describes.admin_full_accessbecomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663 ruled 2026-08-25.The acceptance ruling independently spells the gate the same way: "the ADR conversion is the follow-up card, blocked on #10348 (the #11663 condition is met by today's ruling)." No "leg C" reading survives that sentence.
One place the card's summary and the design differ
Recorded because a card summarising a design is exactly where a rule quietly widens.
The card's shorthand is "no authorization answer is cached without a declared invalidation contract and a TTL bound." The design's §0 is narrower and structural:
"A declared invalidation contract" can be read as a maintained list of call sites that remember to invalidate — and §2.1 explicitly rejects that reading, because a forgotten call site fails as silent over-permission, whereas a seam "cannot be forgotten, because writing through the engine is the only way to write at all."
⇒ The ADR keeps the recognisable shorthand as its title (that is the phrasing future citations will use) and adds D2 to fix what it means, so the weak reading is foreclosed on the record rather than left available. This follows ADR-0124's lesson about a rule whose citations drift from what it decides.
What the record decides
org_user_idsmakes the intuitive mapping wrong in the over-permitting direction)authz.invalidatedon the existing at-most-once bus narrows the typical window only, with a non-optional boot-time posture statementmin(ttl, nextBoundary)— validity windows see no write at their boundary, so write-invalidation is blind there by construction0), supported low-seconds range,0a real path, knobs in deployment config never a settings rowrunAs:'user'automation are never served from cache;sys_sessionis never watchedcached ≡ uncached, never a capability's provenanceThe record states plainly that no implementation ships here and nothing is cached today — an ADR records a decision, and writing the rule before the code that must obey it is the intended order.
Changeset — deliberately none,
skip-changesetThis PR ships nothing executable: one markdown file under
docs/adr/, published by no package, imported by nothing, changing no runtime behaviour. There is no user-visible change for a release note to describe. ⛔ An empty changeset was not used — theskip-changesetlabel is this repo's mechanism for saying "publishes nothing", and it is applied.Verification
Gate union derived on the actual changed set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the--repoassertion held against this checkout's origin; the script took its own change set from the merge base — 1 path). Re-derived on the current tree rather than reused from the dispatch brief.All gates re-run on the clean committed tree at
33f7fa07, exit codes captured before any pipe, under the shared verify lock (VERDICT command-exit 0):check:adr-anchorsOK (52 anchored file(s) … 29290 citation(s) across 3670 file(s) resolve)check-adr-links.mjs564 relative link destination(s) under docs/adr/ resolvecheck:doc-authoring391 files clean — no bare metadata literalscheck:pm-governed-merges159 assertions … live: the real generator declared 9 output(s) and certified this treecheck:doc-formula-expressions22 record-scoped formula example(s) across 423 files / 1450 TS blocks judged cleancheck:docs-single-h1180 page(s) under content/docs/ carry no body-level '# ' headingcheck:doc-anchors278 internal #fragment link(s) across 408 source file(s) all resolve to a real headingcheck:adr-0087-registrationthis PR adds no declared-breaking changesetcheck:nul-bytesscanned 6913 text file(s) … no raw ASCII control bytesThe five derived families are the first four rows plus
check-adr-links.mjs; the rest were run because the docs/ADR families were expected to have an opinion rather than assumed inert.check:doc-formula-expressionsfirst exited 1 withPREREQUISITE NOT MET(@objectstack/formula, then@objectstack/lint, not built). That is not measured, not a red gate — recorded here because the exit code alone reads identically to a finding. Both packages were built and the gate then measured green, as above.Anchors cited by the new record were verified to exist before citing:
ADR-0091 D1/D2,ADR-0069 D4, andADR-0049(noD-headings, so cited bare).Out-of-scope finding — filed for triage, no repair attempted here
#12518 remains open and is not addressed by this PR.
scripts/pm/os-verify-lock.shreads the&in a2>&1redirection as a background operator, downgrading a correctly&&-joined command's verdict tobatch-last-exit. Isolated to one variable in this container:true && truecertifies,true > /dev/null 2>&1 && truedoes not. Filed unassigned; no fix attempted here.Generated by Claude Code