Skip to content

fix(OMN-16690): declare the reads projection contracts already perform + fail-closed access-class gate - #2164

Merged
jonahgabriel merged 2 commits into
devfrom
jonah/omn-16690-projection-access-class-fix
Aug 27, 2026
Merged

jonahgabriel merged 2 commits into
devfrom
jonah/omn-16690-projection-access-class-fix

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

What

Projection contracts that declared a table access: write while their handlers query() that same table were refused at the guard (_assert_read_declared() admits only read/read_write) and every event was routed to the quarantine DLQ. Zero rows were ever written.

  • AC1 — widened access to read_write for public.hook_events (node_hook_event_capture) and omninode_internal.renderer_capability_projection (node_renderer_capability_projection).
  • AC2 — swept every projection contract in the repo for the same declared-vs-actual mismatch: 13 contracts / 14 tables corrected.
  • AC3 — landed scripts/ci/check_projection_contract_access.py, a fail-closed guard that resolves each contract's declared access against the operations its handler actually invokes and fails on mismatch. Wired as both a CI job (.github/workflows/ci.yml) and a pre-commit hook in this same PR, per Rule 5 (detection without enforcement gets ignored).

Why

Live evidence, pod omninode-runtime-effects-849bcb486-6sftf, ns onex-dev:

error_type=PermissionError
error=public.hook_events declares access='write'; read refused
-> onex.dlq.omnibase-infra.quarantine.v1

The failure mode is the worst kind: the gateway answers 202, the runtime logs an ERROR, the event lands in quarantine, and the caller sees success. hook_events read back total_rows = 0 against the real serving DB. The sibling HandlerRendererCapabilityProjection was refusing continuously (measured 234x/20min) on the identical root cause, which is why the fix is the class and the guard, not the two instances.

Verification

  • 485 lines of new tests in tests/test_omn16690_projection_contract_access.py, RED-first against the pre-fix declarations.
  • Governed pre-push selector escalated to the full suite (is_full_suite=True reason=changed_test_unnarrowable) and ran it on the designated .200 host: 16250 passed, 105 skipped, 3 deselected, 1 xfailed, 0 failed in 710.97s.
  • No bypass flags used — PREPUSH_FULL_SUITE, PREPUSH_ALLOW_LOCAL_FULL_SUITE, ENABLE_SMART_TESTS=off and --no-verify were all unused. The escalation target itself had to be repaired first (OMN-16752); this push is the proof that the governed path works end to end.

AC4 (rebuild into the deployed runtime image) and AC5 (canary showing a real row keyed on event_sha ce574770…) follow this merge and are tracked on the ticket.

OMN-16690

Evidence-Ticket: OMN-16690
Evidence-Source: OCC#7287

A db_io contract declares a per-table `access` capability. The runtime
enforces it fail-closed at ProjectionTableOperation._assert_read_declared,
which admits only {"read", "read_write"}. Thirteen omnimarket nodes declared
`access: write` on a table their handler QUERIES — an idempotency lookup
before the upsert. Every event on those paths raised PermissionError at the
read, the projection arm's generic `except Exception` routed the envelope to
onex.dlq.omnibase-infra.quarantine.v1, the offset committed, consumer lag read
0 — and zero rows were ever written, while the gateway kept answering 202.

Contract-first: the declaration must cover what the handler actually does.
This widens `access` to read_write on the 14 tables whose handler reads. It
does NOT weaken the runtime guard and does NOT remove any idempotency read —
both would be the wrong fix. The reads are load-bearing: the injected adapter's
upsert() has no ON CONFLICT ... DO NOTHING mode, so dropping the read would
bump updated_at on every Kafka redelivery.

Swept all 47 omnimarket contracts declaring db_io.db_tables (62 tables). The
ticket named 2 instances; the sweep found 14 across 13 nodes. No other repo in
the registry declares db_io.db_tables — the 91 hits under omnibase_infra are
staged copies of this same tree in its build workspace.

