Skip to content

fix(engine-core): a system-context insert resolves the install's organization, or is refused — the runtime producer of the autonumber fork (#8844) - #8930

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-8844-system-write-organization-stamp
Aug 15, 2026
Merged

fix(engine-core): a system-context insert resolves the install's organization, or is refused — the runtime producer of the autonumber fork (#8844)#8930
hotlong merged 2 commits into
mainfrom
claude/issue-8844-system-write-organization-stamp

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8844

The runtime twin of #8686, one layer up. That card fixed one producer of untenanted rows — the seed loader — and shipped a one-shot backfill for what it had already written. This is the other producer, which is still running: an ordinary application write made under a system execution context (a hook, a scheduled job, a custom endpoint, a runAs: system flow). A backfill cannot reach it, because it mints a fresh duplicate on every tick — which is what makes #8686's repair self-undoing on any install with server-side automation.

Implements the maintainer ruling in comment 5302919977Option 1, all five binding points, not re-adjudicated.

Premises re-verified on today's tree (main @ 8664a2c99, not the escalation's 6b6b606)

The escalation's line references were dated evidence; each was re-located by content and re-checked:

premiseverdict on today's tree
the stamp is an INJECTED field, not an explicit assignmentOK — packages/objectql/src/registry.ts:555, additions.organization_id = { ...TENANT_SCOPE_FIELD_DEF } (same line, re-checked by content)
engine.ts documents the beforeInsert stamp pathOK — packages/objectql/src/engine.ts:8158
the defect is UPSTREAM of the counterOK, measured not inferred: SqlDriver.create calls injectTenantOnInsert at sql-driver.ts:4402 and fillAutoNumberFields at :4421, so the counter reads an already-stamped row. The direction of causation holds; the fix site does not move.
no chokepoint helper existsOK, re-confirmed. The edit site is the engine's stamp resolution.
fillAutoNumberFields is duplicated across driversOK — and driver-turso reaches it by inheritingSqlDriver.create. Relevant to the tests, not the fix site: no driver changed.

What changed

packages/objectql/src/tenancy/system-write-organization.ts (new) — the ruling as one pure, directly-testable decision, plus the refusal class. engine.ts supplies the measured facts and resolves the answer into DriverOptions.tenantId — the same knob a session write sets, so injectTenantOnInsert stamps the column and the counter scopes by the real organization. That is why no driver had to change, on either of the two that carry fillAutoNumberFields.

The refusal is the dangerous half — how it was kept narrow

A refusal that is too broad breaks every system write on a walled install, including unattended automation. Four populations are outside the rule by construction, not by exemption:

populationwhy
no organization column, tenancy.enabled: false (ADR-0066), federated (ADR-0015)there is no tenant column to fork a counter by. tenancy: { enabled: false } is the declared way to hold org-less rows — stated once on the object and checkable — rather than a per-write bypass flag, which is the lenient-consumer accommodation PD #12 forbids.
sys_ / cloud_ / ai_ platform namespacesrows deliberately global. This is #8672's reasoning, which binding point 5 confirms holds for platform objects and does not generalize to application objects. Same regexp the seed loader applies and #8686's backfill re-spells — the three write paths have to agree.
a write already carrying an organizationon the execution context, on the record, or stamped by a beforeInsert hook (the resolution runs after the hooks for exactly this reason). That IS "carrying an explicit organization".
zero organizations exist yetfirst boot. Nothing to derive, and no second partition to fork away from — #8686's sys_organization-insert handoff adopts exactly these rows. Refusing here would refuse first boot itself.

Topology buckets, as measured (Zone 2 point 3): posture read live per call, service first (tenancyPostureProvider, what is ENFORCED) then env (resolveTenancyPosture, what was ASKED for) — the same two-step app-plugin.organizationWallActive uses, and never the demoted resolveMultiOrgEnabled(). single + 1 org: derive. single + 0 orgs: leave untenanted. single + 2 or more orgs: refuse (ambiguous). group/isolated: refuse (walled, without probing the database at all). An unreadable OS_TENANCY_POSTURE: degrades to the walled reading and refuses, because a typo'd posture is exactly where guessing single would be worst.

Scoped to INSERT, deliberately. The ruling's yardstick is "the way a session write does", and stamping the organization is an insert-side mechanism — an update neither stamps it nor can fork a counter. Extending it would not be following the session write, it would be inventing a second rule.

Tests, and the ablation that proves they can fail

  • packages/objectql/src/system-write-organization.test.ts — 20 cases: the decision function's branches, then the engine seam (DriverOptions.tenantId), each with its discriminating control.
  • packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts — 4 cases on a realObjectQL + SqlDriver over better-sqlite3, asserting on the database: _objectstack_sequences, the stored rows, the duplicate count.

Two ablation legs, disjoint red sets — each half of the fix is independently pinned. Directions were predicted before running; one prediction was wrong and the measured direction is reported:

