Files
2026-07-13 13:12:00 +08:00

114 lines
4.9 KiB
Python

"""Structural test for the Sentinel security-review bundle (examples/sentinel).
Sentinel is a report-only security-review orchestrator: it collects audit scope,
delegates read-only code investigation to a ``scanner`` sub-agent (claude-sdk),
and can route the draft through an independent ``reviewer`` sub-agent (codex).
Pure spec-load — no LLM, no credentials — modeled on ``test_example_scribe.py``.
What breaks if this fails:
- a sub-agent is dropped or renamed (Sentinel loses scanning or fact-checking),
- the reviewer collapses onto claude-sdk (the cross-vendor check stops being
independent),
- a sub-agent silently pins a model (re-coupling it to one provider),
- the ``security-audit`` skill is dropped or renamed,
- the report-only purpose guard starts allowing ``implement`` dispatches,
- the ``read_only_os`` write-deny policy is dropped from the orchestrator or a
sub-agent (report-only stops being enforced at the policy layer).
"""
from __future__ import annotations
import importlib
from pathlib import Path
import pytest
from omnigent.spec import load
from omnigent.spec.types import AgentSpec
# tests/e2e/omnigent/test_example_sentinel.py -> repo root is 3 parents up.
_SENTINEL_BUNDLE = Path(__file__).resolve().parents[3] / "examples" / "sentinel"
@pytest.fixture(scope="module")
def sentinel_spec() -> AgentSpec:
"""Load and validate the sentinel bundle once for the module."""
return load(_SENTINEL_BUNDLE)
def test_sentinel_name_and_subagents(sentinel_spec: AgentSpec) -> None: # DoC-1, DoC-2
assert sentinel_spec.name == "sentinel"
fam = {a.name: a.executor.config.get("harness") for a in sentinel_spec.sub_agents}
assert sorted(sentinel_spec.tools.agents) == ["reviewer", "scanner"]
assert fam["scanner"] == "claude-sdk"
assert fam["reviewer"] == "codex"
assert fam["scanner"] != fam["reviewer"]
def test_sentinel_sub_agents_are_unpinned(sentinel_spec: AgentSpec) -> None: # DoC-3
by_name = {a.name: a for a in sentinel_spec.sub_agents}
for name in ("scanner", "reviewer"):
assert by_name[name].executor.model is None, name
assert by_name[name].executor.profile is None, name
def test_sentinel_skills_present(sentinel_spec: AgentSpec) -> None: # DoC-4
assert sorted(s.name for s in sentinel_spec.skills) == ["security-audit"]
def test_sentinel_has_os_env(sentinel_spec: AgentSpec) -> None: # DoC-9
assert sentinel_spec.os_env is not None
assert sentinel_spec.os_env.type == "caller_process"
# Sandboxed by default: the config omits ``sandbox`` so the runtime resolves
# the platform backend (linux_bwrap / darwin_seatbelt, binding cwd
# read-only). An unset spec-level sandbox means "use the platform default",
# NOT unsandboxed -- ``type: none`` is the explicit opt-out, which Sentinel
# deliberately does not use because it reviews untrusted code.
assert sentinel_spec.os_env.sandbox is None
def test_sentinel_orchestrator_guardrails(sentinel_spec: AgentSpec) -> None: # DoC-5
names = {p.name for p in sentinel_spec.guardrails.policies}
assert {"blast_radius", "read_only_os", "headless_subagent_purpose_guard"} <= names
guard = next(
p for p in sentinel_spec.guardrails.policies if p.name == "headless_subagent_purpose_guard"
)
allowed = guard.function.arguments["allowed_purposes"]
assert "implement" not in allowed
assert sorted(allowed) == ["explore", "review", "search"]
def test_sentinel_policy_paths_importable(sentinel_spec: AgentSpec) -> None: # DoC-6
for agent in (sentinel_spec, *sentinel_spec.sub_agents):
for policy in agent.guardrails.policies:
mod, _, fn = policy.function.path.rpartition(".")
assert hasattr(importlib.import_module(mod), fn), policy.function.path
def test_sentinel_function_policies_have_nonempty_arguments(
sentinel_spec: AgentSpec,
) -> None: # DoC-7
# ``read_only_os`` is intentionally argument-free (its deny set is fixed), so
# it is exempt; every other policy must carry the arguments it needs.
for agent in (sentinel_spec, *sentinel_spec.sub_agents):
for policy in agent.guardrails.policies:
if policy.name == "read_only_os":
assert policy.function.arguments is None, (agent, policy.name)
continue
assert policy.function.arguments, (agent, policy.name)
def test_sentinel_subagent_guardrails(sentinel_spec: AgentSpec) -> None: # DoC-8
for agent in sentinel_spec.sub_agents:
names = {p.name for p in agent.guardrails.policies}
assert {"blast_radius", "read_only_os"} <= names, agent.name
def test_sentinel_findings_template_present(sentinel_spec: AgentSpec) -> None: # DoC-10
tokens = ["Critical", "High", "Medium", "Low", "Info"]
prompt = sentinel_spec.instructions
skill = (_SENTINEL_BUNDLE / "skills" / "security-audit" / "SKILL.md").read_text("utf-8")
for token in tokens:
assert token in prompt, ("prompt", token)
assert token in skill, ("skill", token)