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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
CANONICAL_FILE_SHAPE_CORE: "omnibase-core @ git+https://github.com/OmniNode-ai/omnibase_core@f50d00fc7918d0b12ccd0d3497160325e32e7ac5"
CANONICAL_FILE_SHAPE_CORE: "omnibase-core @ git+https://github.com/OmniNode-ai/omnibase_core@031148215620031436cba30619bbae67c0bb38ad"
steps:
- name: Checkout code
uses: actions/checkout@v7
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/imperative-contract-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ permissions:

jobs:
imperative-contract-guard:
# OMN-20943: the caller owns its unchanged, shrink-only allowlist.
# self-gating-ok: central guard is intentionally shared across repos; caller PRs do not modify the referenced workflow.
uses: OmniNode-ai/onex_change_control/.github/workflows/imperative-contract-guard.yml@725d2967b031d7065efd41d0751f903638202cc1
uses: OmniNode-ai/omnibase_core/.github/workflows/imperative-contract-guard.yml@031148215620031436cba30619bbae67c0bb38ad
with:
onex-change-control-ref: 725d2967b031d7065efd41d0751f903638202cc1
allowlist-path: imperative-contract-allowlist.yaml
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ repos:
# lines in a SKILL.md, and any new baseline, allowlist or suppression entry.
# Shrink-only baseline at .onex_ratchets/canonical_file_shape_baseline.txt.
# CI runs the same module at the same commit (job "Canonical File Shape
# (OMN-20304)"). The rev moved from 6363509770 to the OMN-20304 commit for
# both hooks; the lifecycle validator's source is identical at the two commits.
# (OMN-20304)"). OMN-20943 pins canonical-file-shape to the commit that reads
# the imperative guard caller's allowlist-path as a shrink-only declaration.
- repo: https://github.com/OmniNode-ai/omnibase_core
rev: f50d00fc7918d0b12ccd0d3497160325e32e7ac5 # pragma: allowlist secret
rev: 031148215620031436cba30619bbae67c0bb38ad # pragma: allowlist secret
hooks:
- id: no-noncanonical-lifecycle-classes
args: [--allowlist, .onex_ratchets/noncanonical_class_allowlist.yaml, src/omnimemory]
Expand Down
67 changes: 67 additions & 0 deletions contracts/OMN-20943.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
schema_version: "1.0.0"
ticket_id: "OMN-20943"
title: "omnimemory's imperative contract guard calls core with its own shrink-only allowlist"
summary: >-
The imperative-contract-guard caller uses omnibase_core's reusable at 031148215620031436cba30619bbae67c0bb38ad
with the caller-owned allowlist moved unchanged from the historical guard. The canonical-file-shape
hook and CI job use the same core commit to read the caller's shrink-only declaration.
is_seam_ticket: false
interface_change: false
interfaces_touched: []
requirements:
- id: "req-core-guard-caller"
statement: "The caller uses core's reusable, owns the unchanged allowlist, and retains refusal behaviour."
acceptance:
- id: "AC1"
statement: >-
AC1: omnimemory's imperative-contract-guard caller uses omnibase_core's reusable at a full sha
(688ef1735 or later on core dev) and no workflow in omnimemory references onex_change_control
for this guard -- falsifier: a test in tests/ci added by the PR parses the caller workflow and
fails at the merge base (uv run pytest on that file)
- id: "AC2"
statement: >-
AC2: omnimemory carries its own allowlist file whose entries equal, path for path, the omnimemory
entries the onex_change_control guard used at 725d2967, and the reusable's ratchet refuses an
allowlist that gains a path -- falsifier: the same test file compares the allowlist to the recorded
725d2967 entries and a planted extra path is refused by the core ratchet handler (uv run pytest
on that file)
- id: "AC3"
statement: >-
AC3: The guard still refuses what it refused before on omnimemory's tree -- falsifier: on a
lab host, the core handler run over omnimemory's tree with a planted imperative contract exits
non-zero, and over the clean tree exits 0, with the same verdicts as the onex_change_control
script at 725d2967 on both inputs, recorded on the PR body Lab: line
evidence_requirements:
- kind: "tests"
description: "Caller wiring, unchanged historical paths, shrink-only ratchet and live planted-handler
refusal."
command: "uv run pytest tests/ci/test_imperative_contract_guard_caller_omn20943.py -q -n 0"
emergency_bypass:
enabled: false
justification: ""
follow_up_ticket_id: ""
dod_evidence:
- id: "dod-omn20943-ac1-core-caller"
description: "The parsed caller pins core, retains its job/context and names its own allowlist."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/ci/test_imperative_contract_guard_caller_omn20943.py -q -n 0"
binds_ac: ["AC1"]
- id: "dod-omn20943-ac2-own-shrink-only-allowlist"
description: "The recorded paths match; core admits unchanged and shrinking lists and refuses a planted
extra path."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/ci/test_imperative_contract_guard_caller_omn20943.py -q -n 0"
binds_ac: ["AC2"]
- id: "dod-omn20943-ac3-live-guard-refusal"
description: "The core guard accepts the clean tree and refuses raw HTTP in the live retrieval handler;
pinned core/OCC parity is recorded on the Lab line."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/ci/test_imperative_contract_guard_caller_omn20943.py -q -n 0"
binds_ac: ["AC3"]
22 changes: 22 additions & 0 deletions imperative-contract-allowlist.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
allowlisted_handlers:
- path: src/omnimemory/handlers/adapters/adapter_valkey.py
violations:
- direct_db_cache_connection
ticket: OMN-12673
note: Existing live freestanding runtime adapter debt; dependency sweep must not broaden it.
- path: src/omnimemory/nodes/node_memory_retrieval_effect/adapters/adapter_embedding_client.py
violations:
- raw_http_call
ticket: OMN-12673
note: Existing live freestanding runtime adapter debt; dependency sweep must not broaden it.
- path: src/omnimemory/runtime/plugin.py
violations:
- hardcoded_private_ip
ticket: OMN-12673
note: Existing live freestanding runtime plugin debt; dependency sweep must not broaden it.
- path: src/omnimemory/topics.py
violations:
- hardcoded_topic
ticket: OMN-12673
note: Existing live topic registry literals; dependency sweep must not broaden them.
194 changes: 194 additions & 0 deletions tests/ci/test_imperative_contract_guard_caller_omn20943.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# SPDX-FileCopyrightText: 2026 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
"""OMN-20943: the caller owns the unchanged allowlist and uses core's guard.

The four recorded paths come from the guard's omnimemory allowlist at
725d2967b031d7065efd41d0751f903638202cc1. The project locks core 0.47.39,
which predates the ratchet handler, so behavioural probes use an isolated
0.47.41 validator environment without changing the application's lock.
"""

from __future__ import annotations

import json
import re
import shutil
import subprocess
from pathlib import Path

import pytest
import yaml

pytestmark = pytest.mark.unit

ROOT = Path(__file__).resolve().parents[2]
WORKFLOWS = ROOT / ".github/workflows"
CALLER = WORKFLOWS / "imperative-contract-guard.yml"
ALLOWLIST = ROOT / "imperative-contract-allowlist.yaml"
CORE_SHA_PREFIX = "031148215"
CORE_USES = "OmniNode-ai/omnibase_core/.github/workflows/imperative-contract-guard.yml"
RECORDED_PATHS = [
"src/omnimemory/handlers/adapters/adapter_valkey.py",
"src/omnimemory/nodes/node_memory_retrieval_effect/adapters/adapter_embedding_client.py",
"src/omnimemory/runtime/plugin.py",
"src/omnimemory/topics.py",
]
LIVE_HANDLER = "src/omnimemory/nodes/node_memory_retrieval_effect/handlers/handler_memory_retrieval.py"


def _mapping(path: Path) -> dict[str, object]:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
assert isinstance(data, dict)
return data


def _core(*args: str, stdin: str | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["uv", "run", "--no-project", "--with", "omnibase-core==0.47.41", *args],
input=stdin,
capture_output=True,
text=True,
check=False,
timeout=90,
cwd=ROOT,
)


def _caller_sha() -> str:
jobs = _mapping(CALLER)["jobs"]
assert isinstance(jobs, dict)
uses = jobs["imperative-contract-guard"]["uses"]
assert isinstance(uses, str)
repository, sha = uses.split("@")
assert repository == CORE_USES
assert re.fullmatch(r"[0-9a-f]{40}", sha)
assert sha.startswith(CORE_SHA_PREFIX)
return sha


def test_caller_uses_core_and_keeps_the_job_and_context() -> None:
workflow = _mapping(CALLER)
assert workflow["name"] == "Imperative Contract Guard"
jobs = workflow["jobs"]
assert isinstance(jobs, dict)
assert set(jobs) == {"imperative-contract-guard"}
job = jobs["imperative-contract-guard"]
assert job["uses"] == f"{CORE_USES}@{_caller_sha()}"
assert job["with"] == {"allowlist-path": ALLOWLIST.name}
assert workflow["permissions"] == {"contents": "read"}
for path in WORKFLOWS.iterdir():
if path.suffix in {".yml", ".yaml"}:
text = path.read_text(encoding="utf-8")
assert not re.search(
r"onex_change_control/\.github/workflows/imperative-contract-guard\.yml",
text,
), path


def test_canonical_shape_hook_and_ci_use_the_exemption_commit() -> None:
repos = _mapping(ROOT / ".pre-commit-config.yaml")["repos"]
assert isinstance(repos, list)
(hook_repo,) = [
repo
for repo in repos
if any(hook["id"] == "canonical-file-shape" for hook in repo["hooks"])
]
assert hook_repo["repo"] == "https://github.com/OmniNode-ai/omnibase_core"
assert hook_repo["rev"] == _caller_sha()
jobs = _mapping(WORKFLOWS / "ci.yml")["jobs"]
assert isinstance(jobs, dict)
assert jobs["canonical-file-shape"]["env"]["CANONICAL_FILE_SHAPE_CORE"].endswith(
f"@{_caller_sha()}"
)


def test_own_allowlist_matches_the_recorded_paths() -> None:
entries = _mapping(ALLOWLIST)["allowlisted_handlers"]
assert isinstance(entries, list)
assert [entry["path"] for entry in entries] == RECORDED_PATHS


def test_core_ratchet_accepts_unchanged_and_shrinking_but_refuses_an_extra_path() -> (
None
):
text = ALLOWLIST.read_text(encoding="utf-8")
entries = _mapping(ALLOWLIST)["allowlisted_handlers"]
assert isinstance(entries, list)
extra = "src/omnimemory/nodes/node_planted_effect/handlers/handler_planted.py"
request = {
"base": text,
"head": yaml.safe_dump({"allowlisted_handlers": [*entries, {"path": extra}]}),
"shrunk": yaml.safe_dump({"allowlisted_handlers": entries[:-1]}),
}
result = _core(
"python",
"-c",
"import json, sys; "
"from omnibase_core.handlers.handler_imperative_allowlist_ratchet import "
"HandlerImperativeAllowlistRatchet; "
"from omnibase_core.models.nodes.imperative_contract_guard."
"model_allowlist_ratchet_input import ModelAllowlistRatchetInput; "
"p=json.load(sys.stdin); h=HandlerImperativeAllowlistRatchet(); "
"reports=[h.handle(ModelAllowlistRatchetInput(base_text=p['base'], "
"head_text=p[k])) for k in ('base', 'shrunk', 'head')]; "
"print(json.dumps([dict(r.model_dump(), admitted=r.admitted) for r in reports]))",
stdin=json.dumps(request),
)
assert result.returncode == 0, result.stdout + result.stderr
unchanged, shrunk, planted = json.loads(result.stdout)
assert unchanged == {
"added": [],
"removed": [],
"bootstrap": False,
"admitted": True,
}
assert shrunk["admitted"] is True
assert shrunk["removed"] == [RECORDED_PATHS[-1]]
assert planted["admitted"] is False
assert planted["bootstrap"] is False
assert planted["added"] == [extra]


def test_core_guard_accepts_clean_tree_and_refuses_a_planted_live_handler(
tmp_path: Path,
) -> None:
# Use the caller's input; the merge-base caller cannot provide it.
jobs = _mapping(CALLER)["jobs"]
assert isinstance(jobs, dict)
assert jobs["imperative-contract-guard"]["with"]["allowlist-path"] == ALLOWLIST.name
root = tmp_path / "omnimemory"
shutil.copytree(
ROOT / "src", root / "src", ignore=shutil.ignore_patterns("__pycache__")
)
shutil.copyfile(ROOT / "pyproject.toml", root / "pyproject.toml")
shutil.copyfile(ALLOWLIST, root / ALLOWLIST.name)
args = (
"check-imperative-contracts",
"--repo-root",
str(root),
"--allowlist-path",
ALLOWLIST.name,
"--scan-freestanding",
"--json",
)
clean = _core(*args)
assert clean.returncode == 0, clean.stdout + clean.stderr
handler = root / LIVE_HANDLER
original = handler.read_text(encoding="utf-8")
# The scanner reads this source; it does not execute a network request.
handler.write_text(
original + '\nimport httpx\nhttpx.get("http://example.invalid/planted")\n',
encoding="utf-8",
)
planted = _core(*args)
assert planted.returncode == 1, planted.stdout + planted.stderr
report = json.loads(planted.stdout)[0]
assert report["new_violation_count"] > 0
(violation,) = [
violation
for violation in report["new_violations"]
if violation["handler_path"].endswith("handlers/handler_memory_retrieval.py")
]
assert violation["reachability"] == "live"
assert "HTTP" in violation["undeclared_transports"]
assert "undeclared_transport" in violation["violations"]
Loading