Skip to content

fix(objectql): withhold the org-scope predicate from federated objects (#7738) - #7833

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-7738-external-org-scope
Aug 11, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-7738-external-org-scope

Conversation

@claude

@claude claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7738.

The site

packages/objectql/src/engine.ts:2528 on main (const hasTenant = …, inside private buildDriverOptions, engine.ts:2510) — now engine.ts:2556-2560.

The card did not pin it ("not pinned to a single line by the run"), so it was located rather than inferred. buildDriverOptions is the only site in packages/objectql that folds a system-column scope into a read: organization_id appears 6 times in engine.ts and 5 are prose; there is no owner_id read-predicate injection in the engine at all.

The mechanism, end to end

  1. buildDriverOptions folds ExecutionContext.tenantId into DriverOptions.tenantId for every object, gated only on tenancy.enabled !== false.
  2. SqlDriver.applyTenantScope (sql-driver.ts:7739) turns that into (field = :tenantId OR field IS NULL).
  3. resolveTenantField answers organization_id, because computeTenantField finds that column in the platform's field set.

Against the showcase's remote customers (id, created_at, updated_at, name, email, region, lifetime_value):

select * from `customers` where (`organization_id` = ? or `organization_id` is null)
-- bindings=["org_msoroxgurm6423gz"]

A remote SQL error on Postgres/MySQL. On SQLite, worse: the quoted-identifier fallback reinterprets the unresolvable identifier as the string literal 'organization_id', both disjuncts go constant-false, and the object answers 0 rows with HTTP 200.

The fix

tenantId — and the group-posture tenantIds union, which is the same predicate on the same absent column — is withheld for an object with external != null, alongside the existing tenancy.enabled: false exemption (ADR-0066 / #3249). external != null is the same predicate syncObjectSchema already routes a federated object by, not a second reading of it. Withholding at the engine covers every driver at the source rather than one driver's opt-out.

Why the exemption is unconditional (measured, not inherited)

The card assumes a blanket skip. That assumption holds, but not for the reason it reads like — and the measurement matters, because "the remote happens to lack the column" would be the platform reasoning about a schema it does not own:

  • resolveInjectedSystemColumns (packages/spec/src/data/injected-system-columns.ts:136) injects organization_id into every object it registers. There is no external branch — the only opt-outs are systemFields: false, managedBy: 'better-auth' and tenancy.enabled: false.
  • SqlDriver.registerExternalObject is DDL-free by design (ADR-0015 forbids DDL on a remote schema) and runs no columnInfo introspection. It calls computeAndRecordTenantField(key, schema) on the platform's field set.

⇒ On a federated object, organization_id's presence is always the platform's own injection and never evidence about the remote. There is no shape in which scoping a federated read by it is known-correct. The premise is asserted in the test file rather than assumed, so the fix cannot outlive its own mechanism.

Both directions are pinned; the negative one is load-bearing

packages/objectql/src/engine-external-tenant-scope.test.ts, 14 tests. This punches a hole in an implicit tenant wall, so a test proving only the permissive direction would stay green if the fix withheld tenantId from everything.

external object ordinary object
find / findOne / count / aggregate no tenantId, no tenantIds tenantId present, for a non-system caller
group posture union withheld too tenantIds still threaded
write-side stamp (create) — tenantId still present
tenancy.enabled: false — pre-existing exemption unchanged
caller-supplied tenantId by name still wins still wins

The seam is DriverOptions, not SQL: objectql cannot import driver-sql (the dependency runs the other way), and applyTenantScope early-returns an unmodified builder on undefined | null | '', so absence of tenantId is absence of the predicate.

Reverse-verified: with the fix reverted, the 4 external read-door cases and the group-posture case fail with expected 'org_msoroxgurm6423gz' to be undefined; the 8 non-regression/premise cases pass both before and after.

Scope held

Gates

gate result
packages/objectql vitest 3270 passed / 185 files, 0 failed
packages/objectql tsc --noEmit exit 0
eslint on both changed files clean, 0 findings
pnpm build (full workspace) exit 0
dogfood showcase-external-autoconnect 3 passed / 3 — the federated read end to end
pnpm check:tenant-chokepoint ✓ 19 getBuilder() bindings / 3 files, every read builder scoped
pnpm check:empty-changeset ✓ 1 declaring changeset
pnpm check:adr-0087-registration ✓ no declared-breaking changeset

No unexplained failures; nothing skipped, baselined, or ratcheted. Changeset: .changeset/external-object-org-scope-exemption.md (@objectstack/objectql: patch).

A coverage gap this surfaced

packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts:37 asserts an authenticated admin read of showcase_ext_customer returns >= 3 rows, in a required 3-shard CI job — the shape that should have caught #7738. Measured: it passes 3/3 on main too (checked engine.ts back out at main, rebuilt objectql, re-ran), so it never emitted the predicate at all.

Cause: bootStack requests the isolated posture only when opts.multiTenant is truthy (packages/verify/src/harness.ts:268), and this fixture passes no options — it boots single, where execCtx.tenantId is undefined and the wall is dormant. Per BootOptions.multiTenant's own docstring the only honest walled harness is multiTenant: true with the cloud-private @objectstack/organizations, which is unavailable here. So the intersection this bug lives in — federated read × org-walled deployment — has no reachable end-to-end coverage in the dogfood tier. Detail on #7738; not filed separately, and nothing in this PR depends on it.


Generated by Claude Code


Generated by Claude Code

#7738)

`buildDriverOptions` folded the caller's `ExecutionContext.tenantId` into
`DriverOptions.tenantId` for every object, including ADR-0015 federated ones.
The SQL driver turns that into the platform's implicit tenant wall —
`(organization_id = :tenant OR organization_id IS NULL)` — so an authenticated
read of a correctly-bound external object issued that predicate against a
remote table the platform does not own. On Postgres/MySQL that is a remote SQL
error; on SQLite the quoted-identifier fallback reinterprets the unresolvable
identifier as the string literal `'organization_id'`, both disjuncts go
constant-false, and the object answers 0 rows with HTTP 200.

`tenantId` (and the `group`-posture `tenantIds` union) is now withheld for an
object with `external != null`, alongside the existing `tenancy.enabled: false`
exemption (ADR-0066 / #3249). Withholding it at the engine covers every driver
at the source rather than one driver's opt-out.

The exemption is unconditional rather than conditioned on the object carrying
an `organization_id` column, because that column is the platform's own:
`resolveInjectedSystemColumns` injects it into every registered object with no
`external` branch, and `SqlDriver.registerExternalObject` is DDL-free and runs
no introspection. On a federated object the column's presence is always the
injection and never evidence about the remote schema.

The new pin asserts both directions on all four read doors. The load-bearing
half is the negative one: an ordinary object still carries the wall for a
normal non-system caller, on reads and on the write-side stamp, and a
`tenantId` a caller passes by name still wins under both exemptions.

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

vercel Bot commented Aug 11, 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 Aug 11, 2026 8:13pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

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.

Copy link
Copy Markdown
Collaborator

REWORK — two red ratchet gates, one root cause, both mechanical

Returned to draft. The prior ACCEPT on #7738 is withdrawn (correction posted there); this PR was flipped ready while ESLint had been red for 20 minutes and TypeScript Type Check for 9.

The fix is not in question. engine.ts:2549-2560 is the right seam, the unconditional-exemption argument is the load-bearing one, both directions are pinned with the negative half doing real work, and the §8 self-correction (measuring that the dogfood test passes 3/3 in both directions and therefore never emitted the predicate at all) is exactly right. Nothing below asks you to change any of that. Both failures are in the new test file's typing, and both gates print their own remedy.

1 · ESLint job → check:query-options-erasure

✗ query-options-erasure ratchet (1 problem(s)):
  • test surface grew 242 → 252 site(s). Tests are outside the blocking rule,
    not outside the count.

Ten as any option bags in engine-external-tenant-scope.test.ts. They are all in READ_DOORS and the individual it(...) bodies — { context: ctx } as any, { context: MEMBER } as any, { context: { ...MEMBER, accessible_org_ids: [...] } } as any, and the { tenantId: 'org_explicit' } as any query bag.

The gate's own prescription, in its words: type the options, or — only where the input is deliberately off-contract — write as unknown as EngineQueryOptions, which names the contract being bypassed and is not counted. ⛔ It also says explicitly: "Raising this number is a reviewed edit, not a remedy." Do not raise the baseline.

These are not off-contract inputs — { context: MEMBER } is an ordinary well-formed options bag. So the answer here is the first branch: type them (import the options type and annotate MEMBER, the run signatures in READ_DOORS, and the context literals) rather than reaching for as unknown as. The one genuine candidate for the second branch is the explicit-tenantId query bag in the last test, if it does not type cleanly — and if you use it there, say in a comment which contract is being bypassed and why that is the point of the assertion.

2 · TypeScript Type Check job → check:type-check-debt --re-measure

• @objectstack/objectql: TEST_DEBT records 355 raw tsc error(s),
  `tsc --noEmit` now reports 358 (+3). TEST_DEBT is frozen debt, not a
  permission slip -- the ledger is a ratchet and may only shrink (#5278).

Three new raw errors from the same file. Measure them rather than guessing — likely candidates are setTenancyPostureProvider(() => opts.posture) (a string where the posture union is wanted), the registerObject schema literals, and the aggregate call — but take the compiler's word, not mine.

⛔ Do not raise the ledger entry. The gate permits it only for genuinely irreducible errors and requires rewriting the entry's note to match the pile's new composition. Neither applies to three errors in a file added by this PR.

3 · Verify with the gates, not the file-scoped commands

This is the actual lesson and the reason none of this showed up in your report — which was truthful: eslint on the changed files and tsc --noEmit in objectql were genuinely clean. Neither reaches these ratchets, which are repo-wide and run only inside those two CI jobs. Before pushing, run the two gates themselves:

pnpm check:query-options-erasure
pnpm check:type-check-debt          # --re-measure is what CI runs

Both must exit 0. Keep packages/objectql vitest green (it was 3270/185) and re-confirm the reverse-verification still splits 5 red / 8 green — a typing change to the test file must not quietly weaken an assertion, and the negative half is the one guarding a tenant wall.

Scope

⛔ Test-file typing only. No change to engine.ts, the changeset, the exemption's shape, or any assertion's meaning. Nothing outside packages/objectql. Do not fold in #7737, and do not pick up the two follow-ups already filed as #7834 / #7835.


Generated by Claude Code

… them

Gate hygiene on the new test file only — `engine.ts`, the changeset and all
14 assertions are unchanged, and the suite still passes 14/14.

Two repo-wide ratchets were red on 3bdf170, neither of them visible to the
per-package `tsc --noEmit` this file was checked with:

`check:query-options-erasure` — test surface 242 -> 252. Ten `as any` casts sat
in argument positions 1-2 of `find`/`findOne`/`count`/`aggregate`, which is
exactly the erasure #4918 bans: `EngineQueryOptions` is not `.strict()`, so an
unknown key is silently DROPPED rather than rejected, and `tsc` is the only
channel that enforces those keys for an internal caller. Nine are now properly
typed — `ExecutionContext` for the caller identity (`accessible_org_ids` is a
declared key, so the group-posture contexts needed no cast at all) and the
inferred parameter types elsewhere. The tenth is deliberately off-contract:
`tenantId` is a RUNTIME passthrough key (`ENGINE_DRIVER_PASSTHROUGH_KEYS`) that
`EngineQueryOptionsSchema` does not declare, so it says so with
`as unknown as EngineQueryOptions` — the form the rule accepts because it names
the contract being bypassed and leaves the rest of the call checked.

`check:type-check-debt --re-measure` — `@objectstack/objectql` TEST_DEBT 355 vs
358 measured. The three were not the casts: `registerObject` takes
`(schema, packageId, …)` and all three call sites passed one argument (TS2554,
the ledger's second-largest class at x93). Fixed by passing the package id
rather than by raising the ledger — the ratchet may only shrink.

Both re-measured locally on the merged base: erasure 252 -> 242 (at the
ceiling), TEST_DEBT 358 -> 355 (equal to recorded, zero errors in this file).
The 18 commits merged in from main contributed 0 to either number.

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

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Patch round done — both ratchets green, fix untouched. Pushed as 36323fc, on top of a clean merge of origin/main (0a03ddc, 18 commits, zero conflicts).

Before / after

gate ceiling / recorded before (3bdf170f5) after (36323fc)
check:type-check-debt --re-measure → @objectstack/objectql TEST_DEBT 355 358 (+3) 355 — equal to recorded, 0 errors in my file
check:query-options-erasure → test surface 242 252 (+10) 242 — at the ceiling

Both re-measured on the merged base, then re-run once more against the exact committed tree. Ledger untouched; baseline untouched. --re-measure: "33 ledger entries re-measured, 1788 raw tsc errors total, none above its recorded number."

The 18 merged commits contributed 0 to either number — measured, not assumed: the merged base without my test file reports exactly 355, so all 3 were mine.

What the 3 type errors actually were

Not the casts. SchemaRegistry.registerObject(schema, packageId, namespace?, ownership?, priority?) requires a package id, and all three of my call sites passed one argument — TS2554, the ledger's own second-largest class (x93). Fixed by passing the id (registerObject(EXTERNAL_OBJECT, PACKAGE_ID)), not by raising anything.

Worth noting for whoever next writes a test here: the file I used as a template (engine-summary-recompute-context.test.ts) calls registerObject with one argument too. That single-arg spelling is presumably a slice of the frozen 355 — it compiles nowhere and no gate below the ledger reads it. Not mine to sweep in this PR; flagging it because the arity trap is copy-paste-shaped and the ledger is where it lands.

The 10 erasure sites

All ten were as any in argument positions 1–2 of find/findOne/count/aggregate — exactly what #4918 bans, and for the reason the rule states: EngineQueryOptionsSchema is not .strict(), so an unknown key is silently dropped rather than rejected, and for an internal caller tsc is the only channel enforcing those keys.

  • 9 properly typed. ExecutionContext for the caller identity, and inferred parameter types elsewhere. Notably the two group-posture contexts needed no cast at all: accessible_org_ids is a declared key on ExecutionContextSchema (the engine reading it through an as any internally is an engine-side detail, not a contract gap), and ExecutionContext is the z.input type, so a partial slice satisfies it.
  • 1 as unknown as EngineQueryOptions — engine.find(obj, { tenantId: 'org_explicit' }). This one is genuinely off-contract: tenantId is a runtime passthrough key (ENGINE_DRIVER_PASSTHROUGH_KEYS, [P2] A direct engine call silently drops sort/select/skip/populate — declared query contract, zero enforcement #4371) that EngineQueryOptionsSchema does not declare. The sanctioned form names the contract being bypassed and leaves the rest of the call checked, which is precisely the assertion this case makes — that a caller-named tenantId still wins under the federation exemption.

No coverage was traded for a green gate

  • 14/14 still pass, same 14 cases, same both-direction table. Nothing deleted, nothing loosened, no assertion weakened — the typing changes are at the call sites, not in the expectations.
  • Full packages/objectql suite on the merged base: 3288 passed / 186 files, 0 failed.
  • eslint on both changed files: clean.
  • The fix is byte-identical to the accepted commit: git diff 3bdf170 HEAD -- packages/objectql/src/engine.ts .changeset/external-object-org-scope-exemption.md → 0 lines. external != null withholding, the tenantIds union handling and the changeset are exactly as reviewed.

One thing I deliberately did not do

--re-measure reported 9 entries sitting BELOW their ceilings (274 raw errors of surplus) — plugin-approvals TEST_DEBT 547 recorded vs 348 actual (−199), plugin-auth −25, lint −23, metadata −3, and five more. The gate calls each one lowerable and notes the surplus silently licenses that many regressions (#6376).

None of them is mine and none blocks this PR, so I left them alone rather than widen a scoped patch round into someone else's ledger. Recording the numbers here since they were measured anyway — pnpm check:type-check-debt --lower writes them, and it looks like one worthwhile sweep.


Generated by Claude Code

@huangyiirene
huangyiirene deleted the claude/issue-7738-external-org-scope branch August 11, 2026 21:14
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
… a phantom organization_id (objectstack-ai#7835) (objectstack-ai#7859)

* fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (objectstack-ai#7835)

The ObjectQL registry injects `organization_id` into every object that has not
opted out — federated (ADR-0015 `external`) ones included — while
`Engine.syncObjectSchema` returns early for them and issues no DDL, because the
remote schema is owned externally. The column therefore exists in the registered
schema and in no backing store.

`computeLayeredRlsFilter` read that field set, answered `objectHasOrgIdField:
true`, and Layer 0 AND-composed `organization_id = <active org>` onto federated
reads. Measured on the shipped showcase: the composed read filter for
`showcase_ext_customer` was `{organization_id: 'org_alpha'}`, and
`GET /data/showcase_ext_customer` under a walled posture answered HTTP 200 with
zero rows — the wall isolating nothing while the federated catalog silently
stopped existing. On Postgres/MySQL the same predicate raises instead.

This is the plugin-security sibling of objectstack-ai#7738 / PR objectstack-ai#7833, which withheld
`DriverOptions.tenantId` for the same objects one layer down. That fix cannot
reach here: Layer 0 is a `where` predicate composed into the query AST, not a
driver option — so per this card's fence the engine-level skip is not widened.

The test is PROVENANCE, not `external != null`: a federated object may declare a
real remote `organization_id`, and suppressing its wall would delete one that
works. `federated-phantom-anchors.ts` compares the registered field definition
against the shipped `TENANT_SCOPE_FIELD_DEF` the registry spreads verbatim — the
same discipline `platform-tenant-policies.ts` records for ADR-0105 finding F1.
Any inexact match answers "not the platform's anchor" and leaves the wall in
place: the fail direction is toward isolation.

Layer 1 is untouched, so an app-authored policy still reaches the compiler
(ADR-0049) and local objects are unaffected.

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

* chore(changeset): federated objects are no longer walled by a phantom organization_id (objectstack-ai#7835)

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

* test(dogfood): type the federated-RLS lookups by their slot contracts (objectstack-ai#7835)

CI runs `eslint . --no-inline-config`, so the two `eslint-disable` comments this
file carried were inert on the runner and `slot-lookup/no-any-assignment` fired
on both service lookups. The rule's own prescription is to stop erasing the
lookup, which is what this does:

  ql       -> IObjectQLEngine
  security -> ISecurityService

Both are declared in `packages/spec/src/contracts/core-service-contracts.ts` as
the contracts of the `objectql` and `security` slots, and `ObjectKernel
.getService` is already generic over them — so the `stack.kernel as any` handle
was not buying anything either and is gone with them. No assertion changed.

One edge needed a narrowing rather than a type: `IObjectQLEngine.getSchema`
declares `unknown` on purpose (the engine's own return is `ServiceObject |
undefined`, but the contract keeps its edges loose so `spec` never depends on
the engine package, and tells consumers to narrow at the call site). The
PREMISE case reads `schema.external`, so it narrows to the `ServiceObject` the
implementation already declares — a named spec type the package already
depends on, not a re-erasure.

Reverse-verified in both directions, since "the honest types compiled" is only
worth something if tsc actually reads the file: dropping the `ServiceObject`
narrowing turns typecheck red (TS2339 `Property 'external' does not exist`),
and misspelling `getReadFilter` turns it red against `ISecurityService`
(TS2551). Both confirm the contracts are live rather than inferred as `any`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…rtion (objectstack-ai#7834) (objectstack-ai#7919)

`showcase-external-autoconnect.dogfood.test.ts` is a required-CI test that
names the federated-read path and asserts `rows.length >= 3` on an
authenticated admin read — the exact shape that should have caught objectstack-ai#7738's
fail-open federated read, and did not. It boots single-tenant, so the org
predicate is never emitted and the assertion passes without ever crossing
the wall. A green gate over a path it does not exercise reads as coverage.

Per the maintainer's 2026-08-12 ruling on objectstack-ai#7834 (option 2), record the
boundary at the assertion site instead of building a posture-aware fixture:
what the test does cover (the ADR-0062 D8 autoconnect path — real, hence not
skipped), what it does not (the organization wall), why (no `opts.multiTenant`
=> no `isolated` posture request, `autoDefaultOrganization: false`, no org
plugin => `execCtx.tenantId` undefined => `hasTenant` false), and where the
regression defence actually lives (objectstack-ai#7833's seam pin on `DriverOptions` in
`packages/objectql/src/engine-external-tenant-scope.test.ts` — a unit/seam
pin, explicitly not end-to-end proof).

Comment-only; no behaviour change and no new assertion. Verified by
re-measurement: with objectstack-ai#7833's `!isFederated` guard locally removed and
`@objectstack/objectql` rebuilt, this file still passes 3/3, and a probe in
`buildDriverOptions` records `tenantId=undefined`/`hasTenant=false` for
`showcase_ext_customer` and `showcase_ext_order` (0 of 1069 calls in the whole
boot ever get a tenant). Probe reverted; nothing outside the comment changed.


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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ed system columns (objectstack-ai#7865) (objectstack-ai#8115)

Direction B per the 2026-08-12 maintainer ruling: applySystemFields keeps
injecting the platform anchors into external (ADR-0015) objects, and the
anchors it registers without provisioning storage now carry a machine-readable
provenance marker — spelled as an exported derivation
(resolveInjectedColumnProvenance / unprovisionedInjectedColumns /
platformProvisionsStorage in @objectstack/metadata-core, re-exported by
@objectstack/objectql) rather than a provisioned key on the field defs, so no
document byte changes anywhere: FieldSchema is strict (a data key would stamp
_diagnostics invalidity on every served federated object, or become an
authorable forgeable wall-off switch), and the objectstack-ai#7859 Layer-0 guard plus the
objectstack-ai#4326 round-trip strip read the anchor defs by exact key-count identity.

The three existing consumer guards (objectstack-ai#7833 engine, objectstack-ai#7859 plugin-security,
objectstack-ai#7858 plugin-sharing) are deliberately untouched — opportunistic convergence
per the ruling. Proofs: unit matrix in metadata-core, marker-vs-live-injection
parity in objectql, and a real showcase boot pinning the before-picture
(7 anchors on showcase_ext_customer, byte-identical), the marker verdicts,
behavioural parity with the live Layer-0 guard, and /meta serving an unchanged,
valid post-injection document.


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

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.

external-datasource-federated-read: the platform injects its org-scoping predicate onto a federated remote table that has no organization_id column

2 participants