Uh oh!
There was an error while loading. Please reload this page.
feat(spec,security): OrgScopingEntitlement grows platform-global exemption + unbounded-admin suppression, consumed by Layer 0 arming - #12704
Conversation
…+ unbounded-admin suppression Part of objectstack-ai/cloud#1653 (Phase 1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts type alias Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f262c14437f5343972b5672e345b652f1824accb && git checkout f262c14437f5343972b5672e345b652f1824accb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15bf9e859e56862e6ebe7b5c42404de103362457 b49ebc290c0161fe48e0a7940603620af85a70ad && git checkout -B drift-repro 15bf9e859e56862e6ebe7b5c42404de103362457 && git merge --no-ff b49ebc290c0161fe48e0a7940603620af85a70ad
node scripts/docs-audit/affected-docs.mjs --json 15bf9e859e56862e6ebe7b5c42404de103362457
|
os-zhuang
commented
Aug 27, 2026
Enqueue attribution note: the merge-queue enqueue mutation for this PR was fired with the |
Uh oh!
There was an error while loading. Please reload this page.
os-regen-merge four-step: merge commit first (deferral recorded), then the deliberate manifest + surface-baseline deletions re-applied on the merged tree and the whole chain regenerated with repo tooling. Both sides' sources verified present (retirement tombstone + registry entries; #12704 OrgScopingEntitlement tenancy-posture + capabilities). Co-Authored-By: Claude <noreply@anthropic.com>
Fixes#12699
Part of objectstack-ai/cloud#1653
What
OrgScopingEntitlement(packages/spec/src/security/tenancy-posture.ts) grows two per-deployment wall-shaping keys, both declared by the mountedorg-scopingservice (per-deployment, never authorable app metadata), both consumed by plugin-security when arming the Layer 0 organization wall, both fail closed — an absent (or junk) declaration leaves behaviour byte-identical to today:platformGlobalObjects— objects THIS deployment declares platform-global. Layer 0 does not wall them here, exactly as if the object had declaredtenancy: { enabled: false }itself — but only on this deployment. It composes with, never replaces, the per-object authoring channel, which stays wrong for these objects because they DO wall on tenant runtimes (same object, different deployments, different wall).suppressUnboundedOrgAdminGrant— arming a walled posture must not auto-grantorganization_admin's unboundedviewAllRecords/modifyAllRecordssuperbits: the membership-driven auto-grant hands outorganization_admin_no_bypassinstead (the epic's measured regression:sys_secret403-to-200, 17 objects regaining cross-tenant writes, cloud#1257's write gate voided by the posture flip).Dead seams stay dead, per the ruling: host self-declaration of the boundary is not revived (the org-create gate is untouched — only a mounted enterprise runtime can declare anything here), and nothing rides through
objectExtensions(the merge silently dropstenancy— objectstack#12680).Consumer contract — for cloud Phase 2 (objectstack-ai/cloud#1664)
Key names and TypeScript shapes (verbatim)
platformGlobalObjectsentries are exact object machine names (regex^[a-z_][a-z0-9_]*$) — no wildcards, no globs, no empty strings. One junk entry voids the whole key (refused loudly, never partially honoured), and refusal resolves to "absent" = everything walls.@objectstack/spec/security:OrgScopingEntitlementSchema(non-strict object — a live service instance's extra machinery never disqualifies its declaration) andPlatformGlobalObjectsSchema(+ type aliasPlatformGlobalObjects). Consumers validate per key: junk in one key never voids the other's valid declaration.Declaration-site pattern
The keys are readonly properties on the object registered as the
org-scopingservice — the same object that already declaressupportedPostures. The in-repo model isSimulatedOrgScopingPlugininpackages/verify/src/harness.ts; the cloud runtime declares the same way:Read-timing semantics
start().getService('org-scoping')at each consuming decision — mirroring the seam's one pre-existing consumer, plugin-auth'sprobeEntitledPostures(auth-plugin.ts:581, live per call because the provider registers after the reader's init). Validation is memoized per service instance (WeakMap); a junk key warns once per boot, naming the key and the declared value, and fails closed.getObjectSecurityMeta), which is cached per object and invalidated on metadata change or restart. Effective contract for the declarer: declare at registration and treat the declaration as immutable for the kernel's life; a re-registered service instance re-validates. Live resolution is ordering-robust by construction: a deployment where the wall is ARMED hadorg-scopingregistered beforestart()captured the posture, so the declaration is present from the first read; where it registered too late, posture resolvessingleand Layer 0 is inert, so the exemption decides nothing.sys_memberreconcile and at thekernel:readybackfill ("org-admin grant backfill complete" in boot logs). Toggling it converges standing grants in BOTH directions via the existing superseded-variant reconcile — proven in tests, so the fail-closed default protects any deployment that RELIES on today's auto-grant.[security/#12699].What the exemption reaches — and deliberately does not
Reaches, through the single meta fold (all proven or structurally derived in this PR):
computeTenantLayer0Filter'stenancyDisabledinput);computeWriteTenantCheckFilterIS the samelayer0, so read and write cannot disagree;organization_idpolicy drop (thesys_packagecross-org-catalog precedent — without it every read of an exempted catalog would return zero rows);posturePermitsCrossTenantand the superuser write-check bypass — an exempted object carries full platform-global posture semantics on this deployment.Deliberately NOT reached — boundaries Phase 2 must know:
buildDriverOptions(objectqlengine.ts) still threadstenantIdfor exempted objects (it reads schema-declared tenancy only), andSqlDriver's per-table scope is fail-open on NULL org (organization_idequals the active org OR IS NULL) — so NULL-org platform rows stay readable and behaviour is byte-identical to today. Session-context inserts still stamp the active org at the driver, also today's behaviour. Widening this would change behaviour beyond the ruling; if Phase 2 measures a need, that is a new card.system-write-organization.ts) keys on schema-declared tenancy, and thesys_/cloud_/ai_namespaces are excluded by construction — declare exempted platform tables in those namespaces (every control-plane table already is).postureStampsOrganizationhas no consumer in this repo; the D5 stamper is the enterprise runtime — the declarer itself, which holds the exemption list first-hand.Dispatch mechanism assumptions — verified
supportedPosturesconsumer found. The dispatch premise "no consumer inside plugin-security" is confirmed — and the seam is NOT dormant: plugin-auth consumes it (auth-plugin.ts:581feedingcreateTenancyService.probeEntitledPostures, evaluated live insideisolationActive()). SecurityPlugin reads the entitlement viagetService('org-scoping')directly, mirroring that pattern rather than wideningTenancyService(which stays posture-only — routing deployment declarations through it would grow a second interface for zero consumer benefit).security-plugin.ts~6383getObjectSecurityMetaand ~6677loadObjectFieldNames) merge into one local incomputeLayeredRlsFilter; the write path derives from the samelayer0. The fold lands ingetObjectSecurityMetaalone and every consumer follows. Genuinely separate wall-adjacent consumers (driver native scope, system-write refusal, D5 stamper) are enumerated above with the argument for leaving each untouched — none produces the "exempt from read wall but still refused/required to stamp" inconsistency: the only refusal seam (ADR-0123 D2) derives from the exempted layer0, and driver stamping never refuses.orgAdminSetNameForPosture(auto-org-admin-grant.ts), wired from SecurityPlugin'ssys_membermiddleware and thekernel:readybackfill (the "org-admin grant backfill" boot log). Suppression flips BOTH the granted and the superseded variant, so the existing convergence machinery revokes the stale one on the next reconcile — and the fail-closed default (absent = today's grant) is pinned by the untouched ADR-0105 D4 test block plus an explicitsuppressUnboundedOrgAdminGrant: falsecase.Tests
New/extended, all green at
b49ebc290:packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts(12 cases): exempted object not walled (isolated+group, read AND the D2 write derivation) / non-exempted sibling still walled / absent declaration byte-identical (regression pin) / junk shapes refused loudly (bare string, wildcard entry poisoning the whole key, junk suppress flag not voiding the valid key, warn-once-per-boot) /tenancy.enabled:falsecomposition /singleposture inertness / arming-log surface.packages/plugins/plugin-security/src/auto-org-admin-grant.test.ts(+6 cases): suppression on (isolated+group) / explicit off byte-identical / convergence both directions / backfill threading incl. the orphan sweep.packages/spec/src/security/tenancy-posture.test.ts(19 cases): schema accepts valid declarations and live-service shapes, refuses junk; schema/interface parity witness.Suites: plugin-security full suite 87 files / 1578 tests passed; spec security domain 7 files / 197 tests passed;
pnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/plugin-security typecheckgreen. plugin-security's test layer sits outside every tsc program (pre-existing:tsconfig.jsonexcludes test files and the package has no test-typecheck sibling), so the new test files' types are NOT MEASURED by tsc — same posture as every existing test there;check:type-check-coverageandcheck:type-check-debt --re-measureboth green.Ablation, one leg per key (vitest runs on
src/via relative imports — no dist resolution in the loop; mutation and restore both proven on disk by anchorgrep -c, restore additionally by an emptygit diff HEAD):platformGlobalObjects.has(object)anchor 1 to 0 on disk) — 4 of 12 exemption cases went red (both read postures, the write-choke case, the valid-exemption-standing case); restored (anchor back to 1, clean diff), 12/12 green.postureEnforcesWall(posture) && !suppressUnboundedanchor 1 to 0 on disk) — exactly the 4 suppression cases went red; restored (anchor 1, clean diff), 32/32 green.Gates
Derived from the real changeset via
node scripts/pm/dispatch-gates.mjs(answer taken from this worktree at1e61a0de5, re-checked at headb49ebc290). All derived families ran locally and green, including:check:generated("All 14 generated artifacts are up to date"),check:i18n("OK (9 package(s)…)" after building the CLI),check:type-check-debt --re-measure("none above its recorded number"),check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:test-source-alias,check-changeset-no-major,check:nul-bytes, and the docs family (the reference-doc regeneration in this diff is generator output).scripts/pm/check-half-states.mjsran authenticated and is report-only. The twodocs-drift-check.ymlscripts are PR-context CI jobs and run on this PR. Changeset-family gates re-ran on the final headb49ebc290.Verification mode declaration: this ran on a macOS host where
scripts/pm/os-verify-lock.shhas no usable flock — every build/test above went through the entry point in its DECLARED UNLOCKED MODE (its own VERDICT lines recordUNLOCKED (declared)); nothing was serialized against sibling agents.Generated-artifact churn in this diff (
api-surface/security.json,export-origins/security.json,authorable-surface/security.json,json-schema.manifest/security.json,content/docs/references/index.mdx,content/docs/references/security/misc.mdx) is generator output for the three new spec exports, proven stale-then-regenerated bycheck:generated --fix(never the whole set).Session: https://claude.ai/code/session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Generated by Claude Code