guardlayer 0.6.2__tar.gz → 0.6.3__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.
- {guardlayer-0.6.2 → guardlayer-0.6.3}/CHANGELOG.md +9 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/PKG-INFO +1 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/docker-compose.yml +1 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/kubernetes/guardlayer.yaml +1 -1
- guardlayer-0.6.3/docs/getting-started/pilot.md +125 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/rollout.md +2 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/mkdocs.yml +1 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/pyproject.toml +1 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/__init__.py +1 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/audit.py +76 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/cli.py +11 -1
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_agent_guard.py +29 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.env.example +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.gitattributes +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/dependabot.yml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/ci.yml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/docs.yml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/release.yml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/.gitignore +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/CONTRIBUTING.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/DEPLOYMENT.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/Dockerfile +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/LICENSE +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/README.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/SECURITY.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/THREAT_MODEL.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentdojo_benign_texts.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentdojo_eval.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentic_eval.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/candidates/2026-09-29-round1-rejected.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/candidates/2026-09-29.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentdojo-banking.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentic-tagged-strict.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentic-tagged.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/llmail_eval.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/perf.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/public_eval.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/referee.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-allow-egress-2026-09-28.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-postfix-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-qwen2.5-coder-7b-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-inferred-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-tagged-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-tagged-strict-2026-09-27.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/llmail-inject-phase2.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-api-1worker-2026-09-27.json +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-api-4workers-2026-09-27.json +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-balanced-2026-09-27.json +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/referee.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/kubernetes/kustomization.yaml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/changelog.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/agents.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/audit-and-evidence.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/how-it-works.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/presets.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/sessions.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/evaluation.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/getting-started/install.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/getting-started/quickstart.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/hooks.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/index.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/any-framework.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/claude-code.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/langgraph.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/openai-agents.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/rest-api.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/operations/configuration.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/operations/deployment.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/browsing-agent.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/ci.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/custom-rules.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/egress.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/evidence-pack.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/rag.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/cli.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/compliance.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/python-api.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/rules.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/requirements.txt +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/security/policy.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/security/threat-model.md +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/agent_tools.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/chat_app.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/custom_rules.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/guardlayer.toml +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/api.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/canary.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/compliance.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/config.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/__init__.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/eval_sample.jsonl +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/known_attacks.txt +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/evaluation.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/__init__.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/claude_code.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/langgraph.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/openai_agents.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/tools.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/models.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/normalize.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/pipeline.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/presets.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/py.typed +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/rules.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/__init__.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/base.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/heuristics.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/leakage.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/links.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/ml.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/obfuscation.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/pii.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/policy.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/relevance.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/secrets.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/similarity.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/session.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/tools.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/vectorstore.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/__init__.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_agentic_scripted.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_classifier_pinning.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_compliance.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_docs_examples.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_heuristics.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_interfaces.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_pipeline.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_properties.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_redos.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_remote_egress.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_scanners.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_sessions.py +0 -0
- {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_strip_injections.py +0 -0
|
@@ -5,6 +5,15 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.3] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`guardlayer audit report`**: what GuardLayer decided, or in observe mode would have decided, by rule and by tool, with
|
|
12
|
+
the latest notable entries and a count of redactions (`--since-days`, `--min`, `--json`). Built for pilots: run it daily
|
|
13
|
+
and sort each entry into correct, false alarm or unsure.
|
|
14
|
+
- **Docs: "One-week pilot on your own work"**: install from PyPI into its own environment, observe-mode config, hook on one
|
|
15
|
+
project, a five-minute daily review, when to start enforcing, and what to report back.
|
|
16
|
+
|
|
8
17
|
## [0.6.2] - 2026-09-29
|
|
9
18
|
|
|
10
19
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: guardlayer
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: A lightweight security layer that filters the inputs and outputs of LLM and agent applications — prompt injection, jailbreaks, leakage, secrets, PII and unsafe actions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Lijithvmv/Guard-Layer
|
|
6
6
|
Project-URL: Documentation, https://lijithvmv.github.io/Guard-Layer/
|
|
@@ -60,7 +60,7 @@ spec:
|
|
|
60
60
|
matchLabels: {app.kubernetes.io/name: guardlayer}
|
|
61
61
|
containers:
|
|
62
62
|
- name: guardlayer
|
|
63
|
-
image: guardlayer:0.6.
|
|
63
|
+
image: guardlayer:0.6.3 # build from the repo Dockerfile and push to your registry; pin by digest
|
|
64
64
|
imagePullPolicy: IfNotPresent
|
|
65
65
|
ports:
|
|
66
66
|
- {name: http, containerPort: 8000}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Try it on your own work: a one-week pilot
|
|
2
|
+
|
|
3
|
+
The fastest way to learn whether GuardLayer fits is to run it on the agent you already use, on real work, **without
|
|
4
|
+
letting it block anything**. Observe mode records what GuardLayer *would* have done; after a week you read the report
|
|
5
|
+
and decide what to enforce.
|
|
6
|
+
|
|
7
|
+
This page uses Claude Code as the agent, since it's the easiest to try. The same steps work for your own LangGraph or
|
|
8
|
+
OpenAI Agents SDK agent (see the end of the page).
|
|
9
|
+
|
|
10
|
+
## Day 0: set up (15 minutes)
|
|
11
|
+
|
|
12
|
+
**1. Install into its own environment**, the way any user would:
|
|
13
|
+
|
|
14
|
+
=== "Windows"
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
py -m venv C:\guardlayer-pilot
|
|
18
|
+
C:\guardlayer-pilot\Scripts\pip install guardlayer
|
|
19
|
+
C:\guardlayer-pilot\Scripts\guardlayer --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
=== "macOS / Linux"
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 -m venv ~/guardlayer-pilot
|
|
26
|
+
~/guardlayer-pilot/bin/pip install guardlayer
|
|
27
|
+
~/guardlayer-pilot/bin/guardlayer --version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**2. Create a pilot config** next to it, `pilot.toml`:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
preset = "observe" # record what would happen; enforce nothing
|
|
34
|
+
|
|
35
|
+
[audit]
|
|
36
|
+
path = "pilot-audit.jsonl" # relative to this file
|
|
37
|
+
min_verdict = "allow" # log every decision, so the report has the full picture
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The audit log stores hashes of text, not the text itself (unless you add `include_text = true`), so it doesn't become a
|
|
41
|
+
copy of your secrets.
|
|
42
|
+
|
|
43
|
+
**3. Check it behaves** before connecting it to anything:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
guardlayer --config pilot.toml tool-call Bash '{"command": "pytest -q"}' # ALLOW
|
|
47
|
+
guardlayer --preset balanced tool-call Bash '{"command": "rm -rf ~"}' # BLOCK (what enforcement would do)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**4. Connect it to one project only.** Generate the hook settings and paste them into that project's
|
|
51
|
+
`.claude/settings.json`, not your user-level settings, so only this project is observed:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
guardlayer --config pilot.toml hook claude-code --print-config
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The command in the output points at your pilot environment and config. In observe mode the hook never blocks or asks;
|
|
58
|
+
Claude Code behaves exactly as before, apart from about a second per tool call on Windows (less on macOS and Linux).
|
|
59
|
+
|
|
60
|
+
!!! tip "Repositories full of attack samples"
|
|
61
|
+
If the project is a security tool with injection strings in its tests, add `[session] trusted_tools = ["Read", "Grep"]`
|
|
62
|
+
to `pilot.toml`, or every read of those files will count as hostile content.
|
|
63
|
+
|
|
64
|
+
## Days 1–7: work normally, review for five minutes a day
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
guardlayer audit report pilot-audit.jsonl --since-days 1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The report shows how many decisions were notable, what enforcement *would* have done (`(observed)`), which rules fired
|
|
71
|
+
on which tools, and how many results had secrets or personal data redacted. For each notable entry, put it in one of
|
|
72
|
+
three buckets:
|
|
73
|
+
|
|
74
|
+
| Bucket | Example | What it means |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| **Correct** | a destructive command, a secret about to leave in a web request | the rule earns its place |
|
|
77
|
+
| **False alarm** | a normal `git push --force` on your own branch sent to review | tune it: observe that rule longer, allow the tool, or adjust the config |
|
|
78
|
+
| **Unsure** | a flagged page you can't judge | keep watching |
|
|
79
|
+
|
|
80
|
+
Also note **misses**: anything you saw the agent do that you'd have wanted stopped or reviewed, but the report doesn't
|
|
81
|
+
show. Misses matter as much as false alarms.
|
|
82
|
+
|
|
83
|
+
Keep the log honest: `guardlayer audit verify pilot-audit.jsonl` checks nobody (including you) edited it.
|
|
84
|
+
|
|
85
|
+
## Day 7: decide what to enforce
|
|
86
|
+
|
|
87
|
+
Enforce only what had no false alarms, and keep observing the rest:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
preset = "observe"
|
|
91
|
+
|
|
92
|
+
[guard]
|
|
93
|
+
enforce = ["secret", "tool_policy:*"] # redact secrets and enforce the tool policy; everything else still observed
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run another week, then move to `balanced` (or `strict` for agents holding production credentials). See
|
|
97
|
+
[Roll out without breaking anything](../recipes/rollout.md) for the full sequence.
|
|
98
|
+
|
|
99
|
+
## Your own agent instead of Claude Code
|
|
100
|
+
|
|
101
|
+
```py
|
|
102
|
+
from guardlayer.config import build_guard
|
|
103
|
+
from guardlayer.integrations.langgraph import guard_tools
|
|
104
|
+
|
|
105
|
+
guard = build_guard("pilot.toml") # the same observe config and audit log
|
|
106
|
+
tools = guard_tools(guard, [search, fetch_url, run_shell])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For the OpenAI Agents SDK use `guardrails(guard)`, and for anything else `guard_tool` (see Integrations). The daily
|
|
110
|
+
report works the same way.
|
|
111
|
+
|
|
112
|
+
## What to send back
|
|
113
|
+
|
|
114
|
+
A pilot is most useful when its findings come back. Open an issue with, for each false alarm or miss:
|
|
115
|
+
|
|
116
|
+
- the **rule name** and **tool** from the report, and the decision (`block`, `review`, `flag`);
|
|
117
|
+
- one sentence on why it was wrong, or what should have been caught;
|
|
118
|
+
- the GuardLayer version (`guardlayer --version`).
|
|
119
|
+
|
|
120
|
+
Never paste secrets, customer data or full tool output. The rule name and a description are enough.
|
|
121
|
+
|
|
122
|
+
## Removing it
|
|
123
|
+
|
|
124
|
+
Delete the hook entries from the project's `.claude/settings.json` and remove the pilot environment. Session state lives
|
|
125
|
+
in `~/.guardlayer/sessions`; delete that folder too if you want no trace.
|
|
@@ -18,7 +18,8 @@ Nothing is blocked, held or redacted. Every result carries a `shadow_verdict`, a
|
|
|
18
18
|
## 2. Look at what would have happened
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
guardlayer
|
|
21
|
+
guardlayer audit report guardlayer-audit.jsonl --since-days 1 # by rule, by tool, latest; "(observed)" = would have fired
|
|
22
|
+
guardlayer evidence export guardlayer-audit.jsonl # the same log as control-mapped evidence
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
Or read the log directly: each line has `verdict`, `shadow_verdict`, `observed_rules`, `categories` and `direction`. Look
|
|
@@ -75,6 +75,7 @@ nav:
|
|
|
75
75
|
- Getting started:
|
|
76
76
|
- Install: getting-started/install.md
|
|
77
77
|
- Quickstart: getting-started/quickstart.md
|
|
78
|
+
- One-week pilot on your own work: getting-started/pilot.md
|
|
78
79
|
- Concepts:
|
|
79
80
|
- How a verdict is reached: concepts/how-it-works.md
|
|
80
81
|
- Guarding agent actions: concepts/agents.md
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "guardlayer"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.3"
|
|
8
8
|
description = "A lightweight security layer that filters the inputs and outputs of LLM and agent applications — prompt injection, jailbreaks, leakage, secrets, PII and unsafe actions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -7,7 +7,7 @@ Quick start:
|
|
|
7
7
|
<Verdict.BLOCK: 'block'>
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.6.
|
|
10
|
+
__version__ = "0.6.3"
|
|
11
11
|
|
|
12
12
|
from guardlayer.audit import AuditLogger, AuditSigner, AuditVerification, verify_audit_log # noqa: E402
|
|
13
13
|
from guardlayer.canary import Canary, CanaryManager # noqa: E402
|
|
@@ -23,6 +23,7 @@ import hashlib
|
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
25
|
import threading
|
|
26
|
+
import time
|
|
26
27
|
from dataclasses import dataclass
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import IO, Any
|
|
@@ -249,3 +250,78 @@ def verify_audit_log(
|
|
|
249
250
|
if expected_head is not None and prev != expected_head:
|
|
250
251
|
return AuditVerification(False, count, prev, signed, "head hash differs from the expected head (log truncated or replaced)", None)
|
|
251
252
|
return AuditVerification(True, count, prev if count else None, signed)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------------------------------ review report
|
|
256
|
+
_ORDER = {"allow": 0, "flag": 1, "review": 2, "block": 3}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def audit_report(path: str | Path, *, since_days: float | None = None, min_verdict: str = "flag", latest: int = 15) -> dict[str, Any]:
|
|
260
|
+
"""Summarise what GuardLayer decided, or would have decided in observe mode, for a human reviewer.
|
|
261
|
+
|
|
262
|
+
For each entry the effective decision is the stricter of `verdict` and `shadow_verdict`, so an observe-mode pilot
|
|
263
|
+
shows what enforcement would have done. Counts by rule and by tool, and the latest notable entries, are returned;
|
|
264
|
+
raw text is never needed (entries hold only hashes unless `include_text` was on).
|
|
265
|
+
"""
|
|
266
|
+
threshold = _ORDER[str(Verdict(min_verdict).value)]
|
|
267
|
+
cutoff = time.time() - since_days * 86400 if since_days else None
|
|
268
|
+
total, notable, observed_only, redacted, sessions = 0, 0, 0, 0, set()
|
|
269
|
+
by_rule: dict[str, dict[str, Any]] = {}
|
|
270
|
+
by_tool: dict[str, int] = {}
|
|
271
|
+
recent: list[dict[str, Any]] = []
|
|
272
|
+
with open(path, encoding="utf-8") as fh:
|
|
273
|
+
for line in fh:
|
|
274
|
+
if not line.strip():
|
|
275
|
+
continue
|
|
276
|
+
entry = json.loads(line)
|
|
277
|
+
if cutoff and entry.get("timestamp", 0) < cutoff:
|
|
278
|
+
continue
|
|
279
|
+
total += 1
|
|
280
|
+
meta = entry.get("metadata") or {}
|
|
281
|
+
if meta.get("session_id"):
|
|
282
|
+
sessions.add(meta["session_id"])
|
|
283
|
+
redacted += bool(entry.get("modified"))
|
|
284
|
+
enforced = entry.get("verdict", "allow")
|
|
285
|
+
shadow = entry.get("shadow_verdict") or "allow"
|
|
286
|
+
effective = max(enforced, shadow, key=lambda v: _ORDER.get(v, 0))
|
|
287
|
+
if _ORDER.get(effective, 0) < threshold:
|
|
288
|
+
continue
|
|
289
|
+
notable += 1
|
|
290
|
+
only_observed = _ORDER.get(enforced, 0) < threshold
|
|
291
|
+
observed_only += only_observed
|
|
292
|
+
tool = meta.get("tool") or entry.get("direction", "?")
|
|
293
|
+
by_tool[tool] = by_tool.get(tool, 0) + 1
|
|
294
|
+
rules = sorted({d.get("rule", "?") for d in entry.get("detections", [])})
|
|
295
|
+
for rule in rules:
|
|
296
|
+
slot = by_rule.setdefault(rule, {"count": 0, "decisions": {}, "tools": set()})
|
|
297
|
+
slot["count"] += 1
|
|
298
|
+
slot["decisions"][effective] = slot["decisions"].get(effective, 0) + 1
|
|
299
|
+
slot["tools"].add(tool)
|
|
300
|
+
recent.append({"time": entry.get("timestamp"), "session": meta.get("session_id"), "tool": tool,
|
|
301
|
+
"direction": entry.get("direction"), "decision": effective, "observed_only": only_observed,
|
|
302
|
+
"rules": rules, "why": next((d.get("message") for d in entry.get("detections", [])), "")}) # fmt: skip
|
|
303
|
+
for slot in by_rule.values():
|
|
304
|
+
slot["tools"] = sorted(slot["tools"])
|
|
305
|
+
return {"entries": total, "sessions": len(sessions), "notable": notable, "observed_only": observed_only, "redacted": redacted,
|
|
306
|
+
"by_rule": dict(sorted(by_rule.items(), key=lambda kv: -kv[1]["count"])),
|
|
307
|
+
"by_tool": dict(sorted(by_tool.items(), key=lambda kv: -kv[1])), "latest": recent[-latest:][::-1]} # fmt: skip
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def format_audit_report(report: dict[str, Any]) -> str:
|
|
311
|
+
lines = [f"{report['entries']} entries, {report['sessions']} sessions; {report['notable']} notable "
|
|
312
|
+
f"({report['observed_only']} only observed: what enforcement would have done); "
|
|
313
|
+
f"{report['redacted']} with secrets or personal data redacted"] # fmt: skip
|
|
314
|
+
if report["by_rule"]:
|
|
315
|
+
lines += ["", "By rule:"]
|
|
316
|
+
for rule, slot in report["by_rule"].items():
|
|
317
|
+
decisions = ", ".join(f"{k} {v}" for k, v in sorted(slot["decisions"].items(), key=lambda kv: -_ORDER.get(kv[0], 0)))
|
|
318
|
+
lines.append(f" {rule:32} {slot['count']:5} {decisions} tools: {', '.join(slot['tools'])}")
|
|
319
|
+
if report["latest"]:
|
|
320
|
+
lines += ["", "Latest:"]
|
|
321
|
+
for e in report["latest"]:
|
|
322
|
+
when = time.strftime("%Y-%m-%d %H:%M", time.localtime(e["time"] or 0))
|
|
323
|
+
tag = " (observed)" if e["observed_only"] else ""
|
|
324
|
+
lines.append(f" {when} {e['decision']:6}{tag:11} {e['tool']:14} {', '.join(e['rules'])}")
|
|
325
|
+
if e["why"]:
|
|
326
|
+
lines.append(f" {e['why']}")
|
|
327
|
+
return "\n".join(lines)
|
|
@@ -94,6 +94,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
94
94
|
verify.add_argument("--expected-head", help="A head hash recorded earlier, to detect a truncated log.")
|
|
95
95
|
keygen = audit_sub.add_parser("keygen", help="Generate an Ed25519 signing key pair: PREFIX.key and PREFIX.pub.")
|
|
96
96
|
keygen.add_argument("prefix")
|
|
97
|
+
report = audit_sub.add_parser("report", help="What GuardLayer decided (or would have, in observe mode): by rule, by tool, latest.")
|
|
98
|
+
report.add_argument("path")
|
|
99
|
+
report.add_argument("--since-days", type=float, help="Only entries from the last N days.")
|
|
100
|
+
report.add_argument("--min", default="flag", choices=["flag", "review", "block"], help="Lowest decision to include.")
|
|
101
|
+
report.add_argument("--latest", type=int, default=15, help="How many recent notable entries to list.")
|
|
102
|
+
report.add_argument("--json", action="store_true", help="Machine-readable output.")
|
|
97
103
|
|
|
98
104
|
evidence = sub.add_parser("evidence", help="Control-mapped compliance evidence from an audit log.")
|
|
99
105
|
evidence_sub = evidence.add_subparsers(dest="evidence_command", required=True)
|
|
@@ -222,8 +228,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
222
228
|
|
|
223
229
|
|
|
224
230
|
def _audit(args: argparse.Namespace) -> int:
|
|
225
|
-
from guardlayer.audit import AuditSigner, verify_audit_log
|
|
231
|
+
from guardlayer.audit import AuditSigner, audit_report, format_audit_report, verify_audit_log
|
|
226
232
|
|
|
233
|
+
if args.audit_command == "report":
|
|
234
|
+
data = audit_report(args.path, since_days=args.since_days, min_verdict=args.min, latest=args.latest)
|
|
235
|
+
print(json.dumps(data, indent=2) if args.json else format_audit_report(data))
|
|
236
|
+
return 0
|
|
227
237
|
if args.audit_command == "keygen":
|
|
228
238
|
key, pub = Path(f"{args.prefix}.key"), Path(f"{args.prefix}.pub")
|
|
229
239
|
if key.exists() or pub.exists():
|
|
@@ -387,3 +387,32 @@ def test_config_audit_path_per_process(tmp_path):
|
|
|
387
387
|
guard.scan_input(ATTACK)
|
|
388
388
|
expected = tmp_path / f"audit-{socket.gethostname()}-{os.getpid()}.jsonl"
|
|
389
389
|
assert expected.exists() and verify_audit_log(expected).ok
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_audit_report_shows_what_observe_mode_would_have_done(tmp_path, capsys):
|
|
393
|
+
from guardlayer import AuditLogger
|
|
394
|
+
from guardlayer.audit import audit_report
|
|
395
|
+
from guardlayer.cli import main
|
|
396
|
+
|
|
397
|
+
log = tmp_path / "audit.jsonl"
|
|
398
|
+
observe = GuardLayer.from_preset("observe")
|
|
399
|
+
observe.add_hook(AuditLogger(log))
|
|
400
|
+
s = observe.session("pilot")
|
|
401
|
+
s.scan_tool_call("Bash", {"command": "rm -rf ~"})
|
|
402
|
+
s.scan_tool_call("Bash", {"command": "git push --force"})
|
|
403
|
+
s.scan_tool_call("Bash", {"command": "pytest -q"})
|
|
404
|
+
enforce = GuardLayer()
|
|
405
|
+
enforce.add_hook(AuditLogger(log))
|
|
406
|
+
enforce.scan_output("key AKIAIOSFODNN7EXAMPLE")
|
|
407
|
+
|
|
408
|
+
r = audit_report(log)
|
|
409
|
+
assert r["entries"] == 4 and r["sessions"] == 1 and r["redacted"] == 1
|
|
410
|
+
assert r["notable"] == 2 and r["observed_only"] == 2 # nothing was enforced in observe mode
|
|
411
|
+
assert r["by_rule"]["destructive_command"]["decisions"] == {"block": 1}
|
|
412
|
+
assert r["by_rule"]["risky_command"]["decisions"] == {"review": 1}
|
|
413
|
+
assert audit_report(log, min_verdict="block")["notable"] == 1
|
|
414
|
+
assert r["latest"][0]["rules"] == ["risky_command"] # newest first
|
|
415
|
+
|
|
416
|
+
assert main(["audit", "report", str(log)]) == 0
|
|
417
|
+
out = capsys.readouterr().out
|
|
418
|
+
assert "destructive_command" in out and "(observed)" in out and "1 with secrets" in out
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-2026-09-27.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-qwen2.5-coder-7b-2026-09-27.jsonl
RENAMED
|
File without changes
|
{guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-inferred-2026-09-27.jsonl
RENAMED
|
File without changes
|
{guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-tagged-2026-09-27.jsonl
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
|
|
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
|
|
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
|
|
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
|
|
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
|