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.
Files changed (134) hide show
  1. {guardlayer-0.6.2 → guardlayer-0.6.3}/CHANGELOG.md +9 -0
  2. {guardlayer-0.6.2 → guardlayer-0.6.3}/PKG-INFO +1 -1
  3. {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/docker-compose.yml +1 -1
  4. {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/kubernetes/guardlayer.yaml +1 -1
  5. guardlayer-0.6.3/docs/getting-started/pilot.md +125 -0
  6. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/rollout.md +2 -1
  7. {guardlayer-0.6.2 → guardlayer-0.6.3}/mkdocs.yml +1 -0
  8. {guardlayer-0.6.2 → guardlayer-0.6.3}/pyproject.toml +1 -1
  9. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/__init__.py +1 -1
  10. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/audit.py +76 -0
  11. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/cli.py +11 -1
  12. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_agent_guard.py +29 -0
  13. {guardlayer-0.6.2 → guardlayer-0.6.3}/.env.example +0 -0
  14. {guardlayer-0.6.2 → guardlayer-0.6.3}/.gitattributes +0 -0
  15. {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/dependabot.yml +0 -0
  16. {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/ci.yml +0 -0
  17. {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/docs.yml +0 -0
  18. {guardlayer-0.6.2 → guardlayer-0.6.3}/.github/workflows/release.yml +0 -0
  19. {guardlayer-0.6.2 → guardlayer-0.6.3}/.gitignore +0 -0
  20. {guardlayer-0.6.2 → guardlayer-0.6.3}/CONTRIBUTING.md +0 -0
  21. {guardlayer-0.6.2 → guardlayer-0.6.3}/DEPLOYMENT.md +0 -0
  22. {guardlayer-0.6.2 → guardlayer-0.6.3}/Dockerfile +0 -0
  23. {guardlayer-0.6.2 → guardlayer-0.6.3}/LICENSE +0 -0
  24. {guardlayer-0.6.2 → guardlayer-0.6.3}/README.md +0 -0
  25. {guardlayer-0.6.2 → guardlayer-0.6.3}/SECURITY.md +0 -0
  26. {guardlayer-0.6.2 → guardlayer-0.6.3}/THREAT_MODEL.md +0 -0
  27. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentdojo_benign_texts.py +0 -0
  28. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentdojo_eval.py +0 -0
  29. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/agentic_eval.py +0 -0
  30. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/candidates/2026-09-29-round1-rejected.toml +0 -0
  31. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/candidates/2026-09-29.toml +0 -0
  32. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentdojo-banking.toml +0 -0
  33. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentic-tagged-strict.toml +0 -0
  34. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/configs/agentic-tagged.toml +0 -0
  35. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/llmail_eval.py +0 -0
  36. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/perf.py +0 -0
  37. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/public_eval.py +0 -0
  38. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/referee.py +0 -0
  39. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-2026-09-27.jsonl +0 -0
  40. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-allow-egress-2026-09-28.jsonl +0 -0
  41. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentdojo-qwen2.5-coder-7b-postfix-2026-09-27.jsonl +0 -0
  42. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-qwen2.5-coder-7b-2026-09-27.jsonl +0 -0
  43. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-inferred-2026-09-27.jsonl +0 -0
  44. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-tagged-2026-09-27.jsonl +0 -0
  45. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/agentic-scripted-tagged-strict-2026-09-27.jsonl +0 -0
  46. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/llmail-inject-phase2.jsonl +0 -0
  47. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-api-1worker-2026-09-27.json +0 -0
  48. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-api-4workers-2026-09-27.json +0 -0
  49. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/perf-balanced-2026-09-27.json +0 -0
  50. {guardlayer-0.6.2 → guardlayer-0.6.3}/benchmarks/results/referee.jsonl +0 -0
  51. {guardlayer-0.6.2 → guardlayer-0.6.3}/deploy/kubernetes/kustomization.yaml +0 -0
  52. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/changelog.md +0 -0
  53. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/agents.md +0 -0
  54. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/audit-and-evidence.md +0 -0
  55. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/how-it-works.md +0 -0
  56. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/presets.md +0 -0
  57. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/concepts/sessions.md +0 -0
  58. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/evaluation.md +0 -0
  59. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/getting-started/install.md +0 -0
  60. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/getting-started/quickstart.md +0 -0
  61. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/hooks.py +0 -0
  62. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/index.md +0 -0
  63. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/any-framework.md +0 -0
  64. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/claude-code.md +0 -0
  65. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/langgraph.md +0 -0
  66. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/openai-agents.md +0 -0
  67. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/integrations/rest-api.md +0 -0
  68. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/operations/configuration.md +0 -0
  69. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/operations/deployment.md +0 -0
  70. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/browsing-agent.md +0 -0
  71. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/ci.md +0 -0
  72. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/custom-rules.md +0 -0
  73. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/egress.md +0 -0
  74. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/evidence-pack.md +0 -0
  75. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/recipes/rag.md +0 -0
  76. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/cli.md +0 -0
  77. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/compliance.md +0 -0
  78. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/python-api.md +0 -0
  79. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/reference/rules.md +0 -0
  80. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/requirements.txt +0 -0
  81. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/security/policy.md +0 -0
  82. {guardlayer-0.6.2 → guardlayer-0.6.3}/docs/security/threat-model.md +0 -0
  83. {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/agent_tools.py +0 -0
  84. {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/chat_app.py +0 -0
  85. {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/custom_rules.toml +0 -0
  86. {guardlayer-0.6.2 → guardlayer-0.6.3}/examples/guardlayer.toml +0 -0
  87. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/api.py +0 -0
  88. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/canary.py +0 -0
  89. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/compliance.py +0 -0
  90. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/config.py +0 -0
  91. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/__init__.py +0 -0
  92. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/eval_sample.jsonl +0 -0
  93. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/data/known_attacks.txt +0 -0
  94. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/evaluation.py +0 -0
  95. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/__init__.py +0 -0
  96. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/claude_code.py +0 -0
  97. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/langgraph.py +0 -0
  98. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/openai_agents.py +0 -0
  99. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/integrations/tools.py +0 -0
  100. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/models.py +0 -0
  101. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/normalize.py +0 -0
  102. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/pipeline.py +0 -0
  103. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/presets.py +0 -0
  104. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/py.typed +0 -0
  105. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/rules.py +0 -0
  106. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/__init__.py +0 -0
  107. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/base.py +0 -0
  108. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/heuristics.py +0 -0
  109. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/leakage.py +0 -0
  110. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/links.py +0 -0
  111. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/ml.py +0 -0
  112. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/obfuscation.py +0 -0
  113. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/pii.py +0 -0
  114. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/policy.py +0 -0
  115. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/relevance.py +0 -0
  116. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/secrets.py +0 -0
  117. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/scanners/similarity.py +0 -0
  118. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/session.py +0 -0
  119. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/tools.py +0 -0
  120. {guardlayer-0.6.2 → guardlayer-0.6.3}/src/guardlayer/vectorstore.py +0 -0
  121. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/__init__.py +0 -0
  122. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_agentic_scripted.py +0 -0
  123. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_classifier_pinning.py +0 -0
  124. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_compliance.py +0 -0
  125. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_docs_examples.py +0 -0
  126. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_heuristics.py +0 -0
  127. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_interfaces.py +0 -0
  128. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_pipeline.py +0 -0
  129. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_properties.py +0 -0
  130. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_redos.py +0 -0
  131. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_remote_egress.py +0 -0
  132. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_scanners.py +0 -0
  133. {guardlayer-0.6.2 → guardlayer-0.6.3}/tests/test_sessions.py +0 -0
  134. {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.2
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/
@@ -3,7 +3,7 @@
3
3
  services:
4
4
  guardlayer:
5
5
  build: ..
6
- image: guardlayer:0.6.2
6
+ image: guardlayer:0.6.3
7
7
  environment:
8
8
  GUARDLAYER_API_KEY: ${GUARDLAYER_API_KEY:?set GUARDLAYER_API_KEY}
9
9
  GUARDLAYER_CONFIG: /etc/guardlayer/guardlayer.toml
@@ -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.2 # build from the repo Dockerfile and push to your registry; pin by digest
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 evidence export guardlayer-audit.jsonl # a summary by control and verdict
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.2"
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.2"
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