Fixed (node — table):
  node_evidence_dashboard_reducer      evidence_readiness_aggregate_projection
  node_hook_event_capture              hook_events
  node_llm_delegation_projection       llm_delegation_daily_projection
  node_projection_capsule_store        capsule_store
  node_projection_delegation           delegation_events, delegation_budget_state
  node_projection_event_chain          event_chain
  node_projection_pattern_learning     pattern_learning_artifacts
  node_projection_registration         node_service_registry
  node_projection_routing_decision     agent_routing_decisions
  node_projection_sandbox_decisions    sandbox_decisions
  node_projection_traces               traces
  node_projection_voice_sessions       voice_sessions
  node_renderer_capability_projection  renderer_capability_projection

Enforcement (Rule 5 — detection without a gate gets ignored):
scripts/ci/check_projection_contract_access.py, wired as BOTH a pre-commit
hook (`projection-contract-access`) and a ci.yml step. Scoped to any node
contract/handler, not node_projection_* — neither live instance matched that
prefix.

Why a static gate: this class is invisible to isolation tests. Every
projection test in the repo injects a fake adapter implementing upsert/query
with no access enforcement at all, so the handler's read passes in CI and is
refused in production. test_omn16090_real_dispatch_path.py drives the real
dispatch callback and still passed against the broken contract for exactly
that reason.

Tests (tests/test_omn16690_projection_contract_access.py) close that gap by
building the REAL ProjectionTableOperation over the REAL contract-declared
ModelDbTableDeclaration and faking only psycopg2 execution beneath it:
  - real dispatch path asserts the event is NOT quarantined AND a row lands
    (a 202 with zero rows is the failure, so "no exception" is not evidence)
  - redelivery stays idempotent, pinning the read against being deleted
  - a write-only declaration is STILL refused — pins the guard as fail-closed
    so the symptom cannot be "fixed" by relaxing it
  - class sweep over every contract; gate proven RED-derivable on a planted
    defect; test-only queries not miscounted; per-file constant resolution
    (a node-wide TABLE map masked delegation_events on the first sweep)

RED-before on dev @ fecf14b with contracts reverted:
  PermissionError: public.hook_events declares access='write'; read refused
  ... routing malformed/erroring event to the platform quarantine sink
      onex.dlq.omnibase-infra.quarantine.v1
matching the live pod log (omninode-runtime-effects-849bcb486-6sftf, onex-dev)
verbatim. GREEN after: 8 passed; 171 affected-node unit tests pass.

AC4/AC5 (release into the deployed omninode-runtime image, re-run the
OMN-16667 canary, un-fence hook-event-capture) require a deploy and are NOT
done here — recorded as follow-up on the ticket.

Evidence-Ticket: OMN-16690
…generate the source-derived relation inventory

The OMN-16690 contract change widens twelve projection contracts from
access: write to access: read_write, declaring the reads those projections
already perform. Three tests were pinned to the pre-change shape:

* test_event_chain_projection_omn13620 and test_traces_projection_omn13083
  asserted the bare literal table["access"] == "write". Their own docstrings
  state the intent is the WRITE-side capability the materialization path
  needs. Runtime auto-wiring keys off db_io.db_tables being non-empty
  (omnibase_infra.runtime.auto_wiring.handler_wiring._contract_declares_db_io,
  which never inspects the access class), so read_write wires materialization
  identically -- verified by reading the live installed implementation, not
  inferred. The literal pin forbade a strictly more accurate declaration; the
  assertion now requires write capability without forbidding declared reads.
* docs/evidence/OMN-15423-relation-inventory.json is generated from the
  contracts. Regenerated via scripts/generate_application_relation_inventory.py
  --write; the whole diff is the source-derived consequence of the change --
  each projection now correctly appears as a reader of its own table.

Verified: 46 passed (the two projection files, the inventory test, and the
ticket test file tests/test_omn16690_projection_contract_access.py).

OMN-16690
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 25 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 130 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2736a5a-03b4-4f01-a72b-2ad7c3370de5

📥 Commits

Reviewing files that changed from the base of the PR and between 996d621 and 778e678.

📒 Files selected for processing (20)
  • .github/workflows/ci.yml
  • .pre-commit-config.yaml
  • docs/evidence/OMN-15423-relation-inventory.json
  • scripts/ci/check_projection_contract_access.py
  • src/omnimarket/nodes/node_evidence_dashboard_reducer/contract.yaml
  • src/omnimarket/nodes/node_hook_event_capture/contract.yaml
  • src/omnimarket/nodes/node_llm_delegation_projection/contract.yaml
  • src/omnimarket/nodes/node_projection_capsule_store/contract.yaml
  • src/omnimarket/nodes/node_projection_delegation/contract.yaml
  • src/omnimarket/nodes/node_projection_event_chain/contract.yaml
  • src/omnimarket/nodes/node_projection_pattern_learning/contract.yaml
  • src/omnimarket/nodes/node_projection_registration/contract.yaml
  • src/omnimarket/nodes/node_projection_routing_decision/contract.yaml
  • src/omnimarket/nodes/node_projection_sandbox_decisions/contract.yaml
  • src/omnimarket/nodes/node_projection_traces/contract.yaml
  • src/omnimarket/nodes/node_projection_voice_sessions/contract.yaml
  • src/omnimarket/nodes/node_renderer_capability_projection/contract.yaml
  • tests/test_event_chain_projection_omn13620.py
  • tests/test_omn16690_projection_contract_access.py
  • tests/test_traces_projection_omn13083.py

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

✅ Architectural Review — PASSED

Errors: 0
Warnings: 0

What this checks

Rule Description
ARCH-TOPIC-001 No hardcoded Kafka topic strings in handler code
ARCH-DI-001 No event_bus=None bypass
ARCH-DI-002 No direct Handler instantiation outside workflow_runner/adapters
ARCH-DI-003 No reinvented DI containers
ARCH-TOPIC-002 contract.yaml topics follow `onex.{cmd

No architectural violations found.

Static architectural lint — no model inference (OMN-14176).

@github-actions

Copy link
Copy Markdown

✅ Hostile Reviewer — PASSED

Blocking findings (critical/error): 0
Total findings: 0
Models succeeded: qwen3-review,qwen3-review-b


Gate semantics

Verdict Meaning Blocks merge?
passed >=2 models succeeded, no critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Fewer than 2 models succeeded (infra unavailable/timeout) Yes (OMN-15110)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

@jonahgabriel
jonahgabriel enabled auto-merge (squash) August 27, 2026 12:22
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 27, 2026
…imarket#2164 (#7287)

* evidence(OMN-16690): author OCC companion for OmniNode-ai/omnimarket#2164

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 778e6780b46e0b1579de8366e170f20a325423fa.

* evidence(OMN-16690): self-bind OCC#7287 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@jonahgabriel
jonahgabriel merged commit e438c32 into dev Aug 27, 2026
184 of 190 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-16690-projection-access-class-fix branch August 27, 2026 13:11
jonahgabriel added a commit that referenced this pull request Sep 2, 2026
With no event_bus.dlq_topics declared, every erroring event from this node
fell through to the platform-wide quarantine sink
onex.dlq.omnibase-infra.quarantine.v1, shared by every handler on the runtime.
Two consequences, both live on onex-dev on 2026-09-01:

- Unfindable. That sink held 8,878,926 retained records as of OMN-16769. A
  per-node failure signal deposited there is not triageable.
- Blast radius. The Kafka circuit breaker is keyed on the CONNECTION
  (kafka.onex-dev). This node's quarantine publishes were failing
  OutOfOrderSequenceNumber, which opened the shared breaker 108 times in two
  hours and intermittently broke the gateway attach/heartbeat/detach path on
  the same pod.

The node now names its own sink, so its failures stay attributable to it.
The infra-side cut -- a failure-path publish never charges the shared
breaker -- lands in the companion omnibase_infra PR under the same ticket.

Version bumped 0.4.15 -> 0.4.16: packaged source changed, and the
release-identity gate (OMN-16344) refuses to alias two code states under one
published wheel version.

The test also pins the OMN-16690 declaration (access: read_write on
omninode_internal.renderer_capability_projection) that produced the poison
event, so the two halves of this chain cannot drift apart. That declaration
is ADOPTED from OMN-16690 / #2164 (dev e438c32), not re-landed here.

Ticket: OMN-17497
Related: OMN-16690, OMN-13548, OMN-16769, OMN-15957
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant