Skip to content

docs(adr): ADR-0099 — posture-adjudicated tiering, EXTERNAL enforcement path, no explicit deny#3109

Merged
os-zhuang merged 1 commit into
mainfrom
claude/adr-0099-posture-adjudication
Jul 17, 2026
Merged

docs(adr): ADR-0099 — posture-adjudicated tiering, EXTERNAL enforcement path, no explicit deny#3109
os-zhuang merged 1 commit into
mainfrom
claude/adr-0099-posture-adjudication

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

目的

ADR-0095(posture 阶梯 + Layer 0,已 Accepted/实现)的裁决收尾篇:阶梯派生了、#2956 把 rung 挂上了 ExecutionContext——但没有任何 enforcement 裁决读它,每个分级判定仍在各自站点用 capability 位重新推导主体等级。这正是一个发布周期里踩了两次的分叉类 bug(#2946 Finding 2:org admin 越租户墙;#2949:explain 与 enforcement 的 posture 分叉)。

决策

与 ADR-0096 的分工

正交、刻意分篇:0096 管准入(这个调用能否进 engine、以谁的身份);0099 管分级(已准入的主体在每层拿到哪一档行)。若 P1–P3 出现 breaking 面,与 0096 D5 翻转搭同一个 major 窗口。

分期(全部证据门控)

