Skip to content

Promote the experiment probe into the package behind agentseam probe - #145

Merged
jothimani-rajendran merged 5 commits into
mainfrom
claude/armed-probe-verb
Sep 10, 2026
Merged

Promote the experiment probe into the package behind agentseam probe#145
jothimani-rajendran merged 5 commits into
mainfrom
claude/armed-probe-verb

Conversation

@jothimani-rajendran

Copy link
Copy Markdown
Collaborator

What this changes

The eight-trial probe moves out of tools/ into src/agentseam/probe/ and gets a CLI verb, so a pip install agentseam can measure what its own agent's hooks actually enforce — previously the harness existed but shipped to nobody. Adds a CI gate that replays committed recordings and fails on disagreement, and a documented basis vocabulary (witnessed / tested / recorded).

Wave 1 of armed (idea 01 of the portfolio direction); brief and contract in org-plan at plan/armed/, pinned at 83af785.

Claim check

  • No capability claim is widened without a mechanism behind it
  • Any new/changed MATRIX row carries a verified record — no matrix row is added or changed by this PR
  • Payload shapes come from a primary source — unchanged; this PR moves and packages existing measurement code, and adds no new payload shape

No row is labelled witnessed here, and no measurement is synthesized. The coverage the evidence actually supports is unchanged: seven trials, at pre_tool, for one agent. The three gaps (escalate at pre_tool, all trials at prompt_submit, all trials at stop) ship as docs/witness-skeleton.json — copy-paste commands plus a blank report skeleton — with tests/test_coverage_skeleton.py asserting the skeleton is rejected by evidence_report.validate() while still blank, and validates once genuinely filled. Only the owner's machine can fill them; this branch was built with no vendor credentials.

Checks

  • pytest -q passes — 1615 passed, 4 skipped (3.11 and 3.13)
  • ruff check . and ruff format --check . pass — All checks passed! / 147 files already formatted
  • Runtime path is still stdlib-only — dependencies = [] untouched, no new imports outside the standard library
  • Commits are signed off

Notes for the reviewer

One defect found and fixed, and it is the one worth looking at. experiment_report.render() counted disagreements, printed "the matrix and this agent do not match", and then return 0. Harmless while it was a dev script; not harmless the moment the same code becomes a shipped agentseam probe run. A verb that reports a mismatch and tells its caller it succeeded is the exact shape this probe exists to catch in other people's tooling. Reproduced against the committed recording with claude_code.events.pre_tool.block flipped — exit 0 before, exit 1 after, agreement still 0 — with both directions pinned by tests so the gate cannot start crying wolf.

Two other things were deliberately made loud rather than convenient. tools/probe_ci.py reports the checked count beside the disagreement count and prints no recordings are committed -- this is a measurement of nothing, not a pass when there is nothing to check, so an empty run cannot read as green. agentseam probe run --driver recorded with no --trial filter raises NoRecording: claude_code@2.1.263/pre_tool never recorded the 'escalate' trial rather than quietly reporting seven of eight — correct, but it means the default recorded invocation fails until escalate is witnessed. Worth a decision: fail loudly as now, or report per-trial coverage with the unrecorded ones marked absent.

Naming is open. probe was chosen over "armed" because the CLI already speaks in verbs (agents, matrix, doctor, install) — the initiative's name reads oddly as one. Owner's call.

Promotion into src/ put the harness under the strict lint baseline for the first time. That was handled by fixing rather than ignoring: modules split by activity to stay inside the line budget, explicit check=False on every subprocess.run, a complexity split in reference_agent._interpret reusing existing tests unchanged. per-file-ignores were added only for pre-existing load-bearing shapes, each with a stated reason. One real correctness bug surfaced on the way: the generated probe script assumed a ../src checkout layout to find agentseam, which is false in an installed wheel — now derived from agentseam.__file__ and verified by running a trial end to end from a built wheel in a clean venv with no source checkout on sys.path.

🤖 Generated with Claude Code


Generated by Claude Code

`tools/experiment.py` and friends measured what an agent's hooks actually
enforce, but never shipped: `packages.find` reads only `src`, so a `pip
install agentseam` user got the matrix without the means to check whether
their own machine agrees with it (armed, wave 1, T1/T2).

Moves the harness verbatim into `src/agentseam/probe/` (experiment,
experiment_probe, experiment_driver, experiment_escalate, experiment_report,
recorded_driver, reference_agent) with only import paths and a `_PACKAGE_ROOT`
derivation changed -- the latter because the rendered subprocess probe can no
longer assume a "../src" checkout layout once it runs from a wheel. Verified
against a built wheel in a clean venv with no source checkout on sys.path.

`tools/*.py` at the old paths become thin shims (`from
agentseam.probe.X import name as name`, including the underscore-prefixed
names the test suite reaches into directly) so nothing imports a second copy
of the harness. `main()`'s own argparse CLI split out to experiment_cli.py to
keep experiment.py under the 300-line review budget.

