Files
openworker/tests/test_security_bundles.py
T
Rohit C Prasad 1299760400 Personas: requires_folder/subagents/scheduling traits replace family and the workspace enum
Manifest keeps a legacy family shim; telemetry wire fields unchanged.
No behavior change; spec in ocw-context/docs/workspace-scratch-design.md.
2026-08-20 21:19:56 -07:00

164 lines
7.2 KiB
Python

"""Phase C (OPE-61) — the shipped security coworker bundles.
Three builtin bundles (security, cloud-posture, dep-audit) live as self-contained dirs
(manifest.md + skills/) under personas/builtin/. Each is code-family, drives OSS
scanners via the vetted catalog only, and its skills reach only its own sessions.
"""
from __future__ import annotations
from pathlib import Path
from coworker.personas.registry import PersonaRegistry
from coworker.providers import ModelCapabilities, ProviderClient
from coworker.server.manager import SessionManager
from coworker.sessions import SessionRecord
BUNDLES = {
"security": {"semgrep-review", "secret-scan", "security-fix-pr"},
"cloud-posture": {"iac-scan", "aws-posture"},
"dep-audit": {"dependency-audit", "safe-upgrade-pr"},
}
class ScriptedProvider(ProviderClient):
def complete(self, *, model, messages, tools=None, **settings):
raise AssertionError("no turns expected")
def capabilities(self, model):
return ModelCapabilities()
def _reg(tmp_path) -> PersonaRegistry:
return PersonaRegistry(state_path=tmp_path / "personas.json")
def test_bundles_register_as_enabled_code_builtins(tmp_path):
reg = _reg(tmp_path)
for pid in BUNDLES:
entry = reg.get(pid)
assert entry is not None and entry.builtin
assert entry.requires_folder # folder pick at send, like Code
assert reg.is_enabled(pid) is True # in the picker out of the box
agent = reg.agent(pid) # catalog-expanded tools materialize
assert agent.requires_folder and agent.subagents
def test_bundle_skill_folders_match_their_manifests(tmp_path):
reg = _reg(tmp_path)
for pid, expected in BUNDLES.items():
m = reg.get(pid).manifest
assert set(m.skills) == expected
skills_dir = Path(m.source).parent / "skills"
on_disk = {p.name for p in skills_dir.iterdir() if (p / "SKILL.md").is_file()}
# Every listed skill exists; nothing ships unlisted.
assert on_disk == expected
def test_bundle_skills_stay_with_their_persona(tmp_path, monkeypatch):
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
mgr = SessionManager(workspace=tmp_path, provider=ScriptedProvider())
for i, (pid, expected) in enumerate(BUNDLES.items()):
sid = f"s{i}"
mgr.session_store.save(
SessionRecord(session_id=sid, workspace="", model="m", mode="interactive", agent=pid)
)
names = mgr.effective_skill_names(sid)
assert expected <= names
# No leakage from the sibling bundles.
others = set().union(*(v for k, v in BUNDLES.items() if k != pid))
assert not (others & names)
def test_prompts_carry_the_positioning_guardrails(tmp_path):
# "Drive scanners, never replace them" + safe-ops language is the product stance —
# a reworded manifest that drops it should fail loudly, not ship quietly.
reg = _reg(tmp_path)
for pid in BUNDLES:
prompt = reg.get(pid).manifest.system_prompt.lower()
assert "todo_write" in prompt
assert "drive" in prompt # drives scanners; value is judgment/remediation
assert "read-only" in reg.get("cloud-posture").manifest.system_prompt.lower()
assert "never print a discovered secret" in reg.get("security").manifest.system_prompt.lower()
def test_security_prompt_forbids_silently_skipping_a_check(tmp_path):
"""OPE-85, owner-hit 2026-08-13: with gitleaks unavailable the review silently dropped
its git-history secret scan — the check didn't fail, it vanished. For a security tool,
"no tool" rendering as "clean" is the worst possible outcome, so the contract lives in
the prompt and is pinned here."""
reg = _reg(tmp_path)
prompt = reg.get("security").manifest.system_prompt.lower()
assert "never silently skip" in prompt
assert "coverage" in prompt # every review reports what ran and what didn't
assert "request_tool" in prompt # asking is the first option, not skipping
def test_scanner_skills_offer_a_fallback_instead_of_stopping(tmp_path):
"""The skills used to say "if missing … and STOP", which is precisely the instruction
that produced the vanished check. A missing tool must lead to request_tool or a manual
equivalent — never to a dropped step."""
import coworker.personas as personas_pkg
root = Path(personas_pkg.__file__).parent / "builtin" / "security" / "skills"
secret_scan = (root / "secret-scan" / "SKILL.md").read_text()
semgrep = (root / "semgrep-review" / "SKILL.md").read_text()
for body in (secret_scan, semgrep):
assert "request_tool" in body
assert "STOP" not in body
# The history sweep is the check that actually went missing — it must survive without
# gitleaks, and the no-printing rule must survive the manual path too.
assert "git log -p" in secret_scan
assert "REDACTED" in secret_scan
def test_cloud_posture_drives_trivy_config_not_deprecated_tfsec(tmp_path):
"""tfsec was folded into trivy upstream and is maintenance-only; recommending it
sends request_tool (and users) after a dead tool. `trivy config` is the successor.
The only tfsec mention allowed in the bundle is the deprecation ban itself."""
import coworker.personas as personas_pkg
root = Path(personas_pkg.__file__).parent / "builtin" / "cloud-posture"
assert "tfsec" not in (root / "manifest.md").read_text()
skill = (root / "skills" / "iac-scan" / "SKILL.md").read_text()
assert "trivy config" in skill
assert "request_tool" in skill # missing scanner → ask, never a dropped scan
for line in skill.splitlines():
if "tfsec" in line:
assert "deprecated" in line, f"stray tfsec mention: {line!r}"
def test_bundles_offer_a_report_page_rather_than_assuming_one(tmp_path):
"""A long findings list is a document people re-read and share, so the bundles offer a
self-contained HTML report — but ASK first (owner call 2026-08-14). Assuming it would
burn tokens on a page nobody wanted; skipping it leaves the deliverable trapped in chat."""
reg = _reg(tmp_path)
for pid in BUNDLES:
prompt = reg.get(pid).manifest.system_prompt.lower()
assert "ask_user" in prompt, pid # opt-in, not automatic
assert "self-contained" in prompt, pid # opens anywhere, offline
assert "artifact:" in prompt, pid # linked the way the GUI can open it
# The counts ride in the question so the user chooses with the gist in hand.
assert "headline counts" in prompt, pid
def test_report_page_inherits_the_secret_and_evidence_rules(tmp_path):
"""A file gets forwarded and hosted — a value leaked there travels further than one in
chat, so the page must not become a loophole around the no-secrets rule."""
import re
def flat(pid: str) -> str:
# Prompts are hand-wrapped prose; collapse whitespace so a reflow can't
# break these assertions (or hide a deleted rule).
return re.sub(r"\s+", " ", reg.get(pid).manifest.system_prompt.lower())
reg = _reg(tmp_path)
security = flat("security")
assert "never a secret's value" in security
assert "coverage note reproduced in full" in security
for pid in ("cloud-posture", "dep-audit"):
assert "evidence per claim" in flat(pid), pid