ablationobjectqlruntime
A — refusal removed (silently defaults to __global__)4 RED / 16 green: expected undefined to be 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'1 RED ([binding point 2]) / 3 green
B — derive-and-stamp removed, refusal intact2 RED / 18 green: expected undefined to be 'org_msokm9oaz0cal87q'3 RED / 1 green — [binding point 1]expected 5 to be 10, [#8686 regression] the __global__ row reappears beside the organization's

Correction to a prediction. Leg B was predicted to leave [first boot] green; it went RED (expected 2 to be 1). The case's second half asserts that once the organization exists the runtime producer is stamped, so it pins the derive branch too — the test is stronger than predicted, and this reports what was observed rather than what was expected.

packages/runtime resolves @objectstack/objectql from dist/, so every ablation leg was rebuilt and proved live in the artifact with node scripts/ablation-dist-preflight.mjs @objectstack/objectql '__OS_ABLATION_*__' (and proved absent again after restore). Without that, leg A's first runtime run was a false green — measured here, not hypothesised.

Verification — union re-run at final HEAD 445359f0b

Edited packages: spec 10644, objectql 3695, runtime 2456 tests, all passing; typecheck green across objectqland its 43 DOWNSTREAM consumers (prefix filter ...@objectstack/objectql).

Downstream test sweep (consumers, not dependencies): rest 1948, plugin-security 1235, plugin-auth 1238, plugin-sharing 599, plugin-approvals 484, plugin-audit 239, driver-turso 994, driver-mongodb 384, service-datasource 426, service-settings 456, service-messaging 229, service-queue 53, trigger-record-change 78, trigger-schedule 46, cli 1358, dogfood 779, http-conformance 72, verify 28 — all green. #8686's own seed-tenancy-autonumber-split.integration.test.ts runs green beside the new file.

Gates: every family scripts/pm/dispatch-gates.mjs derives from the final changed paths, plus check:error-code-casing, check:nul-bytes, check:engine-double-contract, check:tenant-chokepoint, check:type-check-coverage, check:type-check-debt --re-measure (on the built workspace), check:query-options-erasure, pnpm lint, and pnpm --filter @objectstack/spec check:generated (13/13 current).

One ratchet moved and was fixed at the source, not raised: check:query-options-erasure counted the new this.find(sys_organization, {…} as any) as a ninth erasing site in engine.ts (grandfathered for its existing eight only). The literal type-checks against EngineQueryOptions unmodified, so the cast came out — commit 445359f0b. Ratchet back at baseline, no ceiling touched.

Not done here, deliberately

No driver change (Option 2 is off the table). No renumbering of already-minted identifiers — reported instead, via #8928. #8672 remains open and untouched; this PR is the evidence its on-hold reasoning does not generalize, not a change to it.

The issue body was self-checked for sanitizer truncation: intact — its escaped arrow spans survived as entities and no prose is cut.


Generated by Claude Code

… inserts, or refuse (#8844)
The runtime twin of #8686's ruling, one layer up. A system-context write —
a hook, a scheduled job, a custom endpoint, a `runAs: system` flow — carried
no organization, so the tenant column landed NULL and the SQL driver's
autonumber counter filed the row under the `__global__` pseudo-tenant. One
object then ran two counters that could not see each other, and the
partitioned unique index `(COALESCE(organization_id, '__global__'), <field>)`
could not see across the two partitions either: a field declared `unique`
held the same value twice, with no error and no warning. Measured on 17.0.0
GA across five objects on one single-tenant install.
Per the 2026-08-15 maintainer ruling (Option 1), the engine's stamp
resolution now answers the same question a session write answers:
- single-tenant with exactly one organization: derive and stamp it, by
threading `DriverOptions.tenantId` — the same knob a session write sets, so
no driver changed (`fillAutoNumberFields` is duplicated in driver-sql and
driver-turso);
- multi-organization (a walled posture, or a `single` posture whose data
holds several organizations): refuse loudly with
ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (500, registered in the ADR-0112
ledger) rather than silently default to `__global__`;
- already-minted duplicates are reported, never rewritten — the #8686
posture, ruled again here. Nothing renumbers anything.
Three populations are outside the rule by construction so the refusal cannot
break automation that was never at risk: objects with no organization column,
`tenancy.enabled: false` objects and federated objects; the `sys_`/`cloud_`/
`ai_` platform namespaces (#8672's reasoning, which does not generalize to
application objects); and any write already carrying an organization. Before
any organization exists there is nothing to derive and no partition to fork
from, so first boot is untouched and #8686's handoff still adopts those rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…8844)
`check:query-options-erasure` counted the new `this.find(sys_organization,
{...} as any)` as a ninth erasing site in engine.ts, which is grandfathered for
its EXISTING sites only. The literal type-checks against `EngineQueryOptions`
unmodified, so the cast was never needed — removed at the source rather than
laundered through `as unknown as`, and the ratchet is back at its baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@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 6:17pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

109 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 @objectstack/objectql, 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 packages/objectql, @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/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • 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/deployment/vercel.mdx(via @objectstack/objectql)
  • 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/objectql, @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 packages/objectql, @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/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • 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/objectql, 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/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @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/objectql, @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 packages/objectql, @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/objectql, @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/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/objectql, @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 tests tooling labels Aug 15, 2026
@hotlong
hotlong marked this pull request as ready for review August 15, 2026 20:22
@hotlong
hotlong added this pull request to the merge queueAug 15, 2026
Merged via the queue into main with commit ff08691Aug 15, 2026
28 checks passed
@hotlong
hotlong deleted the claude/issue-8844-system-write-organization-stamp branch August 15, 2026 20:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

2 participants

@hotlong@claude