Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7200315
chore(OMN-12934): continue dev to main promotion for prod redeploy (#…
jonahgabriel Jun 15, 2026
844cde5
fix(OMN-13670): surgical main hotfix — re-apply OMN-13654 dep floors …
jonahgabriel Jun 27, 2026
2cd7208
fix(OMN-13670): floor cryptography>=48.0.1 in runtime builder — clear…
jonahgabriel Jun 28, 2026
ca8ebf9
fix(OMN-13687): bounded retry on docker push to ECR — absorb transien…
jonahgabriel Jun 28, 2026
49927fd
feat(OMN-13762): promote infra dev->main — clean PyPI runtime (core 0…
jonahgabriel Jul 1, 2026
00e5a02
promote(OMN-13762): infra dev->main — Trivy-clean runtime image (ECR …
jonahgabriel Jul 1, 2026
824a8fa
fix(OMN-15115): promote LLM request retry schema to main (#2447)
jonahgabriel Jul 25, 2026
e3c4320
fix(OMN-15147): clear runtime image Trivy high gate
jonahgabriel Jul 26, 2026
3b67018
promote(OMN-15181): dev->main dev-wins bridge — operator-ruled one-ti…
Jul 26, 2026
a09a48a
chore(OMN-15195): disambiguate bridge head SHA from closed duplicate …
Jul 26, 2026
b437e48
fix(OMN-15195): remove direct env reads from runner health bridge
Jul 26, 2026
2126013
ci(OMN-15195): pin deploy gate to pyyaml resolver
Jul 26, 2026
9b7021d
fix(OMN-15195): correct omniclaude deploy-gate pin
jonahgabriel Jul 26, 2026
c0307e9
ci(OMN-15195): retry PR webhook file pagination
Jul 26, 2026
238b7ca
ci(OMN-15195): use org token for env parity sibling checkout
Jul 26, 2026
d7799bb
ci(OMN-15195): fix infra sweep blocker gates
Jul 26, 2026
7bf9601
ci(OMN-15195): use cross-repo token for env parity sibling checkout
Jul 26, 2026
5bb0c25
Merge pull request #2481 from OmniNode-ai/hotfix/omn-15195-devwins-br…
jonahgabriel Jul 26, 2026
3dd848d
Merge remote-tracking branch 'origin/dev' into hotfix/omn-16041-infra…
jonahgabriel Aug 14, 2026
0449c06
chore(OMN-16041): backmerge main lineage into dev alongside the v0.38…
jonahgabriel Aug 14, 2026
50dc2fc
chore(OMN-16041): merge current dev into main-lineage backmerge
jonahgabriel Aug 18, 2026
49ff6be
fix(OMN-16041): sync contract.yaml for node_runner_fleet_health_compu…
jonahgabriel Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/required-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ gates:
producer_kind: cross_repo
caller_workflow: deploy-gate.yml
caller_job: deploy-gate
cross_repo_ref: "OmniNode-ai/omniclaude/.github/workflows/deploy-gate-reusable.yml@ff230264ac3300d7ced43564dc921f44558110fe"
cross_repo_ref: "OmniNode-ai/omniclaude/.github/workflows/deploy-gate-reusable.yml@fcbc85f5642f07e15fe1d6b1c168c96883d16ac3"
skip_semantics: never
rationale: "Runtime-change PRs must cite a ticket with deploy evidence (OMN-8912/DGM-Phase6). Caller job has no if:, no path filter, pull_request+merge_group trigger present."
- name: "Topic Enum Drift Check"
Expand Down
49 changes: 33 additions & 16 deletions .github/workflows/artifact-reconciliation-webhook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,38 @@ jobs:
pull-requests: read

steps:
- name: Check webhook credentials
id: webhook-credentials
env:
KAFKA_BOOTSTRAP_SERVERS: ${{ secrets.KAFKA_BOOTSTRAP_SERVERS }}
KAFKA_SASL_USERNAME: ${{ secrets.KAFKA_SASL_USERNAME }}
KAFKA_SASL_PASSWORD: ${{ secrets.KAFKA_SASL_PASSWORD }}
run: |
if [[ -z "${KAFKA_BOOTSTRAP_SERVERS:-}" || -z "${KAFKA_SASL_USERNAME:-}" || -z "${KAFKA_SASL_PASSWORD:-}" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice title=PR webhook not published::Kafka webhook secrets are unavailable on this runner/event; skipping non-authoritative PR webhook publication."
exit 0
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"

- name: Checkout code
if: ${{ steps.webhook-credentials.outputs.enabled == 'true' }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Get changed files
if: ${{ steps.webhook-credentials.outputs.enabled == 'true' }}
id: changed-files
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_REPO: ${{ github.repository }}
run: |
python3 - <<'PY'
import http.client
import json
import os
import time
import urllib.error
import urllib.request

repo = os.environ["PR_REPO"]
Expand All @@ -70,6 +89,19 @@ jobs:
files = []
page = 1

def load_json_with_retry(request):
last_error = None
for attempt in range(1, 4):
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except (TimeoutError, http.client.IncompleteRead, urllib.error.URLError) as error:
last_error = error
if attempt == 3:
break
time.sleep(attempt * 2)
raise RuntimeError(f"GitHub PR files request failed after retries: {last_error}") from last_error

while True:
url = (
f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files"
Expand All @@ -83,8 +115,7 @@ jobs:
"X-GitHub-Api-Version": "2022-11-28",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
payload = load_json_with_retry(request)
if not payload:
break
files.extend(item["filename"] for item in payload)
Expand All @@ -94,20 +125,6 @@ jobs:
output.write(f"all_changed_files={','.join(files)}\n")
PY

- name: Check webhook credentials
id: webhook-credentials
env:
KAFKA_BOOTSTRAP_SERVERS: ${{ secrets.KAFKA_BOOTSTRAP_SERVERS }}
KAFKA_SASL_USERNAME: ${{ secrets.KAFKA_SASL_USERNAME }}
KAFKA_SASL_PASSWORD: ${{ secrets.KAFKA_SASL_PASSWORD }}
run: |
if [[ -z "${KAFKA_BOOTSTRAP_SERVERS:-}" || -z "${KAFKA_SASL_USERNAME:-}" || -z "${KAFKA_SASL_PASSWORD:-}" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice title=PR webhook not published::Kafka webhook secrets are unavailable on this runner/event; skipping non-authoritative PR webhook publication."
exit 0
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"

- name: Set up CI Python environment
if: ${{ steps.webhook-credentials.outputs.enabled == 'true' }}
uses: ./.github/actions/setup-python-uv
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ concurrency:

jobs:
deploy-gate:
uses: OmniNode-ai/omniclaude/.github/workflows/deploy-gate-reusable.yml@70718ddb45f6df5eb5e07159f2ae812efb1da7db
uses: OmniNode-ai/omniclaude/.github/workflows/deploy-gate-reusable.yml@fcbc85f5642f07e15fe1d6b1c168c96883d16ac3
secrets: inherit
with:
contracts-dir: contracts
5 changes: 3 additions & 2 deletions .github/workflows/env-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ jobs:

- name: Checkout omninode_infra (sibling — provides k8s ConfigMap)
# GITHUB_TOKEN is scoped to omnibase_infra and cannot access the private
# omninode_infra repo. Use CROSS_REPO_PAT (same pattern as dependency-cascade.yml).
# omninode_infra repo. Prefer the org-wide cross-repo token used by
# sibling-repo workflows, then fall back to the legacy token name.
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: OmniNode-ai/omninode_infra
path: omninode_infra
token: ${{ secrets.CROSS_REPO_PAT || secrets.GITHUB_TOKEN }}
token: ${{ secrets.CROSS_REPO_PAT || secrets.OMNI_GITHUB_TOKEN || github.token }}

- name: Setup Python and uv
uses: ./omnibase_infra/.github/actions/setup-python-uv
Expand Down
108 changes: 43 additions & 65 deletions scripts/validate_handler_contracts.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
"""Validate all handler contracts before migration.
"""Validate handler contract descriptors.

This script validates that all handlers previously registered via _KNOWN_HANDLERS
now have valid contract.yaml files with proper configuration.

Run this BEFORE deleting _KNOWN_HANDLERS to ensure no handlers are orphaned.

Part of OMN-1518: Migration from hardcoded _KNOWN_HANDLERS to contract-driven
handler registration.
This script validates the current contract-driven handler descriptors under
``src/omnibase_infra/contracts/handlers/*/handler_contract.yaml``. Older
branches expected legacy ``nodes/handlers/*/contract.yaml`` files, but those
paths no longer exist on main; keeping that expectation makes the compliance
gate fail even when the live descriptors are present.

Usage:
python scripts/validate_handler_contracts.py
Expand Down Expand Up @@ -37,33 +35,6 @@

import yaml

# =============================================================================
# Expected Handlers from _KNOWN_HANDLERS
# =============================================================================
# These must all have valid contract.yaml files before _KNOWN_HANDLERS can be
# deleted from util_wiring.py

EXPECTED_HANDLERS: dict[str, str] = {
"consul": "HashiCorp Consul service discovery handler",
"db": "PostgreSQL database handler",
"graph": "Graph database (Memgraph/Neo4j) handler",
"http": "HTTP REST protocol handler",
"intent": "Intent storage and query handler for demo",
"mcp": "Model Context Protocol handler for AI agents",
"vault": "HashiCorp Vault secret management handler",
}

# Contract locations (relative to src/omnibase_infra/)
HANDLER_CONTRACT_PATHS: dict[str, Path] = {
"consul": Path("nodes/handlers/consul/contract.yaml"),
"db": Path("nodes/handlers/db/contract.yaml"),
"graph": Path("nodes/handlers/graph/contract.yaml"),
"http": Path("nodes/handlers/http/contract.yaml"),
"intent": Path("nodes/handlers/intent/contract.yaml"),
"mcp": Path("nodes/handlers/mcp/contract.yaml"),
"vault": Path("nodes/handlers/vault/contract.yaml"),
}

# =============================================================================
# Validation Functions
# =============================================================================
Expand All @@ -80,8 +51,8 @@ def validate_contract(
Checks for:
- Contract file existence
- Valid YAML syntax
- Required fields (name, node_type, contract_version)
- handler_routing section with valid handlers
- Required fields (handler_id, name, contract_version, descriptor)
- handler_routing section with valid handlers when the descriptor declares it
- operation_bindings validation (if present and using loader)

Args:
Expand Down Expand Up @@ -114,16 +85,12 @@ def validate_contract(
return errors

# Check required fields
if "handler_id" not in contract:
errors.append("Missing 'handler_id' field")

if "name" not in contract:
errors.append("Missing 'name' field")

if "node_type" not in contract:
errors.append("Missing 'node_type' field")
elif contract["node_type"] != "EFFECT_GENERIC":
errors.append(
f"Expected node_type 'EFFECT_GENERIC', got '{contract['node_type']}'"
)

if "contract_version" not in contract:
errors.append("Missing 'contract_version' field")
else:
Expand All @@ -136,11 +103,15 @@ def validate_contract(
if "patch" not in version:
errors.append("contract_version missing 'patch' field")

# Check handler_routing section
descriptor = contract.get("descriptor")
if not isinstance(descriptor, dict):
errors.append("Missing or invalid 'descriptor' section")

# Check handler_routing section when present. Some current handler descriptors
# are capability-only contracts and intentionally do not declare runtime
# routing here.
handler_routing = contract.get("handler_routing", {})
if not handler_routing:
errors.append("Missing 'handler_routing' section")
else:
if handler_routing:
handlers = handler_routing.get("handlers", [])
if not handlers:
errors.append("No handlers defined in handler_routing")
Expand Down Expand Up @@ -171,9 +142,12 @@ def validate_contract(
"payload_type_match",
"first_match",
"all_match",
"operation_match",
}:
if strict:
errors.append(f"Unknown routing_strategy: '{routing_strategy}'")
elif not contract.get("capability_outputs"):
errors.append("Missing 'handler_routing' section or 'capability_outputs'")

# Check operation_bindings (optional but validate if present)
operation_bindings = contract.get("operation_bindings")
Expand Down Expand Up @@ -215,17 +189,26 @@ def validate_all_contracts(
Returns:
Tuple of (validated_count, failed_count, list of (handler_type, error)).
"""
base_path = Path(__file__).parent.parent / "src" / "omnibase_infra"
repo_root = Path(__file__).parent.parent
contracts_root = repo_root / "src" / "omnibase_infra" / "contracts" / "handlers"
contract_paths = sorted(contracts_root.glob("*/handler_contract.yaml"))

total_errors: list[tuple[str, str]] = []
validated = 0
failed = 0

for handler_type, description in EXPECTED_HANDLERS.items():
contract_rel_path = HANDLER_CONTRACT_PATHS[handler_type]
contract_path = base_path / contract_rel_path
if not contract_paths:
return (
0,
1,
[("contracts", f"No handler contracts found under {contracts_root}")],
)

for contract_path in contract_paths:
handler_type = contract_path.parent.name
contract_rel_path = contract_path.relative_to(repo_root)

print(f"Validating: {handler_type} ({description})")
print(f"Validating: {handler_type}")
print(f" Path: {contract_rel_path}")

errors = validate_contract(
Expand Down Expand Up @@ -286,7 +269,7 @@ def main() -> int:
print("Handler Contract Validation")
print("=" * 60)
print()
print(f"Validating {len(EXPECTED_HANDLERS)} handlers from _KNOWN_HANDLERS")
print("Validating handler contracts from src/omnibase_infra/contracts/handlers")
print()

validated, failed, total_errors = validate_all_contracts(
Expand All @@ -298,30 +281,25 @@ def main() -> int:
print("=" * 60)
print("Summary")
print("=" * 60)
print(f" Validated: {validated}/{len(EXPECTED_HANDLERS)}")
print(f" Failed: {failed}/{len(EXPECTED_HANDLERS)}")
total = validated + failed
print(f" Validated: {validated}/{total}")
print(f" Failed: {failed}/{total}")

if total_errors:
print()
print("Errors:")
for handler_type, error in total_errors:
print(f" [{handler_type}] {error}")
print()
print("VALIDATION FAILED - Do NOT delete _KNOWN_HANDLERS yet!")
print("VALIDATION FAILED - handler contract descriptors are invalid")
print()
print("Next steps:")
print(" 1. Create missing contract.yaml files for failed handlers")
print(" 1. Fix the failed handler_contract.yaml descriptors")
print(" 2. Re-run this validation script")
print(" 3. Once all handlers pass, proceed with migration")
return 1

print()
print("ALL HANDLERS VALIDATED - Safe to proceed with migration")
print()
print("Next steps:")
print(" 1. Remove _KNOWN_HANDLERS dict from util_wiring.py")
print(" 2. Update wire_default_handlers() to use contract-driven loading")
print(" 3. Run full test suite to verify migration")
print("ALL HANDLER CONTRACTS VALIDATED")
return 0


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,22 @@
# records action NONE with the reason. Assessment gains the typed
# corroboration facts (github_status / github_busy / docker_status). Still no
# executor and no fleet mutation -- recommendations only.
# - OMN-15195: removed the env-read form of the three classification
# thresholds (CRASHLOOP_RESTART_THRESHOLD, RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS,
# WEDGE_QUEUE_AGE_SECONDS). They are now plain module constants (5 / 4500 /
# 600), not overlay-resolved env reads -- nothing constructed this handler
# with overrides, so no default value changed. This closes the last
# env-overridable surface on this node; see OMN-15234's per-name
# check-env-reads grandfathering for the companion enforcement change.
name: "node_runner_fleet_health_compute"
contract_version:
major: 1
minor: 1
patch: 0
node_version: "1.1.0"
patch: 1
node_version: "1.1.1"
node_type: "COMPUTE_GENERIC"
description: >
Pure, deterministic classifier of a runner-fleet snapshot into a typed health verdict: composite per-runner readiness over six signals, the quarantine set and its bounce-eligible subset, precedence health state, fleet aggregates, and recorded (never executed) recommended actions. Uses the canonical def-B typed ModelRunnerFleetHealthEvaluateCommand handler entrypoint. Heartbeat-staleness classification threshold (RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS, env-overridable) defaults to 4500s as of OMN-15233 (1.0.2), raised from 900s. 900s sat below the ~50-minute IDLE _diag write cadence, so this classifier emitted LISTENER_ZOMBIE -> RESTART_RUNNER at confidence 0.85 for idle-but-healthy runners. The default is held identical to docker/runners/healthcheck.sh and runner-monitor.sh so all three surfaces agree on what "stale" means. OMN-15234 (1.1.0) makes the restart recommendation composite rather than flag-driven: a lone stale heartbeat on an otherwise online, idle, running, zero-restart runner is not bounce-eligible. The per-runner assessment carries the typed corroboration facts it was derived from (github_status, github_busy, docker_status) alongside the existing re-arm signals.
Pure, deterministic classifier of a runner-fleet snapshot into a typed health verdict: composite per-runner readiness over six signals, the quarantine set and its bounce-eligible subset, precedence health state, fleet aggregates, and recorded (never executed) recommended actions. Uses the canonical def-B typed ModelRunnerFleetHealthEvaluateCommand handler entrypoint. Heartbeat-staleness classification threshold (RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS) is 4500s as of OMN-15233, raised from 900s; as of OMN-15195 this is a fixed module constant, not env-overridable. 900s sat below the ~50-minute IDLE _diag write cadence, so this classifier emitted LISTENER_ZOMBIE -> RESTART_RUNNER at confidence 0.85 for idle-but-healthy runners. The value is held identical to docker/runners/healthcheck.sh and runner-monitor.sh so all three surfaces agree on what "stale" means. OMN-15234 (1.1.0) makes the restart recommendation composite rather than flag-driven: a lone stale heartbeat on an otherwise online, idle, running, zero-restart runner is not bounce-eligible. The per-runner assessment carries the typed corroboration facts it was derived from (github_status, github_busy, docker_status) alongside the existing re-arm signals.

input_model:
name: "ModelRunnerFleetHealthEvaluateCommand"
Expand Down Expand Up @@ -105,8 +112,8 @@ metadata:
author: "OmniNode Team"
license: "MIT"
created: "2026-07-04"
updated: "2026-07-27"
ticket: "OMN-15255"
updated: "2026-07-26"
ticket: "OMN-15195"
tags:
- compute
- def-b
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
from __future__ import annotations

import logging
import os
from datetime import UTC, datetime

from omnibase_infra.enums import EnumHandlerType, EnumHandlerTypeCategory
Expand Down Expand Up @@ -105,20 +104,22 @@

logger = logging.getLogger(__name__)

# Same env-overridable defaults as the EFFECT + the legacy bash surfaces
# (runner-monitor.sh, healthcheck.sh) so all three surfaces agree on
# thresholds during the trust-building period (OMN-13109/OMN-13912/OMN-13915/OMN-15233).
_CRASHLOOP_RESTART_THRESHOLD = int(os.environ.get("CRASHLOOP_RESTART_THRESHOLD", "5"))
# Same defaults as the EFFECT + the legacy bash surfaces (runner-monitor.sh,
# healthcheck.sh) so all three surfaces agree on thresholds during the
# trust-building period (OMN-13109/OMN-13912/OMN-13915/OMN-15233).
#
# OMN-15195 removed the env-read form of these three thresholds: they are
# plain constants, not overlay-resolved reads. Nothing constructs this handler
# with overrides, so the values are unchanged by the removal.
_CRASHLOOP_RESTART_THRESHOLD = 5
# 4500s (75 min) matches docker/runners/healthcheck.sh exactly (OMN-15233). An
# IDLE runner writes _diag only on its ~50-minute OAuth/AAD token refresh, so
# the previous 900s default sat below the healthy idle cadence and classified
# idle-but-healthy runners as LISTENER_ZOMBIE for ~35 of every 50 minutes.
# Keep this value equal to healthcheck.sh's -- a divergence means the node
# verdict and the container healthcheck disagree about what "stale" means.
_RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS = int(
os.environ.get("RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS", "4500")
)
_WEDGE_QUEUE_AGE_SECONDS = int(os.environ.get("WEDGE_QUEUE_AGE_SECONDS", "600"))
_RUNNER_HEALTH_MAX_DIAG_AGE_SECONDS = 4500
_WEDGE_QUEUE_AGE_SECONDS = 600
# OMN-15255: ceiling on runner-host disk usage. A host past this is out of
# space for checkouts/caches; every container on it is unfit for work even
# though each one still registers online and reports a fresh heartbeat.
Expand Down
Loading
Loading