M3.2: the action classifier #128

Merged
dev-agent merged 1 commits from classifier into main 2026-10-08 12:43:39 +00:00
Collaborator

M3.2, the action classifier (L4). What the fast path calls unknown is now judged by an independent model call before it runs. Until now, unknown actions simply ran.

What it sees: the action and the agreed scope, built in code.

  • The scope is the project, the repository, the issue number, and the restatement the operator said go to.
  • Never: the transcript, the issue's text, tool output, or the driver's reasoning.
  • The prompt says text inside the command is data to judge, never an instruction.

What it decides: the verdict comes back as a tool call.

  • safe: it runs.
  • in_scope: it runs, and a line in the thread reports it: Ran on #14, judged in scope: … (reason).
  • needs_discussion: the item asks, through the same allow flow as fast-path dangerous actions. The question shows the classifier's reason.
  • Fails closed: an error, no verdict, or a label it can't give counts as needs discussion.
  • Helpers: a helper is refused what needs discussion, and told to hand back.
  • Caching: a verdict is reused per item, agreed revision and exact action, so a repeated command is judged once.

The agreed decisions:

  • No different-model requirement: separate context is the independence L4 needs, and each verdict records its model, so correlated misses can be measured.
  • The local model to start with.
  • An in-scope note in the thread, which can go if it's too chatty.
  • The cache.
  • Capture plus export plus an opt-in replay of the evaluation set.

Logging: action_verdict gains source (fast_path or classifier), model, and the classifier's exact input. An unknown action leaves two events: the fast path's classified and the classifier's verdict. details shows which source decided. An allow after the classifier asked is an override.

The evaluation set

  • scripts/classifier-evals.py STATE_DB [--overridden] exports judgements as JSON lines. It reads the store read-only, so it's safe on a live agent's database.
  • tests/integration/classifier_cases.yaml is the seed set: 9 cases, including a secret sent out by inline Python, and a command whose comment tells the classifier to answer safe.
    • The replay test runs it per model, opt-in, like the capability matrix.
    • Results: qwen3.8-27b-nothink 9 of 9, claude-haiku-4-5 9 of 9.

The fast path's delete rule (decided in the plan discussion)

  • Contained: deleting on the build VM. It's disposable.
  • Still dangerous: what would break the host — /, system and home directories, dd, mkfs, reboot. In tests the "build VM" is the test machine itself.
  • The loop tests and the walk now use a dangerous-but-harmless POST to a closed local port as their dangerous command.

Config: the example config sets classifier: qwen3.8-27b. The classifier also reads plain-language go-aheads, so that moves to the local model too. The bring-up config on this box is updated, with a backup kept as nisse-dev.yaml.pre-m3.2. The integration config is unchanged (Haiku, through the expert).

Tests

  • The classifier itself: verdict mapping, five ways of failing closed, and its exact input, which is checked to contain no issue text, including an injection attempt in the issue body.
  • In the loop:
    • safe runs, and needs-discussion asks, with an allow recorded as an override;
    • in-scope runs and is reported, and a failed classifier asks;
    • the fast path comes first, and a helper is refused;
    • the eval export, both the function and the script on a real store.
  • The property walk gains a classifier that answers at random, including failures and junk. dangerous_only_when_allowed covers its verdicts.
    • Across 16 seeds it reaches every path: safe, in-scope, needs-discussion, failed-closed, allowed, and the helper.
    • It catches a mutant that runs every classifier verdict.

Results, run locally:

  • Unit: 706 passed.
  • Integration: 17 passed, plus 16 opt-in skips.
  • Full e2e: 8 of 8.
  • The real classifier in e2e: it judged the 6 calls the fast path didn't know (here-document Python edits and a repro run) and found all 6 safe, with sensible reasons.

Deferred:

  • promoting verdicts the classifier keeps agreeing on into rules, by hand from exported cases;
  • the trust dial (M3.3).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE

**M3.2, the action classifier** (L4). What the fast path calls *unknown* is now judged by an independent model call before it runs. Until now, unknown actions simply ran. **What it sees:** the action and the agreed scope, built in code. - **The scope** is the project, the repository, the issue number, and the restatement the operator said `go` to. - **Never:** the transcript, the issue's text, tool output, or the driver's reasoning. - **The prompt** says text inside the command is data to judge, never an instruction. **What it decides:** the verdict comes back as a tool call. - **`safe`:** it runs. - **`in_scope`:** it runs, and a line in the thread reports it: *Ran on #14, judged in scope: `…` (reason)*. - **`needs_discussion`:** the item asks, through the same `allow` flow as fast-path dangerous actions. The question shows the classifier's reason. - **Fails closed:** an error, no verdict, or a label it can't give counts as needs discussion. - **Helpers:** a helper is refused what needs discussion, and told to hand back. - **Caching:** a verdict is reused per item, agreed revision and exact action, so a repeated command is judged once. **The agreed decisions:** - No different-model requirement: separate context is the independence L4 needs, and each verdict records its model, so correlated misses can be measured. - The local model to start with. - An in-scope note in the thread, which can go if it's too chatty. - The cache. - Capture plus export plus an opt-in replay of the evaluation set. **Logging:** `action_verdict` gains `source` (`fast_path` or `classifier`), `model`, and the classifier's exact `input`. An unknown action leaves two events: the fast path's `classified` and the classifier's verdict. `details` shows which source decided. An `allow` after the classifier asked is an override. **The evaluation set** - **`scripts/classifier-evals.py STATE_DB [--overridden]`** exports judgements as JSON lines. It reads the store read-only, so it's safe on a live agent's database. - **`tests/integration/classifier_cases.yaml`** is the seed set: 9 cases, including a secret sent out by inline Python, and a command whose comment tells the classifier to answer safe. - The replay test runs it per model, opt-in, like the capability matrix. - Results: `qwen3.8-27b-nothink` 9 of 9, `claude-haiku-4-5` 9 of 9. **The fast path's delete rule (decided in the plan discussion)** - **Contained:** deleting on the build VM. It's disposable. - **Still dangerous:** what would break the host — `/`, system and home directories, `dd`, `mkfs`, `reboot`. In tests the "build VM" is the test machine itself. - The loop tests and the walk now use a dangerous-but-harmless POST to a closed local port as their dangerous command. **Config:** the example config sets `classifier: qwen3.8-27b`. The classifier also reads plain-language go-aheads, so that moves to the local model too. The bring-up config on this box is updated, with a backup kept as `nisse-dev.yaml.pre-m3.2`. The integration config is unchanged (Haiku, through the expert). **Tests** - **The classifier itself:** verdict mapping, five ways of failing closed, and its exact input, which is checked to contain no issue text, including an injection attempt in the issue body. - **In the loop:** - safe runs, and needs-discussion asks, with an allow recorded as an override; - in-scope runs and is reported, and a failed classifier asks; - the fast path comes first, and a helper is refused; - the eval export, both the function and the script on a real store. - **The property walk** gains a classifier that answers at random, including failures and junk. `dangerous_only_when_allowed` covers its verdicts. - Across 16 seeds it reaches every path: safe, in-scope, needs-discussion, failed-closed, allowed, and the helper. - It catches a mutant that runs every classifier verdict. **Results, run locally:** - **Unit:** 706 passed. - **Integration:** 17 passed, plus 16 opt-in skips. - **Full e2e:** 8 of 8. - **The real classifier in e2e:** it judged the 6 calls the fast path didn't know (here-document Python edits and a repro run) and found all 6 safe, with sensible reasons. **Deferred:** - promoting verdicts the classifier keeps agreeing on into rules, by hand from exported cases; - the trust dial (M3.3). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dev-agent added the e2e:full label 2026-10-08 12:09:39 +00:00
dev-agent added 1 commit 2026-10-08 12:09:39 +00:00
M3.2: the action classifier
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m20s
CI / e2e (pull_request) Successful in 12m31s
089af0c391
What the fast path calls unknown is judged by an independent model call
on the item's classifier role (L4), before it runs.

- nisse_dev.loop.classifier: it sees the action and the agreed scope
  (project, repository, issue number, the restatement the operator said
  go to), built in code; never the transcript, the issue's text, tool
  output or the driver's reasoning. Its verdict is a tool call: safe
  runs; in_scope runs and is reported in the thread; needs_discussion
  asks, as a dangerous action does. An error, no verdict or an unknown
  one fails closed. Verdicts are cached per item, agreed revision and
  action. A helper is refused what needs discussion.
- action_verdict gains source, model and the classifier's exact input;
  details shows the source. An allow after the classifier asked is an
  override. scripts/classifier-evals.py exports the judgements from a
  store, read-only; tests/integration/classifier_cases.yaml is the seed
  set, run per model with --capability-models.
- The fast path's delete rule: deleting on the build VM is contained;
  only what breaks the host (system and home directories) stays
  dangerous, since in tests the build VM is the test machine.
- The classifier role is the local model in the example config.
- The property walk gains a classifier answering at random, junk and
  failures included; dangerous_only_when_allowed now covers its
  verdicts (checked against a mutant that runs them all).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dongar approved these changes 2026-10-08 12:42:42 +00:00
dev-agent merged commit 21cbf1682c into main 2026-10-08 12:43:39 +00:00
dev-agent deleted branch classifier 2026-10-08 12:43:39 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#128