@oussamadouhou/agent-enforcement 0.1.2
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.
- package/.claude-plugin/plugin.json +5 -0
- package/README.md +67 -0
- package/dist/adapters/logger.d.ts +23 -0
- package/dist/adapters/logger.d.ts.map +1 -0
- package/dist/adapters/logger.js +58 -0
- package/dist/adapters/logger.js.map +1 -0
- package/dist/adapters/message-reader.d.ts +20 -0
- package/dist/adapters/message-reader.d.ts.map +1 -0
- package/dist/adapters/message-reader.js +123 -0
- package/dist/adapters/message-reader.js.map +1 -0
- package/dist/constants.d.ts +22 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +50 -0
- package/dist/constants.js.map +1 -0
- package/dist/detection/environment.d.ts +7 -0
- package/dist/detection/environment.d.ts.map +1 -0
- package/dist/detection/environment.js +30 -0
- package/dist/detection/environment.js.map +1 -0
- package/dist/hook.d.ts +35 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +223 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -0
- package/dist/schema/verification-schema.d.ts +127 -0
- package/dist/schema/verification-schema.d.ts.map +1 -0
- package/dist/schema/verification-schema.js +132 -0
- package/dist/schema/verification-schema.js.map +1 -0
- package/docs/EVIDENCE_SYSTEM.md +214 -0
- package/fixtures/claude/project-abc/session-123.jsonl +2 -0
- package/fixtures/opencode/messages/session-123/msg_001.json +4 -0
- package/hooks/enforcement.py +255 -0
- package/hooks/hooks.json +15 -0
- package/package.json +70 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Evidence-Based Trust System
|
|
2
|
+
|
|
3
|
+
**Version**: 1.0.0
|
|
4
|
+
**Plugin**: @oussamadouhou/agent-enforcement
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The Evidence-Based Trust System prevents AI agents from marking tasks complete without providing verifiable evidence of completion.
|
|
11
|
+
|
|
12
|
+
### Purpose
|
|
13
|
+
|
|
14
|
+
- **Prevent premature completion**: No marking todos done without proof
|
|
15
|
+
- **Enforce accountability**: Every completion requires evidence
|
|
16
|
+
- **Enable verification**: Evidence must be independently verifiable
|
|
17
|
+
- **Build trust**: Ground truth over assumptions
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Enforcement Levels
|
|
22
|
+
|
|
23
|
+
Set via environment variable: `OPENCODE_ENFORCEMENT_LEVEL`
|
|
24
|
+
|
|
25
|
+
| Level | Value | Behavior |
|
|
26
|
+
|-------|-------|----------|
|
|
27
|
+
| **CREATIVE** | `0` | No enforcement (brainstorming, planning) |
|
|
28
|
+
| **STANDARD** | `1` | Warning in tool output (default) |
|
|
29
|
+
| **STRICT** | `2` | Block tool execution with error |
|
|
30
|
+
|
|
31
|
+
**Set level:**
|
|
32
|
+
```bash
|
|
33
|
+
export OPENCODE_ENFORCEMENT_LEVEL=1 # 0, 1, or 2
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Required Evidence Format
|
|
39
|
+
|
|
40
|
+
Before marking any todo as `completed`, you **MUST** provide an evidence block:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
**Evidence for [todo-id]**:
|
|
44
|
+
**Execution**: [command/tool used]
|
|
45
|
+
**Verification**: [what was checked]
|
|
46
|
+
**Checklist**:
|
|
47
|
+
- [x] Tool executed successfully
|
|
48
|
+
- [x] Output captured
|
|
49
|
+
- [x] Result matches expected
|
|
50
|
+
- [x] No workarounds used
|
|
51
|
+
- [x] Independently verifiable
|
|
52
|
+
**Trust**: 🟢 HIGH | ✅ Ground Truth
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Checklist Items (All Required)
|
|
58
|
+
|
|
59
|
+
### 1. Tool executed successfully
|
|
60
|
+
- ✅ Tool returned exit code 0 (bash)
|
|
61
|
+
- ✅ Tool completed without errors
|
|
62
|
+
- ✅ Output was generated
|
|
63
|
+
- ❌ NOT: "I think it worked"
|
|
64
|
+
|
|
65
|
+
### 2. Output captured
|
|
66
|
+
- ✅ Tool output included in response
|
|
67
|
+
- ✅ Output is readable and complete
|
|
68
|
+
- ✅ Output shown to user
|
|
69
|
+
- ❌ NOT: Hidden or summarized
|
|
70
|
+
|
|
71
|
+
### 3. Result matches expected
|
|
72
|
+
- ✅ Output contains expected data
|
|
73
|
+
- ✅ Changes are visible in files/system
|
|
74
|
+
- ✅ Verification command confirms success
|
|
75
|
+
- ❌ NOT: Assumed without checking
|
|
76
|
+
|
|
77
|
+
### 4. No workarounds used
|
|
78
|
+
- ✅ Proper solution implemented
|
|
79
|
+
- ✅ No hacks or temporary fixes
|
|
80
|
+
- ✅ Follows best practices
|
|
81
|
+
- ❌ NOT: `as any`, ignoring errors, deleting tests
|
|
82
|
+
|
|
83
|
+
### 5. Independently verifiable
|
|
84
|
+
- ✅ Another person/agent can verify
|
|
85
|
+
- ✅ Evidence is objective
|
|
86
|
+
- ✅ Not dependent on hidden state
|
|
87
|
+
- ❌ NOT: "Trust me"
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Trust Markers (Required)
|
|
92
|
+
|
|
93
|
+
### Confidence Level
|
|
94
|
+
- 🟢 **HIGH**: Direct observation, ground truth
|
|
95
|
+
- 🟡 **MEDIUM**: Indirect verification, inference
|
|
96
|
+
- 🔴 **LOW**: Assumption, no verification
|
|
97
|
+
|
|
98
|
+
### Evidence Type
|
|
99
|
+
- ✅ **Ground Truth**: Tool executed, output captured
|
|
100
|
+
- ⚠️ **Simulation**: Mental model, not executed
|
|
101
|
+
- ❌ **Assumption**: No verification attempted
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
### ✅ GOOD: Complete Evidence
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
I'll check the working directory.
|
|
111
|
+
|
|
112
|
+
**Evidence for todo-1**:
|
|
113
|
+
**Execution**: `pwd` command via bash tool
|
|
114
|
+
**Verification**: Output shows `/home/user/project`
|
|
115
|
+
**Checklist**:
|
|
116
|
+
- [x] Tool executed successfully (exit code 0)
|
|
117
|
+
- [x] Output captured: `/home/user/project`
|
|
118
|
+
- [x] Result matches expected (correct directory)
|
|
119
|
+
- [x] No workarounds used (standard pwd command)
|
|
120
|
+
- [x] Independently verifiable (anyone can run pwd)
|
|
121
|
+
**Trust**: 🟢 HIGH | ✅ Ground Truth
|
|
122
|
+
|
|
123
|
+
*Now marking todo-1 complete*
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### ❌ BAD: No Evidence
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
I checked the working directory.
|
|
130
|
+
|
|
131
|
+
*Marking todo-1 complete* ← VIOLATION
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### ❌ BAD: Incomplete Checklist
|
|
135
|
+
|
|
136
|
+
```markdown
|
|
137
|
+
**Evidence for todo-1**:
|
|
138
|
+
**Execution**: `pwd`
|
|
139
|
+
**Verification**: It worked
|
|
140
|
+
**Checklist**:
|
|
141
|
+
- [x] Tool executed
|
|
142
|
+
- [ ] Output captured ← MISSING
|
|
143
|
+
**Trust**: 🟢 HIGH
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## When Evidence is NOT Required
|
|
149
|
+
|
|
150
|
+
- ❌ Todo status is `pending`, `in_progress`, `cancelled`
|
|
151
|
+
- ✅ Only `completed` status requires evidence
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Violations
|
|
156
|
+
|
|
157
|
+
### What Happens (STANDARD Mode)
|
|
158
|
+
|
|
159
|
+
When you try to mark a todo complete without evidence:
|
|
160
|
+
|
|
161
|
+
1. **Tool completes normally** (todo is updated)
|
|
162
|
+
2. **Warning appears in tool output** (you see it in response)
|
|
163
|
+
3. **You should acknowledge** and provide evidence before proceeding
|
|
164
|
+
|
|
165
|
+
### What Happens (STRICT Mode)
|
|
166
|
+
|
|
167
|
+
When you try to mark a todo complete without evidence:
|
|
168
|
+
|
|
169
|
+
1. **Tool execution is BLOCKED** (error thrown)
|
|
170
|
+
2. **Todo is NOT updated**
|
|
171
|
+
3. **You MUST provide evidence** then retry
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## FAQ
|
|
176
|
+
|
|
177
|
+
### Q: Why is this necessary?
|
|
178
|
+
**A**: Prevents agents from claiming tasks are done without proof, improves reliability and accountability.
|
|
179
|
+
|
|
180
|
+
### Q: Can I disable this?
|
|
181
|
+
**A**: Set `OPENCODE_ENFORCEMENT_LEVEL=0` for CREATIVE mode (no enforcement).
|
|
182
|
+
|
|
183
|
+
### Q: What if I'm just planning?
|
|
184
|
+
**A**: Use CREATIVE mode (level 0) for planning/brainstorming.
|
|
185
|
+
|
|
186
|
+
### Q: Can I provide evidence after completion?
|
|
187
|
+
**A**: No - evidence must be provided BEFORE marking complete.
|
|
188
|
+
|
|
189
|
+
### Q: What counts as valid evidence?
|
|
190
|
+
**A**: Tool execution output, file contents, command results - anything independently verifiable.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Technical Details
|
|
195
|
+
|
|
196
|
+
### Hook Points
|
|
197
|
+
- `tool.execute.before`: Validation logic
|
|
198
|
+
- `tool.execute.after`: Warning injection
|
|
199
|
+
|
|
200
|
+
### Detection
|
|
201
|
+
- Monitors `todowrite` tool calls
|
|
202
|
+
- Checks last assistant message for evidence block
|
|
203
|
+
- Validates checklist items and trust markers
|
|
204
|
+
|
|
205
|
+
### Warning Injection
|
|
206
|
+
- Appends `<system-reminder>` to tool output
|
|
207
|
+
- LLM sees warning in tool response
|
|
208
|
+
- Human sees toast notification in TUI
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
**Documentation Location**: `~/.config/opencode/plugins/agent-enforcement/docs/EVIDENCE_SYSTEM.md`
|
|
213
|
+
**Plugin Repository**: https://github.com/oussamadouhou/agent-enforcement
|
|
214
|
+
**Issues**: https://github.com/oussamadouhou/agent-enforcement/issues
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"role":"user","content":[{"type":"text","text":"Run the check"}]}
|
|
2
|
+
{"role":"assistant","content":[{"type":"text","text":"**Evidence for task-1**:\n\n**Execution**:\n- Tool: read\n\n**Verification**:\n- Checked output\n\n**Checklist**:\n- [x] Tool executed successfully\n- [x] Output captured\n- [x] Result matches expected\n- [x] No workarounds used\n- [x] Independently verifiable\n\n**Trust**: 🟢 HIGH | ✅ Ground Truth"}]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"role": "assistant",
|
|
3
|
+
"text": "**Evidence for task-1**:\n\n**Execution**:\n- Tool: read\n\n**Verification**:\n- Checked output\n\n**Checklist**:\n- [x] Tool executed successfully\n- [x] Output captured\n- [x] Result matches expected\n- [x] No workarounds used\n- [x] Independently verifiable\n\n**Trust**: 🟢 HIGH | ✅ Ground Truth"
|
|
4
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Code hook handler for evidence enforcement.
|
|
4
|
+
Reads stdin (hook input), validates evidence, outputs JSON result.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
HOOK_NAME = "evidence-enforcement"
|
|
17
|
+
|
|
18
|
+
ENFORCEMENT_LEVELS = {
|
|
19
|
+
"CREATIVE": 0,
|
|
20
|
+
"STANDARD": 1,
|
|
21
|
+
"STRICT": 2,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
EVIDENCE_PATTERNS = {
|
|
25
|
+
"evidence_block": re.compile(r"\*\*Evidence(?:\s+for\s+[\w-]+)?\*\*:", re.IGNORECASE),
|
|
26
|
+
"execution_section": re.compile(r"\*\*Execution\*\*:", re.IGNORECASE),
|
|
27
|
+
"verification_section": re.compile(r"\*\*Verification\*\*:", re.IGNORECASE),
|
|
28
|
+
"checklist_section": re.compile(r"\*\*Checklist\*\*:", re.IGNORECASE),
|
|
29
|
+
"trust_markers": re.compile(r"\*\*(?:Confidence|Trust)\*\*:", re.IGNORECASE),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
REQUIRED_CHECKLIST_ITEMS = [
|
|
33
|
+
"Tool executed",
|
|
34
|
+
"Output captured",
|
|
35
|
+
"Result matches",
|
|
36
|
+
"No workarounds",
|
|
37
|
+
"Independently verifiable",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Logger:
|
|
42
|
+
def __init__(self, mode: str, path: str, level: str) -> None:
|
|
43
|
+
self.mode = mode
|
|
44
|
+
self.path = path
|
|
45
|
+
self.level = level
|
|
46
|
+
self.level_map = {"debug": 10, "info": 20, "warn": 30, "error": 40}
|
|
47
|
+
|
|
48
|
+
def _should_log(self, level: str) -> bool:
|
|
49
|
+
if self.mode == "silent":
|
|
50
|
+
return False
|
|
51
|
+
return self.level_map.get(level, 20) >= self.level_map.get(self.level, 20)
|
|
52
|
+
|
|
53
|
+
def _log(self, level: str, msg: str) -> None:
|
|
54
|
+
if not self._should_log(level):
|
|
55
|
+
return
|
|
56
|
+
timestamp = datetime.utcnow().isoformat()
|
|
57
|
+
formatted = f"[{timestamp}] [{level.upper()}] {msg}"
|
|
58
|
+
if self.mode == "file":
|
|
59
|
+
try:
|
|
60
|
+
with open(self.path, "a", encoding="utf-8") as handle:
|
|
61
|
+
handle.write(formatted + "\n")
|
|
62
|
+
except Exception:
|
|
63
|
+
return
|
|
64
|
+
elif self.mode == "console":
|
|
65
|
+
print(formatted)
|
|
66
|
+
|
|
67
|
+
def info(self, msg: str) -> None:
|
|
68
|
+
self._log("info", msg)
|
|
69
|
+
|
|
70
|
+
def warn(self, msg: str) -> None:
|
|
71
|
+
self._log("warn", msg)
|
|
72
|
+
|
|
73
|
+
def error(self, msg: str) -> None:
|
|
74
|
+
self._log("error", msg)
|
|
75
|
+
|
|
76
|
+
def debug(self, msg: str) -> None:
|
|
77
|
+
self._log("debug", msg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_enforcement_level() -> int:
|
|
81
|
+
env_level = os.environ.get("ENFORCEMENT_LEVEL") or os.environ.get("OPENCODE_ENFORCEMENT_LEVEL")
|
|
82
|
+
if not env_level:
|
|
83
|
+
return ENFORCEMENT_LEVELS["STANDARD"]
|
|
84
|
+
if env_level.isdigit():
|
|
85
|
+
parsed = int(env_level)
|
|
86
|
+
if parsed in (0, 1, 2):
|
|
87
|
+
return parsed
|
|
88
|
+
upper = env_level.upper()
|
|
89
|
+
return ENFORCEMENT_LEVELS.get(upper, ENFORCEMENT_LEVELS["STANDARD"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_log_config() -> Dict[str, str]:
|
|
93
|
+
return {
|
|
94
|
+
"mode": os.environ.get("ENFORCEMENT_LOG_MODE", "file"),
|
|
95
|
+
"path": os.environ.get("ENFORCEMENT_LOG_PATH", "/tmp/agent-enforcement.log"),
|
|
96
|
+
"level": os.environ.get("ENFORCEMENT_LOG_LEVEL", "info"),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def extract_text(message: Dict[str, Any]) -> Optional[str]:
|
|
101
|
+
text = message.get("text")
|
|
102
|
+
if isinstance(text, str):
|
|
103
|
+
return text
|
|
104
|
+
content = message.get("content")
|
|
105
|
+
if isinstance(content, list):
|
|
106
|
+
for part in content:
|
|
107
|
+
if isinstance(part, dict) and part.get("type") == "text":
|
|
108
|
+
part_text = part.get("text")
|
|
109
|
+
if isinstance(part_text, str):
|
|
110
|
+
return part_text
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_last_assistant_message(session_id: str) -> Optional[str]:
|
|
115
|
+
projects_dir = Path.home() / ".claude" / "projects"
|
|
116
|
+
if not projects_dir.exists():
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
for project_dir in projects_dir.iterdir():
|
|
120
|
+
if not project_dir.is_dir():
|
|
121
|
+
continue
|
|
122
|
+
session_file = project_dir / f"{session_id}.jsonl"
|
|
123
|
+
if not session_file.exists():
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
lines = session_file.read_text(encoding="utf-8").splitlines()
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
for line in reversed(lines):
|
|
130
|
+
try:
|
|
131
|
+
message = json.loads(line)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
continue
|
|
134
|
+
if message.get("role") == "assistant":
|
|
135
|
+
return extract_text(message) or line
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def validate_message_text(message_text: str) -> Tuple[bool, List[str]]:
|
|
140
|
+
missing_items: List[str] = []
|
|
141
|
+
for item in REQUIRED_CHECKLIST_ITEMS:
|
|
142
|
+
item_regex = re.compile(rf"\[\s*[xX]\s*\].*{re.escape(item)}", re.IGNORECASE)
|
|
143
|
+
if not item_regex.search(message_text):
|
|
144
|
+
missing_items.append(item)
|
|
145
|
+
|
|
146
|
+
has_evidence_block = bool(EVIDENCE_PATTERNS["evidence_block"].search(message_text))
|
|
147
|
+
has_trust_markers = bool(EVIDENCE_PATTERNS["trust_markers"].search(message_text))
|
|
148
|
+
|
|
149
|
+
if not has_evidence_block:
|
|
150
|
+
return False, ["Evidence block missing"]
|
|
151
|
+
if missing_items:
|
|
152
|
+
return False, missing_items
|
|
153
|
+
if not has_trust_markers:
|
|
154
|
+
return False, ["Trust markers missing"]
|
|
155
|
+
return True, []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_error_message(missing: List[str], level: int) -> str:
|
|
159
|
+
status = "BLOCKED" if level == ENFORCEMENT_LEVELS["STRICT"] else "WARNING"
|
|
160
|
+
if "Evidence block missing" in missing:
|
|
161
|
+
return (
|
|
162
|
+
f"[{HOOK_NAME}] {status}: Cannot mark todo complete without evidence block.\n\n"
|
|
163
|
+
"Required format:\n"
|
|
164
|
+
"**Evidence for [todo-id]**:\n"
|
|
165
|
+
"**Execution**: [command/tool used]\n"
|
|
166
|
+
"**Verification**: [what was checked]\n"
|
|
167
|
+
"**Checklist**:\n"
|
|
168
|
+
"- [x] Tool executed successfully\n"
|
|
169
|
+
"- [x] Output captured\n"
|
|
170
|
+
"- [x] Result matches expected\n"
|
|
171
|
+
"- [x] No workarounds used\n"
|
|
172
|
+
"- [x] Independently verifiable\n"
|
|
173
|
+
"**Trust**: 🟢 HIGH | ✅ Ground Truth\n\n"
|
|
174
|
+
"See ~/AGENTS.md \"Evidence-Based Trust System\" for details."
|
|
175
|
+
)
|
|
176
|
+
if "Trust markers missing" in missing:
|
|
177
|
+
return (
|
|
178
|
+
f"[{HOOK_NAME}] {status}: Trust markers missing.\n\n"
|
|
179
|
+
"Required:\n"
|
|
180
|
+
"**Confidence**: 🟢 HIGH | 🟡 MEDIUM | 🔴 LOW\n"
|
|
181
|
+
"**Evidence**: ✅ Ground Truth | ⚠️ Simulation | ❌ Assumption"
|
|
182
|
+
)
|
|
183
|
+
checklist = "\n".join([f"- [ ] {item}" for item in missing])
|
|
184
|
+
return (
|
|
185
|
+
f"[{HOOK_NAME}] {status}: Evidence checklist incomplete.\n\n"
|
|
186
|
+
f"Missing checklist items:\n{checklist}\n\n"
|
|
187
|
+
"All items must be checked [x] before marking todo complete."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def parse_todos(raw_todos: Any) -> List[Dict[str, Any]]:
|
|
192
|
+
if isinstance(raw_todos, list):
|
|
193
|
+
return [t for t in raw_todos if isinstance(t, dict)]
|
|
194
|
+
if isinstance(raw_todos, str):
|
|
195
|
+
try:
|
|
196
|
+
parsed = json.loads(raw_todos)
|
|
197
|
+
if isinstance(parsed, list):
|
|
198
|
+
return [t for t in parsed if isinstance(t, dict)]
|
|
199
|
+
except json.JSONDecodeError:
|
|
200
|
+
return []
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main() -> None:
|
|
205
|
+
input_data = json.loads(sys.stdin.read())
|
|
206
|
+
tool = input_data.get("tool", "")
|
|
207
|
+
session_id = input_data.get("sessionID", "")
|
|
208
|
+
args = input_data.get("args", {}) if isinstance(input_data.get("args"), dict) else {}
|
|
209
|
+
raw_todos = args.get("todos", [])
|
|
210
|
+
|
|
211
|
+
if tool != "todowrite":
|
|
212
|
+
print(json.dumps({"action": "allow"}))
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
level = get_enforcement_level()
|
|
216
|
+
log_config = get_log_config()
|
|
217
|
+
logger = Logger(**log_config)
|
|
218
|
+
|
|
219
|
+
if level == ENFORCEMENT_LEVELS["CREATIVE"]:
|
|
220
|
+
logger.info(f"[{HOOK_NAME}] CREATIVE mode - no enforcement")
|
|
221
|
+
print(json.dumps({"action": "allow"}))
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
todos = parse_todos(raw_todos)
|
|
225
|
+
completed = [t for t in todos if t.get("status") == "completed"]
|
|
226
|
+
if not completed:
|
|
227
|
+
print(json.dumps({"action": "allow"}))
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
last_message = read_last_assistant_message(session_id)
|
|
231
|
+
if not last_message:
|
|
232
|
+
message = build_error_message(["Evidence block missing"], level)
|
|
233
|
+
if level == ENFORCEMENT_LEVELS["STRICT"]:
|
|
234
|
+
print(json.dumps({"action": "block", "message": message}))
|
|
235
|
+
else:
|
|
236
|
+
logger.warn(message)
|
|
237
|
+
print(json.dumps({"action": "allow"}))
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
valid, missing = validate_message_text(last_message)
|
|
241
|
+
if valid:
|
|
242
|
+
logger.info(f"[{HOOK_NAME}] Evidence validation passed")
|
|
243
|
+
print(json.dumps({"action": "allow"}))
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
message = build_error_message(missing, level)
|
|
247
|
+
if level == ENFORCEMENT_LEVELS["STRICT"]:
|
|
248
|
+
print(json.dumps({"action": "block", "message": message}))
|
|
249
|
+
else:
|
|
250
|
+
logger.warn(message)
|
|
251
|
+
print(json.dumps({"action": "allow"}))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
main()
|
package/hooks/hooks.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oussamadouhou/agent-enforcement",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Evidence enforcement hooks for OpenCode and Claude Code",
|
|
5
|
+
"main": "dist/plugin.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "dist/plugin.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/plugin.d.ts",
|
|
11
|
+
"import": "./dist/plugin.js",
|
|
12
|
+
"default": "./dist/plugin.js"
|
|
13
|
+
},
|
|
14
|
+
"./index": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./schema": {
|
|
20
|
+
"types": "./dist/schema/verification-schema.d.ts",
|
|
21
|
+
"import": "./dist/schema/verification-schema.js",
|
|
22
|
+
"default": "./dist/schema/verification-schema.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"hooks",
|
|
28
|
+
"docs",
|
|
29
|
+
".claude-plugin",
|
|
30
|
+
"fixtures"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun run clean && bun run compile",
|
|
34
|
+
"clean": "rm -rf dist",
|
|
35
|
+
"compile": "bunx tsc",
|
|
36
|
+
"prepublishOnly": "bun run build",
|
|
37
|
+
"test": "bun test"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"opencode",
|
|
41
|
+
"claude-code",
|
|
42
|
+
"enforcement",
|
|
43
|
+
"evidence",
|
|
44
|
+
"todo"
|
|
45
|
+
],
|
|
46
|
+
"author": "Oussama Douhou",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/oussamadouhou/agent-enforcement.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/oussamadouhou/agent-enforcement#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/oussamadouhou/agent-enforcement/issues"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@opencode-ai/plugin": "^1.1.21"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^20.0.0",
|
|
64
|
+
"typescript": "^5.0.0",
|
|
65
|
+
"bun-types": "^1.0.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|