From 1d31e2e9dfe44bcf1baafb4cdb0f58dd30bc8874 Mon Sep 17 00:00:00 2001 From: "onexbot-occ-writer[bot]" <307849072+onexbot-occ-writer[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:58:15 -0400 Subject: [PATCH 1/4] feat(OMN-20578): vendor the routing-feedback and alert-channel liveness projection migrations ahead of omnimarket#3416 and #3417 omnimarket adds node_projection_routing_feedback (a public platform table written by the tenant projection writer) and node_projection_alert_channel_liveness (an omninode_internal table written by the runtime principal). The node-migration vendor-parity gate refuses both PRs until these byte-identical copies are on dev, and a runtime that installs the nodes without the declared table grants exits at wiring, which the dev-202 lab runtime showed on 2026-10-05. Ledger rows, migration classes, interim bridge declarations with their expiry entries, the regenerated topology instances and catalogs, and a vendor test are included. --- config/migration_classes.yaml | 4 + docker/catalog/database-topology/dogfood.yaml | 4 +- docker/catalog/database-topology/judge.yaml | 4 +- .../catalog/database-topology/lakshman.yaml | 4 +- docker/catalog/database-topology/local.yaml | 4 +- .../catalog/database-topology/onex-dev.yaml | 4 +- .../catalog/database-topology/onex-prod.yaml | 4 +- docker/catalog/database-topology/prod.yaml | 4 +- .../database-topology/stability-test.yaml | 4 +- docker/catalog/database-topology/test.yaml | 4 +- .../_ledger/application-migrations.tsv | 4 + ...create_alert_channel_liveness_verdicts.sql | 103 +++++++++++ ...untime_alert_channel_liveness_verdicts.sql | 104 +++++++++++ ...000_create_delegation_routing_feedback.sql | 18 ++ ...ion_writer_delegation_routing_feedback.sql | 32 ++++ .../topology/instances/local.yaml | 2 + .../topology/instances/onex-dev.yaml | 2 + .../topology/instances/onex-prod.yaml | 2 + .../topology/table_grant_derivation.py | 44 +++++ ...upplemental_declaration_expiry_omn18863.py | 2 + ...mninode_internal_live_acl_gate_omn17886.py | 17 ++ ...dor_routing_feedback_and_alert_liveness.py | 170 ++++++++++++++++++ 22 files changed, 531 insertions(+), 9 deletions(-) create mode 100644 docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql create mode 100644 docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql create mode 100644 docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql create mode 100644 docker/migrations/forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql create mode 100644 tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py diff --git a/config/migration_classes.yaml b/config/migration_classes.yaml index a13e7cf013..cb3bcd8ecb 100644 --- a/config/migration_classes.yaml +++ b/config/migration_classes.yaml @@ -162,6 +162,8 @@ migrations: forward/nodes/node_pr_merged_projection/0001_create_pr_merged_events.sql: forward-only # EXECUTE; ALTER TABLE ADD CONSTRAINT PR_MERGED_EVENTS_PKEY; ALTER TABLE ADD CONSTRAINT PR_MERGED_EVENTS_EVENT_I forward/nodes/node_pr_merged_projection/0002_grant_omninode_runtime_pr_merged_events.sql: forward-only # EXECUTE forward/nodes/node_pr_review_bot/001_create_review_bot_bypass_log.sql: expand-only + forward/nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql: forward-only # CREATE UNIQUE INDEX idx_alert_channel_liveness_cursor on omninode_internal.alert_channel_liveness_verdicts (UNIQUE INDEX class) + forward/nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql: expand-only # additive forward/nodes/node_projection_baselines/0001_create_baselines_tables.sql: forward-only # EXECUTE; ALTER TABLE ADD CONSTRAINT BASELINES_SNAPSHOTS_PKEY; ALTER TABLE ADD CONSTRAINT BASELINES_COMPARISONS forward/nodes/node_projection_baselines/0002_realign_child_tables_to_producer_schema.sql: forward-only # DROP; EXECUTE; ALTER TABLE ADD CONSTRAINT BASELINES_COMPARISONS_PKEY; ALTER TABLE ADD CONSTRAINT BASELINES_TRE forward/nodes/node_projection_baselines/0003_grant_omninode_runtime_baselines_tables.sql: expand-only @@ -327,6 +329,8 @@ migrations: forward/nodes/node_projection_routing_decision/0021_create_agent_routing_decisions.sql: forward-only # EXECUTE; ALTER TABLE ADD CONSTRAINT AGENT_ROUTING_DECISIONS_PKEY forward/nodes/node_projection_routing_decision/0022_agent_routing_decisions_tenant_id_and_rls.sql: forward-only # ALTER TABLE ENABLE ROW LEVEL; ALTER TABLE FORCE ROW LEVEL; DROP forward/nodes/node_projection_routing_decision/0023_grant_tenant_projection_writer_agent_routing_decisions.sql: expand-only + forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql: expand-only # additive + forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql: expand-only # additive forward/nodes/node_projection_runner_fleet/0000_create_runner_fleet_liveness.sql: forward-only # UNIQUE INDEX on an existing table forward/nodes/node_projection_runner_fleet/0001_grant_omninode_runtime_runner_fleet_liveness.sql: expand-only forward/nodes/node_projection_demo_readiness/0000_create_demo_readiness_latest.sql: forward-only # UNIQUE INDEX on created table diff --git a/docker/catalog/database-topology/dogfood.yaml b/docker/catalog/database-topology/dogfood.yaml index 8d5c811276..916e35ac8a 100644 --- a/docker/catalog/database-topology/dogfood.yaml +++ b/docker/catalog/database-topology/dogfood.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/judge.yaml b/docker/catalog/database-topology/judge.yaml index b7a37a9ebe..d16c17e948 100644 --- a/docker/catalog/database-topology/judge.yaml +++ b/docker/catalog/database-topology/judge.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/lakshman.yaml b/docker/catalog/database-topology/lakshman.yaml index e192c0a475..9b7b4f328a 100644 --- a/docker/catalog/database-topology/lakshman.yaml +++ b/docker/catalog/database-topology/lakshman.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/local.yaml b/docker/catalog/database-topology/local.yaml index 3565647cef..81c6145037 100644 --- a/docker/catalog/database-topology/local.yaml +++ b/docker/catalog/database-topology/local.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/onex-dev.yaml b/docker/catalog/database-topology/onex-dev.yaml index c9e2c2e8ee..c2361b3f6a 100644 --- a/docker/catalog/database-topology/onex-dev.yaml +++ b/docker/catalog/database-topology/onex-dev.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: 1a56dc18a7c273ce3457aee4d7604ac6b660670e95b18a7b5ed2e36a6a30d6f9 + sha256: 58db2a4e041c88b30a55377e7abe0b6c79118d3fca3893c9ad6bd1436e548b6c topology_instance: onex-dev diff --git a/docker/catalog/database-topology/onex-prod.yaml b/docker/catalog/database-topology/onex-prod.yaml index c679d9e26d..bd87e8c3a0 100644 --- a/docker/catalog/database-topology/onex-prod.yaml +++ b/docker/catalog/database-topology/onex-prod.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: c82967925d6f584562df02e9d4ba0bd3e68fdde7aa68044b4133375a49d66d6d + sha256: 2f2ab465a0d5252715a993bca4248d3edf26b509a76b5a5616d314e7364567de topology_instance: onex-prod diff --git a/docker/catalog/database-topology/prod.yaml b/docker/catalog/database-topology/prod.yaml index ececd403b5..161ac7cbb5 100644 --- a/docker/catalog/database-topology/prod.yaml +++ b/docker/catalog/database-topology/prod.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/stability-test.yaml b/docker/catalog/database-topology/stability-test.yaml index 9f63c78ceb..56cf5f96da 100644 --- a/docker/catalog/database-topology/stability-test.yaml +++ b/docker/catalog/database-topology/stability-test.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/catalog/database-topology/test.yaml b/docker/catalog/database-topology/test.yaml index f878544f01..5f1cc66528 100644 --- a/docker/catalog/database-topology/test.yaml +++ b/docker/catalog/database-topology/test.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -210,6 +211,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings @@ -382,5 +384,5 @@ source: profile_catalog_path: src/omnibase_infra/topology/application_database_profiles.yaml profile_catalog_sha256: 690ada1927fc31465fd1d80435e82c96394fa05d49bd433e30242ea014fa740f repository: OmniNode-ai/omnibase_infra - sha256: a58dbdde827c5a2a5514d490a7ac42c4aee36bb11cf885e045f98d57fd6f38ee + sha256: 8206b49ae2275e85e5e713466596340d19f7e52145daad02edb73e0fcd60e840 topology_instance: local diff --git a/docker/migrations/forward/_ledger/application-migrations.tsv b/docker/migrations/forward/_ledger/application-migrations.tsv index 28d2b09e13..fa96a24c2f 100644 --- a/docker/migrations/forward/_ledger/application-migrations.tsv +++ b/docker/migrations/forward/_ledger/application-migrations.tsv @@ -41,6 +41,8 @@ nodes/node_pr_lifecycle_state_reducer/0003_grant_omninode_runtime_pr_lifecycle_s nodes/node_pr_merged_projection/0001_create_pr_merged_events.sql node:node_pr_merged_projection node:node_pr_merged_projection omninode_internal node:node_pr_merged_projection:0001_create_pr_merged_events.sql 2835e23571dee7bec83c4ff75ada048929e1efb771a1d3be2bfb7b759a995070 nodes/node_pr_merged_projection/0002_grant_omninode_runtime_pr_merged_events.sql node:node_pr_merged_projection node:node_pr_merged_projection omninode_internal node:node_pr_merged_projection:0002_grant_omninode_runtime_pr_merged_events.sql 54d24039a31c893b105e55a50fb20cc7a07304a6e0e8e68fe40b78830eecf88b nodes/node_pr_review_bot/001_create_review_bot_bypass_log.sql node:node_pr_review_bot node:node_pr_review_bot omninode_internal node:node_pr_review_bot:001_create_review_bot_bypass_log.sql 63e2646a7f8767fad9ec969b224982e00b83aacb665107ab5b103964d5616e00 +nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql node:node_projection_alert_channel_liveness node:node_projection_alert_channel_liveness omninode_internal node:node_projection_alert_channel_liveness:0000_create_alert_channel_liveness_verdicts.sql b6dd14f636a520a00d561ec37fad1f00c43af931ec4b7b82ea224824a22808f9 +nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql node:node_projection_alert_channel_liveness node:node_projection_alert_channel_liveness omninode_internal node:node_projection_alert_channel_liveness:0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql b15100c7de5b769a8a2f946f2123fa5b101311a58b1e885424586faa242b171b nodes/node_projection_baselines/0001_create_baselines_tables.sql node:node_projection_baselines node:node_projection_baselines omninode_internal node:node_projection_baselines:0001_create_baselines_tables.sql f94dae78957a2890ea80655f3d2280331504d6637f9bdbd9ef014e2c33583f9d nodes/node_projection_baselines/0002_realign_child_tables_to_producer_schema.sql node:node_projection_baselines node:node_projection_baselines omninode_internal node:node_projection_baselines:0002_realign_child_tables_to_producer_schema.sql b41a161c5a9e6325ced130c4d0217c72d2a2bd258effb4af5349401f3415ec30 nodes/node_projection_baselines/0003_grant_omninode_runtime_baselines_tables.sql node:node_projection_baselines node:node_projection_baselines omninode_internal node:node_projection_baselines:0003_grant_omninode_runtime_baselines_tables.sql eaeaba5f83c9e2ff62daee6e1d0ba064b53cde0e10c84e625e03989125936df5 @@ -212,6 +214,8 @@ nodes/node_projection_registration/0007_node_service_registry_drop_tenant_postur 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_routing_decision/0023_grant_tenant_projection_writer_agent_routing_decisions.sql node:node_projection_routing_decision node:node_projection_routing_decision tenant node:node_projection_routing_decision:0023_grant_tenant_projection_writer_agent_routing_decisions.sql 206b8658cd9cf583e05e47eb0c48f023b7ecdbf8b30ac15d8ddf3f45f1f4dffc +nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql node:node_projection_routing_feedback node:node_projection_routing_feedback tenant node:node_projection_routing_feedback:0000_create_delegation_routing_feedback.sql 1b6813dd1c1d4fc1953f6bd78157a71eecaaa1d16d4b1406bfff4ddbbadc1357 +nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql node:node_projection_routing_feedback node:node_projection_routing_feedback tenant node:node_projection_routing_feedback:0001_grant_tenant_projection_writer_delegation_routing_feedback.sql 048e90f4046ec68a0de942791c5d14c670744c1e801b58438e323af7f4b60c59 nodes/node_projection_runner_fleet/0000_create_runner_fleet_liveness.sql node:node_projection_runner_fleet node:node_projection_runner_fleet omninode_internal node:node_projection_runner_fleet:0000_create_runner_fleet_liveness.sql 6373341362d9329f3db44fdd996e73b8205e57dccb2a1f40230442afef885b26 nodes/node_projection_runner_fleet/0001_grant_omninode_runtime_runner_fleet_liveness.sql node:node_projection_runner_fleet node:node_projection_runner_fleet omninode_internal node:node_projection_runner_fleet:0001_grant_omninode_runtime_runner_fleet_liveness.sql 45d9034a1cc168db210cca0efbedad23450b245c1bf9017a4562b61f9d5c4208 nodes/node_projection_runtime_error_fingerprints/0000_create_runtime_error_fingerprints.sql node:node_projection_runtime_error_fingerprints node:node_projection_runtime_error_fingerprints omninode_internal node:node_projection_runtime_error_fingerprints:0000_create_runtime_error_fingerprints.sql 1d7500c9f3c9aa82bb37eb17e4b3334f90136c46a8621627e2bb213a27e9bdc9 diff --git a/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql b/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql new file mode 100644 index 0000000000..834a2975ee --- /dev/null +++ b/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0000_create_alert_channel_liveness_verdicts.sql @@ -0,0 +1,103 @@ +-- ============================================================================= +-- MIGRATION: durable alert-channel liveness verdicts +-- ============================================================================= +-- Owner: omnimarket.nodes.node_projection_alert_channel_liveness +-- Version: 1.0.0 +-- +-- WHY THIS EXISTS +-- The checker publishes its verdict independently of the channel it judges, +-- but a published event with no recording subscriber remains unqueryable. +-- A dead Slack channel must leave a fact behind even when Slack cannot +-- deliver the alert that would otherwise have reported that failure. +-- +-- WHY ONE ROW PER PROBED EVENT +-- The deterministic correlation comes from topic/partition/offset, with a +-- content-derived fallback for the bare adapter path. A redelivery of the +-- same event converges on its existing row, while a later probe retains its +-- own measurement. Throttled ticks make no measurement and write no row. +-- +-- WHY STATUS AND HEALTH ARE BOTH STORED +-- Only LIVE proves health. DEAD, NOT_CONFIGURED and PROBE_ERROR retain their +-- different operator actions while all three store healthy=false. Health +-- is derived in the pure fold rather than trusted from an independent wire +-- flag, so a probe failure cannot silently become a healthy channel. +-- +-- WHY checked_at AND projected_at ARE TWO COLUMNS +-- checked_at preserves event time when present. The current flat producer +-- result carries none, so the writer reads the envelope time; when neither +-- supplies one, checked_at equals the single projected_at clock value. +-- projected_at always says when this projection processed the verdict. +-- +-- WHY omninode_internal, EXPLICITLY QUALIFIED +-- The application SQL ownership gate rejects unqualified relations and +-- prohibits public for application tables. The migration loop connects to +-- the application database where this schema already exists; creating a +-- schema here would require unrelated database-level privileges. +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS omninode_internal.alert_channel_liveness_verdicts ( + -- One measured event, and the redelivery key. A UUID end to end rather + -- than a textual key that the writer and the relation interpret differently. + correlation_id UUID NOT NULL, + + -- The vocabulary belongs to the application wire model. TEXT avoids a + -- database enum migration whenever the producing vocabulary evolves. + status TEXT NOT NULL, + healthy BOOLEAN NOT NULL, + + -- The measurement's own explanation and optional Slack error token. The + -- token remains separate so readers never need to parse the sentence. + reason TEXT NOT NULL DEFAULT '', + slack_error TEXT, + + -- The cadence actually used and whether the producer surfaced failure. + -- These are measured producer facts, never projection configuration. + probe_interval_seconds INTEGER NOT NULL, + failure_surfaced BOOLEAN NOT NULL, + + -- Event time where available, otherwise the same value as projected_at. + checked_at TIMESTAMPTZ NOT NULL, + source_topic TEXT NOT NULL DEFAULT '', + + -- Database-assigned monotonic boundary, retained by an upsert redelivery. + projection_cursor BIGSERIAL NOT NULL, + projected_at TIMESTAMPTZ NOT NULL, + + PRIMARY KEY (correlation_id) +); + +-- SHAPE reconciliation, not merely existence (the template's OMN-15376 rule). +-- CREATE TABLE IF NOT EXISTS does not reconcile an already-present relation. +-- Guarded ADDs ensure every declared column arrives before an index uses it. +-- NOT NULL is absent from ADDs without defaults because existing rows cannot +-- satisfy it. The virgin CREATE above already supplies those constraints; +-- drifted rows remain visibly incomplete rather than blocking migration. +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS correlation_id UUID; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS status TEXT; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS healthy BOOLEAN; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS reason TEXT DEFAULT ''; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS slack_error TEXT; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS probe_interval_seconds INTEGER; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS failure_surfaced BOOLEAN; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS checked_at TIMESTAMPTZ; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS source_topic TEXT DEFAULT ''; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS projection_cursor BIGSERIAL; +ALTER TABLE omninode_internal.alert_channel_liveness_verdicts + ADD COLUMN IF NOT EXISTS projected_at TIMESTAMPTZ; + +-- Recent measurements by status, including failures of the probe itself. +CREATE INDEX IF NOT EXISTS idx_alert_channel_liveness_status_time + ON omninode_internal.alert_channel_liveness_verdicts (status, checked_at DESC); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_alert_channel_liveness_cursor + ON omninode_internal.alert_channel_liveness_verdicts (projection_cursor); diff --git a/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql b/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql new file mode 100644 index 0000000000..13ad1f31c1 --- /dev/null +++ b/docker/migrations/forward/nodes/node_projection_alert_channel_liveness/0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql @@ -0,0 +1,104 @@ +-- ============================================================================= +-- MIGRATION: deliver the omninode_runtime grants for alert_channel_liveness_verdicts +-- ============================================================================= +-- Owner: omnimarket.nodes.node_projection_alert_channel_liveness +-- Version: 1.0.0 +-- +-- WHY THIS EXISTS +-- `0000` creates the relation; the deployment topology derives +-- `principals.omninode_runtime.grants[object_type: TABLE, ... +-- alert_channel_liveness_verdicts]` from this node's own `db_io.db_tables`. A +-- DECLARED grant with no delivering migration is an outage waiting for the +-- relation to take traffic, and check_topology_grant_delivery.py refuses +-- it. A live ACL observed on one lane is not a delivered grant: nothing +-- replays it onto a fresh database, which is the whole point of a migration +-- lineage. +-- +-- THE SEQUENCE HALF IS THE ONE THAT MATTERS +-- `projection_cursor` is BIGSERIAL, so every INSERT evaluates a nextval() +-- DEFAULT over a STANDALONE sequence whose own ACL PostgreSQL checks +-- separately. A table grant alone does not make the relation writable. That +-- is OMN-17379 exactly: pr_merged_events sat 24 days behind its topic at +-- consumer LAG 0 -- the consumer read fine and every write failed on the +-- sequence -- and the reason it shipped broken is that its migration +-- asserted only the table INSERT, which was TRUE throughout. Both halves +-- are granted here and BOTH are asserted. +-- +-- WHY THE SEQUENCE IS NAMED STATICALLY +-- A `DO $$ ... EXECUTE format(...)` resolution is refused by OMN-15361's +-- SQL ownership gate -- a static reader cannot prove which relation a +-- dynamically composed statement will touch. The literal name is used +-- instead, and assertion 1 restores what the dynamic form gave for free: +-- the migration fails unless pg_get_serial_sequence resolves THIS column to +-- THAT sequence, so a restore, a rename or an out-of-band apply is a loud +-- failure rather than a silent half-grant. +-- +-- WHY SELECT AND UPDATE ARE GRANTED WHEN db_io SAYS `write` +-- The writer's statement is an INSERT ... ON CONFLICT DO UPDATE and its +-- RETURNING clause reads the accepted row back. A lane where INSERT landed +-- and UPDATE did not would accept the first verdict for a probe and fail +-- every redelivery; one where SELECT did not land would fail the read-back +-- that proves the write. No DELETE: a measured verdict is history and is never +-- retracted. +-- +-- IDEMPOTENCY +-- GRANT is idempotent; re-running is a no-op. +-- ============================================================================= + +-- --------------------------------------------------------------------------- +-- 1. Schema USAGE, mirroring topology. Idempotent, and re-asserted here +-- because a migration must not assume a sibling file ran. +-- --------------------------------------------------------------------------- +GRANT USAGE ON SCHEMA omninode_internal TO omninode_runtime; + +-- --------------------------------------------------------------------------- +-- 2. Table grant (topology-derived). +-- --------------------------------------------------------------------------- +GRANT SELECT, INSERT, UPDATE ON omninode_internal.alert_channel_liveness_verdicts TO omninode_runtime; + +-- --------------------------------------------------------------------------- +-- 3. Sequence grant -- the half whose absence is invisible to a table-only +-- check. Named statically so the ownership gate can prove the target; +-- assertion 1 proves the name is the right one. +-- --------------------------------------------------------------------------- +GRANT USAGE ON SEQUENCE omninode_internal.alert_channel_liveness_verdicts_projection_cursor_seq TO omninode_runtime; + +-- --------------------------------------------------------------------------- +-- 4. Assertions: fail the migration if any fact does not hold. Division by +-- zero when the fact is false -- the fail-loud shape this repository's +-- other grant migrations already use. Every granted privilege is asserted, +-- not only INSERT: asserting only INSERT is exactly what let OMN-17379 +-- ship, because it was TRUE for the whole 24-day outage. +-- --------------------------------------------------------------------------- +SELECT 1 / count(*) AS alert_channel_liveness_cursor_sequence_identity_assertion +WHERE pg_get_serial_sequence( + 'omninode_internal.alert_channel_liveness_verdicts', 'projection_cursor' + ) = 'omninode_internal.alert_channel_liveness_verdicts_projection_cursor_seq'; + +SELECT 1 / count(*) AS alert_channel_liveness_insert_grant_assertion +FROM information_schema.role_table_grants +WHERE table_schema = 'omninode_internal' + AND table_name = 'alert_channel_liveness_verdicts' + AND grantee = 'omninode_runtime' + AND privilege_type = 'INSERT'; + +SELECT 1 / count(*) AS alert_channel_liveness_select_grant_assertion +FROM information_schema.role_table_grants +WHERE table_schema = 'omninode_internal' + AND table_name = 'alert_channel_liveness_verdicts' + AND grantee = 'omninode_runtime' + AND privilege_type = 'SELECT'; + +SELECT 1 / count(*) AS alert_channel_liveness_update_grant_assertion +FROM information_schema.role_table_grants +WHERE table_schema = 'omninode_internal' + AND table_name = 'alert_channel_liveness_verdicts' + AND grantee = 'omninode_runtime' + AND privilege_type = 'UPDATE'; + +SELECT 1 / count(*) AS alert_channel_liveness_cursor_sequence_usage_assertion +WHERE has_sequence_privilege( + 'omninode_runtime', + 'omninode_internal.alert_channel_liveness_verdicts_projection_cursor_seq', + 'USAGE' + ); diff --git a/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql b/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql new file mode 100644 index 0000000000..87a9d03e4b --- /dev/null +++ b/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql @@ -0,0 +1,18 @@ +-- SPDX-FileCopyrightText: 2026 OmniNode.ai Inc. +-- SPDX-License-Identifier: MIT +-- Platform data, owned by omnimarket.nodes.node_projection_routing_feedback. +CREATE TABLE IF NOT EXISTS public.delegation_routing_feedback ( + model_id TEXT NOT NULL, + task_type TEXT NOT NULL, + success_count INTEGER NOT NULL, + failure_count INTEGER NOT NULL, + escalation_count INTEGER NOT NULL, + total_count INTEGER NOT NULL, + success_rate DOUBLE PRECISION NOT NULL, + escalation_rate DOUBLE PRECISION NOT NULL, + avg_latency_ms DOUBLE PRECISION NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + last_updated TIMESTAMPTZ NOT NULL, + PRIMARY KEY (model_id, task_type) +); +GRANT SELECT ON public.delegation_routing_feedback TO app_dashboard; diff --git a/docker/migrations/forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql b/docker/migrations/forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql new file mode 100644 index 0000000000..aa6b9ee7ee --- /dev/null +++ b/docker/migrations/forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql @@ -0,0 +1,32 @@ +-- SPDX-FileCopyrightText: 2026 OmniNode.ai Inc. +-- SPDX-License-Identifier: MIT +-- The platform routing-feedback writer runs as the tenant projection writer principal. +-- Flat migration 103 provisions the cluster role before node migrations run. +DO $require_tenant_projection_writer$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_roles + WHERE rolname = 'tenant_projection_writer' + ) THEN + RAISE EXCEPTION + 'tenant_projection_writer role missing; apply flat migration 103 before node migrations'; + END IF; +END +$require_tenant_projection_writer$; + +GRANT USAGE ON SCHEMA public TO tenant_projection_writer; +GRANT SELECT, INSERT, UPDATE + ON public.delegation_routing_feedback + TO tenant_projection_writer; + +SELECT 1 / count(*) AS delegation_routing_feedback_writer_grants_assertion +FROM ( + SELECT grantee + FROM information_schema.role_table_grants + WHERE table_schema = 'public' + AND table_name = 'delegation_routing_feedback' + AND grantee = 'tenant_projection_writer' + AND privilege_type IN ('SELECT', 'INSERT', 'UPDATE') + GROUP BY grantee + HAVING count(DISTINCT privilege_type) = 3 +) AS granted; diff --git a/src/omnibase_infra/topology/instances/local.yaml b/src/omnibase_infra/topology/instances/local.yaml index c134400116..7a027a1d53 100644 --- a/src/omnibase_infra/topology/instances/local.yaml +++ b/src/omnibase_infra/topology/instances/local.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -191,6 +192,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings diff --git a/src/omnibase_infra/topology/instances/onex-dev.yaml b/src/omnibase_infra/topology/instances/onex-dev.yaml index 930f857aca..80dfe1c1ab 100644 --- a/src/omnibase_infra/topology/instances/onex-dev.yaml +++ b/src/omnibase_infra/topology/instances/onex-dev.yaml @@ -85,6 +85,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -201,6 +202,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings diff --git a/src/omnibase_infra/topology/instances/onex-prod.yaml b/src/omnibase_infra/topology/instances/onex-prod.yaml index 3a2a2b5da2..d2c19670fa 100644 --- a/src/omnibase_infra/topology/instances/onex-prod.yaml +++ b/src/omnibase_infra/topology/instances/onex-prod.yaml @@ -75,6 +75,7 @@ databases: schema: public - object_type: TABLE objects: + - alert_channel_liveness_verdicts - board_probe_results - ci_attempt_outcome - claude_agent_spans @@ -191,6 +192,7 @@ databases: - delegation_eval_results - delegation_events - delegation_judge_verdict_events + - delegation_routing_feedback - delegation_routing_tenant_overlay - delegation_shadow_comparisons - dep_health_findings diff --git a/src/omnibase_infra/topology/table_grant_derivation.py b/src/omnibase_infra/topology/table_grant_derivation.py index e58befeb3f..cdd3c7d1a6 100644 --- a/src/omnibase_infra/topology/table_grant_derivation.py +++ b/src/omnibase_infra/topology/table_grant_derivation.py @@ -370,6 +370,50 @@ class ContractTableDeclaration: role="demo_readiness", ), ), + # OMN-20578: infra vendors delegation_routing_feedback before omnimarket#3416 + # lands the node_projection_routing_feedback contract. This bridge keeps the + # shipped grants derivable until the pin advances; the expiry test then + # requires its removal. + # Retired by: omnimarket#3416 merging and the pin advancing past it. + ContractTableDeclaration( + node="legacy_migration:delegation_routing_feedback", + contract_path=Path( + "docker/migrations/forward/nodes/node_projection_routing_feedback/" + "0000_create_delegation_routing_feedback.sql" + ), + table=ModelDbTableDeclaration( + name="delegation_routing_feedback", + database_ref="application", + schema="public", + migration=( + "docker/migrations/forward/nodes/node_projection_routing_feedback/" + "0000_create_delegation_routing_feedback.sql" + ), + access="read_write", + role="routing_feedback", + ), + ), + # OMN-20578: the same bridge for alert_channel_liveness_verdicts, ahead of + # omnimarket#3417 landing the node_projection_alert_channel_liveness contract. + # Retired by: omnimarket#3417 merging and the pin advancing past it. + ContractTableDeclaration( + node="legacy_migration:alert_channel_liveness_verdicts", + contract_path=Path( + "docker/migrations/forward/nodes/node_projection_alert_channel_liveness/" + "0000_create_alert_channel_liveness_verdicts.sql" + ), + table=ModelDbTableDeclaration( + name="alert_channel_liveness_verdicts", + database_ref="application", + schema="omninode_internal", + migration=( + "docker/migrations/forward/nodes/node_projection_alert_channel_liveness/" + "0000_create_alert_channel_liveness_verdicts.sql" + ), + access="write", + role="alert_channel_liveness", + ), + ), # OMN-18862: migration 089 grants BOTH savings read views to # tenant_projection_writer on ADJACENT lines -- projection_delegation_savings # at :716 and projection_cost_savings_overview at :717 -- and the OMN-17426 diff --git a/tests/ci/test_supplemental_declaration_expiry_omn18863.py b/tests/ci/test_supplemental_declaration_expiry_omn18863.py index 3574a87e07..98ac121ec8 100644 --- a/tests/ci/test_supplemental_declaration_expiry_omn18863.py +++ b/tests/ci/test_supplemental_declaration_expiry_omn18863.py @@ -106,6 +106,8 @@ def _pinned_contracts_root(proof_dependencies: Path) -> Path: # contract; add it in the same pull request that vendors it, and this module # will tell you when to take it out. _INTERIM_ENTRIES: dict[str, str] = { + "delegation_routing_feedback": "omnimarket#3416", + "alert_channel_liveness_verdicts": "omnimarket#3417", "demo_readiness_latest": "omnimarket OMN-19861 (node_projection_demo_readiness)", } diff --git a/tests/integration/migrations/test_omninode_internal_live_acl_gate_omn17886.py b/tests/integration/migrations/test_omninode_internal_live_acl_gate_omn17886.py index 360f286cfa..355dc78949 100644 --- a/tests/integration/migrations/test_omninode_internal_live_acl_gate_omn17886.py +++ b/tests/integration/migrations/test_omninode_internal_live_acl_gate_omn17886.py @@ -447,6 +447,23 @@ def test_fresh_build_matches_the_topology_modulo_the_shrink_only_allowlist( assert board_probe_results_live == board_probe_results_declared assert "DELETE" not in board_probe_results_live + # OMN-20578: the platform routing feedback is written by the tenant writer and + # read by the dashboard; the alert-channel verdicts are written by the runtime + # principal with SELECT, INSERT and UPDATE and no DELETE. + assert _live_privileges( + fresh_build, + "delegation_routing_feedback", + "tenant_projection_writer", + schema="public", + ) == {"SELECT", "INSERT", "UPDATE"} + assert _live_privileges( + fresh_build, "delegation_routing_feedback", "app_dashboard", schema="public" + ) == {"SELECT"} + alert_liveness_live = _live_privileges( + fresh_build, "alert_channel_liveness_verdicts", _RUNTIME + ) + assert alert_liveness_live == {"SELECT", "INSERT", "UPDATE"} + live = set(report["findings"]) allowed = {entry["finding"] for entry in _load_allowlist()} new = sorted(live - allowed) diff --git a/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py new file mode 100644 index 0000000000..c21a1e7b4d --- /dev/null +++ b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""OMN-20578 vendor identity for the routing-feedback and alert-channel liveness projections. + +omnimarket#3416 adds node_projection_routing_feedback (a public platform table granted +to the tenant projection writer) and omnimarket#3417 adds +node_projection_alert_channel_liveness (an omninode_internal table granted to the +runtime principal). The node-migration vendor-parity gate refuses both PRs until the +byte-identical copies are on this repo's dev, and a runtime that installs the nodes +without the topology grants exits at wiring. +""" + +from __future__ import annotations + +import hashlib +import re +from pathlib import Path + +import pytest +import yaml + +pytestmark = pytest.mark.unit + +_ROOT = Path(__file__).resolve().parents[3] +_FORWARD = _ROOT / "docker" / "migrations" / "forward" +_MANIFEST = _FORWARD / "_ledger" / "application-migrations.tsv" +_CLASSES = _ROOT / "config" / "migration_classes.yaml" + +_FEEDBACK_NODE = "node_projection_routing_feedback" +_FEEDBACK_CREATE = "0000_create_delegation_routing_feedback.sql" +_FEEDBACK_GRANT = "0001_grant_tenant_projection_writer_delegation_routing_feedback.sql" +_LIVENESS_NODE = "node_projection_alert_channel_liveness" +_LIVENESS_CREATE = "0000_create_alert_channel_liveness_verdicts.sql" +_LIVENESS_GRANT = "0001_grant_omninode_runtime_alert_channel_liveness_verdicts.sql" + +_FILES = ( + (_FEEDBACK_NODE, _FEEDBACK_CREATE, "tenant"), + (_FEEDBACK_NODE, _FEEDBACK_GRANT, "tenant"), + (_LIVENESS_NODE, _LIVENESS_CREATE, "omninode_internal"), + (_LIVENESS_NODE, _LIVENESS_GRANT, "omninode_internal"), +) +_SHA256 = { + _FEEDBACK_CREATE: "1b6813dd1c1d4fc1953f6bd78157a71eecaaa1d16d4b1406bfff4ddbbadc1357", + _FEEDBACK_GRANT: "048e90f4046ec68a0de942791c5d14c670744c1e801b58438e323af7f4b60c59", + _LIVENESS_CREATE: "b6dd14f636a520a00d561ec37fad1f00c43af931ec4b7b82ea224824a22808f9", + _LIVENESS_GRANT: "b15100c7de5b769a8a2f946f2123fa5b101311a58b1e885424586faa242b171b", +} + + +def _vendored(node: str, filename: str) -> Path: + return _FORWARD / "nodes" / node / filename + + +def _statements(node: str, filename: str) -> str: + return "\n".join( + line + for line in _vendored(node, filename).read_text(encoding="utf-8").splitlines() + if not line.lstrip().startswith("--") + ) + + +@pytest.mark.parametrize(("node", "filename", "domain"), _FILES) +def test_vendor_bytes_and_manifest_binding_are_exact( + node: str, filename: str, domain: str +) -> None: + artifact_path = f"nodes/{node}/{filename}" + assert ( + hashlib.sha256(_vendored(node, filename).read_bytes()).hexdigest() + == _SHA256[filename] + ) + rows = { + row.split("\t", 1)[0]: row.split("\t") + for row in _MANIFEST.read_text(encoding="utf-8").splitlines() + if row.strip() + } + assert rows[artifact_path] == [ + artifact_path, + f"node:{node}", + f"node:{node}", + domain, + f"node:{node}:{filename}", + _SHA256[filename], + ] + + +@pytest.mark.parametrize(("node", "filename", "domain"), _FILES) +def test_migration_classes_match_the_classifier( + node: str, filename: str, domain: str +) -> None: + classes = yaml.safe_load(_CLASSES.read_text(encoding="utf-8"))["migrations"] + declared = classes[f"forward/nodes/{node}/{filename}"] + # The liveness create adds a unique index on the cursor column, which the + # classifier reads as forward-only. Every other file is additive or a grant. + expected = "forward-only" if filename == _LIVENESS_CREATE else "expand-only" + assert declared.split(" #")[0] == expected + + +def test_routing_feedback_table_and_writer_grant_are_present() -> None: + create = _statements(_FEEDBACK_NODE, _FEEDBACK_CREATE) + grants = _statements(_FEEDBACK_NODE, _FEEDBACK_GRANT) + assert re.search( + r"CREATE TABLE IF NOT EXISTS public\.delegation_routing_feedback\s*\(", create + ) + assert "GRANT USAGE ON SCHEMA public TO tenant_projection_writer;" in grants + assert re.search( + r"GRANT\s+SELECT,\s*INSERT,\s*UPDATE\s+" + r"ON public\.delegation_routing_feedback\s+TO tenant_projection_writer;", + grants, + ) + assert "omninode_runtime" not in create + grants + assert not re.search(r"\bDELETE\b", grants, re.IGNORECASE) + + +def test_alert_liveness_table_and_runtime_grant_are_present() -> None: + create = _statements(_LIVENESS_NODE, _LIVENESS_CREATE) + grants = _statements(_LIVENESS_NODE, _LIVENESS_GRANT) + assert re.search( + r"CREATE TABLE IF NOT EXISTS omninode_internal\." + r"alert_channel_liveness_verdicts\s*\(", + create, + ) + assert "GRANT USAGE ON SCHEMA omninode_internal TO omninode_runtime;" in grants + assert not re.search( + r"GRANT\s+[^;]*\bDELETE\b[^;]*\bTO\s+omninode_runtime\b", + grants, + re.IGNORECASE, + ) + + +@pytest.mark.parametrize("profile", ["local", "onex-dev", "onex-prod"]) +def test_both_tables_are_granted_in_the_shipped_topology(profile: str) -> None: + from omnibase_infra.topology.application_database import load_topology_profile + + text = yaml.safe_dump( + load_topology_profile(profile).model_dump(mode="json"), sort_keys=True + ) + assert "delegation_routing_feedback" in text + assert "alert_channel_liveness_verdicts" in text + + +@pytest.mark.parametrize(("node", "filename", "domain"), _FILES) +@pytest.mark.parametrize( + "profile", ["local", "onex-dev", "onex-prod", "stability-test"] +) +def test_migrations_pass_the_application_database_sql_gate( + node: str, filename: str, domain: str, profile: str +) -> None: + from omnibase_infra.topology.application_database import load_topology_profile + from omnibase_infra.validation.application_database_domain_enforcement import ( + lint_application_database_sql, + ) + + sql = _vendored(node, filename).read_text(encoding="utf-8") + violations = lint_application_database_sql(sql, load_topology_profile(profile)) + assert violations == (), f"{profile}/{filename}: {violations}" + + +def test_the_sql_gate_is_live_positive_control() -> None: + from omnibase_infra.topology.application_database import load_topology_profile + from omnibase_infra.validation.application_database_domain_enforcement import ( + lint_application_database_sql, + ) + + sql = _vendored(_LIVENESS_NODE, _LIVENESS_CREATE).read_text(encoding="utf-8") + broken = sql.replace( + "omninode_internal.alert_channel_liveness_verdicts", + "tenant.alert_channel_liveness_verdicts", + ) + assert broken != sql + assert lint_application_database_sql(broken, load_topology_profile("local")) != () From 653c5cbb590c70dfc413113d7b7321727a76b0e2 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Mon, 5 Oct 2026 06:59:03 -0400 Subject: [PATCH 2/4] fix(OMN-20578): re-vendor routing feedback with its shape reconciliation block Re-vendors the SQL from the omnimarket source on branch jonah/omn-20578-routing-feedback-alert-liveness-prereqs (commit 58182a328bd1). The migration adds ALTER TABLE ADD COLUMN IF NOT EXISTS for all 11 columns, SET NOT NULL, and a guarded primary key. The sha256 pin moves to 43803f20610fb1ddeed49046f6c04656c5a27dfe976f0212ecfa5614f06e60f0 in the ledger and test files. This fixes test_node_migration_shape_reconciliation.py and test_whole_corpus_converges_from_drifted_shapes. sync-node-migrations.sh keeps the file frozen, so bytes were copied and the checksum re-pinned by hand.config/migration_classes.yaml now declares the file forward-only, as delegation_dispositions is. --- config/migration_classes.yaml | 2 +- .../_ledger/application-migrations.tsv | 2 +- ...000_create_delegation_routing_feedback.sql | 40 +++++++++++++++++++ ...dor_routing_feedback_and_alert_liveness.py | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/config/migration_classes.yaml b/config/migration_classes.yaml index cb3bcd8ecb..3ca52ee234 100644 --- a/config/migration_classes.yaml +++ b/config/migration_classes.yaml @@ -329,7 +329,7 @@ migrations: forward/nodes/node_projection_routing_decision/0021_create_agent_routing_decisions.sql: forward-only # EXECUTE; ALTER TABLE ADD CONSTRAINT AGENT_ROUTING_DECISIONS_PKEY forward/nodes/node_projection_routing_decision/0022_agent_routing_decisions_tenant_id_and_rls.sql: forward-only # ALTER TABLE ENABLE ROW LEVEL; ALTER TABLE FORCE ROW LEVEL; DROP forward/nodes/node_projection_routing_decision/0023_grant_tenant_projection_writer_agent_routing_decisions.sql: expand-only - forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql: expand-only # additive + forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql: forward-only # ALTER COLUMN; ADD CONSTRAINT forward/nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql: expand-only # additive forward/nodes/node_projection_runner_fleet/0000_create_runner_fleet_liveness.sql: forward-only # UNIQUE INDEX on an existing table forward/nodes/node_projection_runner_fleet/0001_grant_omninode_runtime_runner_fleet_liveness.sql: expand-only diff --git a/docker/migrations/forward/_ledger/application-migrations.tsv b/docker/migrations/forward/_ledger/application-migrations.tsv index fa96a24c2f..4b0bf640b0 100644 --- a/docker/migrations/forward/_ledger/application-migrations.tsv +++ b/docker/migrations/forward/_ledger/application-migrations.tsv @@ -214,7 +214,7 @@ nodes/node_projection_registration/0007_node_service_registry_drop_tenant_postur 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_routing_decision/0023_grant_tenant_projection_writer_agent_routing_decisions.sql node:node_projection_routing_decision node:node_projection_routing_decision tenant node:node_projection_routing_decision:0023_grant_tenant_projection_writer_agent_routing_decisions.sql 206b8658cd9cf583e05e47eb0c48f023b7ecdbf8b30ac15d8ddf3f45f1f4dffc -nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql node:node_projection_routing_feedback node:node_projection_routing_feedback tenant node:node_projection_routing_feedback:0000_create_delegation_routing_feedback.sql 1b6813dd1c1d4fc1953f6bd78157a71eecaaa1d16d4b1406bfff4ddbbadc1357 +nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql node:node_projection_routing_feedback node:node_projection_routing_feedback tenant node:node_projection_routing_feedback:0000_create_delegation_routing_feedback.sql 43803f20610fb1ddeed49046f6c04656c5a27dfe976f0212ecfa5614f06e60f0 nodes/node_projection_routing_feedback/0001_grant_tenant_projection_writer_delegation_routing_feedback.sql node:node_projection_routing_feedback node:node_projection_routing_feedback tenant node:node_projection_routing_feedback:0001_grant_tenant_projection_writer_delegation_routing_feedback.sql 048e90f4046ec68a0de942791c5d14c670744c1e801b58438e323af7f4b60c59 nodes/node_projection_runner_fleet/0000_create_runner_fleet_liveness.sql node:node_projection_runner_fleet node:node_projection_runner_fleet omninode_internal node:node_projection_runner_fleet:0000_create_runner_fleet_liveness.sql 6373341362d9329f3db44fdd996e73b8205e57dccb2a1f40230442afef885b26 nodes/node_projection_runner_fleet/0001_grant_omninode_runtime_runner_fleet_liveness.sql node:node_projection_runner_fleet node:node_projection_runner_fleet omninode_internal node:node_projection_runner_fleet:0001_grant_omninode_runtime_runner_fleet_liveness.sql 45d9034a1cc168db210cca0efbedad23450b245c1bf9017a4562b61f9d5c4208 diff --git a/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql b/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql index 87a9d03e4b..a35c406321 100644 --- a/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql +++ b/docker/migrations/forward/nodes/node_projection_routing_feedback/0000_create_delegation_routing_feedback.sql @@ -15,4 +15,44 @@ CREATE TABLE IF NOT EXISTS public.delegation_routing_feedback ( last_updated TIMESTAMPTZ NOT NULL, PRIMARY KEY (model_id, task_type) ); + +-- ---- BEGIN OMN-15376 shape reconciliation: delegation_routing_feedback ---- +-- Guarded adds converge drifted tables without inventing required values. +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS model_id TEXT; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS task_type TEXT; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS success_count INTEGER; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS failure_count INTEGER; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS escalation_count INTEGER; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS total_count INTEGER; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS success_rate DOUBLE PRECISION; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS escalation_rate DOUBLE PRECISION; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS avg_latency_ms DOUBLE PRECISION; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS window_start TIMESTAMPTZ; +ALTER TABLE public.delegation_routing_feedback ADD COLUMN IF NOT EXISTS last_updated TIMESTAMPTZ; + +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN model_id SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN task_type SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN success_count SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN failure_count SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN escalation_count SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN total_count SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN success_rate SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN escalation_rate SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN avg_latency_ms SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN window_start SET NOT NULL; +ALTER TABLE public.delegation_routing_feedback ALTER COLUMN last_updated SET NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_constraint + WHERE conrelid = 'public.delegation_routing_feedback'::regclass AND contype = 'p' + ) THEN + ALTER TABLE public.delegation_routing_feedback + ADD CONSTRAINT delegation_routing_feedback_pkey + PRIMARY KEY (model_id, task_type); + END IF; +END$$; +-- ---- END OMN-15376 shape reconciliation: delegation_routing_feedback ---- + GRANT SELECT ON public.delegation_routing_feedback TO app_dashboard; diff --git a/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py index c21a1e7b4d..596541ef61 100644 --- a/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py +++ b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py @@ -40,7 +40,7 @@ (_LIVENESS_NODE, _LIVENESS_GRANT, "omninode_internal"), ) _SHA256 = { - _FEEDBACK_CREATE: "1b6813dd1c1d4fc1953f6bd78157a71eecaaa1d16d4b1406bfff4ddbbadc1357", + _FEEDBACK_CREATE: "43803f20610fb1ddeed49046f6c04656c5a27dfe976f0212ecfa5614f06e60f0", _FEEDBACK_GRANT: "048e90f4046ec68a0de942791c5d14c670744c1e801b58438e323af7f4b60c59", _LIVENESS_CREATE: "b6dd14f636a520a00d561ec37fad1f00c43af931ec4b7b82ea224824a22808f9", _LIVENESS_GRANT: "b15100c7de5b769a8a2f946f2123fa5b101311a58b1e885424586faa242b171b", From 8a67465d47b1046c2cc18b0867b664f7694afa7a Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Mon, 5 Oct 2026 07:03:02 -0400 Subject: [PATCH 3/4] test(OMN-20578): expect forward-only for the reconciled routing feedback create The reconciliation block adds ALTER COLUMN and a guarded primary key, so config/migration_classes.yaml now declares it forward-only. test_migration_classes_match_the_classifier in tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py now expects forward-only for 0000_create_delegation_routing_feedback.sql. --- ...est_vendor_routing_feedback_and_alert_liveness.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py index 596541ef61..4e8b9c4f9a 100644 --- a/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py +++ b/tests/unit/migrations/test_vendor_routing_feedback_and_alert_liveness.py @@ -89,9 +89,15 @@ def test_migration_classes_match_the_classifier( ) -> None: classes = yaml.safe_load(_CLASSES.read_text(encoding="utf-8"))["migrations"] declared = classes[f"forward/nodes/{node}/{filename}"] - # The liveness create adds a unique index on the cursor column, which the - # classifier reads as forward-only. Every other file is additive or a grant. - expected = "forward-only" if filename == _LIVENESS_CREATE else "expand-only" + # The liveness create adds a unique index on the cursor column, and the + # routing-feedback create reconciles a drifted shape with ALTER COLUMN and a + # guarded primary key: the classifier reads both as forward-only. Every other + # file is a grant. + expected = ( + "forward-only" + if filename in {_LIVENESS_CREATE, _FEEDBACK_CREATE} + else "expand-only" + ) assert declared.split(" #")[0] == expected From e54b942d8cc2c661b70374ab7019664e7d147826 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Mon, 5 Oct 2026 07:11:24 -0400 Subject: [PATCH 4/4] chore(OMN-20578): re-run CI against the omnimarket#3420 source pair The PR body trailers now name omnimarket#3420 at 58182a328bd1ad297dcb3f6675a89cedf91c2ffa, and pull_request CI reads the body from the synchronize event, so the previous head's run still paired with omnimarket#3416.