Skip to content

fix(authz)!: make the group posture an entitlement again — ADR-0105 D12 correction - #3570

Merged
os-zhuang merged 2 commits into
mainfrom
claude/adr-0105-group-entitlement
Jul 27, 2026
Merged

fix(authz)!: make the group posture an entitlement again — ADR-0105 D12 correction#3570
os-zhuang merged 2 commits into
mainfrom
claude/adr-0105-group-entitlement

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #3559. Tracking: #3541.

What was wrong

#3559 read ADR-0105 D12 as "the group wall ships open" and made the posture self-activatingtenancy-service returned isolationActive: true for group without ever probing for @objectstack/organizations.

Two consequences, both bad:

  1. group became a free multi-org path around the isolated gate (ADR-0081 D2). Anyone wanting multi-org would simply pick group. The weaker isolation ended up as the free one — not a boundary anyone would draw deliberately.
  2. A silent-degradation hole. os serve still gated the enterprise package load on OS_MULTI_ORG_ENABLED, so OS_TENANCY_POSTURE=group skipped both the package load and the ADR-0093 D5 fail-fast — booting an unwalled multi-org deployment with no warning. That is precisely the ADR-0049 class the D5 guard exists to close.

The distinction that was missed

Open code is not free activation. The wall's implementation has always lived in the open packages — equally true of isolated, whose Layer 0 wall sits in plugin-security and is gated on a service the enterprise package registers. Cloud ADR-0016's 铁律 (强制免费、治理收费) guarantees that a deployment running a multi-org shape is safe; that is satisfied by refusing to run one unwalled, not by giving the posture away.

Changes

  • tenancy-servicegroup probes org-scoping exactly like isolated. Without it the posture resolves to single and reports degraded.

  • os serve — the D5 boot guard now keys off the resolved posture, not OS_MULTI_ORG_ENABLED, and its message is posture-aware. A group request without the runtime refuses to boot unless OS_ALLOW_DEGRADED_TENANCY=1.

  • New seam: the runtime declares what it entitles. org-scoping may expose supportedPostures (OrgScopingEntitlement, @objectstack/spec/security); the open side honours it and fails closed on anything unlisted. Whether group and isolated are one commercial tier or two is packaging policy, which belongs to the commercial runtime rather than hard-coded in open core. Omitting the field entitles every walled posture, so existing runtimes are unaffected.

  • organization_id stamping returns to the enterprise runtime.

    This one is worth spelling out. feat(authz)!: group tenancy posture + org scope as a first-class dimension — ADR-0105 Phase 0/1 #3559 moved auto-stamping into the open engine, which removed the closed package's only load-bearing runtime duty. The framework checks the gate by service truthiness, so after that change a five-line forged registration —

    class Fake { async init(ctx) { ctx.registerService('org-scoping', this); } }

    — would have yielded a fully working multi-org deployment. Before it, the same forgery gave you a wall with no stamper: rows land with organization_id = NULL, the wall hides them, and you get a broken deployment instead of an unlicensed working one. Stamping is back where it was.

    Write-side validation stays open and is unchanged, bulk-insert coverage included — rejecting a forged organization_id is a security property, not a packaging one. Only filling an absent value moved back.

  • Default-organization bootstrap returns to single-only; every walled posture keeps its existing owner (ADR-0081 D1).

Still open (not this PR)

Presence-of-service remains a coarse gate — cloud ADR-0081 already listed licence-gating beyond package presence as a deferred ADR-0022 follow-up. The supportedPostures seam added here is the right carrier for a real licence check when that lands. Worth stating plainly: any check living in open code can be patched out, so the realistic goal is raising effort and making circumvention a deliberate licence violation, not technical prevention.

Verification

  • pnpm build — 71/71
  • pnpm test — 131/131 (serial). In the default parallel run packages/lint/src/lazy-deps.test.ts fails reproducibly: it spawns child node processes that import packages/lint/dist while turbo restores that same dist from cache. Pre-existing infrastructure race, not related to this diff — the rule added in feat(authz)!: group tenancy posture + org scope as a first-class dimension — ADR-0105 Phase 0/1 #3559 is a pure, import-free module and the test's structural check passes. Confirmed by TURBO_CONCURRENCY=1 pnpm test going green.
  • pnpm lint + all five repo grep guards — clean
  • Spec drift gates (api-surface regenerated, docs, spec-changes, upgrade-guide) — in sync

New coverage in tenancy-service.test.ts: group degrades without the runtime; entitlement honoured / withheld / empty / throwing; entitlement never consulted when the runtime is absent. security-plugin.test.ts and authz-matrix-gate.test.ts assert the engine does not stamp while still rejecting forged values, bulk included.

🤖 Generated with Claude Code

https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL


Generated by Claude Code

… D12 correction

The first ADR-0105 wave (#3559) read D12 as "the `group` wall ships open" and
made the posture self-activating: it never probed for `@objectstack/organizations`.
That turned `group` into a free multi-org path around the `isolated` gate
(ADR-0081 D2), and left the WEAKER isolation as the free one — not a boundary
anyone would draw deliberately.

The distinction missed: open code is not free activation. The wall's
implementation has always lived in open packages — equally true of `isolated`,
whose Layer 0 wall sits in plugin-security gated on a service the enterprise
package registers. Cloud ADR-0016's 铁律 guarantees a deployment RUNNING a
multi-org shape is safe; that is satisfied by REFUSING to run one unwalled
(ADR-0093 D5), not by giving the posture away.

- tenancy-service: `group` probes `org-scoping` exactly like `isolated`; without
  it the posture resolves to `single` + `degraded`.
- serve.ts: the ADR-0093 D5 boot guard keys off the resolved POSTURE, not
  `OS_MULTI_ORG_ENABLED`. `OS_TENANCY_POSTURE=group` previously skipped both the
  package load AND the fail-fast, silently degrading to an unwalled deployment —
  the exact ADR-0049 class that guard exists to close.
- New seam: `org-scoping` may declare `supportedPostures`
  (`OrgScopingEntitlement`); the open side honours it and fails closed on
  anything unlisted. Whether `group` and `isolated` are one tier or two is
  packaging policy, which belongs to the commercial runtime rather than
  hard-coded in open core. Omitting it entitles every walled posture.
- `organization_id` STAMPING returns to the enterprise runtime. The previous wave
  moved it into the open engine, removing the closed package's only load-bearing
  runtime duty — a five-line forged `org-scoping` registration would then have
  produced a fully working multi-org deployment. Back where it was, a forged
  registration yields NULL-org rows the wall hides.

  Write-side VALIDATION stays open and unchanged, bulk-insert coverage included:
  rejecting a forged organization_id is a security property, not a packaging one.
  Only filling an ABSENT value moved back.
- Default-org bootstrap returns to `single`-only (ADR-0081 D1).

Operator note: `OS_TENANCY_POSTURE=group` without the enterprise runtime now
refuses to boot rather than running single-org.

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

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 6:45am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-security, packages/qa, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…upersedes

The env var gates whether a multi-organization deployment boots, and the boot
failure message names it, but it had no entry in the environment-variables
reference. Records the three postures, the derivation from OS_MULTI_ORG_ENABLED
that keeps existing deployments unchanged, the refuse-to-boot behavior on an
unrecognized value, and the enterprise-runtime requirement for both walled
postures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 07:03
@os-zhuang
os-zhuang merged commit 7fb436c into main Jul 27, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0105-group-entitlement branch July 27, 2026 07:03
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…led, not open (#3570) (#3603)

Founder ruling: OS_TENANCY_POSTURE=group is not an open-edition feature.
The code was already corrected in #3570 (group probes org-scoping exactly
like isolated; no runtime => single + degraded; an os serve boot configured
for group fail-fasts). This brings the ADR text in line:

- D12 rewritten to the code-vs-activation split: the wall's implementation
  ships open, posture activation is entitled for BOTH multi-org postures;
  iron-rule safety is satisfied by refusing to run an unwalled group boot,
  not by free activation. ADR-0081 D2's commercial line stands.
- Amendment block records the original text, why it was wrong (inverted
  ADR-0081 D2 + the silent-degradation hole), and the #3559 -> #3570 trail.
- Consequences clause updated to match; header Status moves to Accepted with
  the amendment note; Tracking points at #3541 / #3539 / #3540 / cloud #874.


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

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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants