Skip to content

feat(spec): stamp-only tenancy.organizationField — read-neutral organization declaration for audit stamping (#8778) - #8905

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-8778-audit-org-field
Aug 15, 2026
Merged

feat(spec): stamp-only tenancy.organizationField — read-neutral organization declaration for audit stamping (#8778)#8905
os-project-manager merged 2 commits into
mainfrom
claude/issue-8778-audit-org-field

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#8778

Implements the maintainer-ruled option A (delegated adjudication, issue comment 5299847449): a read-neutral, stamp-only organization declaration — tenancy.organizationField — so audit rows can follow the organization a record is ABOUT on objects that must stay unwalled. This is the remaining half of #8707 (the precedence half landed in PR #8777); #8707 stays open for its own seat to verify and unblock — no closing keyword targets it here.

What changed

  • packages/spec/src/data/object.zod.tsTenancyConfigSchema gains optional organizationField with a describe() that states loudly it is stamp-only and does NOT tenant-scope reads. Liveness ledger entry added (live, evidence = the audit writer); authorable-surface, reference docs and liveness counts regenerated (check:generated --fix, only the proved-stale set).
  • packages/plugins/plugin-audit/src/audit-writers.tsresolveRecordOrganizationField consults the declaration as limb 0, ahead of the ADR-0066 enabled: false opt-out (an author declaring it on an unwalled object is stating exactly that the trail follows the record even though no wall does), guarded by the same TenancyConfigSchema.tenantField 的默认值是 'tenant_id',但平台的租户列叫 organization_id —— 两个 spec 文件对「默认值」的说法互相矛盾 #5315 field-presence check tenantField carries.
  • packages/platform-objects/src/identity/sys-api-key.object.ts — declares tenancy: { enabled: false, organizationField: 'active_organization_id' }. The revocation row now lands behind the wall of the KEY's organization, not the revoker's.
  • Scope pin honoured: one stamp-only key; the resolver is its only consumer; the describe(), the schema JSDoc, the ledger note and the resolver doc all state that a second consumer needs its own ruling.

The pinned-gap test flipped as designed

The ⛔ KNOWN GAP case in audit-writers.test.ts:1414 went red exactly as it was written to, and is rewritten to expect org-key, plus: the #5315 fall-through case, an organizationField-outranks-tenantField precedence pin, and a control pinning that WITHOUT the declaration the actor's org is still stamped (no hidden column-name heuristic).

Read-neutrality — pinned beside each named read path

  • applyTenantScope / injectTenantOnInsert (driver-sql): new suite in sql-driver-tenant-scope.test.ts — a walled object declaring the key keeps scoping/injecting on organization_id (a row whose wall and stamp columns disagree is the discriminating fixture), and the sys_api_key-shaped object stays unwalled with no injection.
  • computeTenantLayer0Filter (plugin-security): the [finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287 suite in tenant-layer.test.ts now derives BOTH Layer-0 inputs (objectHasOrgIdField, tenancyDisabled) from the REAL shipped SysApiKey post-declaration — filter stays null in all three postures.
  • resolveInjectedSystemColumns (spec): new case — the plan is blind to the key on a plain tenant object and on the better-auth shape.

Declaration-shape measurement (enabled: false vs the previous absent block, for sys_api_key)

Measured on every reader of the tenancy block repo-wide:

  • resolveInjectedSystemColumns bails on managedBy: 'better-auth' before tenancy is read — identical plan.
  • SqlDriver.computeTenantField / computeAndRecordTenantField → null either way (no organization_id, no tenantField); the sticky opt-out entry only preserves a null that is already null.
  • computeTenantLayer0Filter → null either way (tenancyDisabled short-circuit vs objectHasOrgIdField === false short-circuit).
  • ObjectQLEngine.buildDriverOptions now withholds tenantId for this object — every downstream reader (applyTenantScope, injectTenantOnInsert, auditMissingTenant) gates on the null tenant field and was already a no-op for it.
  • Memory/Mongo boot guards count only an explicit enabled: true — unaffected.
  • posturePermitsCrossTenant (plugin-security) is already true via isBetterAuthManaged — unaffected.
  • ⚠️One corner honestly reported, not silently absorbed: plugin-security's RLS field-existence net treats a policy targeting organization_id on an object lacking it as deny (fail-closed) unless the object opted out of tenancy, in which case it skips. With enabled: false, a hypothetical org-targeting wildcard RLS policy on sys_api_key would now be skipped as "not applicable on this object" instead of contributing to the deny sentinel. No shipped policy has this shape (the wildcard tenant policy was retired from the seeds; sys_api_key's Layer 1 is the user_id-based self policy), so shipped behavior is identical; the delta direction on custom metadata is the branch's own documented semantics for opted-out objects, and matches [finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287's design (the credential table is owner-scoped, never org-walled).

Verification

  • @objectstack/spec build + full test suite, check:generated (all gates green after regenerating exactly the two proved-stale artifacts).
  • Consumer suites (downstream direction): plugin-audit 16/16 files, 242 tests; platform-objects 23 files, 409 tests; plugin-security 65 files, 1236 tests; driver-sql + objectql runs and per-package typechecks recorded in the dev report on the issue.
  • Reverse verification (fix committed first): reverting the resolver's limb 0 turns exactly the new stamp cases red — direction and counts in the dev report.
  • Gate union re-derived from the actual changed paths via scripts/pm/dispatch-gates.mjs; run receipts (with the head SHA) in the dev report.

Generated by Claude Code


Generated by Claude Code

…i_key audit stamps through it (#8778)
Option A per the maintainer ruling on #8778: a read-neutral, stamp-only
organization declaration. The audit writer's resolveRecordOrganizationField
consults it first (with the #5315 field-presence guard); sys_api_key declares
{ enabled: false, organizationField: 'active_organization_id' } and stays
unwalled. Read-neutrality pinned beside each named read path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
…-surface, changeset (#8778)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 15, 2026 4:12pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-audit, @objectstack/spec.

107 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 @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @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 @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via 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 @objectstack/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/cli.mdx(via @objectstack/plugin-audit, @objectstack/spec)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/plugin-audit)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • 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/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 @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @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/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • 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/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/platform-objects, @objectstack/plugin-audit, @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/http-protocol.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/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/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @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/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-audit, @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/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 15, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 15, 2026 20:02
@os-project-manager
os-project-manager added this pull request to the merge queueAug 15, 2026
Merged via the queue into main with commit 7901b2dAug 15, 2026
28 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-8778-audit-org-field branch August 15, 2026 20:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-project-manager@claude