M3.1: action classes and the fast path (P3, P4, D6). Every execution tool call is judged by code before it runs, the driver's and a helper's alike.
The agreed decisions:
Unknown actions run, and are logged, until the classifier exists (M3.2).
The shared part goes in nisse-core.
Changing branches by hand is refused outright.
Shared part (nisse_core.actions, nisse-core's first real code)
Action and Verdict types. Effects are harmless, contained, unknown, dangerous or refused.
Shell commands are read statically, segment by segment across &&, ;, | and newlines.
A cd moves where the rest of the line runs.
Wrappers like sudo, env and timeout are seen through, and sh -c '…' is read as the line it runs.
Substitutions, subshells and here-documents are unknown, never guessed at.
A line is as bad as its worst command.
nisse-dev's rules (nisse_dev.loop.actions)
Harmless: reading, git status/diff/log, tests, linters and builds (pytest, uv run …, make test, npm test, cargo test).
Contained: workspace edits, local git (add, commit, restore), installs on the build VM, and other changes there.
Dangerous: pushing, other hosts (ssh, scp, rsync host:), uploads (curl -d/-T/-X POST), publishing, the forge's own tools (tea, gh, twine), deleting outside the workspace, and wrecking the VM (dd, mkfs, reboot, iptables).
Refused:git checkout -b/<branch>, switch, branch <name>/-D, merge, rebase, pull. The reason says the harness owns branches, and points at start_milestone.
In the loop
A dangerous call isn't run. The item waits on the operator with a question rendered in code from the action itself (L3, L11). For example: #14 wants to run rm -rf /srv/x: rm deletes outside the workspace: /srv/x. Say allow to let exactly this run once…
allow binds to that exact action, once. A second identical call is asked about again, and a different command isn't covered. An answer instead of allow goes to the driver as usual.
A helper can't ask, so a dangerous call from a helper is refused, and the helper is told to hand back and say what it needs.
Every verdict is logged as an action_verdict event and an oplog line. No model sees the events; this is M3.2's evaluation set. details shows the counts and every action that wasn't simply run.
Two smaller changes:
a prompt line says such commands are put to the operator, and to never change branches;
the dump tool shows verdicts.
Tests
Rule table: 83 cases. Shell reading: 31 cases in nisse-core.
Loop behaviour: asked, allowed once, not covered, answered instead, branches refused with the run going on, helper refused, details, and no model seeing a verdict.
The property walk gains dangerous_only_when_allowed. The walk's model now tries a dangerous command, and repeats it after an allow, as the prompt tells it to. Across 16 seeds it reaches asked, allowed and helper-refused.
It catches a mutant where the allow isn't used up (seed 6).
The slice e2e checks that every call was judged and none needed an allow.
Results, run locally before pushing:
Unit: 683 passed.
Integration: 17 passed, plus 7 opt-in skips.
Full e2e: 8 of 8 passed.
What the real model's calls looked like across those 8 e2e runs: 113 calls in all — 79 harmless, 32 contained, 2 unknown (inline Python, and a line that can't be read statically), none dangerous or refused.
Deferred:
the classifier, and judging what's in scope (M3.2);
the trust dial (M3.3);
enforcing egress, the real hard wall behind the network rules (X2).
**M3.1: action classes and the fast path** (P3, P4, D6). Every execution tool call is judged by code before it runs, the driver's and a helper's alike.
**The agreed decisions:**
- **Unknown actions** run, and are logged, until the classifier exists (M3.2).
- **The shared part** goes in `nisse-core`.
- **Changing branches by hand** is refused outright.
**Shared part (`nisse_core.actions`, `nisse-core`'s first real code)**
- `Action` and `Verdict` types. Effects are harmless, contained, unknown, dangerous or refused.
- **Shell commands are read statically, segment by segment** across `&&`, `;`, `|` and newlines.
- A `cd` moves where the rest of the line runs.
- Wrappers like `sudo`, `env` and `timeout` are seen through, and `sh -c '…'` is read as the line it runs.
- Substitutions, subshells and here-documents are unknown, never guessed at.
- A line is as bad as its worst command.
**nisse-dev's rules (`nisse_dev.loop.actions`)**
- **Harmless:** reading, `git status`/`diff`/`log`, tests, linters and builds (`pytest`, `uv run …`, `make test`, `npm test`, `cargo test`).
- **Contained:** workspace edits, local git (`add`, `commit`, `restore`), installs on the build VM, and other changes there.
- **Dangerous:** pushing, other hosts (`ssh`, `scp`, `rsync host:`), uploads (`curl -d`/`-T`/`-X POST`), publishing, the forge's own tools (`tea`, `gh`, `twine`), deleting outside the workspace, and wrecking the VM (`dd`, `mkfs`, `reboot`, `iptables`).
- **Refused:** `git checkout -b`/`<branch>`, `switch`, `branch <name>`/`-D`, `merge`, `rebase`, `pull`. The reason says the harness owns branches, and points at `start_milestone`.
**In the loop**
- **A dangerous call isn't run.** The item waits on the operator with a question rendered in code from the action itself (L3, L11). For example: *#14 wants to run `rm -rf /srv/x`: rm deletes outside the workspace: /srv/x. Say `allow` to let exactly this run once…*
- **`allow` binds to that exact action, once.** A second identical call is asked about again, and a different command isn't covered. An answer instead of `allow` goes to the driver as usual.
- **A helper can't ask**, so a dangerous call from a helper is refused, and the helper is told to hand back and say what it needs.
- **Every verdict is logged** as an `action_verdict` event and an oplog line. No model sees the events; this is M3.2's evaluation set. `details` shows the counts and every action that wasn't simply run.
- **Two smaller changes:**
- a prompt line says such commands are put to the operator, and to never change branches;
- the `dump` tool shows verdicts.
**Tests**
- **Rule table:** 83 cases. **Shell reading:** 31 cases in `nisse-core`.
- **Loop behaviour:** asked, allowed once, not covered, answered instead, branches refused with the run going on, helper refused, `details`, and no model seeing a verdict.
- **The property walk** gains `dangerous_only_when_allowed`. The walk's model now tries a dangerous command, and repeats it after an allow, as the prompt tells it to. Across 16 seeds it reaches asked, allowed and helper-refused.
- It catches a mutant where the allow isn't used up (seed 6).
- **The slice e2e** checks that every call was judged and none needed an allow.
**Results, run locally before pushing:**
- **Unit:** 683 passed.
- **Integration:** 17 passed, plus 7 opt-in skips.
- **Full e2e:** 8 of 8 passed.
- **What the real model's calls looked like** across those 8 e2e runs: 113 calls in all — 79 harmless, 32 contained, 2 unknown (inline Python, and a line that can't be read statically), none dangerous or refused.
**Deferred:**
- the classifier, and judging what's in scope (M3.2);
- the trust dial (M3.3);
- enforcing egress, the real hard wall behind the network rules (X2).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Every execution tool call, the driver's and a helper's, gets a verdict
from code before it runs (P3, P4, D6).
- nisse_core.actions, nisse-core's first code: Action and Verdict, and
reading shell commands statically, segment by segment (cd, wrappers,
sh -c). What can't be read that way is unknown; a line is as bad as
its worst command.
- nisse_dev.loop.actions, the rule table: reading, checks and tests are
harmless; workspace edits, local git and installs on the build VM are
contained; pushing, other hosts, uploads, publishing, the forge's own
tools, deleting outside the workspace and wrecking the VM are
dangerous; changing branches by hand is refused (the harness owns
branches) with a pointer to the milestone tools.
- A dangerous call isn't run. The item waits on the operator with a
question rendered from the action, and `allow` lets exactly that
action run once. A helper can't ask, so it is refused and told to
hand back. Unknown actions run, logged, until the classifier (M3.2).
- Every verdict is an action_verdict event, which no model sees, and an
oplog line; `details` shows the counts and every action not simply
run.
- The property walk gains dangerous_only_when_allowed (checked against
a mutant that doesn't use the allow up); the slice e2e checks every
call was judged and none needed an allow.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
M3.1: action classes and the fast path (P3, P4, D6). Every execution tool call is judged by code before it runs, the driver's and a helper's alike.
The agreed decisions:
nisse-core.Shared part (
nisse_core.actions,nisse-core's first real code)ActionandVerdicttypes. Effects are harmless, contained, unknown, dangerous or refused.&&,;,|and newlines.cdmoves where the rest of the line runs.sudo,envandtimeoutare seen through, andsh -c '…'is read as the line it runs.nisse-dev's rules (
nisse_dev.loop.actions)git status/diff/log, tests, linters and builds (pytest,uv run …,make test,npm test,cargo test).add,commit,restore), installs on the build VM, and other changes there.ssh,scp,rsync host:), uploads (curl -d/-T/-X POST), publishing, the forge's own tools (tea,gh,twine), deleting outside the workspace, and wrecking the VM (dd,mkfs,reboot,iptables).git checkout -b/<branch>,switch,branch <name>/-D,merge,rebase,pull. The reason says the harness owns branches, and points atstart_milestone.In the loop
rm -rf /srv/x: rm deletes outside the workspace: /srv/x. Sayallowto let exactly this run once…allowbinds to that exact action, once. A second identical call is asked about again, and a different command isn't covered. An answer instead ofallowgoes to the driver as usual.action_verdictevent and an oplog line. No model sees the events; this is M3.2's evaluation set.detailsshows the counts and every action that wasn't simply run.dumptool shows verdicts.Tests
nisse-core.details, and no model seeing a verdict.dangerous_only_when_allowed. The walk's model now tries a dangerous command, and repeats it after an allow, as the prompt tells it to. Across 16 seeds it reaches asked, allowed and helper-refused.Results, run locally before pushing:
Deferred:
🤖 Generated with Claude Code
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE