Skip to content

[Decision] applySystemFields injects platform anchors into external objects the platform provisions no storage for — three consumers have now independently re-derived "that column is not really there" #7865

Description

@huangyiirene

Filed by the domain:identity seat (#6022) out of PR #7859's review, where I committed publicly to raising it. Unassigned and deliberately carries no domain:* — the answer lands in the registry, not in this lane; triage owns routing and grading.

The producer

applySystemFields adds the platform anchors (organization_id, owner_id, owning_business_unit_id, created_by/updated_by…) to every object that has not opted out — including objects declared external. Engine.syncObjectSchema returns early for external != null and issues no DDL, because the remote schema is owned externally.

So for a federated object the registry advertises columns that do not exist in the remote table and never will. Measured on the shipped showcase during #7835 (real boot, @objectstack/verify):

showcase_ext_customer external={"remoteName":"customers","writable":false}
registered fields: organization_id, created_at, created_by, updated_at,
updated_by, owner_id, owning_business_unit_id,
name, email, region, lifetime_value
remote table: name, email, region, lifetime_value

Why this is a card and not a curiosity: three consumers, three fixes, one fact

Each of these independently discovered that a registered column is phantom, and each patched its own layer:

#consumerfix
#7738 / PR #7833packages/objectql enginewithholds DriverOptions.tenantId for external != null
#7835 / PR #7859plugin-security Layer 0suppresses the tenant predicate when the anchor is provenance-identical to the shipped TENANT_SCOPE_FIELD_DEF
#7858plugin-sharingproposed: same treatment for the owner_id predicate from __readScope own/unit

Three consumers re-deriving the same fact about one producer is the shape that says the answer belongs upstream. Two could be coincidence; three is a pattern, and there is no reason to think a fourth consumer will not appear — the failure is silent (on SQLite an unresolvable identifier degrades to a string literal, so the query goes constant-false: HTTP 200, zero rows, no error).

The question

Should the registry stop injecting platform anchors for external objects — or mark them so consumers can distinguish "declared" from "provisioned"?

  • A — stop injecting for external. Removes the class at the source. Largest blast radius.
  • B — keep injecting, add provenance/provisioned: false so consumers can ask. Each consumer still decides, but asks one authoritative question instead of re-deriving. fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 already effectively hand-rolls this by comparing against the shipped field def.
  • C — leave the three consumer-side fixes; accept re-derivation as it arises. Zero cost today, unbounded tail.

⚠️ Blast radius — why this is a decision and not a cleanup

This is why the #7835 dev explicitly rejected folding it into any of the three cards: a wide registry change fenced inside a plugin-security card is the wrong shape.

Four-lens analysis

① Platform long-term coherence. A registry that advertises columns the platform does not provision is a contract that is not true for one object class, and today every consumer must independently know that. A shrinks the special case; B makes it askable in one place; C grows it once per consumer, forever.

② Measured business pull. Real, not speculative: one measured user-visible defect (#7738, a federated catalog returning zero rows under a walled posture), plus two more the same sweep found. Pull is for the class to stop recurring — no user is asking for a registry redesign as such.

③ AI-agent error-resistance. The strongest lens here. The current shape hands an agent a field list where some entries are real and some are phantom, with nothing in the data distinguishing them — and the failure is silent on the default dev dialect. An agent writing a predicate over owner_id on a federated object gets 200/zero-rows, the single hardest signal to attribute. B is specifically the option that makes the distinction machine-readable rather than folklore.

④ Startup scope discipline. Cuts toward B over A: A is a broad change to a load-bearing path serving /meta, Studio and a ratchet, for a class currently handled by three small guards. B is additive and lets the guards converge on one question as they are touched. C is the option that keeps paying — and each repayment is a security-adjacent guard written by whoever happens to trip over it next.

Recommendation: B, with A as the longer-horizon shape if the marker proves consumers can converge. Not urgent — the three live consumers are already guarded — but it should be decided before a fourth appears, not after.

Related

#7738 / PR #7833 · #7835 / PR #7859 · #7858 · #6562 (/meta serves the post-injection document) · #5378, #6113 (validator blindness to injected columns) · #5677 (ADR-0117 D1 changed what applySystemFields injects)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions