Skip to content

fix(OMN-17887): retire the tenant schema; public is the TENANT domain's schema - #4079

Merged
jonahgabriel merged 11 commits into
devfrom
lakshman/OMN-17887-retire-tenant-topology
Sep 25, 2026
Merged

jonahgabriel merged 11 commits into
devfrom
lakshman/OMN-17887-retire-tenant-topology

Conversation

@Patel230

@Patel230 Patel230 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

OMN-17887, step 3 of 4: retire the tenant schema in omnibase_infra. Per the operator ruling on 2026-09-24, no tenant Postgres schema will be built, and the TENANT domain's schema is public for good.

This builds on step 1, omnimarket#2847 (contracts and manifest declare public; merged as 3441453c), and step 2, omninode_infra#1660 (merged as 8b8d5d3b).

What changed

  • Topology. local, onex-dev and onex-prod each have 4 schema: tenant USAGE grant blocks, 12 in all. 6 are retargeted to public (app_dashboard and onex_api, times 3), and 6 are dropped as duplicates of an existing public block (tenant_projection_writer and validator_ro, times 3). The 3 tenant: schemas entries are removed. The 9 rendered catalogs are regenerated, and _EXPECTED_SCHEMAS and _EXPECTED_SCHEMA_OWNERS drop tenant.
  • app_dashboard and onex_api keep their schema USAGE, retargeted to public. USAGE on tenant was their only declared access to the TENANT domain, so it moves rather than goes. tenant_projection_writer and validator_ro already held USAGE on public, so their tenant block was a duplicate and is dropped. This was found while preparing step 4: omninode_infra's Kubernetes consumer check declares these two bindings' schema access, and the matching translation is [public, platform_catalog]. On the lakshman lane on .201, a read-only readback on 2026-09-24 at 19:16Z shows app_dashboard=U already granted on public, and no tenant schema.
  • The tenant bridge is removed. TENANT_TABLES_PHYSICALLY_IN_PUBLIC_UNTIL_OMN15359 and its branch in physical_grant_schema_for_table are deleted, because tenant-domain relations are now declared public directly. The 7 legacy migration declarations follow. The internal bridge is unchanged.
  • Domain enforcement follows the topology. Previously it banned public outright. Now:
    • A public.<name> SQL target is an ordinary application location, held to the exactly-one-ownership check, when the topology declares public.
    • A catalog relation in public is refused unless its domain matches the schema's declared domain (TENANT), so a non-TENANT relation in public is still refused.
    • tenant.<x> is refused as an unknown topology schema.
  • Tenant GUC parity gate. It resolves each relation's domain per database from the topology's own schemas: block, instead of from the deleted bridge.
  • Domain-enforcement proof. The seed, ownership config, audited safe_report hash and audit id move to public, and the obsolete public-table red control becomes retired-tenant-schema.
  • CI pins. CI reads the omninode_infra ownership manifests at step 2's merge, 8b8d5d3b. The omnimarket contract pin moves forward from 923b76ba to step 1's merge, 3441453c. Against that commit the grant check is in sync on all 9 profiles (69 pass, 0 fail, 0 residual each), so nothing is regenerated. After the 2026-09-25 merge of dev the pin is dev's 8e5b1dbe (bot, chore(OMN-17292): advance omnimarket contract pin to 8e5b1dbeffb8 #4092), which contains 3441453c; the 5 omnimarket commits between them change no contract, manifest or SQL file.
  • Release version. 0.38.57 to 0.38.58, in pyproject.toml and uv.lock.
  • Tests. Tests that pinned tenant now pin the new truth. None were deleted or weakened, and new tests pin that tenant is refused.

How it was verified

  • Grant derivation: generate_application_database_table_grants.py --check --prove against step 1's contracts: 3 instances in sync, 69 passed and 0 failed on each of the 9 profiles. Against the old tenant contracts it fails (drift in 3 instances, 13 residuals per profile), which is the negative control.
  • Tenant GUC parity: the domain map is identical to dev's (55 internal, 23 tenant, 3 unresolved) with 0 violations. A new test covers database awareness and was shown to catch a resolver that ignores the database.
  • Domain-enforcement proof on local PostgreSQL 16: PASS, with 6 relations, 6 catalog objects, 4 pools and 46 red controls, the same counts as untouched dev. The new audited hash was recomputed with the proof's own code, and the same code reproduces the old pin 58b47971… from the old inputs.
  • Suites:
    • Unit, validation, CI, runtime and scripts: 15,702 passed. All 93 failures also fail on untouched dev on the same machine, except one lock-timing test that passes 3 out of 3 in isolation on both trees.
    • Integration files touching tenant: all 12 regressions fixed. The 3 that remain also fail on untouched dev.
  • After the USAGE retarget (second commit): grant derivation is in sync on all 3 instances, tenant GUC parity has 0 violations, and the domain-enforcement proof passes (6 relations, 4 pools, 46 red controls). The unit topology, validation, CI and scripts suites show no failure that untouched dev does not also show on the same machine.
  • After the 2026-09-25 merge of dev (2d7ae9f62): the only conflict was the omnimarket pin, resolved to dev's 8e5b1dbe. On the merged tree, the 14 test files CI runs in its application-database step, plus the two cohort-key test files, pass (632 tests), and the runner-image lock verifies. The SQL gate, run over a range that includes dev's new unqualified migration node_projection_savings/091, passes with 0 grandfathered.
  • Checks: the migration-class, schema-fingerprint and cross-database-migration gates pass, and mypy reports no issues on the changed sources.

Local gates

  • Pre-commit ran on commit: 92 hooks passed, 0 failed.
  • No gate was bypassed — no --no-verify, no SKIP=, no --no-gpg-sign, no core.hooksPath override. Every pre-commit and pre-push hook ran.

Not in this change

  • The empty-tenant drop migration. Onex-lab is read back first, per the ruling. If tenant exists there and is empty, it is dropped with a node-owned migration, since a flat cross-database migration cannot be delivered on the k8s path.
  • Step 4 (omninode_infra): the source-lock refresh, the k8s projections, EXPECTED_SCHEMAS, and removing onex-lab's CREATE SCHEMA tenant.
  • The synthetic ACL-proof fixtures. Their tenant schema is pinned to historical revisions, independent of this topology.

Evidence-Ticket: OMN-17887
Evidence-Source: OCC#11130

…'s schema

Operator ruling 2026-09-24: no tenant Postgres schema will be built and
the TENANT domain lives in public for good (ADR-0027 amendment).

Topology: the local, onex-dev and onex-prod instances drop the 12
`schema: tenant` USAGE grant blocks and the 3 `tenant:` schemas entries;
the 9 rendered catalogs are regenerated. The expected-schema maps drop
tenant. The TENANT_TABLES_PHYSICALLY_IN_PUBLIC_UNTIL_OMN15359 bridge and
its consumers are removed: tenant-domain relations are declared public
directly (omnimarket OMN-17887 step 1), and the 7 legacy migration
declarations follow.

Domain enforcement follows the topology instead of banning public: a
public.<name> target is an ordinary application location held to the
exactly-one-ownership check when the topology declares public, and a
relation in public is refused unless its domain matches the declared
schema domain (TENANT). The retired tenant schema is refused as unknown.

The tenant GUC parity gate resolves domains per database from the
topology's own schemas block; its domain map is identical to dev's
(55 internal / 23 tenant / 3 unresolved, 0 violations). The domain
enforcement proof seed, ownership config, audited function hash and red
controls move to public (proof: PASS, 6 relations, 46 red controls).

CI checks the omninode_infra ownership manifests at the OMN-17887 step 2
merge (8b8d5d3b). Tests that pinned tenant now pin the new truth; none
were deleted or weakened, and new tests pin that tenant is refused.
@jonahgabriel jonahgabriel added the hold:auto-merge Holds auto-merge (check_auto_merge_hold.py); remove to release label Sep 24, 2026
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 24, 2026
#11130)

* evidence: OCC companion pass 1 for OmniNode-ai/omnibase_infra#4079

* evidence: OCC companion self-bind for #11130

---------

Co-authored-by: node-occ-companion-effect <occ-companion-effect@omninode.ai>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 7
Nit-level findings suppressed: 1

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 7 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [CRITICAL] docker/application-domain-enforcement/seed.sql (glm-review) — Tenant owner now owns the schema containing internal-domain tables | ALTER SCHEMA public OWNER TO owner_onex_tenant consolidates ownership of the public schema in the tenant-domain owner. The omnino
  • [MAJOR] docker/application-domain-enforcement/seed.sql (glm-review) — No deployable migration accompanies the schema-model change | The diff changes the proof seed, ownership manifests, topology instances and catalog sha256s for onex-dev and onex-prod, but no forward mi
  • [MAJOR] src/omnibase_infra/validation/application_database_domain_enforcement.py (glm-review) — Tenant and internal families now share one namespace with collision-prone gates | Retiring the tenant schema collapses all tenant-domain declarations into public, which is also the physical home o
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Parity script resolution order can misclassify across databases | logical_domains resolves via relation in internal_bridge before consulting the per-database declared domain, and the internal bridge
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Silent coercion of malformed schema spec entries | The new schema-domain map comprehension includes specs that are dicts or None and silently drops anything else. A schemas entry with a scalar value (
  • [MINOR] docker/application-domain-enforcement/seed.sql (glm-review) — USAGE on public granted where USAGE on tenant was scoped before | The seed rewrites GRANT USAGE ON SCHEMA tenant TO app_dashboard, tenant_projection_writer and onex_api into GRANT USAGE ON SCHEMA publ
  • [MINOR] scripts/ci/prove_application_database_domain_enforcement.py (glm-review) — Proof cross-gate control relies on an untested error-string contract | The retired-tenant-schema control asserts the rejection reason "unknown topology schema" against lint_application_database_sql, b

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Critical findings: 0
Major findings: 3
Total findings: 8
Models succeeded: glm-review

Note: Fewer than 2 reviewer models succeeded. Degraded results are informational (OMN-8468/OMN-8524) and do not block merge. Error: cli_review exit 2 (fewer than 2 models succeeded — partial/total outage)


Semantics (OMN-17492 — the model finds, thread resolution gates)

Surface Meaning Blocks merge?
Review threads Per-finding, posted by the reviewer No (informational)
Hostile Review Thread Gate Deterministic: unresolved hostile-reviewer threads exist Fails until resolved (not yet a required context)
degraded verdict Fewer than 2 models succeeded (infra) No

Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review: qwen3-review, qwen3-review-b, glm-review (OMN-8468/OMN-8524/OMN-17492)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 5
Nit-level findings suppressed: 1

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 5 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] src/omnibase_infra/validation/application_database_domain_enforcement.py:_record_sql_target (glm-review) — Removal of the blanket public-schema prohibition weakens a fail-closed control | Previously any application relation targeted at public was refused outright, making public placement impossible regar
  • [MAJOR] src/omnibase_infra/validation/application_database_domain_enforcement.py:validate_application_database_relation_states (glm-review) — No demonstrated RLS requirement for tenant relations now living in public | The deleted check application relations are prohibited in public previously forced every application relation out of publi
  • [MAJOR] src/omnibase_infra/topology/application_database.py:_EXPECTED_SCHEMAS (glm-review) — Divergent treatment of public between the parity checker and application_database.py | check_tenant_guc_domain_parity.py was carefully reworked so public resolves to TENANT only per database, via ea
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py:_walk_database_table_blocks (glm-review) — Silent empty domain for malformed schemas entries in parity checker | _walk_database_table_blocks builds schema_domains with a comprehension accepting spec is None, mapping a null spec to domain "
  • [MINOR] scripts/ci/prove_application_database_domain_enforcement.py:_run_cross_gate_controls (glm-review) — Renamed cross-gate red control loses its former public-placement coverage | The public-table red control (CREATE TABLE public.events refused as prohibited) was replaced by retired-tenant-schema. T

…ted to public

The retire commit removed the `schema: tenant` USAGE blocks outright. For
tenant_projection_writer and validator_ro that was right: both already
held USAGE on public, so the tenant block was a duplicate claim on the
same domain. For app_dashboard and onex_api it was not: USAGE on tenant
was their only declared access to the TENANT domain, and removing it
left them declaring platform_catalog alone.

"Tenant lives in public" means the declaration moves, not that it goes.
Both principals now declare USAGE on public in local, onex-dev and
onex-prod, and the 9 rendered catalogs are regenerated.

Found by the omninode_infra Kubernetes consumer check: its schema_access
for these two bindings reads [tenant, platform_catalog], and the honest
translation is [public, platform_catalog], which the projection has to
carry.

Verified: grant derivation --check --prove in sync on 3 instances; tenant
GUC parity 0 violations; domain-enforcement proof PASS on PostgreSQL 16
(6 relations, 4 pools, 46 red controls); unit topology, validation, ci
and scripts suites show no failure that untouched dev does not also show.
496cd1b and d043526 were written without a trailing newline, which
the end-of-file-fixer hook rejects, and d043526 bumped pyproject.toml
to 0.38.58 without the matching uv.lock line, which uv rewrites on the
next run. Content is otherwise unchanged: the pin stays at 3441453c.
…lock

uv.lock participates in the runner image's shared_env_digest as raw bytes,
so the release-version line moving to 0.38.58 made the recorded digest
stale and runner-image-build-smoke refused the build (run 36073948714).
Regenerated with scripts/ci/runner_image_identity.py --mode generate, as
the version bumps on dev do (#3996): --mode verify exits 1 before and 0
after. shared_env_digest 99110eda... -> 105217eb...; identity v9
9e6e5ba6... -> 08efb837....

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 5
Nit-level findings suppressed: 1

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 5 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] src/omnibase_infra/validation/application_database_domain_enforcement.py (glm-review) — SQL-target gate for public now fails open on ownership provenance | _record_sql_target previously refused every application relation target in public, failing closed regardless of census state. The
  • [MAJOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Per-database domain resolution not namespaced by database for relation keys | _walk_database_table_blocks correctly scopes the schema-to-domain map per database, but logical_domains accumulates result
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Internal bridge check precedes declared-domain check, contradicting per-database semantics | Resolution order in logical_domains is: NO_GUC schemas, then global internal_bridge membership, then the pe
  • [MINOR] src/omnibase_infra/topology/application_database.py (glm-review) — No migration or verification path for existing tenant-schema objects during retirement | The diff removes the tenant schema from _EXPECTED_SCHEMAS, seed.sql, and every topology manifest, and rewrite
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Parity gate test coverage for schemas block with null or malformed entries | _walk_database_table_blocks builds schema_domains with if isinstance(spec, dict) or spec is None, mapping a null schemas

…ic, not the retired tenant

test_an_upstream_contract_addition_cannot_red_an_unrelated_infra_pr replays
an omnimarket merge that adds a db_io table and expects the grant check to
go red. Its replay contract declared schema: tenant; with tenant retired
that declaration is an unmappable residual, derives no grant, and the red
half of the proof passed vacuously (Application Database Domain
Enforcement, run 36074333168). public is the TENANT domain's schema, so
the replay now declares public and derives a tenant_projection_writer grant
the checked-in topology lacks, which is the drift the proof needs.

Verified with omnimarket at the pin (3441453c) as .proof-dependencies: the
test passes, and with the old tenant replay it fails again. The other eight
suites that read .proof-dependencies: 119 passed, 1 skipped (needs GNU
realpath, Linux only).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 5
Nit-level findings suppressed: 1

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 5 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] docker/application-domain-enforcement/seed.sql (glm-review) — Tenant data in public inherits default search_path resolution | Relocating the TENANT domain into the public schema removes a namespace defense-in-depth layer. Every role's default search_path inc
  • [MAJOR] scripts/ci/prove_application_database_domain_enforcement.py (glm-review) — Public-prohibition red control deleted, replacement refusal unproven | The proof script's cross-gate controls previously included a public-table control asserting a CREATE TABLE in public is refused
  • [MINOR] src/omnibase_infra/validation/application_database_domain_enforcement.py (glm-review) — Public-target admission guard not demonstrably database-scoped | The new branch in _record_sql_target admits a public.<name> target whenever public appears in application_schemas. The excerpt do
  • [MINOR] docker/application-domain-enforcement/seed.sql (glm-review) — Tenant owner now owns the default public schema | ALTER SCHEMA public OWNER TO owner_onex_tenant transfers ownership of the database's conventional default schema to the tenant-domain owner. Any too
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py (glm-review) — Parity resolution silently degrades when a database omits its schemas block | _walk_database_table_blocks defaults schema_domains to an empty dict when a database has no schemas key. Under the old l

…unqualified in the SQL lint

Removing the tenant bridge also removed the qualification lint's allowance
for those 24 relations, so deployed, append-only migrations that name them
without a schema started failing: the Application Database Domain
Enforcement SQL gate refused node_projection_delegation/0046's bare
ALTER TABLE delegation_events (#4062, run 36075133104).

The allowance comes back as exactly what it was: the same 24 names, as a
lint-only set that maps nothing to a schema. A qualified public.<name>
target is still held to the ownership check. It is not widened to every
public relation: 36 grandfathered 'must be schema-qualified' entries in
application_database_sql_baseline.yaml name public tables outside it, and
widening would turn them stale at the dev->main promotion.

Tests: 0046's statement lints clean; an unknown name and a public-granted
but never-bridged name (dispatch_eval_results) are still refused; the set is
pinned to the retired bridge. Removing the allowance turns the 0046 test red.
Domain proof PASS (46 red controls); unit/validation plus the SQL-gate, pin
and enforcement-contract suites: 1580 passed.
Conflict in .github/omnimarket-contract-pin.yaml: dev's refresh bot advanced
the pin to 7112b8e8 (#4078); this branch pins 3441453c, omnimarket#2847's
merge, which contains 7112b8e8. Kept 3441453c, the forward pin.

After the merge: grant derivation --check --prove in sync on 3 instances,
every rendered catalog passes --check, tenant GUC parity 0 violations;
tests/unit/topology, tests/unit/validation, tests/ci: 6880 passed, 17 failed,
all 17 also failing on untouched dev.
…ets the retired tenant schema

tests/unit/migrations/test_omn18930_cohort_key_vendor.py (from #4062) proved the
linter live by rewriting 0046's target to public.pg_class and expecting a
lint violation. That relied on the old outright ban on public. With public
the TENANT domain's schema, a public.<name> target passes the lint and is
held to the SQL gate's exactly-one-ownership check instead, like every other
application schema (Tests Split 7/15, run 36080758457).

The control now rewrites the target to the retired tenant schema, which the
lint refuses as unknown, and separately asserts that public.pg_class becomes
an ownership requirement the gate must satisfy. 4 passed; Split 7/15 was the
only failing split in that run.
…ire-tenant-topology-r2

# Conflicts:
#	.github/omnimarket-contract-pin.yaml

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 7
Nit-level findings suppressed: 1

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 7 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] src/omnibase_infra/validation/application_database_domain_enforcement.py, _TENANT_RELATIONS_REFERENCED_UNQUALIFIED and _record_sql_target (glm-review) — Unqualified-name lint allowance is global, not restricted to grandfathered files | The new _TENANT_RELATIONS_REFERENCED_UNQUALIFIED frozenset accepts unqualified references to 23 tenant-domain relatio
  • [MAJOR] docker/application-domain-enforcement/seed.sql (glm-review) — Tenant data in the public schema collides with the default search_path namespace | Retiring the tenant schema and making public the TENANT domain means every role whose search_path defaults to "$user"
  • [MAJOR] src/omnibase_infra/topology/application_database.py, _EXPECTED_SCHEMAS (glm-review) — No teardown or drift path for environments where a tenant schema already exists | The diff deletes the tenant schema from every topology catalog, _EXPECTED_SCHEMAS, and the proof seed, and the comment
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py, logical_domains (glm-review) — Internal bridge resolution now precedes the declared schema domain, changing precedence | In check_tenant_guc_domain_parity.logical_domains, the rewritten resolution order tests the internal bridge be
  • [MINOR] scripts/ci/prove_application_database_domain_enforcement.py, _run_cross_gate_controls (glm-review) — Removed public-table red control narrows the cross-gate proof | _run_cross_gate_controls previously proved that CREATE TABLE public.events is refused. The replacement control proves CREATE TABLE tenan
  • [MINOR] docker/application-domain-enforcement/seed.sql (glm-review) — RLS policies remain TO PUBLIC on tables in the public schema | The moved CREATE POLICY statements retain AS PERMISSIVE FOR ALL TO PUBLIC. Within the proof database this is belt-and-suspenders with FOR
  • [MINOR] scripts/validation/check_tenant_guc_domain_parity.py, _walk_database_table_blocks (glm-review) — Per-database schema-domain resolution leaves databases without a schemas block unresolved | _walk_database_table_blocks yields an empty schema_domains map when a database lacks a schemas key. Every re

@jonahgabriel
jonahgabriel merged commit 21d5d62 into dev Sep 25, 2026
230 of 239 checks passed
@jonahgabriel
jonahgabriel deleted the lakshman/OMN-17887-retire-tenant-topology branch September 25, 2026 14:18
@Patel230

Copy link
Copy Markdown
Contributor Author

Close-out. Merged 2026-09-25T14:18:31Z as 21d5d62161 by jonahgabriel, for OMN-17887. 11 commits, squashed.

In short: I didn't answer the hostile reviewer's findings here before merge, and only one commit was made because of one of them: c4cb317fb7 put back the final newlines that the second pass pointed out (point 16). Most of the findings are about three things. The first is what replaced the blanket public refusal. The second is the tenant data now being "in" public. The third is how the tenant GUC parity gate resolves domains. The code and CI on the final head answer those. The tenant data isn't moving: it was already physically in public, and this PR changes what we declare, not where anything lives. Three points are right and still open: nothing here retires an existing tenant schema (point 5, open in omnibase_infra#4197), a stale comment (point 16), and a missing test for malformed schemas entries (point 10). One thing broke after merge and wasn't found in review: a positive control on dev stopped firing, and omnibase_infra#4147 fixed it (point 1). Details per point below.

Point by point

The reviewer made five passes: on b496ae2cd (09-24 23:09Z), on d043526d0 (23:15Z), on f136fd645 (23:50Z), on ef9b6e313 (09-25 00:00Z) and on the final head 2d7ae9f62 (07:14Z). Codex failed on every pass ("codex CLI not found"), so every finding is glm-review's alone. That puts all 29 below quorum, so none became a thread, and the Hostile Review Thread Gate passed on the final head (job 107980626147). Each pass also dropped one "nit-level" finding. Those five appear only in the job logs (1, 2, 3, 4, 5), and I answer them in point 16. Points raised more than once are grouped. Line numbers are from 21d5d62161. "The final-head CI" means run 36106286708.

1. "Removing the blanket public prohibition weakens a fail-closed control" / "the SQL-target gate for public now fails open on ownership provenance" / "the public-table red control was deleted and its replacement refusal is unproven" (major on d043526d0, f136fd645 and ef9b6e313; minor on d043526d0 and the final head)
The refusal moved; it didn't go away. A public.<name> target now goes into the target locations (application_database_domain_enforcement.py L1258-1265). The SQL gate then requires an ownership declaration for it, or it fails with "requires exactly one ownership declaration" (scripts/ci/check_application_database_sql.py L591-612). That gate is the linter's only production caller. Two tests run the gate end to end on an undeclared public table: test_qualified_create_requires_an_authoritative_ownership_declaration and test_qualified_non_create_target_requires_authoritative_ownership (tests/ci/test_application_database_sql_gate.py L247, L270). In the catalog, a non-TENANT relation declared in public is still refused, because its domain differs from the declared domain of public (_schema_owner_for, L1346-1350; test_red_control_public_application_table, test_application_database_domain_enforcement.py L1004). The live proof keeps a red control for public too: public-catalog-leak creates public.rogue_shadow and must be refused as undeclared (prove_application_database_domain_enforcement.py L1857-1868). All of this passed on the final head: 577 passed across the 14 test files, application_database_sql_gate=PASS, and the proof passed on PostgreSQL 16 with 46 red controls (job 107989091637).
The reviewer was right about one thing, though. The linter on its own no longer refuses public.<name>. omnibase_infra#4102 merged at 08:41Z, after this branch last took dev at 07:01Z. Its positive control relied on the linter refusing public.pg_class. Once this PR merged, that control returned (), and dev went red on two of its parameters (split 7 on the merge commit). omnibase_infra#4147 fixed it at 21:04:54Z by pointing that control at a relation that isn't registered. On the same run, split 3 also failed, in test_dod_verify_runs_omn18900.py (job 108111643109). omnibase_infra#4147 says omnibase_infra#4102 caused that one. I haven't verified that.

2. "No demonstrated RLS requirement for tenant relations now in public" (major, d043526d0)
The RLS requirement follows the domain, not the schema: every relation declared TENANT goes through _validate_tenant_state (L1804-1818). The proof's tenant relation is now public.events (prove_...py L2268). The missing-enable-rls, missing-force-rls, using-drift and with-check-drift controls run against it (L1661-1686), and all four passed on the final head (job above).

3. "The tenant owner now owns the public schema, which holds internal-domain tables" (critical, b496ae2cd; minor, ef9b6e313)
In the proof seed, public holds only the four tenant objects. The internal table is in omninode_internal (seed.sql L15-18, L66). On real databases, the typed topology already made owner_onex_tenant the owner of public before this PR (application_database.py L163, the same line at the parent c7150a15a9). This PR only drops the tenant entry beside it. A schema's owner can drop objects inside it, and internal tables are still physically in public until the OMN-15359 cutover (43 names, physical_schema_mapping.py L36). So the concern is real, but it isn't new here, and this PR doesn't change it.

4. "Tenant data in public resolves through the default search_path" / "safe_report()'s SECURITY DEFINER search_path includes public" (major on ef9b6e313 and the final head; the second is the hint on f136fd645)
The tables were already physically in public (the retired bridge routed tenant to public at the parent). How unqualified names resolve is the same as before this PR. Against name capture: the seed revokes all privileges on public from PUBLIC (seed.sql L16), none of the three topology instances grants CREATE on any schema (0 matches in local, onex-dev and onex-prod), and the SQL gate refuses unqualified targets except for the 24 grandfathered names in point 7. safe_report() puts pg_catalog first and names public.events in full (seed.sql L60-62). Nobody but its owner can create in public, so this is the same exposure it had with tenant.

5. "No deployable migration" / "no teardown or drift path where a tenant schema already exists" (major on b496ae2cd and the final head; minor on f136fd645)
No relation moves, so there's nothing to migrate. Retiring an existing tenant schema is right and isn't done here, as the description says under "Not in this change". It's omnibase_infra#4197, which is still open: a node migration that drops tenant only if it's empty and owned (DROP SCHEMA IF EXISTS tenant RESTRICT). Until that PR is applied, a lane that still has an empty tenant schema keeps it. By its own description, omnibase_infra#4197's census of four .201 lanes found no tenant schema. That's that PR's measurement; I didn't redo it for this close-out.

6. "The tenant and internal families now share one namespace, so the gates can collide" (major, b496ae2cd)
They already shared it physically: both bridges pointed at public. The two name sets don't overlap: 43 internal-bridged names against the 24 tenant names in point 7, overlap 0, measured at 21d5d62161. If one did collide, the ownership loader would raise, because any identity with more than one ownership declaration fails (L2159-2168).

7. "The unqualified-name allowance is global, not limited to grandfathered files" / "the tenant name set is maintained twice" (major, final head; the second is the hint on the final head)
It's global, and the retired bridge was global too: before this PR, the same names were accepted unqualified through _PHYSICALLY_PUBLIC_APPLICATION_OBJECTS. _TENANT_RELATIONS_REFERENCED_UNQUALIFIED (L108-135) equals the parent's TENANT_TABLES_PHYSICALLY_IN_PUBLIC_UNTIL_OMN15359 exactly: 24 and 24, no difference. So the reach is the same as before, not wider. Limiting it by file would have been new behaviour, not a like-for-like replacement, so I didn't do it. The second copy is the pin in test_unqualified_tenant_allowance_equals_the_retired_bridge (test_application_database_domain_enforcement.py L1081), and it's there on purpose, so the allowance can only change through an explicit edit. test_unqualified_allowance_does_not_extend_to_other_public_relations (L1069) shows that a public relation outside the set is still refused. Not changed.

8. "The parity gate checks the internal bridge before the declared domain" (minor on b496ae2cd, f136fd645 and the final head)
That order is required. generation_events is schema: public in the topology, and the bridge is the only checked-in statement that it's internal. If the database's public: TENANT won, the gate would miss the exact pair OMN-18774 was filed for. The old code checked the internal bridge before the tenant bridge too. test_the_internal_bridge_classifies_a_public_declared_relation pins it (tests/ci/test_tenant_guc_domain_parity.py L255). The result didn't move: I ran logical_domains at 21d5d62161 and at its parent c7150a15a9, and both gave 55 internal, 23 tenant and 3 unresolved, identical relation by relation, with 0 violations. CI reported 0 violations on the final head (job 107989088167).

9. "The per-database result isn't keyed by database" (major, f136fd645)
Correct: seen is keyed by relation name (check_tenant_guc_domain_parity.py L415-439), and so is the policy corpus. It fails closed, though. A relation name that two databases resolve differently becomes conflict:, which is a violation whenever the relation carries a GUC policy (L433-437, L460-470). test_instances_disagreeing_on_a_domain_is_reported_not_tie_broken (L424) covers it. test_public_resolves_to_tenant_only_where_its_database_declares_it (L215) shows that public in a service database doesn't resolve to tenant. Not changed.

10. Malformed or missing schemas blocks: null entries map to "", scalar entries are dropped, a database without the block resolves nothing, and there's no test for it (minor on all five passes)
Any of those cases ends at unresolved, which is reported by design and never counts as a violation (docstring L92-100). It can only turn a TENANT relation into unresolved, and a TENANT relation can never be a violation, so it can't hide one. For the application database, the schema map is pinned exactly by validate_application_database_invariants (application_database.py L346-351), so a null or scalar entry there fails that check. A database with no schemas block is covered by test_rls_on_with_no_resolvable_domain_is_reported_not_dropped (L444). All nine databases across the three instances declare the block today. There's no test for null or scalar entries, and the reviewer is right about that. Not changed.

11. "application_database.py treats public differently from the parity checker" (major, d043526d0)
It doesn't. _EXPECTED_SCHEMAS (L155-160) is compared only against the application database (L337-351). The service databases have their own maps, where public is OMNINODE_INTERNAL (L46, L71). That's the same per-database rule the parity checker reads from each database's schemas block.

12. "The public admission isn't scoped by database" (minor, ef9b6e313)
Correct as stated: application_schemas is the union over every database in the topology (L2431-2435). That was already true of every other schema. The SQL gate doesn't know which database a target is in, and it matches ownership on schema and name. Admission is followed by the ownership check from point 1, so an undeclared public target is still refused. Not changed.

13. "The retired-tenant-schema control depends on an untested error string" (minor, b496ae2cd)
The string is tested. 'tenant.<x>' uses unknown topology schema is asserted in test_application_database_sql_enforcement_regressions.py (L463, L480, L498, L728, L794), and the proof already asserted the same text for the unknown-schema control (prove_...py L2205-2208). Both controls passed on the final head.

14. "RLS policies are still TO PUBLIC on tables in the public schema" (minor, final head)
TO PUBLIC names the PUBLIC role, not the schema. It's the canonical role list the enforcement requires (_EXPECTED_CANONICAL_POLICY_ROLES = ("PUBLIC",), L67, checked at L1578-1581), and this PR didn't change it. Not changed.

15. "USAGE on public is granted where USAGE on tenant was scoped before" (minor, b496ae2cd)
USAGE on a schema grants no access to any table in it. The seed's table grants are the same per-object grants as before, now on the public names (seed.sql L82-86). In the topology, the later commit b52b24255d moved app_dashboard's and onex_api's USAGE to public, because that was their only declared access to the TENANT domain. tenant_projection_writer and validator_ro already had USAGE on public, so their tenant blocks were duplicates and were dropped. I made that change to line up with the Kubernetes consumer check, as the description says, not because of this finding.

16. The five nit-level hints

  • Stale docstring about "two bridge frozensets" (b496ae2cd): that line (test_tenant_guc_domain_parity.py L22-24) describes what happened before, and L26-30 say the tenant bridge is gone. Not changed.
  • Final newline dropped from .github/omnimarket-contract-pin.yaml and pyproject.toml (d043526d0): right. c4cb317fb7 put them back, and both files end in a newline at the merge commit.
  • SECURITY DEFINER search_path (f136fd645): see point 4.
  • A parenthesized single expression is misleading (ef9b6e313): right. _PHYSICALLY_PUBLIC_APPLICATION_TABLES (src/omnibase_infra/validation/application_database_domain_enforcement.py L92-94) is now one name in parentheses, and the comment above it (L74-89) still says "one of these two frozensets" and warns about combining two sets. It doesn't change behaviour, but the comment is out of date. Not fixed.
  • Duplicate maintenance of the tenant name set (final head): see point 7.

The issue comment

The DEGRADED summary (09-24 23:09Z) belongs to the first pass, and everything in it is answered above.

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold:auto-merge Holds auto-merge (check_auto_merge_hold.py); remove to release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants