coding-guardrails 0.7.2__tar.gz → 0.7.4__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.
- coding_guardrails-0.7.4/CLAUDE.md +174 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/PKG-INFO +1 -1
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/pyproject.toml +1 -1
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/handler.py +14 -7
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/loop_detection.py +6 -2
- coding_guardrails-0.7.2/CLAUDE.md +0 -189
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/.github/workflows/ci.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/.gitignore +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/Dockerfile +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/LICENSE +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/README.md +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/aider.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/claude-code.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/guardrail-config.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/opencode.json +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/pi.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docker-compose.yaml +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/agents.md +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/architecture.md +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/models.md +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/rules.md +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/__main__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/cli.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/config.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/eval.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/middleware.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/profiles.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/registry.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/client.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/layer1.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/server.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/base.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/commands.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/network.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/path_safety.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/prerequisites.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/secrets.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sensitive_files.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sequencing.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/session_budget.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/thoroughness.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/tool_resolution.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/value_fidelity.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/integration/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/integration/test_proxy.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/__init__.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_commands.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_config.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_loop_detection.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_middleware.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_network.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_path_safety.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_prerequisites.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_secrets.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_sensitive_files.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_sequencing.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_session_budget.py +0 -0
- {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_tool_resolution.py +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Coding Guardrails
|
|
2
|
+
|
|
3
|
+
An LLM proxy with safety guardrails, built on [Forge](https://github.com/antoinezambelli/forge) (vendored at `.vendors/forge/`).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
source .venv/bin/activate
|
|
9
|
+
pytest tests/unit/ -q # 233 tests, ~0.1s
|
|
10
|
+
uv pip install -e ".[dev]" # refresh editable install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
Two-layer proxy between agent and LLM:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Agent → :8081 (our proxy)
|
|
19
|
+
├── Layer 1: Forge (rescue, validate, retry, thinking capture)
|
|
20
|
+
├── Layer 2: Coding Guardrails (11 rules)
|
|
21
|
+
└── → :8080 (llama-server / LLM backend)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Request Flow
|
|
25
|
+
|
|
26
|
+
1. Agent sends OpenAI-compatible request to `:8081`
|
|
27
|
+
2. **Preprocessing**: empty user messages fixed, stale assistant text stripped
|
|
28
|
+
3. **Tool enforcement**: for real coding agents (bash/read/edit/write tools), inject guidance
|
|
29
|
+
4. **Layer 1 (Forge)**: run inference with rescue + retry + thinking capture
|
|
30
|
+
5. **Layer 2 (Guardrails)**: check tool calls against 11 rules
|
|
31
|
+
6. **Response**: blocked calls return text nudge to agent; allowed calls pass through
|
|
32
|
+
|
|
33
|
+
### Key Modules
|
|
34
|
+
|
|
35
|
+
| File | Purpose |
|
|
36
|
+
|------|---------|
|
|
37
|
+
| `proxy/handler.py` | Request pipeline: preprocessing → L1 → L2 → response |
|
|
38
|
+
| `proxy/layer1.py` | Instrumented Forge wrapper with thinking capture |
|
|
39
|
+
| `proxy/client.py` | `SafeLlamafileClient` — thinking tokens, max_tokens |
|
|
40
|
+
| `proxy/server.py` | Asyncio HTTP server, `/v1/chat/completions` |
|
|
41
|
+
| `middleware.py` | Composes all rules, `check()` / `record()` API |
|
|
42
|
+
| `cli.py` | `coding-guardrails serve` CLI |
|
|
43
|
+
| `rules/` | 11 guardrail rules |
|
|
44
|
+
|
|
45
|
+
### Guardrail Rules
|
|
46
|
+
|
|
47
|
+
| Rule | Action | Purpose |
|
|
48
|
+
|------|--------|---------|
|
|
49
|
+
| `prerequisites` | block | Read-before-edit |
|
|
50
|
+
| `path_safety` | block | Path traversal |
|
|
51
|
+
| `command_safety` | block | Destructive commands |
|
|
52
|
+
| `network` | block | Network access |
|
|
53
|
+
| `sensitive_files` | block | Protect sensitive files |
|
|
54
|
+
| `secrets` | block/mask | Secret detection |
|
|
55
|
+
| `loop_detection` | nudge→block | Repeated identical calls (3 nudge, 5 block) |
|
|
56
|
+
| `session_budget` | nudge | File/command budgets |
|
|
57
|
+
| `thoroughness` | nudge | Premature terminal submission |
|
|
58
|
+
| `sequencing` | nudge | Test-after-change |
|
|
59
|
+
| `tool_resolution` | nudge | Empty/error results |
|
|
60
|
+
|
|
61
|
+
## Production Rules
|
|
62
|
+
|
|
63
|
+
### Block Response Design
|
|
64
|
+
|
|
65
|
+
When Layer 2 blocks a tool call, the proxy returns a **text response** (not an empty tool call).
|
|
66
|
+
|
|
67
|
+
**Why**: Returning an empty tool call with the blocked tool name confuses agents — they see a tool call, execute it with empty args, get nothing, and retry the same call. Returning text makes the nudge visible to the agent and the model, providing a clear escape path.
|
|
68
|
+
|
|
69
|
+
**Loop detection blocks specifically** tell the model: "If the task is done, call respond() with your final answer. Otherwise try a completely different approach." This gives the model an explicit exit instead of vague guidance.
|
|
70
|
+
|
|
71
|
+
### Tool Enforcement
|
|
72
|
+
|
|
73
|
+
Only injected for **real coding agents** (requests that include bash/read/edit/write tools). The enforcement says:
|
|
74
|
+
|
|
75
|
+
> "Respond by calling tools. If the task is complete, call respond(). If unsure, call bash with 'echo ready'."
|
|
76
|
+
|
|
77
|
+
**Never** say "NEVER respond with plain text" — the model needs to finish tasks. The `respond()` tool is the proper terminal action.
|
|
78
|
+
|
|
79
|
+
### For eval/workflow requests (no coding tools)
|
|
80
|
+
|
|
81
|
+
No enforcement, no injection, no modification. The proxy is a transparent pass-through.
|
|
82
|
+
|
|
83
|
+
### Thinking Capture & Injection
|
|
84
|
+
|
|
85
|
+
Layer 1 captures thinking tokens from the model's response. On retry (failed validation), the thinking is injected into the retry nudge so the model doesn't re-think from scratch.
|
|
86
|
+
|
|
87
|
+
### Nudges vs Blocks
|
|
88
|
+
|
|
89
|
+
- **Nudge**: advisory — the call proceeds, message is logged. Only visible if the agent's runner supports nudge injection (Forge does for its own evals). For streaming agents (Pi), nudges are silently logged.
|
|
90
|
+
- **Block**: hard stop — the call is NOT passed through. The proxy returns a text response to the agent explaining why. This is the only reliable way to change model behavior mid-conversation.
|
|
91
|
+
|
|
92
|
+
## Running the Proxy
|
|
93
|
+
|
|
94
|
+
### tmux Sessions
|
|
95
|
+
|
|
96
|
+
| Session | Purpose | Port |
|
|
97
|
+
|---------|---------|------|
|
|
98
|
+
| `llm-server` | `llama-server` with model | :8080 |
|
|
99
|
+
| `guardrails` | `coding-guardrails serve` | :8081 |
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Start proxy
|
|
103
|
+
source .venv/bin/activate
|
|
104
|
+
coding-guardrails serve \
|
|
105
|
+
--backend-url http://localhost:8080 \
|
|
106
|
+
--model Qwen3.5-9B-UD-Q4_K_XL \
|
|
107
|
+
--port 8081 -v
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Client Quirks
|
|
111
|
+
|
|
112
|
+
- `SafeLlamafileClient` needs `gguf_path` (stem = model name): use `/tmp/Qwen3.5-9B-UD-Q4_K_XL.gguf`
|
|
113
|
+
- `recommended_sampling=False` — model not in Forge's registry
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pytest tests/unit/ -q # All 233 tests
|
|
119
|
+
pytest tests/unit/ -q -k "loop" # Specific rule
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
All 233 tests must pass before committing.
|
|
123
|
+
|
|
124
|
+
## Eval
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Full Forge 30-scenario benchmark (proxy mode)
|
|
128
|
+
python eval/scripts/run_forge_eval.py --mode proxy --runs 5
|
|
129
|
+
|
|
130
|
+
# Direct vs proxy comparison
|
|
131
|
+
python eval/scripts/run_forge_eval.py --mode both --runs 5
|
|
132
|
+
|
|
133
|
+
# Specific scenarios
|
|
134
|
+
python eval/scripts/run_forge_eval.py --mode proxy --scenario data_gap_recovery_extended
|
|
135
|
+
|
|
136
|
+
# Layer 2 guardrails
|
|
137
|
+
python eval/scripts/run_layer2_eval.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Results go to `eval/runs/<timestamp>/` (gitignored).
|
|
141
|
+
|
|
142
|
+
**Best result: 93% (140/150)** on Forge 30-scenario eval, +9pp over Forge's 84% baseline.
|
|
143
|
+
|
|
144
|
+
## Development Guidelines
|
|
145
|
+
|
|
146
|
+
- **Do NOT hack Forge source** — extend via public API, subclassing, wrapping
|
|
147
|
+
- All 233 unit tests must pass
|
|
148
|
+
- No hardcoded scenario-specific logic
|
|
149
|
+
- Block responses must return **text**, not empty tool calls
|
|
150
|
+
- Enforcement prompts must mention `respond()` as the exit tool
|
|
151
|
+
- No injection/modification for non-coding-agent requests
|
|
152
|
+
|
|
153
|
+
## Versioning & Releases
|
|
154
|
+
|
|
155
|
+
SemVer: `MAJOR.MINOR.PATCH`
|
|
156
|
+
|
|
157
|
+
- **PATCH**: Bug fixes, no new features
|
|
158
|
+
- **MINOR**: New features, new rules, backward-compatible
|
|
159
|
+
- **MAJOR**: Breaking API changes
|
|
160
|
+
|
|
161
|
+
### Release Process
|
|
162
|
+
|
|
163
|
+
1. Update `version` in `pyproject.toml`
|
|
164
|
+
2. Update README/CLAUDE.md if needed
|
|
165
|
+
3. `pytest tests/unit/ -q` — all 233 must pass
|
|
166
|
+
4. `git commit -m "vX.Y.Z: description" && git push`
|
|
167
|
+
5. `git tag vX.Y.Z && git push origin vX.Y.Z`
|
|
168
|
+
6. GitHub Actions: test → build → verify CLI → publish to PyPI
|
|
169
|
+
|
|
170
|
+
### CI Pipeline
|
|
171
|
+
|
|
172
|
+
- Push to `main`: tests + lint
|
|
173
|
+
- Tag `v*`: build → verify → publish to PyPI (trusted publishing, no tokens)
|
|
174
|
+
- Refresh local: `uv pip install -e ".[dev]" && coding-guardrails --version`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-guardrails
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Safe, reliable local coding agent proxy. Forge (rescue, retry, thinking capture) + 11 composable guardrail rules. 93% on Forge eval (Qwen3.5-9B).
|
|
5
5
|
Project-URL: Homepage, https://github.com/stawils/coding-guardrails
|
|
6
6
|
Project-URL: Repository, https://github.com/stawils/coding-guardrails
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coding-guardrails"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "Safe, reliable local coding agent proxy. Forge (rescue, retry, thinking capture) + 11 composable guardrail rules. 93% on Forge eval (Qwen3.5-9B)."
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
license = "MIT"
|
|
@@ -221,8 +221,9 @@ async def handle_chat_completions(
|
|
|
221
221
|
|
|
222
222
|
if _REAL_AGENT_TOOLS & tool_names_lower:
|
|
223
223
|
enforcement = (
|
|
224
|
-
"
|
|
225
|
-
"
|
|
224
|
+
"IMPORTANT: Respond by calling tools (bash, read, edit, write). "
|
|
225
|
+
"If you have completed the task and need to give a final answer, "
|
|
226
|
+
"call the respond tool. "
|
|
226
227
|
"If unsure what to do, call bash with 'echo ready'."
|
|
227
228
|
)
|
|
228
229
|
if openai_messages:
|
|
@@ -313,11 +314,15 @@ async def handle_chat_completions(
|
|
|
313
314
|
other_calls = [tc for tc in tool_calls if tc.tool != RESPOND_TOOL_NAME]
|
|
314
315
|
|
|
315
316
|
if respond_calls and not other_calls:
|
|
317
|
+
# Convert respond() to text — most agents (Pi, Cline, etc.)
|
|
318
|
+
# don't have a respond tool. The model is saying "I'm done."
|
|
319
|
+
msg = respond_calls[0].args.get("message", respond_calls[0].args.get("answer", ""))
|
|
316
320
|
attempts_tag = f"[%d attempt%s]" % (attempts, "s" if attempts != 1 else "") if attempts > 1 else ""
|
|
317
|
-
logger.info("✅ Layer 1 done %s (%s, respond: %s)",
|
|
318
|
-
attempts_tag, _fmt_elapsed(elapsed_l1),
|
|
319
|
-
|
|
320
|
-
|
|
321
|
+
logger.info("✅ Layer 1 done %s (%s, respond → text: %s)",
|
|
322
|
+
attempts_tag, _fmt_elapsed(elapsed_l1), _short(msg, 60))
|
|
323
|
+
if is_stream:
|
|
324
|
+
return text_to_sse_events(msg, model=model_name)
|
|
325
|
+
return text_response_to_openai(msg, model=model_name)
|
|
321
326
|
|
|
322
327
|
if not other_calls:
|
|
323
328
|
logger.info("⚠️ No actionable tool calls")
|
|
@@ -375,7 +380,9 @@ async def handle_chat_completions(
|
|
|
375
380
|
|
|
376
381
|
if is_stream:
|
|
377
382
|
return text_to_sse_events(nudge_text, model=model_name)
|
|
378
|
-
|
|
383
|
+
# Return as text so the agent sees the nudge directly.
|
|
384
|
+
# Empty-args tool-call blocks confuse agents into retrying.
|
|
385
|
+
return text_response_to_openai(nudge_text, model=model_name)
|
|
379
386
|
|
|
380
387
|
# All clear
|
|
381
388
|
logger.info("✅ Request PASSED (%s)", _fmt_elapsed(elapsed_l2))
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/loop_detection.py
RENAMED
|
@@ -78,7 +78,9 @@ class LoopDetectionRule:
|
|
|
78
78
|
return RuleResult.block(
|
|
79
79
|
call.tool,
|
|
80
80
|
nudge=f"You've called {call.tool} with the same arguments "
|
|
81
|
-
f"{count + 1} times. This isn't working
|
|
81
|
+
f"{count + 1} times. This isn't working. "
|
|
82
|
+
f"If the task is done, call respond() with your final answer. "
|
|
83
|
+
f"Otherwise, try a completely different approach.",
|
|
82
84
|
reason=f"loop detected: {call.tool} repeated {count + 1}x",
|
|
83
85
|
)
|
|
84
86
|
|
|
@@ -86,7 +88,9 @@ class LoopDetectionRule:
|
|
|
86
88
|
return RuleResult.nudge(
|
|
87
89
|
call.tool,
|
|
88
90
|
message=f"You've tried {call.tool} {count + 1} times with "
|
|
89
|
-
"the same arguments.
|
|
91
|
+
"the same arguments. If the task is complete, "
|
|
92
|
+
"call respond() with your answer. "
|
|
93
|
+
"Otherwise try a different approach.",
|
|
90
94
|
)
|
|
91
95
|
|
|
92
96
|
# ── Check 2: Stagnation (cycling same few tools, different args) ──
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
# Coding Guardrails
|
|
2
|
-
|
|
3
|
-
An LLM proxy with safety guardrails, built on [Forge](https://github.com/antoinezambelli/forge) (vendored at `.vendors/forge/`).
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Activate venv
|
|
9
|
-
source .venv/bin/activate
|
|
10
|
-
|
|
11
|
-
# Run tests
|
|
12
|
-
pytest tests/unit/ -q # 233 tests, ~0.1s
|
|
13
|
-
|
|
14
|
-
# Install editable
|
|
15
|
-
pip install -e .
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Architecture
|
|
19
|
-
|
|
20
|
-
Two-layer proxy sitting between agent and LLM:
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
Agent → :8081 (our proxy)
|
|
24
|
-
├── Layer 1: Forge (rescue, validate, retry, thinking capture)
|
|
25
|
-
├── Layer 2: Coding Guardrails (11 rules)
|
|
26
|
-
└── → :8080 (llama-server / LLM backend)
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Key Modules
|
|
30
|
-
|
|
31
|
-
| File | Purpose |
|
|
32
|
-
|------|---------|
|
|
33
|
-
| `src/coding_guardrails/proxy/handler.py` | Request pipeline: preprocessing → L1 (Forge) → L2 (guardrails) → response |
|
|
34
|
-
| `src/coding_guardrails/proxy/layer1.py` | Instrumented wrapper around Forge's `run_inference()` with thinking capture |
|
|
35
|
-
| `src/coding_guardrails/proxy/client.py` | `SafeLlamafileClient` — thinking token capture, max_tokens forwarding |
|
|
36
|
-
| `src/coding_guardrails/proxy/server.py` | Asyncio HTTP server, OpenAI-compatible `/v1/chat/completions` |
|
|
37
|
-
| `src/coding_guardrails/middleware.py` | Composes all guardrail rules, provides `check()` / `record()` API |
|
|
38
|
-
| `src/coding_guardrails/cli.py` | `coding-guardrails serve` CLI with `--backend-url`, `--model`, `--port`, `--log-file` |
|
|
39
|
-
| `src/coding_guardrails/rules/` | 11 guardrail rules (see below) |
|
|
40
|
-
|
|
41
|
-
### Guardrail Rules (Layer 2)
|
|
42
|
-
|
|
43
|
-
| Rule | Action | What it does |
|
|
44
|
-
|------|--------|-------------|
|
|
45
|
-
| `prerequisites` | block | Enforce read-before-edit |
|
|
46
|
-
| `path_safety` | block | Path traversal blocking |
|
|
47
|
-
| `command_safety` | block | Destructive command blocking |
|
|
48
|
-
| `network` | block | Network access control |
|
|
49
|
-
| `sensitive_files` | block | Protect sensitive files |
|
|
50
|
-
| `secrets` | block/mask | Secret detection |
|
|
51
|
-
| `loop_detection` | nudge/block | Stagnation detection (≤2 unique tools over 14+ calls) |
|
|
52
|
-
| `session_budget` | nudge | File/command/read budget limits |
|
|
53
|
-
| `thoroughness` | block | Premature terminal submission (low tool exploration ratio) |
|
|
54
|
-
| `sequencing` | nudge | Test-after-change suggestions |
|
|
55
|
-
| `tool_resolution` | nudge | Empty/error result handling |
|
|
56
|
-
|
|
57
|
-
## Running the Proxy
|
|
58
|
-
|
|
59
|
-
### tmux Sessions
|
|
60
|
-
|
|
61
|
-
Four tmux sessions are used:
|
|
62
|
-
|
|
63
|
-
| Session | Purpose | Port |
|
|
64
|
-
|---------|---------|------|
|
|
65
|
-
| `llm-server` | `llama-server` with Qwen3.5-9B-UD-Q4_K_XL | :8080 |
|
|
66
|
-
| `guardrails` | `coding-guardrails serve` proxy | :8081 |
|
|
67
|
-
| `skills-master` | General workspace | — |
|
|
68
|
-
| `pi` | Pi coding agent | — |
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
# Start llama-server (in tmux: llm-server)
|
|
72
|
-
llama-server -m /path/to/Qwen3.5-9B-UD-Q4_K_XL.gguf --port 8080 --host 0.0.0.0
|
|
73
|
-
|
|
74
|
-
# Start proxy (in tmux: guardrails)
|
|
75
|
-
source .venv/bin/activate
|
|
76
|
-
coding-guardrails serve --backend-url http://localhost:8080 --model Qwen3.5-9B-UD-Q4_K_XL --port 8081 -v
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Important Client Quirks
|
|
80
|
-
|
|
81
|
-
- `SafeLlamafileClient` needs `gguf_path` for model identity (stem = model name), not `model` — file doesn't need to exist: use `/tmp/Qwen3.5-9B-UD-Q4_K_XL.gguf`
|
|
82
|
-
- `recommended_sampling=False` required — Qwen3.5-9B-UD-Q4_K_XL not in Forge's MODEL_SAMPLING_DEFAULTS registry
|
|
83
|
-
|
|
84
|
-
## Testing
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
source .venv/bin/activate
|
|
88
|
-
pytest tests/unit/ -q # All 233 unit tests
|
|
89
|
-
pytest tests/unit/ -q -k "loop" # Specific rule tests
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
All 233 tests must pass before committing.
|
|
93
|
-
|
|
94
|
-
## Eval
|
|
95
|
-
|
|
96
|
-
### Forge 30-Scenario Eval
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
source .venv/bin/activate
|
|
100
|
-
|
|
101
|
-
# Proxy mode (5 runs × 30 scenarios = 150 calls)
|
|
102
|
-
python eval/scripts/run_forge_eval.py --mode proxy --runs 5
|
|
103
|
-
|
|
104
|
-
# Direct mode (bypass proxy, LLM only)
|
|
105
|
-
python eval/scripts/run_forge_eval.py --mode direct --runs 5
|
|
106
|
-
|
|
107
|
-
# Both modes (direct vs proxy comparison)
|
|
108
|
-
python eval/scripts/run_forge_eval.py --mode both --runs 5
|
|
109
|
-
|
|
110
|
-
# Specific scenarios
|
|
111
|
-
python eval/scripts/run_forge_eval.py --mode proxy --scenario data_gap_recovery_extended argument_transformation
|
|
112
|
-
|
|
113
|
-
# Verbose (shows tool calls, thinking, results)
|
|
114
|
-
python eval/scripts/run_forge_eval.py --mode proxy --runs 1 -v
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Run Folder Structure
|
|
118
|
-
|
|
119
|
-
Each run creates `eval/runs/<timestamp>/`:
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
eval/runs/2026-05-31_204247Z/
|
|
123
|
-
├── run.json # Run metadata (mode, model, timestamps)
|
|
124
|
-
├── eval.log # Forge's verbose trace (tee'd from stdout)
|
|
125
|
-
├── proxy.log # Proxy's logging module output
|
|
126
|
-
├── proxy.jsonl # Per-scenario JSON results
|
|
127
|
-
├── proxy_summary.txt # Human-readable summary table
|
|
128
|
-
├── comparison.md # Direct vs proxy comparison (both mode)
|
|
129
|
-
└── direct.jsonl # Direct mode results (both mode)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Layer 2 Guardrails Eval
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
python eval/scripts/run_layer2_eval.py # 17 JSON scenarios
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Compare Direct vs Proxy
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
python eval/scripts/compare.py eval/runs/<timestamp>/direct.jsonl eval/runs/<timestamp>/proxy.jsonl
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Benchmark Results
|
|
145
|
-
|
|
146
|
-
Latest run: `eval/runs/2026-05-31_204247Z/` — **93% accuracy** (140/150), 100% completion.
|
|
147
|
-
Forge's published benchmark: ~84% accuracy. Our proxy: +9pp with zero regressions.
|
|
148
|
-
|
|
149
|
-
## Development Guidelines
|
|
150
|
-
|
|
151
|
-
- **Do NOT hack Forge source** — extend via public API, subclassing, or wrapping only
|
|
152
|
-
- Changes must survive upstream Forge updates (vendored at `.vendors/forge/`)
|
|
153
|
-
- All 233 unit tests must pass
|
|
154
|
-
- No hardcoded scenario-specific logic — guardrails must be general purpose
|
|
155
|
-
- Use `edit` for targeted changes, `write` for new files or complete rewrites
|
|
156
|
-
- Keep the proxy clean: no injection/annotation of messages for non-coding-agent requests
|
|
157
|
-
|
|
158
|
-
## Versioning & Releases
|
|
159
|
-
|
|
160
|
-
Uses [SemVer](https://semver.org/): `MAJOR.MINOR.PATCH`
|
|
161
|
-
|
|
162
|
-
- **PATCH** (0.7.1): Bug fixes, no new features or API changes
|
|
163
|
-
- **MINOR** (0.8.0): New features, new rules, backward-compatible
|
|
164
|
-
- **MAJOR** (1.0.0): Breaking API changes
|
|
165
|
-
|
|
166
|
-
### Release Process
|
|
167
|
-
|
|
168
|
-
1. Update `version` in `pyproject.toml`
|
|
169
|
-
2. Update README if needed (eval results, new rules, new features)
|
|
170
|
-
3. Update CLAUDE.md if architecture changed
|
|
171
|
-
4. Ensure all 233 tests pass: `pytest tests/unit/ -q`
|
|
172
|
-
5. Commit: `git commit -m "vX.Y.Z: description"`
|
|
173
|
-
6. Push: `git push`
|
|
174
|
-
7. Tag: `git tag vX.Y.Z && git push origin vX.Y.Z`
|
|
175
|
-
8. GitHub Actions auto-builds, tests, and publishes to PyPI
|
|
176
|
-
|
|
177
|
-
### CI Pipeline
|
|
178
|
-
|
|
179
|
-
- On every push to `main`: runs tests + lint
|
|
180
|
-
- On tag `v*`: builds wheel, verifies CLI (`coding-guardrails --version`), publishes to PyPI
|
|
181
|
-
- PyPI uses trusted publishing (OIDC), no API tokens needed
|
|
182
|
-
|
|
183
|
-
### Refresh Local Install
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
source .venv/bin/activate
|
|
187
|
-
uv pip install -e ".[dev]"
|
|
188
|
-
coding-guardrails --version # verify
|
|
189
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/__init__.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/profiles.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/path_safety.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/prerequisites.py
RENAMED
|
File without changes
|
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sensitive_files.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sequencing.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/session_budget.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/thoroughness.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/tool_resolution.py
RENAMED
|
File without changes
|
{coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/value_fidelity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|