From 9b83171fba94154778180d29ac0f9b1268054547 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Sat, 8 Aug 2026 09:40:57 -0400 Subject: [PATCH 1/3] fix(OMN-15732): break infra-dev merge deadlock -- exempt node-migration-sync-forced vendor files from OMN-15361 legacy-schema lint infra dev is merge-deadlocked: node-migration-sync (OMN-13332) forces every open infra dev PR to vendor two upstream omnimarket migrations verbatim (node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql, node_projection_registration/0004_node_service_registry_no_force_rls.sql), but vendoring them trips "Application Database Domain Enforcement (OMN-15361)" -- a required CI Summary STRICT_GATE_JOBS member -- in the "Enforce schema qualification in changed SQL (trusted)" step of scripts/ci/check_application_database_sql.py: not vendoring -> red node-migration-sync; vendoring -> red OMN-15361. Both sides are hard required gates, neither can be waived, and the failure reproduces identically on 5 open PRs (2681/2680/2677/2676/2668) plus 2682 (head 1aa87f7e2a, run 31259117513/job 93107117177), which currently carries the only PR-side reproduction of the OMN-15361 half. ROOT CAUSE: both files are new migrations in existing streams whose physical tables (public.capability_scores, public.node_service_registry) have been unqualified/default-schema since their original CREATE, pending the separate, larger governed OMN-15359 schema cutover. This is the exact case scripts/ci/check_application_database_sql.py's _LEGACY_DEFAULT_SCHEMA_SQL_EXACT_PATHS exists to cover -- it already carries 11 precedent entries (OMN-15503, OMN-15655) exempting sibling follow-up migrations in the same physical-table-stays-in-public state, most directly node_canary_score_reducer/0002_capability_scores_tenant_id_and_rls.sql, which 0003 in this fix directly continues (same table, same fail-closed tenant-mapping pattern, next step of the OMN-15356 program). FIX: add both new file paths to the existing exemption set, with inline comments citing the precedent and this ticket. No SQL content changed at the omnimarket source and no ownership-manifest edits were needed -- the migration content is already correct for its precedented class; only the infra-side CI exemption registry (the validator's own documented mechanism for exactly this situation) was missing the two new entries. This does not touch validator logic, add a skip token, or weaken what the gate checks for any non-exempt file. Also vendors the two migrations for real (via scripts/sync-node-migrations.sh, matching infra PR #2682's approach) and adds their declaration rows to docker/migrations/forward/_ledger/application-migrations.tsv (checksums 8bf81c98../adb21c75.. -- byte-identical to the content already vendored on #2682, confirmed via sha256sum), bumping tests/unit/scripts/validation/test_application_migration_manifest.py's declaration-count assertion 94->96, so this PR itself clears both sides of the deadlock (node-migration-sync AND OMN-15361) rather than only fixing the gate in the abstract. VALIDATOR PROOF (not just theoretical): reproduced the exact 5 findings from live CI job 93107117177 by running check_application_database_sql.py against the exact vendored SQL content from infra PR #2682 head 1aa87f7e2a (fetched via `git show 1aa87f7e2a:`) with the pre-fix script -- application_database_sql_gate=FAIL, violation lines byte-identical to the live log. Same run with this fix applied -- application_database_sql_gate=PASS, using the exact 3-manifest CI argument set (omnimarket + both omninode_infra manifests). RE-VENDOR CASCADE: none beyond the normal git-history mechanism. This fix changes no *content* on the two SQL files and no checksums vs the #2682 precedent, so it carries no separate TSV re-sync obligation for other branches. Once merged to infra dev, every other deadlocked PR (2681/2680/2677/2676/2668) clears the OMN-15361 half of the deadlock as soon as it (a) vendors the same two files -- forced by node-migration-sync regardless of this fix -- and (b) is based on/rebased onto a dev commit containing this exemption-list change; the exemption keys on a fixed logical file path, not git ancestry or content hash, so no per-branch checksum resync is required. Local proof: uv run ruff format/check --fix (clean), uv run mypy (clean), pre-commit run --files on all changed files (full chain, no --no-verify), uv run pytest tests/ci/test_application_database_sql_gate.py tests/ci/test_application_database_sql_authority_isolation.py tests/ci/test_application_database_domain_enforcement_contract.py tests/unit/validation/test_application_database_sql_enforcement_regressions.py tests/unit/scripts/validation/test_application_migration_manifest.py -- all passed. Ticket: OMN-15732 --- .../_ledger/application-migrations.tsv | 2 + ...03_capability_scores_tenant_id_to_uuid.sql | 132 ++++++++++++++++++ ...004_node_service_registry_no_force_rls.sql | 57 ++++++++ scripts/ci/check_application_database_sql.py | 29 ++++ .../test_application_migration_manifest.py | 11 +- 5 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql create mode 100644 docker/migrations/forward/nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql diff --git a/docker/migrations/forward/_ledger/application-migrations.tsv b/docker/migrations/forward/_ledger/application-migrations.tsv index 8701fedc35..eb91c4e5bb 100644 --- a/docker/migrations/forward/_ledger/application-migrations.tsv +++ b/docker/migrations/forward/_ledger/application-migrations.tsv @@ -1,5 +1,6 @@ nodes/node_canary_score_reducer/0001_create_capability_scores.sql node:node_canary_score_reducer node:node_canary_score_reducer omninode_internal node:node_canary_score_reducer:0001_create_capability_scores.sql 7195b07a7fae809f141a136a67b799ad492e79d280f54c325ac14875dcfcc2cd nodes/node_canary_score_reducer/0002_capability_scores_tenant_id_and_rls.sql node:node_canary_score_reducer node:node_canary_score_reducer tenant node:node_canary_score_reducer:0002_capability_scores_tenant_id_and_rls.sql 74d38cca4463ed4f4d20169164d788c5afc79eea58b7f2abd02ef23093388ba4 +nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql node:node_canary_score_reducer node:node_canary_score_reducer tenant node:node_canary_score_reducer:0003_capability_scores_tenant_id_to_uuid.sql 8bf81c98e0ac6b611b95722061613e18b308a3fd28c386c97f42b4cd1c548f4b nodes/node_contract_registry/0000_create_contract_registry.sql node:node_contract_registry node:node_contract_registry omninode_internal node:node_contract_registry:0000_create_contract_registry.sql bff49882c3c724de24f75b97c847ab78581064a72b77429a2251a02448fad668 nodes/node_deployment_evidence_reducer/0001_create_deployment_evidence_projection_tables.sql node:node_deployment_evidence_reducer node:node_deployment_evidence_reducer omninode_internal node:node_deployment_evidence_reducer:0001_create_deployment_evidence_projection_tables.sql 1f3d9bcc7b90bfd9bed76bac2f0b815d742c11b3deadbf5b24a682350ccbbbcd nodes/node_evidence_dashboard_reducer/0001_create_evidence_dashboard_projection_tables.sql node:node_evidence_dashboard_reducer node:node_evidence_dashboard_reducer omninode_internal node:node_evidence_dashboard_reducer:0001_create_evidence_dashboard_projection_tables.sql b3c4be68802a73d5f865b6730b191a15fa9d2729d3b8a5e75ed82ce3c94b70e8 @@ -73,6 +74,7 @@ nodes/node_projection_registration/0000_create_node_service_registry.sql node:no nodes/node_projection_registration/0001_add_heartbeat_columns.sql node:node_projection_registration node:node_projection_registration omninode_internal node:node_projection_registration:0001_add_heartbeat_columns.sql 2cda779401d041ae1383937df3f4c614df74d09c80c78f24f7993685ba7a6aed nodes/node_projection_registration/0002_node_service_registry_tenant_rls.sql node:node_projection_registration node:node_projection_registration omninode_internal node:node_projection_registration:0002_node_service_registry_tenant_rls.sql 3e8a3c12dbefc4432262a1c2533667f4a5f342c39149915bd80b45241c182e98 nodes/node_projection_registration/0003_reconcile_heartbeat_observability.sql node:node_projection_registration node:node_projection_registration omninode_internal node:node_projection_registration:0003_reconcile_heartbeat_observability.sql 0bc9286ec9b0f7ad36e1d29e54d34bc11fe2be64cf5b874d7432384869ade69e +nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql node:node_projection_registration node:node_projection_registration omninode_internal node:node_projection_registration:0004_node_service_registry_no_force_rls.sql adb21c75ec2503b50a7272857e660ff07cfda16374698cc245af93047e3da6f2 nodes/node_projection_routing_decision/0021_create_agent_routing_decisions.sql node:node_projection_routing_decision node:node_projection_routing_decision omninode_internal node:node_projection_routing_decision:0021_create_agent_routing_decisions.sql 46d169e595f65ad89e7f524efffedb59eb0e1a37f9527e67f1ff2e8eaaa7711e nodes/node_projection_routing_decision/0022_agent_routing_decisions_tenant_id_and_rls.sql node:node_projection_routing_decision node:node_projection_routing_decision tenant node:node_projection_routing_decision:0022_agent_routing_decisions_tenant_id_and_rls.sql bac8be9bbe6167601ca805d35a78f469a6bce9ca9fc22e88a363c50dac0b7f83 nodes/node_projection_sandbox_decisions/0001_create_sandbox_decisions.sql node:node_projection_sandbox_decisions node:node_projection_sandbox_decisions omninode_internal node:node_projection_sandbox_decisions:0001_create_sandbox_decisions.sql dcf92194d262bb957dd5537a2d9e16083b89d2d1c259f08f869460b76145b96c diff --git a/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql b/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql new file mode 100644 index 0000000000..1962581cc6 --- /dev/null +++ b/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql @@ -0,0 +1,132 @@ +-- OMN-15356: convert capability_scores.tenant_id from the legacy TEXT slug +-- to the canonical UUID identity, for the classified-TENANT relation set. +-- +-- WHY THIS TABLE FIRST +-- capability_scores is the relation the OMN-15356 identity module +-- (`omnimarket.projection.tenant_isolation.resolve_tenant_uuid`) and its +-- test (`test_the_tenant_classified_relations_carry_a_tenant_id_migration`) +-- already use as the canonical worked example. This migration lands the +-- full end-to-end pattern -- mapping function, fail-closed conversion, +-- index/constraint preservation, RLS policy cast -- for ONE relation as the +-- reviewable shape. The remaining classified-TENANT relations (see the +-- `expected` table in `test_house_tenant_identity.py` plus +-- delegation_events/delegation_judge_verdict_events/delegation_budget_state/ +-- inference_response_text/savings_estimates/node_service_registry) convert +-- via the identical mechanical pattern in follow-up migrations under this +-- same ticket -- NOT silently dropped, deferred and named in the PR body. +-- +-- FAIL-CLOSED: NO SENTINEL SURVIVES +-- `house_tenant_map_slug_to_uuid` is the SQL mirror of +-- `omnimarket.projection.tenant_isolation.resolve_tenant_uuid` -- same +-- closed mapping (today: 'omninode' only), same refusal behavior. Postgres +-- evaluates the `USING` expression for every row as part of the single +-- `ALTER TABLE` DDL statement; if ANY row's tenant_id is not an exact key in +-- the mapping, the function RAISEs, the statement fails, and -- because DDL +-- in PostgreSQL is transactional -- the entire migration transaction rolls +-- back. There is no partial conversion, no invented UUID, and no +-- 'unmapped rows keep their old value' fallback: the column stays TEXT +-- until every row maps, exactly the "no sentinel/default survives" +-- acceptance criterion. +-- +-- CONSTRAINTS AND INDEXES SURVIVE THE TYPE CHANGE +-- `ALTER COLUMN ... TYPE` rewrites the column in place; the pre-existing +-- `idx_capability_scores_tenant_id` index and the +-- `capability_scores_model_key_task_type_key` UNIQUE constraint (on +-- unrelated columns, untouched here) are NOT dropped and NOT recreated by +-- this migration -- PostgreSQL preserves an index across a column type +-- change automatically when the new type has a binary-compatible or +-- USING-expressed conversion path, which this is. The Docker proof in +-- omnibase_infra (docker/tenant-uuid-conversion-proof) asserts this +-- directly rather than assuming it. +-- +-- RLS POLICY: THE GUC COMPARISON GAINS AN EXPLICIT CAST +-- `current_setting('app.tenant_id', true)` always returns TEXT (GUCs have +-- no native UUID type); the policy predicate must cast it explicitly now +-- that the column itself is UUID. `::uuid` on a non-UUID-shaped GUC value +-- raises rather than silently coercing, so an unset or malformed GUC still +-- fails closed -- proving that behavior is OMN-15416's scope (real +-- non-owner pools), not duplicated here. +-- +-- Idempotent: the mapping function is CREATE OR REPLACE; the column-type +-- guard only runs the ALTER when the column is not already UUID, so a +-- second application is a no-op. + +CREATE OR REPLACE FUNCTION house_tenant_map_slug_to_uuid(p_value TEXT) +RETURNS UUID +LANGUAGE plpgsql +IMMUTABLE +AS $$ +BEGIN + IF p_value = 'omninode' THEN + RETURN '820272f9-4aaf-5add-a2df-0af942852ab2'::uuid; + END IF; + RAISE EXCEPTION + 'OMN-15356: no canonical UUID mapping for tenant value % -- refusing ' + 'to invent or default one; extend house_tenant_map_slug_to_uuid only ' + 'after confirming this is a real, reviewed tenant identity', + p_value; +END; +$$; + +DO $$ +DECLARE + v_current_type TEXT; +BEGIN + SELECT atttypid::regtype::text INTO v_current_type + FROM pg_attribute + WHERE attrelid = 'public.capability_scores'::regclass + AND attname = 'tenant_id' + AND NOT attisdropped; + + IF v_current_type IS NULL THEN + RAISE EXCEPTION + 'OMN-15356: public.capability_scores.tenant_id column not found -- ' + 'expected migration 0002 to have already landed it'; + ELSIF v_current_type = 'uuid' THEN + RAISE NOTICE + 'public.capability_scores.tenant_id is already uuid; skipping conversion'; + ELSIF v_current_type = 'text' THEN + -- The pre-existing tenant_isolation POLICY (migration 0002) depends + -- on this column -- PostgreSQL refuses ALTER COLUMN ... TYPE while + -- any policy references it, so the policy must be dropped first and + -- recreated (with the ::uuid cast) after the type change, not just + -- before/after this DO block. Caught by the Docker fixture proof + -- (docker/tenant-uuid-conversion-proof) before this ever reached a + -- real database. + DROP POLICY IF EXISTS tenant_isolation ON public.capability_scores; + + -- The pre-existing TEXT DEFAULT ('omninode') is not automatically + -- castable to uuid -- Postgres tries to cast the DEFAULT expression + -- itself (not just the stored rows) when the column type changes, + -- and 'omninode'::uuid is not a valid uuid literal, so the DEFAULT + -- must be dropped before the TYPE change and a new uuid-typed + -- DEFAULT set after it. Also caught by the same fixture proof. + ALTER TABLE public.capability_scores + ALTER COLUMN tenant_id DROP DEFAULT; + ALTER TABLE public.capability_scores + ALTER COLUMN tenant_id TYPE UUID + USING house_tenant_map_slug_to_uuid(tenant_id); + ALTER TABLE public.capability_scores + ALTER COLUMN tenant_id SET DEFAULT '820272f9-4aaf-5add-a2df-0af942852ab2'::uuid; + ELSE + RAISE EXCEPTION + 'OMN-15356: public.capability_scores.tenant_id has unexpected type %, ' + 'expected text or uuid -- operator schema ruling required', + v_current_type; + END IF; +END$$; + +-- Idempotent whether or not the DO block above dropped it (the 'uuid' +-- branch above leaves the existing (already-cast) policy in place, so this +-- DROP + CREATE additionally covers a hand-repaired or partially-applied +-- prior state without erroring). +DROP POLICY IF EXISTS tenant_isolation ON public.capability_scores; +CREATE POLICY tenant_isolation ON public.capability_scores + FOR ALL + USING (tenant_id = current_setting('app.tenant_id', true)::uuid) + WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid); + +-- OMN-14894 ratchet: every file that (re)creates this policy must grant +-- app_dashboard SELECT in the same file. Idempotent; already granted by +-- migration 0002, restated here so this file alone satisfies the ratchet. +GRANT SELECT ON public.capability_scores TO app_dashboard; diff --git a/docker/migrations/forward/nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql b/docker/migrations/forward/nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql new file mode 100644 index 0000000000..f63fc1acb3 --- /dev/null +++ b/docker/migrations/forward/nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql @@ -0,0 +1,57 @@ +-- OMN-15336 item 4 (required-fix #4) / operator ruling R-q (2026-08-05, +-- rolling ledger 23:45Z): registry and orchestration state +-- (node_service_registry) is omninode_internal runtime state, not tenant +-- data. FORCE ROW LEVEL SECURITY on internal-domain runtime state is the +-- wrong posture: it forces EVERY reader, including the table owner, through +-- a tenant_id predicate that internal registry rows do not carry +-- meaningfully (0002 stamped a DEFAULT 'omninode' single-tenant value — +-- there is no real multi-tenant partition of "which node is running where"). +-- +-- This reverses ONLY the FORCE half of 0002_node_service_registry_tenant_rls +-- (leaves RLS ENABLEd and the tenant_isolation POLICY in place, so a future +-- tenant-scoped consumer is not blocked from reintroducing FORCE explicitly +-- with a considered ruling of its own) — narrower than DISABLE ROW LEVEL +-- SECURITY or DROP POLICY, matching item 4's own recommendation "(b) strip +-- FORCE-RLS" precisely rather than removing the RLS posture wholesale. +-- +-- Idempotent on both paths, matching 0003's own precedent: the registration +-- trio (0000/0001/0002) is operator-fenced by default (OMN-15379/OMN-15349) +-- and released only on the compose dev lane and the k8s onex-dev Job +-- (ruling 21) — a fresh, still-fenced database has no node_service_registry +-- table at all, and this migration must be a deterministic no-op there, not +-- an error. +-- +-- Does NOT need a fence-manifest entry: the OMN-15336 item-4 guard +-- (migration_declares_unclassified_force_rls in run-forward-migrations.sh) +-- only refuses migrations that ENABLE FORCE ROW LEVEL SECURITY without a +-- fence entry. `NO FORCE ROW LEVEL SECURITY` is the disabling form and is +-- explicitly excluded from that match — the guard exists to gate the +-- hazard this migration removes, not to block its own remedy. +-- +-- Reconciles with ruling 15 (OMN-15379, "the registration trio moves +-- together") and ruling 21 (OMN-15332 comment 1a067542, the k8s durable +-- release): neither ruling is reversed by this migration. Both rulings +-- govern whether 0000/0001/0002 apply at all (fence membership); this +-- migration only changes the FORCE posture of a table that has already been +-- released and created, and ships unfenced so it applies on every lane +-- where the trio has already landed, converging them to the corrected +-- posture without re-litigating the release decision itself. +-- +-- Ticket: OMN-15336 (item 4). Introduced-by: OMN-15343 (0002's FORCE). +-- Domain corroboration: node_projection_registration/contract.yaml +-- db_io.schema=omninode_internal; 2026-08-02 operator domain ruling +-- ("registry... stays omninode_internal"); OMN-15656's landed +-- grants-derivation correction ("node_service_registry/live_events are +-- internal not tenant"); R-q's generalized precedent ("FORCE-RLS on +-- internal-domain runtime state is wrong"). +DO $unforce_registry_rls$ +BEGIN + IF to_regclass('public.node_service_registry') IS NOT NULL THEN + ALTER TABLE public.node_service_registry NO FORCE ROW LEVEL SECURITY; + ELSE + RAISE NOTICE + 'node_service_registry is absent; FORCE-RLS reversal is a no-op ' + '(the registration trio is still fenced on this database)'; + END IF; +END +$unforce_registry_rls$; diff --git a/scripts/ci/check_application_database_sql.py b/scripts/ci/check_application_database_sql.py index 3b010908b4..c60f4d4d38 100644 --- a/scripts/ci/check_application_database_sql.py +++ b/scripts/ci/check_application_database_sql.py @@ -42,6 +42,35 @@ "docker/migrations/forward/nodes/node_canary_score_reducer/" "0002_capability_scores_tenant_id_and_rls.sql" ), + # OMN-15732: 0003 is the direct sequel to 0002 above -- it continues + # the OMN-15356 tenant_id TEXT->UUID conversion on the SAME physical + # public.capability_scores table 0002 already exempted for the + # identical reason (physical table intentionally remains in public + # until the governed OMN-15359 schema cutover). The mapping function + # `house_tenant_map_slug_to_uuid` it defines is deployed alongside + # that table and shares its schema fate; qualifying only this new + # statement would target objects that do not exist under a qualified + # name in the runner today. node-migration-sync (OMN-13332) forces + # this file to be vendored verbatim from omnimarket dev regardless of + # this gate, so exempting it here (not editing the SQL) is the + # canonical fix -- see docs/tracking/ROLLING_WORK_LEDGER.md + # 2026-08-08 [mergesweep-0808-deadlock] for the full deadlock + # analysis. + Path( + "docker/migrations/forward/nodes/node_canary_score_reducer/" + "0003_capability_scores_tenant_id_to_uuid.sql" + ), + # OMN-15732: node_service_registry has been unqualified (default/ + # public schema) since its 0000 CREATE TABLE; 0004 only flips its + # FORCE ROW LEVEL SECURITY posture (OMN-15336 item 4 / operator + # ruling R-q, 2026-08-05) inside a DO block that references the same + # already-unqualified table -- it introduces no new physical-schema + # authority. Same node-migration-sync-forced-vendoring rationale as + # above. + Path( + "docker/migrations/forward/nodes/node_projection_registration/" + "0004_node_service_registry_no_force_rls.sql" + ), Path( "docker/migrations/forward/nodes/node_projection_context_roi/" "003_context_roi_scores_tenant_id_and_rls.sql" diff --git a/tests/unit/scripts/validation/test_application_migration_manifest.py b/tests/unit/scripts/validation/test_application_migration_manifest.py index 81df3a5c2d..81152dc4e4 100644 --- a/tests/unit/scripts/validation/test_application_migration_manifest.py +++ b/tests/unit/scripts/validation/test_application_migration_manifest.py @@ -99,10 +99,13 @@ def _minimal_fixture(tmp_path: Path) -> tuple[Path, Path]: def test_checked_in_manifest_is_exact_and_all_blockers_are_explicit() -> None: result = _validate() - # 94 as of the OMN-14894 vendor-parity repair: the manifest mirrors the - # vendored node migration tree after restoring the nine house-tenant RLS - # migrations that are still source-owned in omnimarket dev. - assert len(result.declarations) == 94 + # 96 as of OMN-15732: 94 from the OMN-14894 vendor-parity repair (the + # manifest mirrors the vendored node migration tree after restoring the + # nine house-tenant RLS migrations that are still source-owned in + # omnimarket dev), +2 for node_canary_score_reducer/0003 and + # node_projection_registration/0004 (see OMN-15361 exemption fix above + # for why these are the deadlock-triggering vendor files). + assert len(result.declarations) == 96 assert result.blocked == () assert len(result.cloud_aliases) == 30 From 4a90c75d5b821a17054636e702ad503dfb1e90fb Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Sat, 8 Aug 2026 12:46:48 -0400 Subject: [PATCH 2/3] docs(OMN-15732): update exemption comment for the reverted 0003 inline mapping omnimarket's OMN-15732 0003/0004 split was adjudicated and reverted (no gate widened): platform_catalog is inadmissible for a node:% migration stream, so the mapping function is inlined back into the single 0003_capability_scores_tenant_id_to_uuid.sql file instead of living as a standalone catalog object. No structural change here -- the existing _LEGACY_DEFAULT_SCHEMA_SQL_EXACT_PATHS entry and TSV ledger row already name that exact pre-split path and stay a checksum-only delta once node-migration-sync re-vendors. This commit only corrects the comment's stale "mapping function it defines" clause. --- scripts/ci/check_application_database_sql.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scripts/ci/check_application_database_sql.py b/scripts/ci/check_application_database_sql.py index c60f4d4d38..29bb5c21da 100644 --- a/scripts/ci/check_application_database_sql.py +++ b/scripts/ci/check_application_database_sql.py @@ -46,16 +46,17 @@ # the OMN-15356 tenant_id TEXT->UUID conversion on the SAME physical # public.capability_scores table 0002 already exempted for the # identical reason (physical table intentionally remains in public - # until the governed OMN-15359 schema cutover). The mapping function - # `house_tenant_map_slug_to_uuid` it defines is deployed alongside - # that table and shares its schema fate; qualifying only this new - # statement would target objects that do not exist under a qualified - # name in the runner today. node-migration-sync (OMN-13332) forces - # this file to be vendored verbatim from omnimarket dev regardless of - # this gate, so exempting it here (not editing the SQL) is the - # canonical fix -- see docs/tracking/ROLLING_WORK_LEDGER.md - # 2026-08-08 [mergesweep-0808-deadlock] for the full deadlock - # analysis. + # until the governed OMN-15359 schema cutover). OMN-15732 AC2 + # adjudication (2026-08-08) rejected a standalone, schema-qualified + # mapping function as inadmissible for a node migration stream (no + # `node:%` domain -- {tenant, omninode_internal} -- admits + # platform_catalog); the mapping is now inlined into this same file's + # `ALTER COLUMN ... USING` clause, so there is no separate object to + # qualify. node-migration-sync (OMN-13332) forces this file to be + # vendored verbatim from omnimarket dev regardless of this gate, so + # exempting it here (not editing the SQL) is the canonical fix -- see + # docs/tracking/ROLLING_WORK_LEDGER.md 2026-08-08 + # [mergesweep-0808-deadlock] for the full deadlock analysis. Path( "docker/migrations/forward/nodes/node_canary_score_reducer/" "0003_capability_scores_tenant_id_to_uuid.sql" From e2c379cb408e213ed4e3b0d1efedd5b59153ce9f Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Sat, 8 Aug 2026 13:06:27 -0400 Subject: [PATCH 3/3] fix(OMN-15732): re-vendor 0003 from the reverted omnimarket inline mapping Re-vendors docker/migrations/forward/nodes/node_canary_score_reducer/ 0003_capability_scores_tenant_id_to_uuid.sql from omnimarket jonah/omn-15732-0003-optc@09c53bfd (PR #2026, adjudicated 2026-08-08): the standalone house_tenant_map_slug_to_uuid function is deleted and its mapping inlined into this file's own ALTER COLUMN ... USING clause. The previously-vendored copy here still carried the pre-adjudication function-based content; scripts/sync-node-migrations.sh (OMNIMARKET_SRC override) brings it back in sync. Refreshes the TSV declaration row's checksum to match. No exemption- registry, gate-logic, or domain/CHECK-constraint edit -- the existing 0003 exemption entry and domain=tenant declaration were already correct for the single-file shape per omnibase_infra#2683's prior commit. PR body separately gains an Omnimarket-Source-Ref field pinning node-migration-sync to the same unmerged omnimarket branch, since dev does not yet carry the omnimarket-side fix. OMN-15732. --- .../_ledger/application-migrations.tsv | 2 +- ...03_capability_scores_tenant_id_to_uuid.sql | 106 ++++++++++++------ 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/docker/migrations/forward/_ledger/application-migrations.tsv b/docker/migrations/forward/_ledger/application-migrations.tsv index eb91c4e5bb..1918ba03ae 100644 --- a/docker/migrations/forward/_ledger/application-migrations.tsv +++ b/docker/migrations/forward/_ledger/application-migrations.tsv @@ -1,6 +1,6 @@ nodes/node_canary_score_reducer/0001_create_capability_scores.sql node:node_canary_score_reducer node:node_canary_score_reducer omninode_internal node:node_canary_score_reducer:0001_create_capability_scores.sql 7195b07a7fae809f141a136a67b799ad492e79d280f54c325ac14875dcfcc2cd nodes/node_canary_score_reducer/0002_capability_scores_tenant_id_and_rls.sql node:node_canary_score_reducer node:node_canary_score_reducer tenant node:node_canary_score_reducer:0002_capability_scores_tenant_id_and_rls.sql 74d38cca4463ed4f4d20169164d788c5afc79eea58b7f2abd02ef23093388ba4 -nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql node:node_canary_score_reducer node:node_canary_score_reducer tenant node:node_canary_score_reducer:0003_capability_scores_tenant_id_to_uuid.sql 8bf81c98e0ac6b611b95722061613e18b308a3fd28c386c97f42b4cd1c548f4b +nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql node:node_canary_score_reducer node:node_canary_score_reducer tenant node:node_canary_score_reducer:0003_capability_scores_tenant_id_to_uuid.sql d1eeefff11fbec255216154ba26aa6f0f29602a0b6fd8fd89e8de5d8d3282b2c nodes/node_contract_registry/0000_create_contract_registry.sql node:node_contract_registry node:node_contract_registry omninode_internal node:node_contract_registry:0000_create_contract_registry.sql bff49882c3c724de24f75b97c847ab78581064a72b77429a2251a02448fad668 nodes/node_deployment_evidence_reducer/0001_create_deployment_evidence_projection_tables.sql node:node_deployment_evidence_reducer node:node_deployment_evidence_reducer omninode_internal node:node_deployment_evidence_reducer:0001_create_deployment_evidence_projection_tables.sql 1f3d9bcc7b90bfd9bed76bac2f0b815d742c11b3deadbf5b24a682350ccbbbcd nodes/node_evidence_dashboard_reducer/0001_create_evidence_dashboard_projection_tables.sql node:node_evidence_dashboard_reducer node:node_evidence_dashboard_reducer omninode_internal node:node_evidence_dashboard_reducer:0001_create_evidence_dashboard_projection_tables.sql b3c4be68802a73d5f865b6730b191a15fa9d2729d3b8a5e75ed82ce3c94b70e8 diff --git a/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql b/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql index 1962581cc6..903d9fe82a 100644 --- a/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql +++ b/docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql @@ -1,12 +1,39 @@ -- OMN-15356: convert capability_scores.tenant_id from the legacy TEXT slug -- to the canonical UUID identity, for the classified-TENANT relation set. -- +-- OMN-15732 AC2 ADJUDICATION (2026-08-08, no gate widened) +-- An earlier revision of this migration split the mapping into a +-- standalone, schema-qualified `platform_catalog.house_tenant_map_slug_to_ +-- uuid` function so it could be ownership-declared as a new application +-- object. That placement was rejected on adjudication: the migration-ledger +-- `domain` column is the *declared target schema domain* (omnibase_infra +-- docker/migrations/forward/_ledger/bootstrap.sql), and `platform_catalog` +-- (topology domain PLATFORM_CATALOG) is inadmissible for a `node:%` +-- migration stream at both the static gate +-- (scripts/validation/validate_application_migration_manifest.py) and the +-- runtime `schema_migrations_stream_domain_check` CHECK constraint, which +-- together restrict node-stream domains to exactly {tenant, +-- omninode_internal}. `tenant` and `omninode_internal` both fail +-- apply-time schema presence today (the former needs the still-pending +-- OMN-15359 cutover; the latter is created only by the cutover-only +-- initializer, never by the forward-migration stream). The function has +-- exactly one consumer -- this file's own DDL-time `USING` clause -- and no +-- runtime caller; the runtime implementation is the single Python +-- function `omnimarket.projection.tenant_isolation.resolve_tenant_uuid`. +-- A persistent catalog object read once by one ALTER buys nothing here and +-- cannot be given a truthful ownership/domain declaration on every surface +-- at once. The mapping is therefore inlined below with no CREATE FUNCTION +-- and no new schema-qualified object; OMN-15359's governed tenant/internal +-- cutover is the right place to promote this into a shared, +-- truthfully-declared `tenant.house_tenant_map_slug_to_uuid` alongside the +-- other classified-TENANT relations' conversions. +-- -- WHY THIS TABLE FIRST -- capability_scores is the relation the OMN-15356 identity module -- (`omnimarket.projection.tenant_isolation.resolve_tenant_uuid`) and its -- test (`test_the_tenant_classified_relations_carry_a_tenant_id_migration`) -- already use as the canonical worked example. This migration lands the --- full end-to-end pattern -- mapping function, fail-closed conversion, +-- full end-to-end pattern -- inline fail-closed mapping, conversion, -- index/constraint preservation, RLS policy cast -- for ONE relation as the -- reviewable shape. The remaining classified-TENANT relations (see the -- `expected` table in `test_house_tenant_identity.py` plus @@ -15,17 +42,19 @@ -- via the identical mechanical pattern in follow-up migrations under this -- same ticket -- NOT silently dropped, deferred and named in the PR body. -- --- FAIL-CLOSED: NO SENTINEL SURVIVES --- `house_tenant_map_slug_to_uuid` is the SQL mirror of --- `omnimarket.projection.tenant_isolation.resolve_tenant_uuid` -- same --- closed mapping (today: 'omninode' only), same refusal behavior. Postgres --- evaluates the `USING` expression for every row as part of the single --- `ALTER TABLE` DDL statement; if ANY row's tenant_id is not an exact key in --- the mapping, the function RAISEs, the statement fails, and -- because DDL --- in PostgreSQL is transactional -- the entire migration transaction rolls --- back. There is no partial conversion, no invented UUID, and no --- 'unmapped rows keep their old value' fallback: the column stays TEXT --- until every row maps, exactly the "no sentinel/default survives" +-- FAIL-CLOSED: NO SENTINEL SURVIVES (two independent guards) +-- (1) A pre-guard `IF EXISTS` check below RAISEs before any DDL runs if any +-- row's tenant_id is not the one closed mapping key this codebase knows +-- about. (2) Even if that guard were ever bypassed, the `ALTER COLUMN ... +-- TYPE UUID USING (CASE ... END)` below has no ELSE branch: an unmapped +-- value evaluates the CASE to NULL, which then violates the column's +-- pre-existing NOT NULL constraint (migration 0002) and aborts the +-- statement. Postgres evaluates the `USING` expression for every row as +-- part of the single `ALTER TABLE` DDL statement, and because DDL in +-- PostgreSQL is transactional, either guard failing rolls back the entire +-- migration transaction. There is no partial conversion, no invented UUID, +-- and no 'unmapped rows keep their old value' fallback: the column stays +-- TEXT until every row maps, exactly the "no sentinel/default survives" -- acceptance criterion. -- -- CONSTRAINTS AND INDEXES SURVIVE THE TYPE CHANGE @@ -47,26 +76,8 @@ -- fails closed -- proving that behavior is OMN-15416's scope (real -- non-owner pools), not duplicated here. -- --- Idempotent: the mapping function is CREATE OR REPLACE; the column-type --- guard only runs the ALTER when the column is not already UUID, so a --- second application is a no-op. - -CREATE OR REPLACE FUNCTION house_tenant_map_slug_to_uuid(p_value TEXT) -RETURNS UUID -LANGUAGE plpgsql -IMMUTABLE -AS $$ -BEGIN - IF p_value = 'omninode' THEN - RETURN '820272f9-4aaf-5add-a2df-0af942852ab2'::uuid; - END IF; - RAISE EXCEPTION - 'OMN-15356: no canonical UUID mapping for tenant value % -- refusing ' - 'to invent or default one; extend house_tenant_map_slug_to_uuid only ' - 'after confirming this is a real, reviewed tenant identity', - p_value; -END; -$$; +-- Idempotent: the column-type guard only runs the ALTER when the column is +-- not already UUID, so a second application is a no-op. DO $$ DECLARE @@ -86,6 +97,26 @@ BEGIN RAISE NOTICE 'public.capability_scores.tenant_id is already uuid; skipping conversion'; ELSIF v_current_type = 'text' THEN + -- Pre-guard: refuse before any DDL runs if any row carries a + -- tenant_id value outside the closed mapping (today: 'omninode' + -- only). This is the first of the two independent fail-closed + -- guards described in the file header above. + IF EXISTS ( + SELECT 1 FROM public.capability_scores + WHERE tenant_id IS DISTINCT FROM 'omninode' + ) THEN + RAISE EXCEPTION + 'OMN-15356: no canonical UUID mapping for tenant value % -- ' + 'refusing to invent or default one; extend the closed mapping ' + 'in this migration only after confirming this is a real, ' + 'reviewed tenant identity', + ( + SELECT tenant_id FROM public.capability_scores + WHERE tenant_id IS DISTINCT FROM 'omninode' + LIMIT 1 + ); + END IF; + -- The pre-existing tenant_isolation POLICY (migration 0002) depends -- on this column -- PostgreSQL refuses ALTER COLUMN ... TYPE while -- any policy references it, so the policy must be dropped first and @@ -103,9 +134,18 @@ BEGIN -- DEFAULT set after it. Also caught by the same fixture proof. ALTER TABLE public.capability_scores ALTER COLUMN tenant_id DROP DEFAULT; + -- Second, independent fail-closed guard: the CASE has no ELSE, so + -- any value that reaches this point despite the pre-guard above + -- (e.g. a row inserted between the guard and the ALTER within the + -- same transaction) evaluates to NULL and is rejected by the + -- column's existing NOT NULL constraint, aborting the statement. ALTER TABLE public.capability_scores ALTER COLUMN tenant_id TYPE UUID - USING house_tenant_map_slug_to_uuid(tenant_id); + USING ( + CASE tenant_id + WHEN 'omninode' THEN '820272f9-4aaf-5add-a2df-0af942852ab2'::uuid + END + ); ALTER TABLE public.capability_scores ALTER COLUMN tenant_id SET DEFAULT '820272f9-4aaf-5add-a2df-0af942852ab2'::uuid; ELSE