Skip to content

authz caching leg B: grants cache — coarse invalidation, default TTL=0 (off), expiry-boundary expiry, bypass list #11971

Description

@os-support-ai

Leg B (ship third — the leg the design is really about) of the accepted #11633 cross-request caching design. Provenance: design document = #11633 comment 5394942824 (§4 leg B, §10.4); maintainer acceptance = #11633 comment 5404681591 (2026-08-25, verbatim 「接受你的建议,继续」, forks 1A / 2B / 3A / TTL-0). Filed by PM session session_01KWRU3s15AJz7PGW7a7wdCh.

Blocked-by: #10348
Blocked-by: #11968

(#10348 per the design's dependency disclosure; #11968 is the invalidation substrate this leg consumes.)

Ruled shape (⛔ not re-litigable here):

  • Fork 1A — coarse invalidation: any write to a watched object retires the whole leg-B cache. Keyed invalidation is ⛔ not a scheduled follow-up; it needs a write-heavy-tenant measurement first.
  • Fork 4 — default TTL 0 (cache off by default), supported low-seconds range; enabling it is the deployment's explicit acceptance of a staleness window.
  • Expiry-boundary rule (accepted as part of the ruling): entries expire at min(ttl, nextBoundary) — ADR-0091 validity windows see no write at their boundary, so write-invalidation is structurally blind there and the timer is the only mechanism for that class.
  • Bypass list: explain-engine.ts:473 and service-automation/src/plugin.ts:812 read uncached — an explainer answering from cache explains a state that no longer exists at exactly the moment an admin verifies a revocation.
  • Keying traps (measured): sys_session stays out of the watched set (last_activity_at writes every minute would destroy the cache); a sys_member write retires the organization's entries, not the writing user's (org_user_ids depends on other users' rows).

Test plan: carries pins 1–7 and 9 from the design §7, plus the required ablation of write-invalidation (prove the TTL bound alone is insufficient for the read-after-write pins — i.e. the invalidation is load-bearing).

Discipline: Clause-②: yes (authorization answers); needs:contract-review at review; contract-review tier mandatory.

Acceptance criterion: with the cache ON (TTL>0), grant/revoke/role/session mutations are observed by the next request on the writing node (write-invalidation, not TTL); with default config the cache is OFF and behaviour is bit-identical to today; "cached answer ≡ uncached answer" identity pins green.

Activity

  1. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    ⭐ Unlock: both blockers discharged and verified by content — pm:blocked → pm:queue

    PM seat domain:services, session session_0194kbQJxUvv2yvsGRtuXpP5. ⛔ Not claiming and ⛔ not dispatching — see the tier note at the end.

    This card was found by a substance sweep of this lane's pm:blocked inventory, not by an unlock trigger. Both of its blockers closed — one four days ago, one yesterday — and nothing re-read the card.

    Blockers — closed, and their deliverables measured on origin/main@b9dd923b9

    ⛔ "Upstream is closed" is not the test. Each deliverable was read in the tree:

    Blocker Closed Deliverable verified by content
    #10348 2026-08-24, PR #11686 merged The id-shaped judge is consolidated and consumed: isPlatformAdminUserId at packages/plugins/plugin-auth/src/auth-manager.ts:1683, :2912, :3285
    #11968 2026-08-27, PR #12652 merged All three substrate pieces present — see below

    #11968's three deliverables, individually:

    1. Engine-seam write epoch — packages/objectql/src/write-epoch.ts:13 states it is "a generalisation of the private writeEpoch field"; packages/objectql/src/engine.ts:3268 readonly writeEpoch = new WriteEpoch(), bumped at :3354; already consumed by packages/core/src/security/resolve-authz-context.ts:891.
    2. The authz.invalidated channel — packages/core/src/security/authz-invalidation-channel.ts:73 export const AUTHZ_INVALIDATED_CHANNEL = 'authz.invalidated', with AuthzInvalidatedPayload at :106, the bridge at packages/objectql/src/authz-invalidation-bridge.ts, exported from packages/core/src/security/index.ts:181.
    3. Boot-time posture statement — packages/core/src/security/authz-cache-posture.ts exists, with the loud line at :103/:122: "authz.invalidated invalidation bus — … A grant revoked on …".

    ⚠️ And the ruling's hard requirement survived into the code, which matters because this leg is the consumer that could quietly contradict it: authz-invalidation-channel.ts:10 — "⭐ THE TTL IS THE CORRECTNESS CONTRACT. THIS CHANNEL IS NOT." ⇒ this leg's TTL bound stays the correctness mechanism; the bus only narrows the window.

    The "ship third" ordering is also satisfied

    This card's body says leg B "ships third", which is an ordering constraint over and above its Blocked-by: set — so it was checked rather than assumed. The full #11633 tranche:

    Card Leg State
    #11968 substrate ✅ closed
    #11966 leg C — localization read cache ✅ closed
    #11967 leg D — getMetaItems overlay cache ✅ closed
    #11969 ADR conversion (governed surface) ✅ closed
    #11971 leg B — grants cache the only one left

    ⇒ Nothing precedes it any more. This is the last leg of the tranche, and "the leg the design is really about" by its own description.

    ⛔ Above this seat's tier — flipped, not dispatched

    This card declares Clause-②: yes (authorization answers) with needs:contract-review at review and the contract-review tier mandatory. This seat measures claude-opus-5; the contract-review tier is claude-fable-5. It can dispatch at tier but cannot clear the label, flip ready, or arm the merge — so landing it here would strand the PR.

    ⇒ Left pm:queue, unassigned, for a seat that can carry it end to end. Nothing else about the ruled shape is touched by this comment: forks 1A / 4 (default TTL 0, cache off), the expiry-boundary rule min(ttl, nextBoundary), the bypass list (explain-engine.ts:473, service-automation/src/plugin.ts:812), and the measured keying traps (sys_session stays out; a sys_member write retires the organization's entries) all stand, ⛔ not re-litigable here. The required ablation of write-invalidation — proving the TTL bound alone cannot satisfy the read-after-write pins — remains part of the test plan.


    Generated by Claude Code

  2. zhuangjianguo commented on Aug 30, 2026

    @zhuangjianguo
    Collaborator

    ⚖️ 契约复审 PASS — PR #13415(项目总监席,达档机读 last_served_model = claude-fable-5)

    维护者召唤「处理 CONTRACT_REVIEW」下执行。审的是契约增量:授权答案面的跨请求缓存(#11633 leg B,条款② yes)。

    对裁定逐条核(2026-08-25 接受的设计,非重开):

    • Fork 4 默认关是真路径:openUserGrantsCache 在 TTL≤0 时于任何 wiring 之前返回 undefined——零 middleware、零 epoch 订阅、零存储,实测于模块源码非采信正文。
    • Fork 1A 两缝如裁:缝 1 中间件按 GRANTS_CACHE_WATCHED_OBJECTS 过滤、finally 内 bump(含 throw,过失效系安全方向);缝 2 只吃非 'write' reason(metadata/remote/manual),本地写不重复消费——sys_session 陷阱双向守住(不入 watched set + raw epoch 不用于本地写)。
    • In-flight 写安全:entry 存 genAtOpen,命中要求 gen === state.gen ⇒ 跨写解析 dead on arrival。
    • Expiry-boundary 规则:min(ttl, nextBoundary);独立核验了 load-bearing 声明「boundary 扫描恰覆盖 isGrantActive 检过的行」——PR head 上 isGrantActive 全部调用点恰为 members(:462/:485)、userPositionRows(:504)、upsRowsAll(:525)三族,与扫描入参 [...members, ...userPositionRows, ...upsRowsAll] 一一对应,无漏族;含 inactive 行(future valid_from 翻转)正确。nextGrantValidityBoundary 与 isGrantActive 共用解析器,unparseable 永久 fail-closed 故不设界,自洽。
    • Bypass 名单两站点如裁(explain-engine buildContextForUser、automation runAs:'user'),bypass 不读不写缓存,双半 pin 在案。
    • 偏离申报收下:seeds 入键(弃 §4 B.2 的 seedless+重放)系实测否定「pure prepend」前提后取设计点名的另一选项——种子影响 ADR-0095 posture 档与 needsUserRow 查询抑制,外部重放即 [finding] Three spellings of the ADR-0068 platform-admin read now live in plugin-auth, and one of them skips the system read context #10348 漂移形状;键为 JSON 数组防分隔符别名。
    • 键安全:WeakMap per-engine 隔离、wiring 半途失败投毒不缓存、命中发 structuredClone 防调用方污染。
    • 消融方向对:去掉双 bump 后恰 5 红(写失效族)/23 绿(TTL/boundary/identity/off),失效链 load-bearing 得证。
    • changeset 评级(core minor / 两插件 patch)与 env-vars 文档行更新准确,「explainer 与 runAs:'user' 恒不缓存」已写入部署文档。

    处置(清标即落地):PR 侧清 needs:contract-review(卡侧未挂,dev 依 2026-08-28 裁定留予本席,无需清);PR 转 ready + 挂 auto-merge(非受管 code PR)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions