Skip to content

feat(OMN-15750): gateway attach/session control-plane effect node (G6) - #2694

Merged
jonahgabriel merged 5 commits into
devfrom
jonah/omn-15750-gateway-attach-ingress-node
Aug 12, 2026
Merged

jonahgabriel merged 5 commits into
devfrom
jonah/omn-15750-gateway-attach-ingress-node

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

First slice of the OMN-15739 ADOPT ruling (candidate A: edge-gateway agent -> OmniNode-owned attach ingress -> MSK server-side). Builds node_gateway_attach_effect, the attach/session control-plane node (G6 in docs/design/2026-08-08-gateway-node-architecture-lift.md) recommended by docs/design/2026-08-08-customer-cloud-connectivity-design.md's Open Question 2.

Three def-B EFFECT handlers, contract-driven config, secrets by ref:

  • gateway.attach -- decodes a per-tenant Keycloak client-credentials access token, registers a tenant-bound ModelGatewaySession.
  • gateway.heartbeat -- re-validates via RFC 7662 introspection on every tick. This is the revocation mechanism: disabling the tenant's Keycloak confidential client makes the next heartbeat observe active:false, tear the session down, and emit REVOKED -- independent of the token's own unexpired exp.
  • gateway.detach -- explicit edge-initiated teardown.

Also in this PR:

  • docker/docker-compose.gateway-attach-test-lane.yml (OMN-15752) -- a .201-side test-lane connector, parallel to and independent of the live bastion-path docker-compose.gateway.yml. The bastion is untouched.
  • scripts/proof/gateway_attach_e2e_proof.py (OMN-15753) -- an e2e slice-proof harness skeleton (attach -> heartbeat -> Keycloak client disable -> heartbeat observes revocation), matching the existing e2e_cloud_workflow_harness.py convention: --live defaults off, live stage bodies raise StageNotImplementedError until run against a deployed node.

What is proven, and how (proof_class: code-only + receipt-bound)

  • 16/16 unit tests pass, including test_heartbeat_after_keycloak_revocation_tears_down_session -- the literal handler-level revocation proof (introspection active:false -> session torn down, REVOKED event, gone from the store).
  • mypy --strict clean on the new node package (22 files).
  • ruff format + ruff check clean.
  • Full local unit suite (23,188 tests, .200-shaped governed selector via the repo's own pre-push hook) green with this diff.
  • pre-commit run full suite green, including the non-canonical-lifecycle-class ratchet, ONEX Architecture Validation, runtime_profiles validation, protocol-ownership, and subscribe-wiring-health gates -- each genuine finding fixed via the repo's existing allowlist mechanisms (see commit 2), not weakened.

What is NOT proven -- deploy and live e2e are explicitly out of scope for this PR:

  • OMN-15754 (deploy to onex-dev) is not done. This PR is unmerged (by design -- PRs open, not merged, per operator instruction); onex-dev's running omninode-runtime/omninode-runtime-effects/omninode-runtime-worker pods are built from released/merged code. Standing doctrine (reference_runtime_deployment) is image-rebuild deploy, not hot-install -- there is no live-attach path for this node until it merges and a new image ships.
  • OMN-15753's live half (real Kafka round trip + a real Keycloak client disable against a live tenant) did not run -- it requires the deployed node from OMN-15754. The harness is built and ready (scripts/proof/gateway_attach_e2e_proof.py --live ...).
  • OMN-15751 (house tenant provisioning via POST /v1/tenants) is BLOCKED, not done. Live investigation on onex-dev (i-06169517a92b45f86, read-only + one kubectl exec diagnostic) found POST /v1/tenants sits behind m4_usage_metering_middleware, which requires an already-resolved tenant credential (Bearer JWT or x-api-key) -- it is not in PUBLIC_V1_PATHS. This is circular for a first/house tenant: there is no existing tenant credential to present. The two endpoints that are public (/v1/auth/provision, /v1/alpha/invite) create a Keycloak user (human login), not the P0B confidential client (machine client-credentials identity) that /v1/tenants -> _provision_tenant_credentials_p0b provisions and that this node's attach flow actually needs. Direct DB/Keycloak provisioning via kubectl exec into the onex-api pod was attempted but the SSM-wrapped kubectl exec output capture is unreliable (diagnostic echo markers did not reliably appear in captured stdout) -- root-causing that tooling gap was out of scope for this PR. No tenant was created, no Keycloak client was created, nothing was mutated on onex-dev.

Scope discipline

Does not touch node_bus_forwarder_effect, any file in the gateway Phase-0 lane (OMN-15740-44, wf_51aa94f0-854), the bastion (docker-compose.gateway.yml), or any AWS SG rule. No AWS IAM/DNS/SG mutation. No prod. Config is contract + overlay only; secrets resolve by ref via SecretResolver at the effect boundary -- no new env-var reads.

Tickets

OMN-15750, OMN-15752, OMN-15753 (code + proof harness). OMN-15751 and OMN-15754 remain open -- see blockers above.

Evidence-Source / OCC companion: not yet opened for this PR (informational-only for now; will pair before merge per repo Evidence-Source: OCC#<PR> receipt-gate convention if/when this heads toward merge).

Evidence-Ticket: OMN-15750
Evidence-Source: OCC#6224

Gate-fix pass (2026-08-12)

Root-caused and fixed the full red set (gh pr checks 2694 at pickup: CI Summary, Pin Reachability (OMN-15538), dispatch-parity-gate, gate / CodeRabbit Thread Check, imperative-contract-guard, runner-image-build-smoke, Topic Drift Check, Version Pin Compliance):

  • Imperative Contract Guard (genuine, code fix): the new service_keycloak_token_validator.introspect() performed a raw httpx.AsyncClient call from a freestanding services/ module (outside node_*/handlers/), which scan_freestanding_imperative_io flags unconditionally regardless of DI shape. Moved the introspection call into HandlerGatewayHeartbeat._introspect (its sole caller) so the only I/O this node performs lives under handlers/; decode_claims (no I/O) stays in the service module. Declared metadata.transport_type: "HTTP" on the contract (sanctioned pattern, matches node_github_pr_poller_effect). Verified locally: check_imperative_contracts.py now reports live: 0 for this repo.
  • dispatch-parity-gate (genuine, fixture regen): the PR's three new gateway.* dispatchers were never in the committed Mode-A oracle. Regenerated tests/fixtures/dispatch_parity/baseline-selection-v2.json per the test's own documented recipe; diff is scoped exactly to the three new node_gateway_attach_effect dispatcher entries.
  • Topic Drift Check / Version Pin Compliance / Pin Reachability / runner-image-build-smoke (runner-infra faults, not code): byte-cited from each failing job's own log —
    • Topic Drift Check: The job has exceeded the maximum execution time of 10m0s + fetch-pack: invalid index-pack output / early EOF / RPC failed; curl 56 GnuTLS recv error (-54) during sibling-repo checkout.
    • Version Pin Compliance: uv sync hung mid-download (CACHE_ENABLED: false, UV_CONCURRENT_DOWNLOADS: 1) and was killed by the same 10-minute job timeout (The operation was canceled.).
    • Pin Reachability: onex_change_control @ 2dd26ade7c... — transport error: The read operation timed out resolving the pin (network, not a code defect).
    • runner-image-build-smoke: runner image shared_env_digest is stale (recorded=... recomputed=...) — the PR's 4-day-old head (41 commits behind dev) had a stale runner-image.lock.json relative to current dev.
      All four are consistent with a stale, contended head rather than a code defect. Fixed by rebasing onto origin/dev (git rebase origin/dev, clean, no conflicts) rather than a code change.
  • CodeRabbit Thread Check (9 threads): 3 fixed in code (hardcoded topic literal removed from a docstring; heartbeat's unknown-session test now asserts the exact SessionNotFoundError instead of bare Exception; attach now rejects an already-expired token with TokenValidationError before registering a session, with a new regression test). 6 acknowledged with reasoning and resolved: the test-lane compose file's Kafka-bootstrap exposure (it's the OMN-15752 proof-only test lane, not the customer-facing edge connector, which doesn't exist yet); the e2e harness's missing stage_detach() (every stage in that skeleton raises StageNotImplementedError until OMN-15754 deploys — wiring a real detach stage now would be untestable); and four independent heavy-lift architecture items (DI-container handler construction, atomic session-lifecycle transitions, JWT signature verification via JWKS, and circuit-breaker-based Keycloak-outage-vs-revocation distinction) tracked in follow-up ticket OMN-15918.

Seams (OMN-14208) — claims produced by OMN-15877 vs consumed here

This node's decode_claims is the consumer side of whatever token OmniWeb mints (OMN-15877, PR omnibase_infra#2726). Field-by-field, checked against #2726's live diff (docker/keycloak/desired-clients.json, omniweb client):

Claim Required by decode_claims Emitted by #2726 (omniweb client) Match
iss yes standard Keycloak claim yes
sub yes standard Keycloak claim yes
aud (must include gateway-attach) yes (required_audience) gateway-attach-audience mapper, NEW in #2726 yes
tenant_id yes tenant_id user-attribute mapper (pre-existing) yes
tenant_slug yes tenant_slug user-attribute mapper (pre-existing) yes
principal_id yes principal_id user-attribute mapper, NEW in #2726 yes
azp (-> ClaimSet.client_id) yes standard Keycloak claim = literal "omniweb" for every user of every tenant field present, semantics do not match
exp yes standard Keycloak claim yes

Wire shape: fully compatible — decode_claims will not raise a validation error against a real omniweb token once #2726 lands.

Credential-class design question (per OMN-15877's explicit requirement — recorded, not silently picked): this node (#2694) was built assuming a client-credentials machine token from a per-tenant confidential Keycloak client (the P0B chain) — see the contract's capabilities.introspection_revocation and every handler docstring: "disabling the tenant's Keycloak client" is the revocation mechanism, and session.keycloak_client_id (from azp) is assumed per-tenant. omniweb's actual client config is standardFlowEnabled: true, serviceAccountsEnabled: false — a user-session (authorization-code) token from one shared client, which #2726 extends with the gateway-attach audience rather than issuing a second, tenant-scoped credential (OMN-15877's option (a): "one token satisfies both validators"). Under that shape azp is always "omniweb", which breaks per-tenant revocation and the heartbeat's defense-in-depth client_id match as designed. This is an open PAIR_INCOMPATIBLE risk, not resolved by either PR — byte-cited comment posted on OMN-15877 (2026-08-12); resolution belongs to that ticket's confirmation loop with Daniyal's team, not a unilateral pick on either PR.

Summary by CodeRabbit

  • New Features
    • Added gateway session lifecycle support for attach, heartbeat, and detach operations.
    • Added token validation, tenant-bound sessions, configurable expiration, and heartbeat status tracking.
    • Added session lifecycle events for attachment, heartbeat, degradation, revocation, and detachment.
    • Registered the gateway attach capability for runtime discovery and deployment.
  • Tests
    • Added coverage for successful sessions, token revocation, invalid sessions, detachment, and validation failures.
  • Documentation
    • Added a gateway attach end-to-end proof flow with dry-run support.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 16 minutes

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).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bdc8cdd7-79f1-4f0e-99d4-66e06efec06d

📥 Commits

Reviewing files that changed from the base of the PR and between f5d78db and 05a96a2.

📒 Files selected for processing (12)
  • docker/runners/runner-image.lock.json
  • pyproject.toml
  • src/omnibase_infra/nodes/node_gateway_attach_effect/__init__.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/contract.yaml
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_attach.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_heartbeat.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_attach_config.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_session_event.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/services/service_keycloak_token_validator.py
  • tests/fixtures/dispatch_parity/baseline-selection-v2.json
  • tests/unit/nodes/node_gateway_attach_effect/test_handlers.py
  • tests/unit/nodes/node_gateway_attach_effect/test_service_keycloak_token_validator.py
📝 Walkthrough

Walkthrough

Added a contract-driven gateway attach effect node. It validates tokens, stores tenant-bound sessions, handles attach, heartbeat, and detach operations, emits lifecycle events, and adds a dry-run proof harness with Docker Compose wiring.

Changes

Gateway attach control plane

Layer / File(s) Summary
Session contracts and node registration
src/omnibase_infra/nodes/node_gateway_attach_effect/..., src/omnibase_infra/validation/validation_exemptions.yaml
Added the node contract, Pydantic request and response models, session lifecycle enums, node class, public exports, and validation exemptions.
Token validation and session storage
src/omnibase_infra/nodes/node_gateway_attach_effect/services/..., tests/unit/nodes/node_gateway_attach_effect/test_service_keycloak_token_validator.py
Added JWT claim validation, Keycloak introspection, an asynchronous session-store protocol, and an in-memory session store with unit tests.
Attach, heartbeat, and detach handlers
src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/..., tests/unit/nodes/node_gateway_attach_effect/test_handlers.py
Added session registration, heartbeat re-validation, revocation cleanup, detach processing, lifecycle events, and handler tests.
Gateway attach proof harness
scripts/proof/gateway_attach_e2e_proof.py
Added dry-run planning and live-mode stage control for attach, heartbeat, optional revocation, and post-revocation verification.
Runtime and validation wiring
docker/docker-compose.gateway-attach-test-lane.yml, pyproject.toml, scripts/check_subscribe_wiring_health.py, scripts/ci/infra-node-allowlist.txt
Registered the node entry point, added the test-lane service, and updated topic and infrastructure ownership allowlists.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new gateway attach/session control-plane effect node, which is the main change in the pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonah/omn-15750-gateway-attach-ingress-node

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker/docker-compose.gateway-attach-test-lane.yml`:
- Line 42: Remove the GATEWAY_ATTACH_TEST_LANE_BOOTSTRAP_SERVERS argument from
the edge-side connector configuration in the gateway attach test-lane setup.
Update the proof harness to use the gateway ingress API for edge requests, while
retaining Kafka bootstrap configuration only within infrastructure-side
components.

In `@scripts/proof/gateway_attach_e2e_proof.py`:
- Around line 146-152: Add an explicit stage_detach() call in run_live after the
active heartbeat and before optional revocation, register that stage in
STAGE_SURFACES, and make it consume detach_request_topic while asserting
DETACHED state and session removal.

In
`@src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_attach.py`:
- Around line 51-57: Update the handler constructors to accept
ModelONEXContainer instead of direct configuration and service dependencies,
resolving required values with container.get_service(...). In
handler_gateway_attach.py lines 51-57, resolve the configuration and
ProtocolGatewaySessionStore; in handler_gateway_detach.py lines 39-40, resolve
ProtocolGatewaySessionStore; and in handler_gateway_heartbeat.py lines 53-61,
resolve the configuration, ProtocolGatewaySessionStore, and secret resolver.
Update stored fields and constructor call sites as needed while preserving
handler behavior.
- Around line 72-91: Validate claims.expires_at_epoch before calculating TTL or
constructing ModelGatewaySession, and reject the request when the token is
already expired. Keep session registration through self._session_store.put
limited to tokens with a positive remaining lifetime.

In
`@src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_heartbeat.py`:
- Around line 74-114: The heartbeat handler must prevent stale reads from
resurrecting sessions after detach. In handler_gateway_heartbeat.py, make the
revoke and updated-session writes conditional on the session version/state read
before introspection, using a per-session lock or shared
compare-and-delete/compare-and-put lifecycle operation; in
handler_gateway_detach.py, replace the direct deletion with that same atomic
lifecycle operation so detach and heartbeat cannot interleave inconsistently.

In
`@src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_session_event.py`:
- Line 8: Remove the hardcoded Kafka topic literal from the gateway session
event model and reference the topic exposed by the contract declaration instead.
Update the surrounding model definition in model_gateway_session_event.py to use
the existing generated or imported contract symbol, ensuring the contract YAML
remains the sole source of truth.

In
`@src/omnibase_infra/nodes/node_gateway_attach_effect/services/service_keycloak_token_validator.py`:
- Around line 57-111: Update decode_claims to verify the JWT signature using
trusted Keycloak keys and reject alg:none or altered signed claims before
constructing ClaimSet; validate issuer, audience, and expiration as part of this
flow. In
tests/unit/nodes/node_gateway_attach_effect/test_service_keycloak_token_validator.py
lines 59-87, replace the unsigned success fixture with a verified-token fixture
and add coverage rejecting alg:none tokens and post-signature claim changes.
- Around line 156-179: Update ServiceKeycloakTokenValidator to inherit from
MixinAsyncCircuitBreaker and route Keycloak introspection through its resilience
mechanism, raising InfraUnavailableError when the circuit opens or transport,
non-200, or malformed responses indicate infrastructure failure; return False
only for an explicit active: false response. Update
tests/unit/nodes/node_gateway_attach_effect/test_service_keycloak_token_validator.py
lines 191-222 to expect InfraUnavailableError for transport and upstream
failures, while preserving active: false as the sole revocation result.

In `@tests/unit/nodes/node_gateway_attach_effect/test_handlers.py`:
- Around line 216-227: Update test_heartbeat_unknown_session_raises to import
SessionNotFoundError from handler_gateway_heartbeat under an alias and pass that
exact exception type to pytest.raises instead of the broad Exception class.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67c383aa-e4af-4319-8481-9170c6c251cd

📥 Commits

Reviewing files that changed from the base of the PR and between 211e81e and f5d78db.

⛔ Files ignored due to path filters (1)
  • src/omnibase_infra/enums/generated/enum_omnibase_infra_topic.py is excluded by !**/generated/**
📒 Files selected for processing (33)
  • docker/docker-compose.gateway-attach-test-lane.yml
  • pyproject.toml
  • scripts/check_subscribe_wiring_health.py
  • scripts/ci/infra-node-allowlist.txt
  • scripts/proof/gateway_attach_e2e_proof.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/__init__.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/contract.yaml
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/__init__.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_attach.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_detach.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/handlers/handler_gateway_heartbeat.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/__init__.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/enum_gateway_session_event_type.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/enum_gateway_session_status.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_attach_config.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_attach_request.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_attach_response.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_detach_request.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_detach_response.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_heartbeat_request.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_heartbeat_response.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_session.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/models/model_gateway_session_event.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/node.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/services/__init__.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/services/protocol_gateway_session_store.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/services/service_keycloak_token_validator.py
  • src/omnibase_infra/nodes/node_gateway_attach_effect/services/store_gateway_session_memory.py
  • src/omnibase_infra/validation/validation_exemptions.yaml
  • tests/unit/contracts/test_protocol_ownership.py
  • tests/unit/nodes/node_gateway_attach_effect/__init__.py
  • tests/unit/nodes/node_gateway_attach_effect/test_handlers.py
  • tests/unit/nodes/node_gateway_attach_effect/test_service_keycloak_token_validator.py

Comment thread docker/docker-compose.gateway-attach-test-lane.yml
Comment thread scripts/proof/gateway_attach_e2e_proof.py
Comment thread tests/unit/nodes/node_gateway_attach_effect/test_handlers.py
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 8, 2026
…ibase_infra#2694 (#6224)

* evidence(OMN-15750): author OCC companion for OmniNode-ai/omnibase_infra#2694

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

* evidence(OMN-15750): self-bind OCC#6224 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

✅ Hostile Reviewer — PASSED

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


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical findings No
blocked CRITICAL findings found Yes
degraded All models unavailable (infra) No (pilot)

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

jonahgabriel added a commit that referenced this pull request Aug 10, 2026
…d PR events (#2703)

* fix(OMN-14241): occ-preflight eval-path callers must listen for `edited` PR events

ci.yml (implicit default types, no `edited`) and hostile-reviewer.yml
(`types: [opened, synchronize, reopened]`) each declare a top-level
`occ-preflight` job calling the canonical reusable occ-preflight.yml, but
neither retriggers when a PR body is edited without a new commit. The
reusable workflow already live-fetches the current PR body via `gh pr view`
on every run it does execute -- the defect was never a frozen-body read, it
was that no new run fired at all when a body-only edit (typically stamping
`Evidence-Source: OCC#<n>` after the companion OCC PR merges) landed. The
last commit-triggered run -- correctly FAILURE before the stamp existed --
then sits as the permanent `occ-preflight / eligibility` status, and CI
Summary fails closed on it forever without a manual empty-commit retrigger.

Live-root-caused on infra#2696 and #2694 (2026-08-09): both PRs stamped
Evidence-Source via a body edit; CI's and Hostile Reviewer's occ-preflight
copies stayed FAILURE from before the edit while call-reject-skip.yml's
copy (which already lists `edited`) correctly re-ran and passed.

Adds tests/ci/test_occ_eval_path_trigger_coverage.py (RED before this
change on both workflows, GREEN after), sibling to the existing
test_occ_born_path_trigger_coverage.py (OMN-14987) guard for the
autobind/companion-effect minting workflows.

OMN-14241 (adopted existing Backlog ticket; relatedTo OMN-15725/OMN-15214
lineage). No debounce/diff guard added -- today's actual behavior already
pays a full-CI-run cost per stamp fix via manual empty-commit retriggers, so
automating the same retrigger via `edited` is net-neutral CI cost, not new
cost; it removes the manual-toil failure mode currently blocking both PRs.

* chore(OMN-14241): [mergesweep-0809-infraunblock] retrigger CI

Deploy Agent Tests (OMN-15378) failed once with a docker-teardown flake
unrelated to this PR's diff (subprocess.TimeoutExpired on `docker rm -f`
in test_executor_digest_verification_image_id_omn15181.py, a file this PR
never touches). First-strike retry per two-strike allowance.

* chore(OMN-14241): [mergesweep-0809-infraunblock] retrigger CodeRabbit gate

gate/CodeRabbit Thread Check's prior run (31333667026) was CANCELLED by a
concurrent same-SHA run collision, not a real failure (log confirmed
"Unresolved CodeRabbit threads: 0"). An issue_comment retrigger attempt
resolved to the wrong head SHA (dev tip, not this PR's head) due to how
cr-thread-gate-caller.yml's issue_comment path checks out ref -- a genuine
workflow quirk, not applicable to this PR's actual commit. Using a
synchronize-triggering empty commit instead, which correctly targets this
PR's head per the workflow's pull_request trigger.

* chore(OMN-14241): [mergesweep-0809-infraunblock] retrigger CI Summary poll

CI Summary FAILED (run 31334333041) purely on poller-deadline timing: its
own log shows CodeQL still listed as "missing/pending" at the 20:37:31Z
deadline, but CodeQL's own run (31334296327) completed SUCCESS at
20:36:49Z -- 42 seconds earlier. A fresh poll will observe CodeQL as
already-passed. Not a repeat of the same failure class (prior strikes were
Deploy Agent Tests docker-teardown timeout and a CodeRabbit gate wrong-SHA
issue) -- this is a first-instance poller/job-completion race.

* chore(OMN-14241): [mergesweep-0809-infraunblock] retrigger past known flake OMN-15749

Deploy Agent Tests failed twice on TestRepoDigestsOnContainerIsRealDockerFailureMode
(docker create / docker rm -f timing out at 30s). Root-caused, not transient
hand-waving: this is the pre-existing, already-ticketed OMN-15749 ("deploy-agent
digest-verification fixture tests time out on real docker create/docker rm -f --
30s too short under runner contention", Backlog, unrelated to this PR's diff --
this PR touches zero deploy-agent files). Confirmed non-systemic: sibling PRs
#2701/#2702/#2700 all passed the identical test on concurrent runs at the same
time, so this is isolated runner-instance docker contention, not a fleet-wide
or code-caused failure. Retrying past the known flake rather than blocking an
unrelated CI-workflow-trigger fix on it.

* chore(OMN-14241): [mergesweep-0809-infraunblock] retrigger past transient pin-reachability timeout

Tests (Split 1/15) failed on tests/integration/ci/test_pin_reachability_live_omn15538.py
with "transport error: The read operation timed out" resolving the pre-existing
core-ref pin in ci.yml/hostile-reviewer.yml (unrelated to this PR's diff --
neither line is touched here; the stale pin itself is OMN-15725's scope,
PR #2680 still open). Confirmed transient, not a durable unreachable-pin
state: sibling PR #2688/#2697 passed this same check hours ago; sibling
PR #2701 failed the identical check in the same ~30-40min window as this
PR, consistent with a shared transient GitHub API rate-limit/timeout window
(cf. OMN-14352) rather than a code-caused or permanently-broken pin.
Adds node_gateway_attach_effect: the attach ingress node recommended by
OMN-15739's ADOPT ruling (candidate A) and the gateway node-architecture
lift's G6 item. Three def-B EFFECT handlers, stateless, no direct bus
access (runtime publishes the typed response's embedded session_event --
node_owned_publish):

  - HandlerGatewayAttach (gateway.attach): decodes a per-tenant Keycloak
    client-credentials access token's claims, registers a tenant-bound
    ModelGatewaySession, returns ATTACHED session_event.
  - HandlerGatewayHeartbeat (gateway.heartbeat): re-validates the token via
    RFC 7662 introspection against Keycloak on every tick -- this is the
    revocation mechanism. Disabling the tenant's confidential client makes
    the next heartbeat observe active:false, tear the session down, and
    emit REVOKED, independent of the token's own unexpired exp.
  - HandlerGatewayDetach (gateway.detach): explicit edge-initiated teardown.

Config is 100% contract + overlay (ModelGatewayAttachConfig, no env reads);
secrets resolve by ref via SecretResolver at the effect boundary
(service_keycloak_token_validator.py). Session storage is in-process
(StoreGatewaySessionMemory) behind ProtocolGatewaySessionStore for this
first slice -- documented as a known gap in contract.yaml.

Also (OMN-15752): docker/docker-compose.gateway-attach-test-lane.yml, a
.201-side test-lane connector parallel to (and independent of) the live
bastion-path docker-compose.gateway.yml -- untouched by this PR.

Also (OMN-15753): scripts/proof/gateway_attach_e2e_proof.py, an e2e slice
proof harness skeleton matching e2e_cloud_workflow_harness.py's convention
-- --live defaults off, live stage bodies raise StageNotImplementedError
until run against a deployed node (OMN-15754, not landed by this PR).

Local-gate fixes applied after pre-commit passes: split enum/model mixed
files per ONEX Architecture Validation; runtime_profiles [effects, canary]
so the subscribing contract has a draining consumer (OMN-12957); added the
handler files to scripts/ci/infra-node-allowlist.txt (infra-owned
trust-boundary node, sibling of node_bus_forwarder_effect); registered the
node entry point in pyproject.toml; regenerated topic enums; added
validation_exemptions.yaml entries for non-UUID id fields.

Local proof: 16/16 unit tests pass (including
test_heartbeat_after_keycloak_revocation_tears_down_session, the literal
revocation proof), mypy --strict clean (22 files), ruff clean.

Does not touch node_bus_forwarder_effect, the gateway Phase-0 lane's files
(OMN-15740-44), the bastion, or any SG rule.

OMN-15750, OMN-15752, OMN-15753
Two pre-push full-suite failures surfaced after the first commit, both
addressed with the repo's existing allowlist mechanisms rather than
weakening either gate:

  - tests/unit/contracts/test_protocol_ownership.py: ProtocolGatewaySessionStore
    is a genuine [NODE] DI seam (swap in-process -> Valkey later without
    touching handlers), same category as the existing node-internal
    protocol entries. Added to KNOWN_INFRA_PROTOCOLS.
  - tests/unit/scripts/test_check_subscribe_wiring_health.py: the three
    gateway-attach command topics are published by the edge-side dialer
    (docker-compose.gateway-attach-test-lane.yml / the eventual onex
    connect client), never by a contract-declared node -- the same shape
    as the existing baselines-batch-compute CLI-publisher entry. Added to
    _EXTERNAL_PUBLISHER_ALLOWLIST with owner/expiry, not the closed
    baseline ratchet list.

OMN-15750
occ-preflight / eligibility is stuck FAILURE (both CI's and Hostile
Reviewer's copies) from before the PR body was edited to add
Evidence-Source: OCC#6224 (21:24:59Z). This is the OMN-14241 failure class:
ci.yml/hostile-reviewer.yml lack `edited` in on.pull_request.types, so the
body edit never retriggered them, and the stale pre-stamp FAILURE never
self-heals. Retriggering via a synchronize event (empty commit) per the
allowed mechanisms until OMN-14241 (infra#2703) lands.
…ach node

- Move Keycloak RFC 7662 introspection HTTP call from the freestanding
  services/ module into HandlerGatewayHeartbeat._introspect so the raw
  transport call lives under handlers/, satisfying the
  imperative-contract-guard's handlers/-only I/O boundary (root cause of
  the Imperative Contract Guard failure: 1 LIVE violation). Declare
  metadata.transport_type: HTTP on the node contract per the sanctioned
  pattern (node_github_pr_poller_effect).
- Regenerate tests/fixtures/dispatch_parity/baseline-selection-v2.json:
  the PR's three new gateway.* dispatchers were never in the committed
  Mode-A oracle (dispatch-parity-gate diff is scoped exactly to the new
  handlers).
- Reject already-expired tokens at attach time before session
  registration (CodeRabbit Major/security finding).
- Remove a hardcoded topic literal from a docstring; assert the exact
  SessionNotFoundError type instead of bare Exception in a heartbeat
  test (CodeRabbit Minor findings).
- Resolve the remaining 4 CodeRabbit threads (DI container, atomic
  session-lifecycle transitions, JWT signature verification, Keycloak
  outage vs revocation) with reasoning + follow-up ticket OMN-15918 --
  each is an independent heavy-lift architecture change, not a fix
  scoped to this PR.

Topic Drift Check / Version Pin Compliance / Pin Reachability /
runner-image-build-smoke were runner-infra faults (job timeout mid
uv-sync, git fetch transport errors, stale shared_env_digest) on a
4-day-old head 41 commits behind dev -- addressed by rebasing onto dev
rather than a code fix.

Evidence-Ticket: OMN-15750
@jonahgabriel
jonahgabriel force-pushed the jonah/omn-15750-gateway-attach-ingress-node branch from 0bf0be8 to a5fc3c8 Compare August 12, 2026 13:42
…validation

R4 (runner-image-build-smoke, merge-blocking): this PR's pyproject.toml
entry-point addition for node_gateway_attach_effect moves the runner-image
shared_env_digest (pyproject.toml + uv.lock are the only digest inputs).
Regenerated via `scripts/ci/runner_image_identity.py --mode generate`;
verify mode now confirms recorded==recomputed (638933d1c8de12afe5c8024c).
Diff is scoped to docker/runners/runner-image.lock.json only.

R3 (dead security config field): keycloak_issuer_ref was declared in
ModelGatewayAttachConfig and contract.yaml but had zero readers --
decode_claims presence-checked the iss claim but never compared it to the
configured issuer, so a token from any issuer that satisfied the other
claims would attach. Wired it: HandlerGatewayAttach now takes a
SecretResolver (same DI shape as HandlerGatewayHeartbeat), resolves
keycloak_issuer_ref before calling decode_claims, and decode_claims raises
TokenValidationError on a mismatched iss. decode_claims stays I/O-free
(receives the already-resolved issuer string) so the imperative-contract-
guard boundary (I/O only under handlers/) is preserved -- same pattern
HandlerGatewayHeartbeat._introspect already uses for the introspection
endpoint ref.

Added test_attach_rejects_mismatched_issuer +
test_attach_accepts_matching_issuer (handler-level, end-to-end through
SecretResolver) and test_mismatched_issuer_raises +
test_matching_issuer_happy_path (decode_claims-level). Updated all
existing HandlerGatewayAttach call sites for the new required
secret_resolver param.

19/19 focused unit tests pass, mypy --strict clean (22 files), ruff clean,
pre-commit run --files clean on all 5 changed files.

Ticket: OMN-15750
@jonahgabriel
jonahgabriel merged commit 52d8b60 into dev Aug 12, 2026
165 of 177 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-15750-gateway-attach-ingress-node branch August 12, 2026 15:35
jonahgabriel added a commit that referenced this pull request Aug 12, 2026
… verification, identity binding, atomic transitions, outage/revocation split (#2727)

* feat(OMN-15918): node_gateway_attach_effect hardening — JWKS signature verification, identity binding, atomic transitions, outage/revocation split

CodeRabbit-flagged hardening follow-ups on OMN-15750 (PR #2694), TDD RED-before/GREEN-after for each:

- R1 (JWT signature verification): `decode_claims` (structural-only, never referenced the
  signature segment) replaced with `verify_and_decode_claims` — verifies against a
  resolved JWKS keyset via PyJWT before trusting any claim. `alg:none`, wrong-key-signed,
  and unknown-kid tokens are all rejected. JWKS fetch (network I/O) lives inline in each
  handler (`_fetch_jwks`), circuit-breaker guarded; verification itself stays I/O-free in
  the service module, matching the existing `_introspect` I/O-boundary pattern.
- R2 (identity binding): heartbeat and detach now re-verify the presented token's
  signature and bind its tenant_id/principal_id/client_id to the STORED session's
  identity from attach time before acting. Detach previously took zero credential at
  all (session_id + free-text reason); ModelGatewayDetachRequest now requires
  access_token (contract minor bump 0.1.0 -> 0.2.0, wire-breaking on this new node
  with no external consumers yet).
- R3 (atomic transitions): ProtocolGatewaySessionStore.put_if_present closes the
  heartbeat resurrection race — a concurrent detach landing in the read-introspect-write
  gap is no longer silently resurrected by the heartbeat's final write.
- R4 (outage vs revocation): JWKS fetch and RFC 7662 introspection are now
  MixinAsyncCircuitBreaker-guarded; a transport error, non-200, or malformed body
  raises InfraUnavailableError and leaves the session untouched, instead of the
  previous fail-closed False that made every Keycloak outage read as mass revocation.

Ticket item 1 (DI container for handler construction) is explicitly deferred — out of
scope for this hardening slice, tracked as a known_gap in contract.yaml.

Filed OMN-15952 (linked blocker to OMN-15877) for the unattended pairing/renewal
contract gap the ground-truth verification surfaced separately.

35 tests (12 validator + 20 handler + 3 store), all new/changed assertions verified
RED against the pre-hardening source via git stash before GREEN. ruff + mypy --strict
+ pre-commit (98 hooks, file-scoped) all clean.

* chore(OMN-15918): trigger clean CI Summary re-run after Evidence-Source/Evidence-Ticket PR-body fix
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