Files
clawmates/templates/workflows/security_hardening.toml
T
Omar SobhandClaude Opus 5 ceec0423ad feat(teams): staff research phases with a research team
`research_only` is repo-less, one research phase, "produce a markdown
artifact" — and it defaulted to `rust_sdlc`. So it was staffed with a
planner, a coder, a tester, a reviewer and a committer, four of whom had
nothing to do, each carrying the code-and-commit skills its role is bound
to. Measured 2026-08-21: 9 distinct skills across 5 role prompts, ~50KB,
one applicable. That is what "most skills score not_applicable" in the
Skill-Use baseline has been measuring all along — the skills were
correctly bound to their roles; the roles were wrong for the workflow.

None of the three existing research templates fit, so this adds
`topic_research`: frame the brief into answerable questions, gather
evidence with the URL and the quoted passage, check every claim against
its source, write the report. Three roles, four skills, each checked
against its own `when_to_use` before binding — and two obvious candidates
deliberately NOT bound, because `executive-summary-writing` tells the
writer to discard any item not tied to a named project and
`signal-to-noise-ranking` scores relevance the same way. On a standalone
topic report that discards the deliverable.

`default_phase_teams` lets a recipe staff each phase PURPOSE separately,
resolved into `config.phase_teams` at create. A multi-phase recipe does
not have one job: `research_and_code`'s research phase spends a paragraph
of `task` telling its team not to change source files, because
`rust_sdlc` gave that phase a coder and a committer and they did what
coders do — mission 01a00c57 shipped both INT items during RESEARCH and
the coding phase then delivered +0/-0. Prose was the only lever
available; staffing is the actual one.

Also fixed in the three existing research templates, all verified rather
than inferred:

  - `papers_research` bound `arxiv-daily` to its DOMAIN SCOUT. That
    skill's entire content is "Do not search arXiv yourself — the harvest
    already ran", and its `when_to_use` names Continuous Research
    missions, which are the only ones the platform writes a harvest
    manifest for. The role whose job is searching was bound a skill
    forbidding it.
  - Its PAPER READER was told to "fetch the PDF, extract text". The
    runtime image has no pdftotext, no mutool and no pypdf — checked in
    the container. Every paper would have hit the `[read: abstract only]`
    fallback, which reads identically to the fallback working as designed.
  - `insight_research` cross-referenced "our repos'" history. A mission
    binds ONE repo (`missions.repo_id`).
  - `codebase_research` wrote to "the Obsidian vault"; no vault is
    mounted, and both it and `papers_research` were committing in "PRs",
    which the platform does not open.

And `research_only` itself had neither `task` nor `done_when` — the same
defect `benchmark`, `security_hardening` and `research_and_code` were each
fixed for, and it was left out. A phase with no `done_when` is never
judged. It also still asked for `pdf`, a format nothing generates.

Two new guards, both negative-controlled: every team a recipe names must
exist (a typo currently only logs, and the mission is staffed by the
fallback crew looking deliberate), and every `default_phase_teams` key
must be a purpose `purposes_for` actually emits.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
2026-08-21 09:35:33 -07:00

133 lines
6.7 KiB
TOML

key = "security_hardening"
title = "Security Hardening"
blurb = "Scan the repo for vulnerabilities, research patches, then apply + verify."
requires_repo = true
default_team_template = "rust_sdlc"
# The middle phase turns findings into a patch strategy — reading, judging and
# writing, not coding. It gets the research team; the scan and the fix keep the
# SDLC crew, which is the right shape for both.
[default_phase_teams]
research = "topic_research"
security = "rust_sdlc"
coding = "rust_sdlc"
# ── What is real here, and what is decoration ────────────────────────
#
# The scanners themselves are REAL: `gitleaks`, `trivy`, `semgrep` and
# `cargo-audit` are installed in the runtime image and `runtime_preflight`
# probes for all four at boot, naming the consequence when one is absent. An
# agent in a security_scan phase can run them from Bash today.
#
# `tools` is REAL too: `security_scan::run` reads it to pick which scanners to
# run and upserts each finding as a mission_task. What was missing was anything
# that FIRED it — it was reachable only from an operator button, so this
# workflow's scan phase never scanned. `phase_runner::scan_finished_security_phases`
# now runs it when the phase finishes, guarded on a completion marker so a
# clean repo is not rescanned forever.
#
# What remains decoration is annotated inline below. And the part that actually
# mattered: no phase carried a `task` or a `done_when`.
# A phase with no `done_when` never enters `evaluating`, is never judged, and
# reports `completed` whatever it did. So this recipe could run all three
# phases, scan nothing, patch nothing, and go green. That is the same defect
# `research_and_code.toml` was fixed for, and it is why the tasks below name
# the scanners explicitly rather than trusting `tools` to deliver them.
[[phases]]
kind = "security_scan"
order_idx = 0
[phases.config]
# READ by `security_scan::run` — this list gates which scanners the platform
# runs against the checkout after the phase finishes. The agent ALSO runs them
# itself during the phase (see the task): the platform pass is the independent
# record, the agent pass is what lets it write a report about what it found.
tools = ["cargo_audit", "gitleaks", "trivy_fs", "semgrep"]
produces = ["md"]
task = """
Scan this repository for security problems and write findings down.
Run the scanners that are installed in your container — `cargo audit`, \
`gitleaks detect`, `trivy fs .` and `semgrep --config auto` — from the repo \
root. If one is missing or errors, say so explicitly in the report rather than \
omitting it: a section absent because a tool failed reads identically to a \
section absent because nothing was found, and those are opposite conclusions.
Write SECURITY-FINDINGS.md with one entry per finding: the identifier \
(CVE / RUSTSEC / rule id), the file and line, what an attacker could actually \
do with it, and whether it is reachable in our code or sits in an unused \
dependency path. Rank by exploitability, not by the scanner's severity field.
A clean scan is a real and useful result. Say which tools ran, on what, and \
that they found nothing — do not manufacture findings to fill the report.
"""
done_when = "SECURITY-FINDINGS.md exists and states, for each of the four scanners, whether it ran and what it found, with every reported finding carrying an identifier, a location, and a reachability judgement"
max_iterations = 2
commit_policy = "always"
[[phases]]
kind = "research"
order_idx = 1
[phases.config]
# `pdf` dropped: PDF rendering was removed from the delivery path (artifacts are
# served as Markdown), so asking for it produced a format nothing generates.
produces = ["md"]
default_topology = "hub_spoke"
# NOTE: `input_from_phase` is INERT — DECLARED_BUT_UNREAD. Phases do not receive
# a structured hand-off from a named predecessor; the next phase reads the
# previous phase's committed FILES out of the shared checkout. That is why the
# task names SECURITY-FINDINGS.md by path.
input_from_phase = "security_scan"
task = """
Turn the scan findings into a patch strategy.
Read SECURITY-FINDINGS.md from the repo root — that is the previous phase's \
output, committed to this mission's branch. For each finding that is actually \
reachable, write into SECURITY-PLAN.md: the fix, the specific file and \
function it touches, what could break, and how the fix will be verified.
Where the fix is a dependency bump, check what the new version changes — a \
major bump presented as "update the version" is how a security patch becomes \
an outage. Where a finding is not worth fixing, say so and say why; an \
unreachable advisory in a dev-dependency is a legitimate "no action".
"""
done_when = "SECURITY-PLAN.md exists and gives, for every reachable finding in SECURITY-FINDINGS.md, either a named fix with the file it touches and how it will be verified, or an explicit justification for taking no action"
max_iterations = 2
commit_policy = "always"
[[phases]]
kind = "coding"
order_idx = 2
[phases.config]
# NOTE: `loop` is INERT — DECLARED_BUT_UNREAD ("phase iteration uses
# max_iterations + done_when"). Kept so the intent stays visible beside the two
# keys that actually drive the loop.
loop = "until_all_findings_closed"
max_iterations = 3
# Security requires reviewer approval on top of green tests.
commit_policy = "on_reviewer_approval"
# NOTE: `mcp_bundles` is INERT AT PHASE LEVEL — bundles come from the TEAM
# template (`mission_orchestrator` binds `template.mcp_bundles`). This phase
# gets nothing from this line. `gitea_forge` and `security_scan` were removed
# from it entirely: neither is defined anywhere, and now that provision_claw
# HONOURS the team template's bundle list, naming a bundle that does not exist
# stopped being harmlessly inert.
mcp_bundles = ["clawmates_door", "clawmates_skills"]
task = """
Apply the patch strategy and prove it worked.
Work through SECURITY-PLAN.md. Make the smallest change that closes each \
finding, and run the test suite after each one so a regression is attributable \
to a single fix rather than to the batch.
Then RE-RUN the scanner that produced each finding and record the new output in \
SECURITY-FINDINGS.md under a "after remediation" heading. A fix that was not \
re-scanned is a claim, not a result — and this phase's whole value is the \
difference between those two.
If a fix cannot be made safely, leave the finding open and say why. An open \
finding that is documented is worth more than a closed one that is not true.
"""
done_when = "every finding in SECURITY-PLAN.md marked for fixing is either closed with a re-run of the scanner that found it recorded in SECURITY-FINDINGS.md, or left open with a stated reason, and the test suite passes"