mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
Manifest keeps a legacy family shim; telemetry wire fields unchanged. No behavior change; spec in ocw-context/docs/workspace-scratch-design.md.
164 lines
7.2 KiB
Python
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
|