P0 矩阵格(本 ADR 随行)→ P1 Layer 0 豁免门 + explain 收敛读 rung(#2956 后已解锁,小)→ P2 Layer 1 级判定收敛 → P3 EXTERNAL(门户立项时)。

编号与验证

  • 0099 推送前已查 open PR 无认领(仓库此前发生过 0096/0097 两次碰撞)。
  • check:role-word ✓(新文件 0 命中)· check:doc-authoring ✓(195 files clean)。
  • 纯 docs 变更,无代码/changeset。

关联:#2920 · #2946 Finding 2 · #2949 · #2947#2956 · ADR-0095/0096/0090/0093/0094/0066。

🤖 Generated with Claude Code

https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs


Generated by Claude Code

…nt path, no explicit deny

Follow-through on ADR-0095: the derived posture rung is carried on the
ExecutionContext since #2956 but no enforcement decision reads it — each tier
decision re-derives principal tier from capability bits at its own site, the
divergence class behind #2946 Finding 2 (org admin crossed the tenant wall)
and #2949 (explain/enforcement posture split).

- D1: posture becomes the single tier-adjudication input (Layer 0 exemption,
  Layer 1 short-circuit tier, explain); capability probe demoted to narrowing
  fallback; behavior-preserving behind an equivalence gate in the authz matrix.
- D2: normative posture → layer action map with six invariants (TENANT_ADMIN
  never crosses Layer 0; enforcement and explain read the same carried value;
  posture selects tier, never side; never bypasses system-only write guards).
- D3: EXTERNAL rung activation chain (audience:'external' → derivePosture →
  explicit-shares-only injection), matrix cells land now, implementation
  demand-gated on an external principal type (portal track).
- D4: explicit deny/muting rejected as an adjudication primitive; needs mapped
  to capability gates / FLS / admission / validity windows; assembly-time
  muting reserved to a future permission-set-groups ADR.

Composes with ADR-0096 (admission vs tiering — orthogonal axes). Number 0099
verified free of open-PR claims before push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 9:57am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation size/m labels Jul 17, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 17, 2026 10:02
@os-zhuang
os-zhuang merged commit a34b11c into main Jul 17, 2026
15 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0099-posture-adjudication branch July 17, 2026 10:02
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…ture rung (#3211 M2) (#3226)

The Layer 0 cross-tenant exemption gate in computeLayeredRlsFilter now
reads the carried ctx.posture rung (#2956) as authoritative: crossing the
tenant wall requires PLATFORM_ADMIN. The hasPlatformAdminPosture capability
probe demotes to a fallback for resolver-less contexts (delegated-admin
bridge, sharing service, getReadFilter consumers), where behavior is
byte-for-byte unchanged. Read and write (insert/update post-image) tenant
checks share the one decision, so they cannot drift. A probe/rung
disagreement logs an [authz/ADR-0099] defect breadcrumb and enforces the
narrower rung verdict. Retires the stale 'posture not plumbed' comment.

Security narrowing (multi-org / @objectstack/organizations only, the G1-
adjudicated deltas): a scoped admin_full_access grant (a) and a piecemeal
platform-capability grant (b) resolve below PLATFORM_ADMIN and are now
walled to their own org on private / platform-global / better-auth objects,
where the posture-blind probe used to exempt them. Fail-safe (rung is a
strict subset of the probe, ADR-0099 I3); recover with an unscoped
admin_full_access grant. Single-org / env-per-database unaffected.

authz-matrix-gate.test.ts: new ADR-0099 P1 describe (narrowing read/write,
PLATFORM_ADMIN stays exempt, TENANT_ADMIN stays walled per I1, resolver-less
fallback preserved, defect-breadcrumb on disagreement / silence on
agreement). dogfood multi-tenant-exemption-posture ledger note updated.
Docs: authorization.mdx records the unscoped-grant rule.

Verified: plugin-security 503 passed, dogfood 296 passed, build 71/71, tsc +
check:role-word + check:doc-authoring clean.

Refs #3211 · ADR-0099 / #3109 · #2946 Finding 2 · #2956


Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 19, 2026
…ect scope bit; amendment + P2′ pins (#3211 M3/M3′) (#3245)

* docs(adr): ADR-0099 two-axis amendment — Layer 1 tier stays the per-object scope bit; P2 superseded by P2' (#3211)

The pre-P2 verification found D2's Layer 1 row was not behavior-preserving
as written: Layer 1's tier input is the per-object super-bit, a
per-principal-x-per-object delegation primitive (custom set granting
viewAllRecords on one object to a non-admin position — the auditor
pattern, the industry-standard shape). Posture is per-principal and
structurally cannot carry per-object scope; enforcing the MEMBER cell
literally would inert a declared grantable bit (the ADR-0049
declared-but-unenforced class) and delete a mainstream delegation
capability.

Amendment (maintainer, 2026-07-18): two orthogonal axes, each with a
single authority — posture is the boundary axis (Layer 0 exemption at
PLATFORM_ADMIN, EXTERNAL replacement; both ladder ends), the per-object
super-bits are the scope axis (the only Layer 1 tier input, unchanged).
D1's single-input principle refines to single-authority-per-axis; new
invariant I7 (scope never crosses a boundary posture has not opened —
enforced by P1); I2 clarified as a per-rung floor. Original P2 rejected;
P2' = documentation cells only (seeded-face agreement, the delegation
cell, I7), zero behavior change.

Inline markers added at the D1 bullet, D2 table, and Sequencing P2 entry;
full rationale in the Amendment section. Docs-only; no changeset.

check:role-word OK - check:doc-authoring 200 files clean.

Refs #3211 - ADR-0099 / #3109 / #3218 / #3226

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

* test(security): ADR-0099 P2′ — pin the per-object super-bit as the Layer 1 scope axis (#3211 M3′)

The two-axis Amendment's documentation cells (zero behavior change). The
original P2 (collapse Layer 1 tier onto posture) was rejected; these cells
pin the corrected model in CI:

- seeded-face agreement: every seeded super-bit holder is already
  >= TENANT_ADMIN, so the two axes coincide on the seeded surface (why the
  collapse looked safe until the delegation case);
- the delegation cell (LOAD-BEARING): a MEMBER holding a delegated
  per-object viewAllRecords/modifyAllRecords on a private object
  short-circuits Layer 1 (sees all rows in-org) AND stays walled by Layer 0
  (org-1 only) — the auditor pattern; a future posture-convergence cleanup
  must keep this green or it silently deletes the capability;
- I7: the holder cannot read/write/insert cross-tenant (the scope axis never
  crosses a boundary posture has not opened);
- contrast: a plain member without the bit is denied on the private object,
  so the bit is a real grantable capability, not conditionally inert
  (the ADR-0049 class the collapse would have introduced).

New fixture invoice_auditor (delegated per-object super-bit). G2 audit:
single authority per axis holds — boundary fact derived once (core resolver)
+ consumed at the Layer 0 gate (fallback probe narrows only); scope fact
evaluated only via hasSuperuser*Bypass.

plugin-security 510 passed; tsc + check:role-word + check:doc-authoring clean;
plugin-security build green.

Refs #3211 - ADR-0099 two-axis Amendment (#3245)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

* chore: changeset for ADR-0099 P2′ two-axis matrix cells (#3211)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants