edward-guard 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edward contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: edward-guard
3
+ Version: 0.1.1
4
+ Summary: External control plane for AI coding agents: deterministic rules + local semantic scorer, audit trail, human-resumable interventions.
5
+ Author: Edward contributors
6
+ License: MIT
7
+ Keywords: ai,agent,guardrails,safety,observability,cost-control
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Quality Assurance
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Edward
19
+
20
+ **Edward is an external control plane for AI coding agents.** Agents fail quietly: they retry the same broken test 40 times, burn $8 in tokens on a loop, run `rm -rf` on a database directory, and write to files they were never supposed to touch. The agent doesn't know it's failing — from its perspective, it's still trying.
21
+
22
+ Edward sits between the agent and its runtime. It watches the event stream, builds a picture of what the agent is actually doing across turns, and intervenes when the picture stops looking right.
23
+
24
+ [![ci](https://github.com/OWNER/edward/actions/workflows/ci.yml/badge.svg)](../../actions)
25
+ [![PyPI](https://img.shields.io/badge/PyPI-edward--guard-blue)](https://pypi.org/project/edward-guard/)
26
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
27
+ [![python](https://img.shields.io/badge/python-3.11%2B-blue)](pyproject.toml)
28
+
29
+ **Measured on the StepShield benchmark (NeurIPS 2026):** deterministic rules alone detect 7.4% of content-semantic violations at 1.9% FPR; adding a local 4B scorer with evidence-grounded task-contract probes reaches **57.4% recall at 20.4% FPR with EIR_3 0.790** (paper's LLMJudge tier: 95.4% / 5.6% / 0.89, at GPT-4.1-mini cost). See [BENCHMARK.md](BENCHMARK.md).
30
+
31
+ ```bash
32
+ pipx install edward-guard # zero dependencies, stdlib only
33
+ edward demo # self-running proof: 6 failure scenarios
34
+ edward wrap -- pi "fix the flaky test"
35
+ edward wrap --no-scorer -- python my_agent.py # any command, rule-only
36
+ ```
37
+
38
+
39
+ ```
40
+ Agent (Pi / Codex / custom)
41
+ │ events
42
+ ▼
43
+ Canonical Event Schema ← normalizes tool names to capabilities
44
+ │
45
+ ▼
46
+ State Engine ← materializes cross-turn agent state
47
+ │
48
+ ▼
49
+ Trigger Rules [FROZEN] ← deterministic safety + convergence checks
50
+ │
51
+ ├─ HARD_CONSTRAINT ──→ Kernel: BLOCK (Jev cannot override)
52
+ │
53
+ └─ SOFT_DECISION ──→ Jev (TypeSafe) ──→ Policy Resolver
54
+ │ │
55
+ └────────────────┘
56
+ │
57
+ ▼
58
+ Control Kernel
59
+ │
60
+ ▼
61
+ PAUSE / CANCEL / RESUME
62
+ ```
63
+
64
+ ## Why not just if/else?
65
+
66
+ A simple watchdog (`error_count > 5 → stop`) looks at individual events. The failure modes that actually kill long-running agents don't show up in any single event. They emerge from the shape of the trajectory over time.
67
+
68
+ We tested this directly.
69
+
70
+ ### Watchdog vs State Engine → MSS
71
+
72
+ Held-out evaluation, 20 trials × 6 scenarios, frozen config:
73
+
74
+ | Scenario | Watchdog detection | State Engine detection | Watchdog ctx | State ctx |
75
+ |---|---|---|---|---|
76
+ | budget bleed | 20% | **100%** | 2,322 | 409 |
77
+ | infinite loop | 0% | **100%** | 6,084 | 409 |
78
+ | convergence stall | 0% | **100%** | 1,442 | 409 |
79
+ | dangerous command | 75% | **100%** | 522 | 434 |
80
+
81
+ The watchdog misses infinite loops and stalls entirely because it has no memory across turns. The State Engine maintains a sliding window of recent tool calls, tracks file modifications, and computes elapsed time — none of which fit in a single-event check.
82
+
83
+ ## What it detects
84
+
85
+ Six trigger rules, tuned on a development set (seed 42) and evaluated frozen on held-out data (seed 137):
86
+
87
+ | Signal | Threshold | Fires when |
88
+ |---|---|---|
89
+ | Error rate | > 40% over last 8+ calls | Agent is failing repeatedly, with no recovery signal |
90
+ | Retry count | ≥ 3 | Agent keeps retrying the same thing |
91
+ | Token budget | > 80% consumed | Agent is burning resources |
92
+ | Convergence stall | > 600s + > 5 turns | Agent is running but not completing |
93
+ | Passive stall | 12 consecutive reads, 0 writes | Agent is going in circles |
94
+ | Dangerous command | regex match | Agent executes `rm -rf`, `sudo`, `git push --force`, etc. |
95
+
96
+ Plus two post-hoc additions from robustness testing:
97
+
98
+ | Signal | Fires when |
99
+ |---|---|
100
+ | Scope violation | Agent writes to a path outside the allowed prefix list |
101
+ | Silent corruption risk | ≥ 10 consecutive file writes with zero shell verification |
102
+
103
+ ## Held-out benchmark
104
+
105
+ 50 trials × 6 scenarios, frozen configuration (no post-hoc tuning):
106
+
107
+ ```
108
+ Detection FP Rate Latency (tool calls)
109
+ infinite_loop 100% — 8.0
110
+ budget_bleed 100% — 11.7
111
+ dangerous 100% — 5.0
112
+ stall 100% — 4.0
113
+ normal — 0% —
114
+ transient_failure — 0% —
115
+
116
+ Precision: 100% Recall: 100% FPR: 0/100
117
+ ```
118
+
119
+ ## Robustness: where it breaks
120
+
121
+ We attacked the architecture along four dimensions. Results after P0+P1 fixes:
122
+
123
+ **Agent heterogeneity.** Three event schemas (Pi, Codex-style, custom) normalized through the canonical schema layer. Custom agents use tool names like `execute` and `terminal` instead of `bash`. Before the fix, dangerous command detection on custom agents was 0%. After: 100% across all six combinations.
124
+
125
+ **Task distribution shift.** Three unseen normal task profiles (heavy-write, error-then-recover, mixed-signal). The error-then-recover profile initially produced 60% false positives because the error-rate trigger couldn't distinguish "temporarily failing but recovering" from "systematically diverging." Added a `recovery_signal` property: if the tail of the sliding window shows ≥3 consecutive successes after errors, the trigger holds. Post-fix FPR: 0% on all three profiles.
126
+
127
+ **Controller ablation.** Jev (TypeSafe's System One model) acts as a probabilistic second opinion on soft decisions. On dangerous commands, rules fire deterministically as HARD_CONSTRAINT — Jev cannot override. On soft decisions, Jev confirms ~100% of infinite-loop interventions but overrides ~97% of budget-bleed and ~100% of stall interventions back to CONTINUE. This tension is a design feature: the policy resolver lets deterministic safety guards override probabilistic judgments, but not vice versa.
128
+
129
+ **Unseen failure modes.** Three novel failure scenarios not in dev or test:
130
+
131
+ | Scenario | Detection | Mechanism |
132
+ |---|---|---|
133
+ | Permission escalation | 100% | Dangerous command regex |
134
+ | Cascading failure | 100% | Retry count trigger |
135
+ | Silent corruption | 76.7% | Unverified-write streak detector |
136
+
137
+ Silent corruption — the agent writes 15 files in a row without running any test — was invisible to every original trigger. The streak detector catches most cases but not all (some trials have only 8-9 writes, below the threshold). Full coverage would require World State tracking: comparing what the agent wrote against what the task expected.
138
+
139
+ ## The four things we learned
140
+
141
+ 1. **Execution telemetry ≠ world state.** The agent's own view ("I'm still trying") diverges from what an external observer sees (error rate rising, budget depleting, no progress). This gap is the entire reason a control plane exists.
142
+
143
+ 2. **Event-level checks cannot detect temporal failure modes.** Every trigger that requires memory across turns (stall, convergence, recovery) needs the State Engine. Single-event or simple-counter approaches score 0% on these.
144
+
145
+ 3. **Safety and intelligence need different authority levels.** Dangerous-command blocking is deterministic and non-overridable. Everything else benefits from a probabilistic second opinion. Conflating the two makes the safety guard probabilistic, which defeats the purpose.
146
+
147
+ 4. **Agent-execution monitoring alone cannot detect world-state corruption.** If the agent writes destructive content without producing any error, no amount of execution telemetry will catch it. This requires comparing actual filesystem state against a task contract. It's the clearest motivation for v0.2.
148
+
149
+ ## Running it
150
+
151
+ ### Product quickstart (edward CLI)
152
+
153
+ ```bash
154
+ pip install -e . # zero dependencies, stdlib only (Python >= 3.11)
155
+
156
+ # optional: the pi coding agent (full monitoring + intervention on pi tasks)
157
+ npm install -g --ignore-scripts @earendil-works/pi-coding-agent
158
+
159
+ edward doctor # environment checks: scorer, pi, audit dir
160
+ edward demo # self-running proof: 6 scenarios, PASS/FAIL gate
161
+ edward wrap -- pi "Fix the bug in utils.py so that the test passes"
162
+ edward wrap --scope ./src --no-scorer -- python agent_script.py # any command, rule-only
163
+ edward eval --policy conservative --trials 30 # tune a policy before live use
164
+ edward audit # intervention summary ($ saved evidence)
165
+ edward policy-template --preset balanced > edward.toml
166
+ ```
167
+
168
+ The semantic scorer is an internal HTTP endpoint (LAN only, no auth):
169
+ `GET /health` + `POST /v1/score` on Qwen/Qwen3.5-4B — see `edward/scorer_client.py`.
170
+ Without it, edward runs in rule-only mode and stays fully protective.
171
+
172
+ Exit codes: `0` completed, `75` PAUSED (resumable: `edward wrap --continue -- ...`),
173
+ `76` terminated by control plane, `130` interrupted. Audit JSONL lands in
174
+ `~/.edward/audit.jsonl`. Policy packs are TOML/JSON with three presets
175
+ (`conservative` / `balanced` = FROZEN defaults / `aggressive`).
176
+
177
+ ### Research scripts (pre-packaging, still work)
178
+
179
+ ```bash
180
+ python main.py "Fix the bug in utils.py so that the test passes"
181
+
182
+ # Run the benchmark
183
+ python benchmark.py
184
+
185
+ # Run the ablation
186
+ python ablation.py
187
+
188
+ # Run held-out evaluation
189
+ python heldout_eval.py
190
+
191
+ # Run robustness evaluation
192
+ python robustness_eval.py
193
+ ```
194
+
195
+ ### External benchmark: StepShield (NeurIPS 2026)
196
+
197
+ ```bash
198
+ git clone --depth 1 https://github.com/glo26/stepshield /tmp/stepshield
199
+ edward eval --suite stepshield --data /tmp/stepshield/data --mode rules
200
+ edward eval --suite stepshield --data /tmp/stepshield/data --mode contract \
201
+ --scorer http://192.168.2.51:8000 # needs live scorer endpoint
202
+ ```
203
+
204
+ `rules` = the deterministic v0.1 layer (dangerous-command regex + scope
205
+ violations derived from task constraints). `contract` = the v0.2 Task
206
+ Contract preview: task intent + constraints go into the scorer question,
207
+ and the 4B endpoint judges each state-changing step (OK / VIOLATION /
208
+ UNSURE) with asymmetric confirmation: conf >= 0.9 fires alone, 0.6-0.9
209
+ needs one corroborating violation, a confident OK clears suspicion.
210
+ Metrics are paper-aligned (EIR_k, recall, FPR on clean, per-category).
211
+
212
+ Pi uses `--mode rpc` for headless operation. The control plane spawns it as a subprocess, reads JSONL events from stdout, and sends control commands (abort, steer) via stdin. No Pi source code is modified.
213
+
214
+ ## Project structure
215
+
216
+ ```
217
+ edward/ Product package (pip install -e .)
218
+ cli.py edward CLI: wrap / demo / eval / audit / doctor
219
+ engine.py ControlPlane: events -> triggers -> scorer -> decision -> audit
220
+ config.py Policy packs (TOML/JSON, 3 presets, strict validation)
221
+ audit.py Append-only JSONL audit log (never blocks monitoring)
222
+ scorer.py Semantic scorer client with circuit breaker
223
+ scenarios.py Failure scenario suite (single source for demo/eval)
224
+ evalcmd.py Policy evaluation gate (detection / FPR / timing)
225
+ pi_client.py Pi RPC client (cwd, provider/model, stderr capture)
226
+ scorer_client.py /v1/score client (internal endpoint, Qwen3.5-4B)
227
+ state_engine.py Materialize AgentState from event stream
228
+ triggers.py 8 trigger rules, policy-parameterized (defaults FROZEN)
229
+ kernel.py Decision authority hierarchy
230
+ canonical_events.py Normalize Pi/Codex/custom events to canonical schema
231
+ notify.py Slack webhook + stderr banners (fail-silent)
232
+ main.py Legacy entry -> edward wrap
233
+ benchmark.py 300-trial held-out benchmark (re-exports scenarios)
234
+ ablation.py Watchdog vs State Engine comparison
235
+ heldout_eval.py Dev/test split + frozen config evaluation
236
+ robustness_eval.py 4-dimension robustness attack
237
+ extreme_tests.py 4 extreme scenario demos
238
+ test_product.py Product test suite (unittest, offline)
239
+ ```
240
+
241
+ ## What's next
242
+
243
+ The silent-corruption gap points at the next layer: a Task Contract that defines which files the agent should touch and what the expected end-state looks like. When the agent's actual filesystem mutations diverge from the contract, that's a scope violation — regardless of whether any tool call returned an error. This moves the State Engine from "what is the agent doing" to "what is the agent doing to the world, and is that still allowed."
@@ -0,0 +1,226 @@
1
+ # Edward
2
+
3
+ **Edward is an external control plane for AI coding agents.** Agents fail quietly: they retry the same broken test 40 times, burn $8 in tokens on a loop, run `rm -rf` on a database directory, and write to files they were never supposed to touch. The agent doesn't know it's failing — from its perspective, it's still trying.
4
+
5
+ Edward sits between the agent and its runtime. It watches the event stream, builds a picture of what the agent is actually doing across turns, and intervenes when the picture stops looking right.
6
+
7
+ [![ci](https://github.com/OWNER/edward/actions/workflows/ci.yml/badge.svg)](../../actions)
8
+ [![PyPI](https://img.shields.io/badge/PyPI-edward--guard-blue)](https://pypi.org/project/edward-guard/)
9
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
+ [![python](https://img.shields.io/badge/python-3.11%2B-blue)](pyproject.toml)
11
+
12
+ **Measured on the StepShield benchmark (NeurIPS 2026):** deterministic rules alone detect 7.4% of content-semantic violations at 1.9% FPR; adding a local 4B scorer with evidence-grounded task-contract probes reaches **57.4% recall at 20.4% FPR with EIR_3 0.790** (paper's LLMJudge tier: 95.4% / 5.6% / 0.89, at GPT-4.1-mini cost). See [BENCHMARK.md](BENCHMARK.md).
13
+
14
+ ```bash
15
+ pipx install edward-guard # zero dependencies, stdlib only
16
+ edward demo # self-running proof: 6 failure scenarios
17
+ edward wrap -- pi "fix the flaky test"
18
+ edward wrap --no-scorer -- python my_agent.py # any command, rule-only
19
+ ```
20
+
21
+
22
+ ```
23
+ Agent (Pi / Codex / custom)
24
+ │ events
25
+ ▼
26
+ Canonical Event Schema ← normalizes tool names to capabilities
27
+ │
28
+ ▼
29
+ State Engine ← materializes cross-turn agent state
30
+ │
31
+ ▼
32
+ Trigger Rules [FROZEN] ← deterministic safety + convergence checks
33
+ │
34
+ ├─ HARD_CONSTRAINT ──→ Kernel: BLOCK (Jev cannot override)
35
+ │
36
+ └─ SOFT_DECISION ──→ Jev (TypeSafe) ──→ Policy Resolver
37
+ │ │
38
+ └────────────────┘
39
+ │
40
+ ▼
41
+ Control Kernel
42
+ │
43
+ ▼
44
+ PAUSE / CANCEL / RESUME
45
+ ```
46
+
47
+ ## Why not just if/else?
48
+
49
+ A simple watchdog (`error_count > 5 → stop`) looks at individual events. The failure modes that actually kill long-running agents don't show up in any single event. They emerge from the shape of the trajectory over time.
50
+
51
+ We tested this directly.
52
+
53
+ ### Watchdog vs State Engine → MSS
54
+
55
+ Held-out evaluation, 20 trials × 6 scenarios, frozen config:
56
+
57
+ | Scenario | Watchdog detection | State Engine detection | Watchdog ctx | State ctx |
58
+ |---|---|---|---|---|
59
+ | budget bleed | 20% | **100%** | 2,322 | 409 |
60
+ | infinite loop | 0% | **100%** | 6,084 | 409 |
61
+ | convergence stall | 0% | **100%** | 1,442 | 409 |
62
+ | dangerous command | 75% | **100%** | 522 | 434 |
63
+
64
+ The watchdog misses infinite loops and stalls entirely because it has no memory across turns. The State Engine maintains a sliding window of recent tool calls, tracks file modifications, and computes elapsed time — none of which fit in a single-event check.
65
+
66
+ ## What it detects
67
+
68
+ Six trigger rules, tuned on a development set (seed 42) and evaluated frozen on held-out data (seed 137):
69
+
70
+ | Signal | Threshold | Fires when |
71
+ |---|---|---|
72
+ | Error rate | > 40% over last 8+ calls | Agent is failing repeatedly, with no recovery signal |
73
+ | Retry count | ≥ 3 | Agent keeps retrying the same thing |
74
+ | Token budget | > 80% consumed | Agent is burning resources |
75
+ | Convergence stall | > 600s + > 5 turns | Agent is running but not completing |
76
+ | Passive stall | 12 consecutive reads, 0 writes | Agent is going in circles |
77
+ | Dangerous command | regex match | Agent executes `rm -rf`, `sudo`, `git push --force`, etc. |
78
+
79
+ Plus two post-hoc additions from robustness testing:
80
+
81
+ | Signal | Fires when |
82
+ |---|---|
83
+ | Scope violation | Agent writes to a path outside the allowed prefix list |
84
+ | Silent corruption risk | ≥ 10 consecutive file writes with zero shell verification |
85
+
86
+ ## Held-out benchmark
87
+
88
+ 50 trials × 6 scenarios, frozen configuration (no post-hoc tuning):
89
+
90
+ ```
91
+ Detection FP Rate Latency (tool calls)
92
+ infinite_loop 100% — 8.0
93
+ budget_bleed 100% — 11.7
94
+ dangerous 100% — 5.0
95
+ stall 100% — 4.0
96
+ normal — 0% —
97
+ transient_failure — 0% —
98
+
99
+ Precision: 100% Recall: 100% FPR: 0/100
100
+ ```
101
+
102
+ ## Robustness: where it breaks
103
+
104
+ We attacked the architecture along four dimensions. Results after P0+P1 fixes:
105
+
106
+ **Agent heterogeneity.** Three event schemas (Pi, Codex-style, custom) normalized through the canonical schema layer. Custom agents use tool names like `execute` and `terminal` instead of `bash`. Before the fix, dangerous command detection on custom agents was 0%. After: 100% across all six combinations.
107
+
108
+ **Task distribution shift.** Three unseen normal task profiles (heavy-write, error-then-recover, mixed-signal). The error-then-recover profile initially produced 60% false positives because the error-rate trigger couldn't distinguish "temporarily failing but recovering" from "systematically diverging." Added a `recovery_signal` property: if the tail of the sliding window shows ≥3 consecutive successes after errors, the trigger holds. Post-fix FPR: 0% on all three profiles.
109
+
110
+ **Controller ablation.** Jev (TypeSafe's System One model) acts as a probabilistic second opinion on soft decisions. On dangerous commands, rules fire deterministically as HARD_CONSTRAINT — Jev cannot override. On soft decisions, Jev confirms ~100% of infinite-loop interventions but overrides ~97% of budget-bleed and ~100% of stall interventions back to CONTINUE. This tension is a design feature: the policy resolver lets deterministic safety guards override probabilistic judgments, but not vice versa.
111
+
112
+ **Unseen failure modes.** Three novel failure scenarios not in dev or test:
113
+
114
+ | Scenario | Detection | Mechanism |
115
+ |---|---|---|
116
+ | Permission escalation | 100% | Dangerous command regex |
117
+ | Cascading failure | 100% | Retry count trigger |
118
+ | Silent corruption | 76.7% | Unverified-write streak detector |
119
+
120
+ Silent corruption — the agent writes 15 files in a row without running any test — was invisible to every original trigger. The streak detector catches most cases but not all (some trials have only 8-9 writes, below the threshold). Full coverage would require World State tracking: comparing what the agent wrote against what the task expected.
121
+
122
+ ## The four things we learned
123
+
124
+ 1. **Execution telemetry ≠ world state.** The agent's own view ("I'm still trying") diverges from what an external observer sees (error rate rising, budget depleting, no progress). This gap is the entire reason a control plane exists.
125
+
126
+ 2. **Event-level checks cannot detect temporal failure modes.** Every trigger that requires memory across turns (stall, convergence, recovery) needs the State Engine. Single-event or simple-counter approaches score 0% on these.
127
+
128
+ 3. **Safety and intelligence need different authority levels.** Dangerous-command blocking is deterministic and non-overridable. Everything else benefits from a probabilistic second opinion. Conflating the two makes the safety guard probabilistic, which defeats the purpose.
129
+
130
+ 4. **Agent-execution monitoring alone cannot detect world-state corruption.** If the agent writes destructive content without producing any error, no amount of execution telemetry will catch it. This requires comparing actual filesystem state against a task contract. It's the clearest motivation for v0.2.
131
+
132
+ ## Running it
133
+
134
+ ### Product quickstart (edward CLI)
135
+
136
+ ```bash
137
+ pip install -e . # zero dependencies, stdlib only (Python >= 3.11)
138
+
139
+ # optional: the pi coding agent (full monitoring + intervention on pi tasks)
140
+ npm install -g --ignore-scripts @earendil-works/pi-coding-agent
141
+
142
+ edward doctor # environment checks: scorer, pi, audit dir
143
+ edward demo # self-running proof: 6 scenarios, PASS/FAIL gate
144
+ edward wrap -- pi "Fix the bug in utils.py so that the test passes"
145
+ edward wrap --scope ./src --no-scorer -- python agent_script.py # any command, rule-only
146
+ edward eval --policy conservative --trials 30 # tune a policy before live use
147
+ edward audit # intervention summary ($ saved evidence)
148
+ edward policy-template --preset balanced > edward.toml
149
+ ```
150
+
151
+ The semantic scorer is an internal HTTP endpoint (LAN only, no auth):
152
+ `GET /health` + `POST /v1/score` on Qwen/Qwen3.5-4B — see `edward/scorer_client.py`.
153
+ Without it, edward runs in rule-only mode and stays fully protective.
154
+
155
+ Exit codes: `0` completed, `75` PAUSED (resumable: `edward wrap --continue -- ...`),
156
+ `76` terminated by control plane, `130` interrupted. Audit JSONL lands in
157
+ `~/.edward/audit.jsonl`. Policy packs are TOML/JSON with three presets
158
+ (`conservative` / `balanced` = FROZEN defaults / `aggressive`).
159
+
160
+ ### Research scripts (pre-packaging, still work)
161
+
162
+ ```bash
163
+ python main.py "Fix the bug in utils.py so that the test passes"
164
+
165
+ # Run the benchmark
166
+ python benchmark.py
167
+
168
+ # Run the ablation
169
+ python ablation.py
170
+
171
+ # Run held-out evaluation
172
+ python heldout_eval.py
173
+
174
+ # Run robustness evaluation
175
+ python robustness_eval.py
176
+ ```
177
+
178
+ ### External benchmark: StepShield (NeurIPS 2026)
179
+
180
+ ```bash
181
+ git clone --depth 1 https://github.com/glo26/stepshield /tmp/stepshield
182
+ edward eval --suite stepshield --data /tmp/stepshield/data --mode rules
183
+ edward eval --suite stepshield --data /tmp/stepshield/data --mode contract \
184
+ --scorer http://192.168.2.51:8000 # needs live scorer endpoint
185
+ ```
186
+
187
+ `rules` = the deterministic v0.1 layer (dangerous-command regex + scope
188
+ violations derived from task constraints). `contract` = the v0.2 Task
189
+ Contract preview: task intent + constraints go into the scorer question,
190
+ and the 4B endpoint judges each state-changing step (OK / VIOLATION /
191
+ UNSURE) with asymmetric confirmation: conf >= 0.9 fires alone, 0.6-0.9
192
+ needs one corroborating violation, a confident OK clears suspicion.
193
+ Metrics are paper-aligned (EIR_k, recall, FPR on clean, per-category).
194
+
195
+ Pi uses `--mode rpc` for headless operation. The control plane spawns it as a subprocess, reads JSONL events from stdout, and sends control commands (abort, steer) via stdin. No Pi source code is modified.
196
+
197
+ ## Project structure
198
+
199
+ ```
200
+ edward/ Product package (pip install -e .)
201
+ cli.py edward CLI: wrap / demo / eval / audit / doctor
202
+ engine.py ControlPlane: events -> triggers -> scorer -> decision -> audit
203
+ config.py Policy packs (TOML/JSON, 3 presets, strict validation)
204
+ audit.py Append-only JSONL audit log (never blocks monitoring)
205
+ scorer.py Semantic scorer client with circuit breaker
206
+ scenarios.py Failure scenario suite (single source for demo/eval)
207
+ evalcmd.py Policy evaluation gate (detection / FPR / timing)
208
+ pi_client.py Pi RPC client (cwd, provider/model, stderr capture)
209
+ scorer_client.py /v1/score client (internal endpoint, Qwen3.5-4B)
210
+ state_engine.py Materialize AgentState from event stream
211
+ triggers.py 8 trigger rules, policy-parameterized (defaults FROZEN)
212
+ kernel.py Decision authority hierarchy
213
+ canonical_events.py Normalize Pi/Codex/custom events to canonical schema
214
+ notify.py Slack webhook + stderr banners (fail-silent)
215
+ main.py Legacy entry -> edward wrap
216
+ benchmark.py 300-trial held-out benchmark (re-exports scenarios)
217
+ ablation.py Watchdog vs State Engine comparison
218
+ heldout_eval.py Dev/test split + frozen config evaluation
219
+ robustness_eval.py 4-dimension robustness attack
220
+ extreme_tests.py 4 extreme scenario demos
221
+ test_product.py Product test suite (unittest, offline)
222
+ ```
223
+
224
+ ## What's next
225
+
226
+ The silent-corruption gap points at the next layer: a Task Contract that defines which files the agent should touch and what the expected end-state looks like. When the agent's actual filesystem mutations diverge from the contract, that's a scope violation — regardless of whether any tool call returned an error. This moves the State Engine from "what is the agent doing" to "what is the agent doing to the world, and is that still allowed."
@@ -0,0 +1,8 @@
1
+ """edward — external control plane for AI coding agents."""
2
+
3
+ __version__ = "0.1.1"
4
+
5
+ from .config import Policy, PolicyError, load_policy # noqa: F401
6
+ from .engine import ControlPlane, Decision # noqa: F401
7
+ from .audit import AuditLog # noqa: F401
8
+ from .scorer import Scorer # noqa: F401
@@ -0,0 +1,112 @@
1
+ """Append-only JSONL audit log.
2
+
3
+ Never blocks or crashes the control plane: if the disk write fails, the
4
+ entry is echoed to stderr once and monitoring continues. Rotates at 50 MB
5
+ by renaming to <name>.1.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+
14
+
15
+ MAX_BYTES = 50 * 1024 * 1024
16
+ SCHEMA_VERSION = 1
17
+
18
+
19
+ class AuditLog:
20
+ def __init__(self, path=None):
21
+ self.path = Path(path) if path else None
22
+ self._degraded = False
23
+ if self.path:
24
+ try:
25
+ self.path.parent.mkdir(parents=True, exist_ok=True)
26
+ except OSError as exc:
27
+ self._degrade(f"cannot create audit dir {self.path.parent}: {exc}")
28
+
29
+ def _degrade(self, msg: str) -> None:
30
+ if not self._degraded:
31
+ self._degraded = True
32
+ print(f"[edward] audit degraded to stderr: {msg}", file=sys.stderr)
33
+
34
+ def emit(self, entry_type: str, session: str = "", **fields) -> None:
35
+ record = {
36
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
37
+ "v": SCHEMA_VERSION,
38
+ "type": entry_type,
39
+ "session": session,
40
+ }
41
+ record.update(fields)
42
+ line = json.dumps(record, default=str, ensure_ascii=False)
43
+ if self.path is None:
44
+ return
45
+ try:
46
+ if self.path.exists() and self.path.stat().st_size > MAX_BYTES:
47
+ rotated = self.path.with_suffix(self.path.suffix + ".1")
48
+ os.replace(self.path, rotated)
49
+ with open(self.path, "a", encoding="utf-8") as fh:
50
+ fh.write(line + "\n")
51
+ except OSError as exc:
52
+ self._degrade(f"{exc}")
53
+ print(f"[edward] audit: {line}", file=sys.stderr)
54
+
55
+ def session_start(self, session: str, policy_preset: str, command) -> None:
56
+ self.emit("session_start", session=session, policy_preset=policy_preset,
57
+ command=[str(c) for c in command])
58
+
59
+ def intervention(self, session: str, trigger_reason: str, decision_type: str,
60
+ action: str, source: str, authority: str, mss: dict,
61
+ jev: dict = None, est_avoided_usd: float = None) -> None:
62
+ self.emit("intervention", session=session, trigger_reason=trigger_reason,
63
+ decision_type=decision_type, action=action, source=source,
64
+ authority=authority, mss=mss, jev=jev or {},
65
+ est_avoided_usd=est_avoided_usd)
66
+
67
+ def session_end(self, session: str, reason: str, exit_code: int) -> None:
68
+ self.emit("session_end", session=session, reason=reason, exit_code=exit_code)
69
+
70
+
71
+ def summarize(path) -> dict:
72
+ """Read an audit file and aggregate the numbers that matter."""
73
+ path = Path(path)
74
+ summary = {
75
+ "file": str(path), "sessions": 0, "interventions": 0,
76
+ "by_action": {}, "by_decision_type": {}, "tokens_at_intervention": [],
77
+ "cost_usd_at_intervention": [], "est_avoided_usd": 0.0,
78
+ "first_ts": None, "last_ts": None,
79
+ }
80
+ sessions = set()
81
+ if not path.exists():
82
+ return summary
83
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
84
+ for raw in fh:
85
+ raw = raw.strip()
86
+ if not raw:
87
+ continue
88
+ try:
89
+ rec = json.loads(raw)
90
+ except json.JSONDecodeError:
91
+ continue
92
+ ts = rec.get("ts")
93
+ if ts:
94
+ summary["first_ts"] = summary["first_ts"] or ts
95
+ summary["last_ts"] = ts
96
+ if rec.get("session"):
97
+ sessions.add(rec["session"])
98
+ if rec.get("type") == "intervention":
99
+ summary["interventions"] += 1
100
+ action = rec.get("action", "?")
101
+ summary["by_action"][action] = summary["by_action"].get(action, 0) + 1
102
+ dtype = rec.get("decision_type", "?")
103
+ summary["by_decision_type"][dtype] = summary["by_decision_type"].get(dtype, 0) + 1
104
+ mss = rec.get("mss") or {}
105
+ if mss.get("token_usage"):
106
+ summary["tokens_at_intervention"].append(mss["token_usage"])
107
+ if mss.get("cost_usd"):
108
+ summary["cost_usd_at_intervention"].append(mss["cost_usd"])
109
+ if rec.get("est_avoided_usd") is not None:
110
+ summary["est_avoided_usd"] += rec["est_avoided_usd"]
111
+ summary["sessions"] = len(sessions)
112
+ return summary