Repository navigation
feat(spec,security): OrgScopingEntitlement grows platform-global exemption + unbounded-admin suppression, consumed by Layer 0 arming #12699
Description
Activity
Claim: PM loop (epic objectstack-ai/cloud#1653, maintainer direct-dispatch channel)
Session:session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Branch:claude/issue-12699-org-scoping-entitlement
Worktree:objectstack-issue-12699
Domain:domain:services
File surface:packages/spec/src/security/tenancy-posture.ts(+ generated manifests),packages/plugins/plugin-security/src/**(stop on breach; explain in the report)
Container & model: L,mode:subagent,model: claude-fable-5— Clause-② mandatory tier (widens the spec contract surface)
Clause-②: yes
Serial constraints cleared: no open PR touchestenancy-posture.tsor plugin-security's arming path (checked this hour). Held siblings #11768 (plugin-auth) / #11959 (spec, different file) stay out of this batch.mode:subagenttier not self-provable ⇒needs:contract-reviewchain applies at ACCEPT regardless.
Audit: maintainer instruction 2026-08-26, verbatim: 「那时你的建议并自主完成所有的开发。」; direction for the superbit key accepted with the same ruling.os-dev-report
{ "issue": 12699, "status": "done", "branch": "claude/issue-12699-org-scoping-entitlement", "pr": "https://github.com/objectstack-ai/objectstack/pull/12704", "premise_still_valid": true, "summary": "OrgScopingEntitlement (packages/spec/src/security/tenancy-posture.ts) grew platformGlobalObjects (readonly string[], exact machine names, no wildcards) and suppressUnboundedOrgAdminGrant (boolean), plus Zod twins OrgScopingEntitlementSchema / PlatformGlobalObjectsSchema. plugin-security consumes both when arming Layer 0: the exemption folds into getObjectSecurityMeta — the single choke point feeding the read wall, the ADR-0123 D2 write refusal + forge guard (computeWriteTenantCheckFilter IS the same layer0), the Layer 1 wildcard organization_id policy drop, and posturePermitsCrossTenant — read LIVE off getService('org-scoping') (mirroring the seam's one real consumer, plugin-auth probeEntitledPostures at auth-plugin.ts:581; the dispatch premise 'no consumer inside plugin-security' held, but the seam is not dormant), validated per key with MembershipPolicy-style loud refusal, fail closed. Suppression flips orgAdminSetNameForPosture AND its superseded twin so the existing convergence reconcile revokes stale variants in both directions; fail-closed default protects deployments relying on the auto-grant. Deliberately NOT reached, argued in the PR body: driver native scope (fail-open on NULL org, byte-identical today), objectql system-write refusal (sys_/cloud_/ai_ excluded by construction), postureStampsOrganization (no consumer in-repo; the D5 stamper is the declarer itself). PR body carries the dedicated cloud#1664 consumer section (names, shapes, declaration-site pattern, read-timing). Note: dispatch prompt asked for an HTML-comment report marker; os-dev.md forbids HTML comments in report comments and wins per its own conflict rule — plain-text marker used.", "tests": "All via scripts/pm/os-verify-lock.sh in its DECLARED UNLOCKED MODE (macOS host, no usable flock — its own VERDICT lines record 'UNLOCKED (declared)'). At head b49ebc290: plugin-security full suite 'Test Files 87 passed (87) / Tests 1578 passed (1578)'; spec security domain '7 passed / Tests 197 passed'; new suites deployment-platform-global-exemption.test.ts 12/12, auto-org-admin-grant.test.ts 32/32 (6 new suppression cases), spec tenancy-posture.test.ts 19/19; both package typechecks exit 0 (plugin-security's test layer sits outside every tsc program — pre-existing posture, so new test files are NOT MEASURED by tsc; check:type-check-coverage and check:type-check-debt --re-measure both green: 'none above its recorded number'). spec check:generated: 'All 14 generated artifacts are up to date' after --fix regenerated exactly the 3 proved stale. Gate union derived by scripts/pm/dispatch-gates.mjs (no paths passed; answer from this worktree at 1e61a0de5) — every derived family green locally, incl. check:i18n 'OK (9 package(s))' after CLI build, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check-changeset-no-major, check:nul-bytes; changeset-family gates re-ran on final head b49ebc290. Ablation, one leg per key, vitest-on-src (no dist in the resolution path), mutation proven on disk by anchor grep (1 to 0) and restore by anchor grep (back to 1) plus empty 'git diff HEAD', trap-restored, from committed state: leg 1 (meta fold removed) 4/12 exemption cases red — both read postures, the write-choke case, the valid-key-standing case — then 12/12 green after restore; leg 2 (suppression veto removed) exactly the 4 suppression cases red, then 32/32 green after restore.", "mcp_calls": "0 — all GitHub traffic went through gh REST (repo-scoped reads green on this seat); no MCP GitHub calls", "open_questions": [], "out_of_scope_findings": [ "dispatch-gates' 9 dead-glob spec gate families (quiet-green class) already filed as #12514 — verified by search, not re-filed" ] }ACCEPT — PR #12704. Verified against the branch and GitHub, not the report. Landing once the remaining checks report green (33 running, 0 failing at review time).
- The contract is the ruled one, and its documentation carries the whole decision trail: two optional deployment-fact keys on
OrgScopingEntitlement, both fail-closed in both directions (absent ⇒ byte-identical; junk ⇒ loud refusal that resolves to absent), no wildcards with the audit rationale stated, per-key independent validation with the reason the schema is deliberately non-strict (the service instance carries machinery; unknown keys are not junk). The docblock names the dead seams — paywall bypass, [finding] mergeObjectDefinitions docblock promises 'other props: later value wins' but the implementation silently drops every non-enumerated prop #12680's silent merge drop, and why the per-object authoring channel cannot express a per-deployment fact (it travels with the object; the same object genuinely walls on tenant runtimes). A future reader gets the entire why. - The dispatch's hardest assumption resolved correctly: the exemption folds into
getObjectSecurityMeta— measured as the single choke point feeding the read wall, the ADR-0123 write refusal/forge guard (same layer0), the Layer 1 wildcard policy drop, andposturePermitsCrossTenant— so an exempted object is exempt consistently, not read-exempt-but-stamp-required. The deliberately-not-reached list (driver native scope fail-open today, objectql system-write exclusion by construction,postureStampsOrganizationhaving no in-repo consumer) is argued rather than silent. - Read timing is LIVE at each use site (
deploymentOrgScopingEntitlement()per call), mirroring the seam's one real consumer (plugin-authprobeEntitledPostures— the dev corrected my "no consumer" premise: none inside plugin-security, but the seam is not dormant), avoiding the start()-cache trap named in the dispatch. - Suppression is coherent, not bolted on: it flips both the grant name AND its superseded twin so the existing convergence reconcile revokes stale variants in either direction — the doc records why D4's "Layer 0 bounds it" rationale stops holding exactly when a deployment carves objects out of the wall.
- Tests: 12/12 exemption + 32/32 grant (6 new suppression cases) + 19/19 spec shape; both ablation legs red on exactly the discriminating cases, mutations proven on disk. Marker-spelling conflict resolved per the role file again (A), consistent with the Phase-0 ruling.
Contract review (
needs:contract-review→ PASS): reviewed the spec increment as an actual diff —tenancy-posture.ts+78 (two optional readonly keys + two Zod exports + doc), four generated manifests mechanically updated, no existing key/shape/default moved, accept-set change is purely additive and fail-closed. Reviewer tier: this dispatch seat, machine-verified atCONTRACT_REVIEW_TIER(claude-fable-5, host-recorded transcript field, last 3 records) — self-review of an own-dispatched card per the 2026-08-21 relaxation; the seat wrote no code on this card. Label cleared on both carriers with this comment as the standing record.Next: land → unlock Phase 2 (cloud#1664) with the PR body's consumer section as its verbatim input.
- The contract is the ruled one, and its documentation carries the whole decision trail: two optional deployment-fact keys on
- added 3 commits that reference this issue
on Oct 7, 2026
Part of objectstack-ai/cloud#1653 (epic — the ruling and the measured evidence live there; read the ruling comment and cost-analysis comment 5410466814 before designing).
Deliverable — Phase 1: the deployment-entitlement contract Layer 0 consumes
OrgScopingEntitlement(packages/spec/src/security/tenancy-posture.ts) grows two declared-by-the-runtime keys, both consumed by plugin-security when arming the Layer 0 wall:platformGlobalObjects?: readonly string[]): objects the deployment declares platform-owned — Layer 0 must not wall them on this deployment. Semantics: per-deployment, declared by the mounted org-scoping service, NOT authorable app metadata. Fail closed: key absent ⇒ every object walls exactly as today. Junk shapes refused loudly at the seam (theMembershipPolicyentry-validation precedent).organization_adminrole's unboundedviewAllRecords/modifyAllRecordssuperbits (the epic's measured regression:sys_secret403→200, 17 objects regaining cross-tenant writes, cloud#1257's write gate voided). Fail closed the same way: absent ⇒ today's behaviour.Why this exists (ruled, not re-adjudicable)
The alternative seams are dead: host self-declaration of the boundary is a paywall bypass (ruled out), and the objectExtensions merge silently drops
tenancy(objectstack#12680 — measured). This contract keeps the paywall intact (only a mounted enterprise runtime can declare anything) while letting a deployment that genuinely IS walled carve out its platform tables.Mechanism assumptions (MUST verify; refuting = good outcome)
supportedPosturestoday and mirror its read pattern — this seat's grep found no consumer inside plugin-security, so locate where the tenancy service reads the entitlement and whether SecurityPlugin should read via the same path or viagetService('org-scoping')directly. Argue the placement in the report.security-plugin.ts~6345/~6639 readstenancy.enabled/systemFields.tenantfrom the live registry) is the single choke point, so the exemption composes there and nowhere else.Tests
Exempted object not walled / non-exempted still walled / absent-key = byte-identical behaviour to today / junk entitlement refused loudly / superbit suppression on + off / the entitlement shape itself. Ablation on both keys.
Consumer
cloud Phase 2 consumes this verbatim: PR body MUST publish the final key names, shapes, and the exact declaration site pattern.
Clause-②: YES (widens the spec contract surface) — contract-tier dispatch,
needs:contract-reviewchain applies at ACCEPT regardless.