Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docker/migrations/forward/_ledger/application-migrations.tsv
Original file line number Diff line number Diff line change
@@ -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 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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
-- 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 -- 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
-- 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 (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
-- `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 column-type guard only runs the ALTER when the column is
-- not already UUID, so a second application is a no-op.

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
-- 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
-- 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;
-- 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 (
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
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;
Original file line number Diff line number Diff line change
@@ -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$;
30 changes: 30 additions & 0 deletions scripts/ci/check_application_database_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@
"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). 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"
),
# 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"
Expand Down
Loading
Loading