Adds the `probe` verb (`agentseam probe list` / `run`), wired in
src/agentseam/probe/cli.py per cli.py's own `add_parser` + `set_defaults(fn=)`
idiom, sharing the identical engine tools/experiment.py's own CLI calls --
`probe` was chosen over "armed" itself since the CLI already speaks in verbs
(agents, matrix, doctor, install), and the direction doc's project name reads
oddly as a verb.

Promoting into src/ puts the harness under the strict lint baseline for the
first time; fixes review-budget line splits, explicit `check=False` on every
subprocess.run, and a G1/G2 helper split in reference_agent's `_interpret`
(same logic, lower complexity), plus per-file-ignores for the print-based
CLI output and pre-existing, widely-tested exception names (NoRecording,
Undocumented) that would otherwise churn every caller for no behavioural
gain. `permissionDecisionReason` crossing into src/ for the first time also
crossed the literal-duplication threshold against the two adapters that
already name it; allowlisted with a reason (three independent, deliberately
un-shared readers of the same wire-protocol key, not an accidental repeat).

Also adds `agentseam.recordings.agents()` (every agent with a committed
recording), needed by the CI gate in a following commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
The recorded witness table covers claude_code, pre_tool only, minus escalate
(armed, wave 1, T3). A cloud worker holds no vendor credentials and cannot
produce a witnessed row (contract, "The constraint that shapes every
brief") -- this is the command lines and report shape for whoever can.

docs/witness-skeleton.json names the three gaps (escalate at pre_tool,
every trial at prompt_submit, every trial at stop), each with the exact
`agentseam probe run ... --record --report` command and an evidence-report
skeleton shaped like that command's own --report output, with the
owner-dependent fields (basis, date, version) left blank on purpose.
tests/test_coverage_skeleton.py asserts every skeleton block, exactly as
committed, is REJECTED by evidence_report.validate() -- so a block nobody
has filled in can never be mistaken for a witnessed one -- and that the same
block validates once genuinely filled in, proving the blanks are the only
thing standing between it and real evidence.

docs/coverage-gaps.md, README.md and CONTRIBUTING.md spell out
witnessed/tested/recorded as a closed, three-way vocabulary tied to a
report's own `driver` and `basis` fields, so a reader can tell them apart
without opening evidence_report.py (T5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
The matrix's claims were never defended in CI: a change could make a
`recorded` row untrue and nothing would say so (armed, wave 1, T4).

tools/probe_ci.py replays every committed data/recordings/<agent>@<version>
recording, at every event and trial it covers, through the recorded driver
and diffs the result against src/agentseam/data/matrix.json. Exits 1 on any
DISAGREES row. Modeled on tools/watch_versions.py's own shape, but the
opposite intent: staleness.yml explicitly never fails the job (drift is a
fact about the world, not a defect in the PR); this one exists specifically
to fail the job, because a DISAGREES row here means the change under review
made a claim untrue, not that the vendor moved.

Reports the trial count checked alongside the disagreement count either way
-- a run with nothing committed to check prints "0 trial(s) checked ...
this is a measurement of nothing, not a pass" rather than a bare "0
disagreements" that would read as a clean bill of health (contract invariant
4). tests/test_probe_ci.py asserts the gate actually agrees with what is
committed today, and separately breaks it on purpose (a monkeypatched lying
diff) to prove a real disagreement is reported loudly and returns 1, not
swallowed.

.github/workflows/probe.yml runs it on push, on PRs touching the probe or
its data, and on demand; `set -o pipefail` so a disagreement's exit code
survives the `tee` into the step summary.

Verified by hand: flipping claude_code's pre_tool `block` claim to False in
a scratch copy of matrix.json makes both `agentseam probe run` and
tools/probe_ci.py report a DISAGREES row, the latter exiting 1; reverting
the file returns both to a clean, agreeing run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
render() counted disagreements, printed "the matrix and this agent do not
match", and returned 0. Harmless while this was a dev script under tools/;
not harmless now that the same code is a shipped `agentseam probe run`.

A verb that reports a mismatch and tells its caller it succeeded is the exact
shape this probe exists to catch in other people's tooling. Until now only
tools/probe_ci.py gated on the result.

Both callers -- the new verb and tools/experiment.py's own CLI -- get the
honest exit code. Two tests pin the pair: DISAGREES exits 1, agreement stays
0, so the gate cannot start crying wolf either.

Reproduced before and after against the committed recording with
claude_code.events.pre_tool.block flipped: exit 0 -> exit 1, agreement 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

semgrep is red, and it is this PR's — but the fix is a decision, not a keystroke

Failing check: semgrep, 4 findings of agentseam-no-shell-true, all Blocking:

  • src/agentseam/probe/experiment_driver.py:29
  • src/agentseam/probe/reference_agent.py:148, :203, :237

Why it went red, precisely. .github/workflows/security.yml:71 scans src/ tests/ — not tools/. On main, git grep -c shell=True -- src/ returns nothing and tools/ returns exactly these four (experiment_driver.py 1, reference_agent.py 3). T1's promotion did not introduce a single shell=True; it moved four pre-existing ones from an unscanned directory into a scanned one. The rule is working correctly. This is the same story as the lint baseline in the PR body: promotion put this code under the project's real standards for the first time.

Why the rule's own prescribed fix is the wrong one here. The message says "pass an argv list instead." For this code that would be actively harmful:

  • reference_agent.py exists to be Claude Code's protocol made executable. Claude Code runs hooks by handing a command string to a shell. Emulating that with an argv list would make the emulation false, and this driver is the thing every tested-basis row is measured against — a false emulation publishes wrong conclusions about vendors with total confidence.
  • :203 and :237 run pre["updated"] or command: the command as rewritten by the hook. That is the transform trial, whose entire purpose is measuring whether a rewritten input is what actually runs. Rewriting it as argv would measure something else.
  • experiment_driver.py:29 runs the operator's own --driver template with {prompt} substituted — a command line the operator typed on their own machine.

So shell=True is intrinsic to what this package does, while the rule remains correct for the surface it was written for: .semgrep/agentseam.yml's sibling rules are scoped paths: include: ["src/agentseam/*"] with the rationale "code runs in the path of every tool call an agent makes". The probe is not that path — it is a deliberate command-runner that ships beside it.

Not fixing this by silencing it. no-weakening-the-check is a blocking gate, and this file's own header says "never delete one without a decision record." Narrowing a security rule is the owner's call, so this PR stays red until that call is made rather than going green by suppression.

Recommended patch — one audited chokepoint, rule untouched and still global:

Add src/agentseam/probe/_shell.py with a single run_shell() carrying one nosemgrep: agentseam-no-shell-true and a written trust-boundary rationale; route all four call sites through it. Four scattered suppressions become one reviewable place, the justification lives beside the condition as CLAUDE.md requires, and the invariant keeps biting everywhere else in src/.

The alternative, if you would rather the rule state its own scope: add paths: include: ["src/agentseam/*"], exclude: ["src/agentseam/probe/*"] to agentseam-no-shell-true, matching the convention its two siblings already follow — plus the decision record the file's header demands.

Everything else on this branch is green: 1615 passed, 4 skipped on 3.11 and 3.13, ruff check . and ruff format --check . clean, DCO satisfied on all four commits.


Generated by Claude Code

Promotion into src/ put four pre-existing shell=True calls under
agentseam-no-shell-true for the first time: security.yml scans src/ and tests/,
never tools/, so the rule had not seen this code. On main, src/ has no shell=True
and tools/ has exactly these four. The rule is working; the code is not new.

Its prescribed fix -- pass an argv list -- would be wrong here. reference_agent
is Claude Code's protocol made executable, and Claude Code runs a hook by handing
a command string to a shell; an argv list would emulate a vendor that does not
exist, and every tested-basis row is measured against this driver. The transform
trial runs the command as the hook rewrote it, so measuring the rewrite means
running the rewrite.

So the exception is real, and now lives in one place instead of four: _shell.py's
run_shell() carries the single nosemgrep and the trust-boundary rationale beside
it. The rule stays global and unmodified. Verified narrow: a canary shell=True
added elsewhere in the same package still fails semgrep, and passes again once
removed.

semgrep exit 0 on CI's own invocation; 1615 passed, 4 skipped; ruff and
ruff format clean. Probe re-checked end to end: agreement exits 0, a forced
disagreement prints DISAGREES and exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Fixed at 0c510e1 — one audited chokepoint, rule untouched

Owner picked the chokepoint option. src/agentseam/probe/_shell.py now holds the single run_shell() carrying one nosemgrep and the trust-boundary rationale; all four call sites route through it. .semgrep/agentseam.yml is unmodified — no rule narrowed, no path excluded, no decision record needed.

Verified narrow, not blanket. A canary shell=True dropped into src/agentseam/probe/_canary.py — the same package as the exception — still trips agentseam-no-shell-true and exits 1; removing it returns to exit 0. The rule keeps biting everywhere in src/, including beside the one function that is allowed to do this.

Green on CI's own invocation:

semgrep --config .semgrep/agentseam.yml --error --quiet src/ tests/   → exit 0
pytest -q                     → 1615 passed, 4 skipped
ruff check .                  → All checks passed!
ruff format --check .         → 148 files already formatted

One thing worth noting from the fix: routing through the wrapper initially tripped PLW1510 (subprocess.run without explicit check), because kwargs.setdefault("check", False) is invisible to ruff. Fixed by making check and capture_output explicit keyword parameters rather than silencing the rule — the wrapper is now more readable at the call site too.

Probe re-checked end to end after the change: agreement exits 0, a forced disagreement prints DISAGREES and exits 1.


Generated by Claude Code

@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review September 10, 2026 01:46
@jothimani-rajendran
jothimani-rajendran merged commit f95f9a3 into main Sep 10, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants