diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03d4858a1f..16d9e3bb3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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). @@ -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 @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b76385bc9..1065321ae3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh b/plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh new file mode 100755 index 0000000000..c5e2c25133 --- /dev/null +++ b/plugins/onex/hooks/scripts/grep_guard_no_polymorphic_agent.sh @@ -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 "$@" diff --git a/tests/unit/hooks/test_no_polymorphic_agent_references.py b/tests/unit/hooks/test_no_polymorphic_agent_references.py new file mode 100644 index 0000000000..90c13d6f9a --- /dev/null +++ b/tests/unit/hooks/test_no_polymorphic_agent_references.py @@ -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/ -> 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"]))