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
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2536,6 +2536,25 @@ jobs:
working-directory: ${{ github.workspace }}
run: bash scripts/check-skill-monorepo-refs.sh

# Regression guard (OMN-9143): block reintroduction of the retired
# `polymorphic-agent` dispatch target in the omniclaude enforcement surface.
no-polymorphic-agent:
name: "Arch: No polymorphic-agent in enforcement paths (OMN-9143)"
# OMNI_RUNNER_SELECTOR_V1 - trusted CI defaults to self-hosted omnibase-ci; public forks default to ubuntu-latest
runs-on: >-
${{
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.repository)
&& fromJSON(vars.OMNI_PUBLIC_PR_RUNS_ON_JSON || '["ubuntu-latest"]')
|| fromJSON(vars.OMNI_TRUSTED_CI_RUNS_ON_JSON || '["self-hosted","omnibase-ci"]')
}}
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: Reject legacy polymorphic-agent dispatch target in enforcement paths
working-directory: ${{ github.workspace }}
run: bash plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh

# ============================================================================
# GATE AGGREGATORS
# Gate names are API-stable per CI/CD Standards (Invariant 1).
Expand Down Expand Up @@ -2601,6 +2620,8 @@ jobs:
- agent-contract-validation
# Skill monorepo-ref lint gate (OMN-8795 SD-08)
- skill-monorepo-ref-gate
# Polymorphic-agent regression guard (OMN-9143)
- no-polymorphic-agent
if: always()
steps:
- name: Check quality results
Expand Down Expand Up @@ -2644,7 +2665,8 @@ jobs:
"golden-chain-integrity=${{ needs.golden-chain-integrity.result }}" \
"aislop-sweep=${{ needs.aislop-sweep.result }}" \
"agent-contract-validation=${{ needs.agent-contract-validation.result }}" \
"skill-monorepo-ref-gate=${{ needs.skill-monorepo-ref-gate.result }}"; do
"skill-monorepo-ref-gate=${{ needs.skill-monorepo-ref-gate.result }}" \
"no-polymorphic-agent=${{ needs.no-polymorphic-agent.result }}"; do
NAME="${check%%=*}"
RESULT="${check##*=}"
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
Expand Down
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,21 @@ repos:
pass_filenames: true
stages: [pre-commit]

# OMN-9143: regression guard — the legacy `polymorphic-agent` dispatch
# target was retired in favour of `onex:`-prefixed subagents. Block its
# reintroduction anywhere in the omniclaude enforcement surface
# (plugins/onex/hooks/scripts, .../lib, hooks.json). Historical
# observability SQL and routing-fallback tests outside that surface are
# out of scope and intentionally not matched. Blocking — enforcement,
# not advisory (CLAUDE.md Rule #5).
- id: no-polymorphic-agent
name: Reject legacy polymorphic-agent dispatch target in enforcement paths (OMN-9143)
entry: plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh --
language: script
files: '^plugins/onex/hooks/(scripts|lib)/.*|^plugins/onex/hooks/hooks\.json$'
pass_filenames: true
stages: [pre-commit]

# OMN-13341 (R6, epic OMN-13325): reject worker prompts / receipts /
# handoff & evidence docs that cite a local-clone path, ticket text, or a
# statusCheckRollup verdict AS proof of state. Verify against origin/dev
Expand Down
111 changes: 111 additions & 0 deletions plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
#
# Regression guard (OMN-9143): block reintroduction of the legacy
# `polymorphic-agent` dispatch target in omniclaude enforcement code paths.
#
# The canonical dispatch namespace is `onex:`-prefixed subagents (with
# `general-purpose` allowed unprefixed). The pre-`onex:` `polymorphic-agent`
# routing target was scrubbed from every enforcement hook; this guard fails
# closed if it ever reappears in the enforcement surface.
#
# SCOPE (enforcement-critical only):
# - plugins/onex/hooks/scripts/**
# - plugins/onex/hooks/lib/**
# - plugins/onex/hooks/hooks.json
#
# Out of scope on purpose (historical/diagnostic, NOT enforcement):
# - scripts/observability/** routing metrics SQL that measures the legacy
# agent's transformation rate against historical event rows
# - tests/lib/core/** routing-fallback tests that assert the documented
# low-confidence fallback behaviour
#
# Usage:
# grep_guard_no_polymorphic_agent.sh # scan the enforcement tree
# grep_guard_no_polymorphic_agent.sh -- FILE [FILE] # scan only given files (pre-commit)
#
# Exit 0 = clean, exit 1 = offender found.
set -euo pipefail

# Repo root: this script lives at plugins/onex/hooks/scripts/, four levels deep.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"

# Enforcement surface scanned in full-tree mode.
ENFORCEMENT_GLOBS=(
"plugins/onex/hooks/scripts"
"plugins/onex/hooks/lib"
"plugins/onex/hooks/hooks.json"
)

# Forbidden token: both the kebab-case dispatch target and the snake_case form.
PATTERN='polymorphic[-_]agent'

# This guard script names the token in its own header; allowlist itself so it
# does not flag on its own documentation.
SELF_BASENAME="$(basename "${BASH_SOURCE[0]}")"

is_allowlisted() {
local f="$1"
[[ "$(basename "$f")" == "$SELF_BASENAME" ]]
}

scan_files() {
local rc=0
local f
for f in "$@"; do
[[ -z "$f" ]] && continue
[[ -f "$f" ]] || continue
is_allowlisted "$f" && continue
if grep -E -n -- "$PATTERN" "$f" >/dev/null 2>&1; then
echo "FORBIDDEN: legacy 'polymorphic-agent' dispatch target in enforcement path: $f"
grep -E -n -- "$PATTERN" "$f" | sed 's/^/ /'
rc=1
fi
done
return "$rc"
}

main() {
local files=()
if [[ "${1:-}" == "--" ]]; then
shift
# Pre-commit mode: only scan passed files that fall inside the enforcement surface.
local f keep
for f in "$@"; do
keep=0
for g in "${ENFORCEMENT_GLOBS[@]}"; do
case "$f" in
"$g"|"$g"/*) keep=1; break ;;
esac
done
[[ "$keep" == 1 ]] && files+=("$f")
done
[[ "${#files[@]}" -eq 0 ]] && exit 0
else
# Full-tree mode: enumerate the enforcement surface.
local g
for g in "${ENFORCEMENT_GLOBS[@]}"; do
local target="${REPO_ROOT}/${g}"
if [[ -d "$target" ]]; then
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$target" -type f -print0)
elif [[ -f "$target" ]]; then
files+=("$target")
fi
done
fi

if scan_files "${files[@]+"${files[@]}"}"; then
echo "OK: no 'polymorphic-agent' references in enforcement paths"
exit 0
else
echo ""
echo "The 'polymorphic-agent' dispatch target was retired (OMN-9143)."
echo "Use an 'onex:'-prefixed subagent (or 'general-purpose') instead."
exit 1
fi
}

main "$@"
124 changes: 124 additions & 0 deletions tests/unit/hooks/test_no_polymorphic_agent_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""Regression test (OMN-9143): no `polymorphic-agent` in enforcement paths.

The legacy `polymorphic-agent` dispatch target was retired in favour of
`onex:`-prefixed subagents (with `general-purpose` allowed unprefixed). This
test fails closed if the token reappears anywhere in the omniclaude hook
enforcement surface, and verifies that the companion grep guard agrees.

Scope is the enforcement surface only — historical observability SQL and
routing-fallback tests outside `plugins/onex/hooks/` legitimately reference the
retired agent name and are intentionally not scanned here.
"""

from __future__ import annotations

import re
import subprocess
from pathlib import Path

import pytest

# tests/unit/hooks/<this file> -> repo root is three parents up.
REPO_ROOT = Path(__file__).resolve().parents[3]

ENFORCEMENT_ROOTS: tuple[Path, ...] = (
REPO_ROOT / "plugins" / "onex" / "hooks" / "scripts",
REPO_ROOT / "plugins" / "onex" / "hooks" / "lib",
)
ENFORCEMENT_FILES: tuple[Path, ...] = (
REPO_ROOT / "plugins" / "onex" / "hooks" / "hooks.json",
)

GUARD_SCRIPT = (
REPO_ROOT
/ "plugins"
/ "onex"
/ "hooks"
/ "scripts"
/ "grep_guard_no_polymorphic_agent.sh"
)

# kebab-case dispatch target and the snake_case form.
FORBIDDEN = re.compile(r"polymorphic[-_]agent")


def _enforcement_files() -> list[Path]:
files: list[Path] = []
for root in ENFORCEMENT_ROOTS:
if root.is_dir():
files.extend(p for p in root.rglob("*") if p.is_file())
files.extend(p for p in ENFORCEMENT_FILES if p.is_file())
return files


def test_enforcement_surface_exists() -> None:
"""Sanity: the enforcement directories we scan are actually present."""
assert any(r.is_dir() for r in ENFORCEMENT_ROOTS), (
"no enforcement hook directories found — scan scope is wrong"
)


def test_no_polymorphic_agent_in_enforcement_paths() -> None:
"""No enforcement-path file may reference the retired dispatch target."""
offenders: list[str] = []
for path in _enforcement_files():
# The guard script documents the token in its own header; skip it.
if path.name == GUARD_SCRIPT.name:
continue
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
for lineno, line in enumerate(text.splitlines(), start=1):
if FORBIDDEN.search(line):
rel = path.relative_to(REPO_ROOT)
offenders.append(f"{rel}:{lineno}: {line.strip()}")

assert not offenders, (
"legacy 'polymorphic-agent' dispatch target found in enforcement "
"paths (retired in OMN-9143; use an 'onex:'-prefixed subagent):\n"
+ "\n".join(offenders)
)


def test_grep_guard_script_passes() -> None:
"""The companion grep guard must agree (exit 0) on the current tree."""
assert GUARD_SCRIPT.is_file(), f"guard script missing: {GUARD_SCRIPT}"
result = subprocess.run(
["bash", str(GUARD_SCRIPT)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, (
f"grep guard failed (exit {result.returncode}):\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)


def test_grep_guard_detects_injected_offender(tmp_path: Path) -> None:
"""The grep guard must fail closed on an injected enforcement-path offender."""
offender = ENFORCEMENT_ROOTS[0] / "_test_tmp_polymorphic_offender.sh"
offender.write_text("#!/bin/bash\necho polymorphic-agent\n", encoding="utf-8")
try:
result = subprocess.run(
["bash", str(GUARD_SCRIPT)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
finally:
offender.unlink(missing_ok=True)
assert result.returncode == 1, (
"grep guard did not fail on an injected enforcement-path offender; "
f"exit={result.returncode}\nstdout:\n{result.stdout}"
)


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-v"]))
Loading