docs(adr): ADR-0127 - no authorization answer is cached without a declared invalidation contract and a TTL bound - #12519
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>
os-zhuang
left a comment
There was a problem hiding this comment.
Maintainer-authorized merge of a governed surface — this review transcribes the maintainer's ruling; it is not a seat self-landing.
Provenance (who / verbatim / where):
- Who: the maintainer (project owner), 2026-08-27, PM adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh. - Verbatim, untranslated: 「12519 同意,你帮我合并吧」
- Context: the ruling followed a full business-angle briefing of this ADR in the same session — the D1 rule (no authorization answer cached without an observed engine write seam AND a TTL bound), the business meaning of D4/D5/D7/D8/D9, the fact that nothing is cached today and no behavior changes, and the disclosed unguarded
cookieCachedoor. The maintainer accepted the transcription as faithful to his 2026-08-25 ruling on #11633 (「接受你的建议,继续」) and explicitly instructed this seat to perform the merge on his behalf.
Scope of this authorization: this PR only. The standing rule is unchanged — governed surfaces (docs/adr/**, .claude/**, skills, AGENTS.md, CLAUDE.md) stay draft for the maintainer's own merge unless he grants a per-PR authorization like this one. Future readers: do not cite this PR as precedent for seat-landing governed changes without such a recorded verbatim authorization.
Content basis: this seat performed the contract-review-tier reading of the full 164-line ADR diff at head 33f7fa07 before briefing the maintainer; the diff decides D1–D9 exactly as the PR body tables them, ships no code, and touches one file under docs/adr/.
Approving so the Governed Surface Queue Guard's merge_group run — now in the required set per #12427 — sees the decisive APPROVED review it exists to demand.
Generated by Claude Code
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