wardproof 0.3.2__tar.gz → 0.3.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.
Files changed (68) hide show
  1. {wardproof-0.3.2 → wardproof-0.3.4}/PKG-INFO +34 -4
  2. {wardproof-0.3.2 → wardproof-0.3.4}/README.md +33 -3
  3. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/README.md +23 -0
  4. wardproof-0.3.4/examples/integrations/swarms_guarded.py +186 -0
  5. {wardproof-0.3.2 → wardproof-0.3.4}/pyproject.toml +1 -1
  6. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/__init__.py +1 -1
  7. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/cli.py +8 -0
  8. wardproof-0.3.4/wardproof/server.py +119 -0
  9. {wardproof-0.3.2 → wardproof-0.3.4}/.gitignore +0 -0
  10. {wardproof-0.3.2 → wardproof-0.3.4}/CONTRIBUTING.md +0 -0
  11. {wardproof-0.3.2 → wardproof-0.3.4}/LICENSE +0 -0
  12. {wardproof-0.3.2 → wardproof-0.3.4}/SECURITY.md +0 -0
  13. {wardproof-0.3.2 → wardproof-0.3.4}/THREAT_MODEL.md +0 -0
  14. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/README.md +0 -0
  15. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/corpus.jsonl +0 -0
  16. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/README.md +0 -0
  17. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/__init__.py +0 -0
  18. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/_screen.py +0 -0
  19. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/agentdojo.py +0 -0
  20. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/fetch_data.py +0 -0
  21. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/external/injecagent.py +0 -0
  22. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/heldout.py +0 -0
  23. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/latency.py +0 -0
  24. {wardproof-0.3.2 → wardproof-0.3.4}/benchmarks/run_benchmark.py +0 -0
  25. {wardproof-0.3.2 → wardproof-0.3.4}/examples/agent_to_agent_transfer.py +0 -0
  26. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/agentkit_guarded.py +0 -0
  27. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/anthropic_tools_guarded.py +0 -0
  28. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/crewai_guarded.py +0 -0
  29. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/langgraph_guarded.py +0 -0
  30. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/mcp_guarded.py +0 -0
  31. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/openai_tools_guarded.py +0 -0
  32. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/skills_guard.py +0 -0
  33. {wardproof-0.3.2 → wardproof-0.3.4}/examples/integrations/venice_guarded.py +0 -0
  34. {wardproof-0.3.2 → wardproof-0.3.4}/examples/morse_injection_blocked_at_action.py +0 -0
  35. {wardproof-0.3.2 → wardproof-0.3.4}/examples/protect_defi_agent.py +0 -0
  36. {wardproof-0.3.2 → wardproof-0.3.4}/examples/protect_mcp_agent.py +0 -0
  37. {wardproof-0.3.2 → wardproof-0.3.4}/examples/protect_rag_app.py +0 -0
  38. {wardproof-0.3.2 → wardproof-0.3.4}/examples/protect_x402_payments.py +0 -0
  39. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/agents/__init__.py +0 -0
  40. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/agents/base.py +0 -0
  41. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/agents/detector.py +0 -0
  42. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/agents/responder.py +0 -0
  43. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/agents/verifier.py +0 -0
  44. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/audit/__init__.py +0 -0
  45. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/audit/ledger.py +0 -0
  46. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/audit/stix.py +0 -0
  47. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/config.py +0 -0
  48. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/__init__.py +0 -0
  49. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/_normalize.py +0 -0
  50. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/base.py +0 -0
  51. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/mcp_guard.py +0 -0
  52. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/memory_poisoning.py +0 -0
  53. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/prompt_injection.py +0 -0
  54. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/tool_misuse.py +0 -0
  55. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/transfer.py +0 -0
  56. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/guardrails/x402_payment.py +0 -0
  57. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/llm/__init__.py +0 -0
  58. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/llm/base.py +0 -0
  59. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/llm/null.py +0 -0
  60. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/llm/ollama_client.py +0 -0
  61. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/orchestration/__init__.py +0 -0
  62. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/orchestration/engine.py +0 -0
  63. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/orchestration/factory.py +0 -0
  64. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/sandbox/__init__.py +0 -0
  65. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/sandbox/executor.py +0 -0
  66. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/sandbox/permissions.py +0 -0
  67. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/schema.py +0 -0
  68. {wardproof-0.3.2 → wardproof-0.3.4}/wardproof/standards.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wardproof
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Local-first, verifiable defensive AI agent swarms that protect other AI agent systems.
5
5
  Project-URL: Homepage, https://wardproof.xyz
6
6
  Project-URL: Repository, https://github.com/Impossible-Mission-Force/wardproof
@@ -74,7 +74,7 @@ It is deliberately **small, transparent, and forkable**. The security core has
74
74
  **zero third-party dependencies** and runs **fully offline**, with a local
75
75
  model via Ollama, or with no model at all.
76
76
 
77
- > **Status: v0.3.1.** The deterministic core is built, tested, and benchmarked
77
+ > **Status: v0.3.4.** The deterministic core is built, tested, and benchmarked
78
78
  > (see [Benchmark](#benchmark)), and ships dedicated guards for x402 agent
79
79
  > payments, on-chain transfers, MCP tool calls, and skill/tool definitions, a
80
80
  > controls-to-standards map (OWASP Agentic Top 10, OWASP LLM 2025, MITRE ATLAS,
@@ -210,7 +210,7 @@ gate a shell pipeline or an agent skill on it:
210
210
 
211
211
  ```bash
212
212
  # A tool call (tool name as the content, arguments as a JSON string)
213
- wardproof check "get_weather" --args '{"city":"Hanoi"}' # ALLOW, exits 0
213
+ wardproof check "get_weather" --args '{"city":"Berlin"}' # ALLOW, exits 0
214
214
 
215
215
  # An untrusted input
216
216
  wardproof check "ignore all previous instructions" --kind input # BLOCK, exits non-zero
@@ -220,6 +220,36 @@ Add `--json` to get a structured `{"verdict": ..., "allowed": ..., "risk": ...,
220
220
  "reasons": [...]}` result to parse. A portable guard skill that wires this check
221
221
  into a host agent lives in [`skill/wardproof-guard/`](https://github.com/Impossible-Mission-Force/wardproof/tree/main/skill/wardproof-guard).
222
222
 
223
+ ### Run it as a local service with `wardproof serve`
224
+
225
+ When a host needs to screen many actions, run the swarm as a small local HTTP
226
+ service instead of spawning a process per call. It builds the swarm once at
227
+ startup and binds to localhost by default (meant to run next to the agent it
228
+ guards, not exposed publicly):
229
+
230
+ ```bash
231
+ wardproof serve --port 8787
232
+ # GET /health -> {"status": "ok", "version": "..."}
233
+ # POST /check gates one input or tool call:
234
+ curl -s -X POST http://127.0.0.1:8787/check \
235
+ -d '{"kind":"input","content":"ignore all previous instructions"}'
236
+ # -> {"verdict": "block", "allowed": false, "risk": 1.0, "reasons": [...]}
237
+ ```
238
+
239
+ `/check` replies with `allowed: true` only when the verdict is `ALLOW`, so a
240
+ host can gate on one field.
241
+
242
+ ### Guard a Swarms agent
243
+
244
+ [`examples/integrations/swarms_guarded.py`](https://github.com/Impossible-Mission-Force/wardproof/tree/main/examples/integrations/swarms_guarded.py)
245
+ screens a [Swarms](https://github.com/kyegomez/swarms) agent's tool calls before
246
+ they run. `GuardedToolExecutor.run` screens one `{"function": {"name", "arguments"}}`
247
+ tool call and `run_many` screens a batch (Swarms can dispatch several in one
248
+ step); each call executes only when the verdict is `ALLOW`, and anything else is
249
+ refused and recorded to the audit ledger. The guard works on the plain tool-call
250
+ dict, so it adds no dependency; the optional production adapter lazy-imports
251
+ `swarms.tools.execute_tool_call_simple`.
252
+
223
253
  ---
224
254
 
225
255
  ## Architecture
@@ -321,7 +351,7 @@ No need to touch the engine, the ledger, or the agent base classes.
321
351
  Wardproof is built to become a complete, auditable control layer for AI agents.
322
352
  The direction:
323
353
 
324
- **Now (v0.3.1)**
354
+ **Now (v0.3.4)**
325
355
  The deterministic core: schema, guardrails, Detector / Verifier / Responder, a
326
356
  capability sandbox, circuit breaker and watchdog, a hash-chained and optionally
327
357
  signed audit ledger, a reproducible adversarial benchmark, a published threat
@@ -24,7 +24,7 @@ It is deliberately **small, transparent, and forkable**. The security core has
24
24
  **zero third-party dependencies** and runs **fully offline**, with a local
25
25
  model via Ollama, or with no model at all.
26
26
 
27
- > **Status: v0.3.1.** The deterministic core is built, tested, and benchmarked
27
+ > **Status: v0.3.4.** The deterministic core is built, tested, and benchmarked
28
28
  > (see [Benchmark](#benchmark)), and ships dedicated guards for x402 agent
29
29
  > payments, on-chain transfers, MCP tool calls, and skill/tool definitions, a
30
30
  > controls-to-standards map (OWASP Agentic Top 10, OWASP LLM 2025, MITRE ATLAS,
@@ -160,7 +160,7 @@ gate a shell pipeline or an agent skill on it:
160
160
 
161
161
  ```bash
162
162
  # A tool call (tool name as the content, arguments as a JSON string)
163
- wardproof check "get_weather" --args '{"city":"Hanoi"}' # ALLOW, exits 0
163
+ wardproof check "get_weather" --args '{"city":"Berlin"}' # ALLOW, exits 0
164
164
 
165
165
  # An untrusted input
166
166
  wardproof check "ignore all previous instructions" --kind input # BLOCK, exits non-zero
@@ -170,6 +170,36 @@ Add `--json` to get a structured `{"verdict": ..., "allowed": ..., "risk": ...,
170
170
  "reasons": [...]}` result to parse. A portable guard skill that wires this check
171
171
  into a host agent lives in [`skill/wardproof-guard/`](https://github.com/Impossible-Mission-Force/wardproof/tree/main/skill/wardproof-guard).
172
172
 
173
+ ### Run it as a local service with `wardproof serve`
174
+
175
+ When a host needs to screen many actions, run the swarm as a small local HTTP
176
+ service instead of spawning a process per call. It builds the swarm once at
177
+ startup and binds to localhost by default (meant to run next to the agent it
178
+ guards, not exposed publicly):
179
+
180
+ ```bash
181
+ wardproof serve --port 8787
182
+ # GET /health -> {"status": "ok", "version": "..."}
183
+ # POST /check gates one input or tool call:
184
+ curl -s -X POST http://127.0.0.1:8787/check \
185
+ -d '{"kind":"input","content":"ignore all previous instructions"}'
186
+ # -> {"verdict": "block", "allowed": false, "risk": 1.0, "reasons": [...]}
187
+ ```
188
+
189
+ `/check` replies with `allowed: true` only when the verdict is `ALLOW`, so a
190
+ host can gate on one field.
191
+
192
+ ### Guard a Swarms agent
193
+
194
+ [`examples/integrations/swarms_guarded.py`](https://github.com/Impossible-Mission-Force/wardproof/tree/main/examples/integrations/swarms_guarded.py)
195
+ screens a [Swarms](https://github.com/kyegomez/swarms) agent's tool calls before
196
+ they run. `GuardedToolExecutor.run` screens one `{"function": {"name", "arguments"}}`
197
+ tool call and `run_many` screens a batch (Swarms can dispatch several in one
198
+ step); each call executes only when the verdict is `ALLOW`, and anything else is
199
+ refused and recorded to the audit ledger. The guard works on the plain tool-call
200
+ dict, so it adds no dependency; the optional production adapter lazy-imports
201
+ `swarms.tools.execute_tool_call_simple`.
202
+
173
203
  ---
174
204
 
175
205
  ## Architecture
@@ -271,7 +301,7 @@ No need to touch the engine, the ledger, or the agent base classes.
271
301
  Wardproof is built to become a complete, auditable control layer for AI agents.
272
302
  The direction:
273
303
 
274
- **Now (v0.3.1)**
304
+ **Now (v0.3.4)**
275
305
  The deterministic core: schema, guardrails, Detector / Verifier / Responder, a
276
306
  capability sandbox, circuit breaker and watchdog, a hash-chained and optionally
277
307
  signed audit ledger, a reproducible adversarial benchmark, a published threat
@@ -266,6 +266,29 @@ as `action.invoke(args_dict)`; the only abstract method on `ActionProvider` is
266
266
  construction and each invoke, so the example no-ops that one call to stay
267
267
  offline; it touches telemetry only, never wallet logic.
268
268
 
269
+ ## Swarms (`swarms_guarded.py`)
270
+
271
+ Screen a Swarms agent's tool calls before they run. Swarms passes a tool call as
272
+ `{"function": {"name", "arguments"}}` (the shape it uses for native and MCP
273
+ tools). `GuardedToolExecutor.run` screens one such call and `run_many` screens a
274
+ batch (Swarms can dispatch several tool calls in one step); each runs only on an
275
+ `ALLOW` verdict, and anything else is refused and recorded to the ledger.
276
+
277
+ ```python
278
+ from wardproof import build_default_swarm, AuditLedger
279
+ from swarms_guarded import GuardedToolExecutor
280
+
281
+ guarded = GuardedToolExecutor(my_executor) # my_executor(tool_call) -> str
282
+ guarded.run({"function": {"name": "get_weather", "arguments": {"city": "Berlin"}}})
283
+ guarded.run_many([call_a, call_b]) # safe ones run, dangerous ones refused
284
+ ```
285
+
286
+ The guard works on the plain tool-call dict, so the module imports nothing from
287
+ `swarms`; only the optional `make_swarms_executor` adapter lazy-imports
288
+ `swarms.tools.execute_tool_call_simple`. The example runs fully offline with a
289
+ stub executor (one benign `get_weather`, one `run_command` carrying `rm -rf /`
290
+ that is blocked, one `send_email`).
291
+
269
292
  ## x402 payments (`../protect_x402_payments.py`)
270
293
 
271
294
  Not a framework wrapper, but the same pattern for paid APIs: decode a real
@@ -0,0 +1,186 @@
1
+ """Put Wardproof in front of a Swarms agent's tool calls.
2
+
3
+ Swarms (https://github.com/kyegomez/swarms) is a multi-agent orchestration
4
+ framework. An agent calls tools, and tool calls flow as objects shaped like
5
+ ``{"function": {"name": ..., "arguments": {...}}}`` (the OpenAI tool-call shape
6
+ Swarms uses for both native tools and MCP tools). The clean place to add a
7
+ deterministic safety layer is right before a tool call is executed: screen the
8
+ call with Wardproof's default swarm and only run it when the verdict is ALLOW.
9
+
10
+ How the interception works (verified against the Swarms MCP tool API at
11
+ https://docs.swarms.world/integrations/mcp, not from memory). Swarms executes a
12
+ single tool call with ``execute_tool_call_simple(response=tool_call, ...)`` where
13
+ ``tool_call`` is ``{"function": {"name", "arguments"}}``. So the uniform
14
+ interception point is a thin wrapper that takes that same tool-call dict, screens
15
+ ``(name, arguments)`` through ``swarm.handle(Event(kind="tool_call", ...))``, and
16
+ only forwards to the real executor on ALLOW. Anything else is refused and recorded
17
+ to the audit ledger; the tool is never executed.
18
+
19
+ What this example is, and is NOT. It runs the REAL screening engine and a REAL
20
+ tamper-evident ledger. It does NOT spin up a live Swarms agent or an LLM: building
21
+ and screening a tool call needs no model, so the guard, the verdicts, and the
22
+ audit record are all real. The "executor" here is an offline stub that returns a
23
+ canned string and runs no real tool, so the example needs no network and no API
24
+ keys. In a real deployment you would pass Swarms' ``execute_tool_call_simple`` (or
25
+ your own tool dispatcher) as the executor instead of the stub.
26
+
27
+ Honest note on detection. Screening uses the default swarm's deterministic
28
+ guardrails (prompt-injection and tool-misuse baselines). They catch a destructive
29
+ command in the tool name or arguments (for example ``rm -rf /``) and an injection
30
+ that rides in the tool name; they are a transparent baseline, not a guarantee
31
+ against a novel phrasing. One known gap: an injection hidden inside an argument
32
+ *value* (plain prose in ``arguments``) is not caught by this deterministic
33
+ tool-call screen on its own; that is the job of the optional LLM second opinion
34
+ (``build_default_swarm(llm=...)``). The baseline's value is screening the concrete
35
+ tool call and its arguments at the moment of invocation, and recording every
36
+ decision so the trail is verifiable afterwards.
37
+
38
+ Swarms is an OPTIONAL dependency. This example's guard does not import swarms at
39
+ all (it operates on the plain tool-call dict), so it runs as-is. To wire it to a
40
+ real Swarms executor:
41
+
42
+ pip install -U swarms wardproof
43
+
44
+ # then pass swarms.tools.execute_tool_call_simple as the executor
45
+
46
+ Run the offline demonstration:
47
+
48
+ python examples/integrations/swarms_guarded.py
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ from typing import Any, Callable
54
+
55
+ from wardproof import AuditLedger, Event, Verdict, build_default_swarm
56
+
57
+ # A tool call as Swarms passes it: {"function": {"name": ..., "arguments": {...}}}
58
+ ToolCall = dict[str, Any]
59
+ # An executor takes a tool call and returns the tool's result string.
60
+ Executor = Callable[[ToolCall], str]
61
+
62
+
63
+ class GuardedToolExecutor:
64
+ """Screen each Swarms tool call before executing it.
65
+
66
+ Wrap any executor that takes a ``{"function": {"name", "arguments"}}`` dict
67
+ and returns a string (for example ``swarms.tools.execute_tool_call_simple``,
68
+ adapted to a sync call). On a verdict other than ALLOW the wrapped executor
69
+ is never called; a refusal string is returned and the decision is recorded.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ executor: Executor,
75
+ *,
76
+ swarm: Any | None = None,
77
+ ledger: AuditLedger | None = None,
78
+ agent_name: str = "swarms-agent",
79
+ ) -> None:
80
+ self._executor = executor
81
+ self._ledger = ledger if ledger is not None else AuditLedger()
82
+ self._swarm = swarm if swarm is not None else build_default_swarm(ledger=self._ledger)
83
+ self._agent_name = agent_name
84
+
85
+ def run(self, tool_call: ToolCall) -> str:
86
+ function = tool_call.get("function", {}) or {}
87
+ name = str(function.get("name", ""))
88
+ arguments = function.get("arguments", {}) or {}
89
+ if not isinstance(arguments, dict):
90
+ arguments = {"_raw": arguments}
91
+
92
+ out = self._swarm.handle(
93
+ Event(
94
+ kind="tool_call",
95
+ source=self._agent_name,
96
+ content=name,
97
+ metadata={"args": arguments},
98
+ )
99
+ )
100
+
101
+ if out.verdict is not Verdict.ALLOW:
102
+ seen: set[str] = set()
103
+ ordered: list[str] = []
104
+ for d in (out.detector, out.verifier):
105
+ for f in d.findings:
106
+ if f.triggered and f.reason and f.reason not in seen:
107
+ seen.add(f.reason)
108
+ ordered.append(f.reason)
109
+ reasons = "; ".join(ordered)
110
+ return (
111
+ f"BLOCKED by Wardproof: verdict={out.verdict.value}. "
112
+ f"The tool '{name}' was not executed. {reasons}".strip()
113
+ )
114
+
115
+ return self._executor(tool_call)
116
+
117
+ def run_many(self, tool_calls: list[ToolCall]) -> list[str]:
118
+ """Screen and run a batch of tool calls, one verdict each.
119
+
120
+ Swarms can execute several tool calls in one step (see
121
+ ``execute_multiple_tools_on_multiple_mcp_servers``). Each call is screened
122
+ independently: the allowed ones run, the rest are refused and recorded,
123
+ and the returned list lines up one-to-one with ``tool_calls``. One
124
+ poisoned call in a batch never blocks the safe ones, and never runs.
125
+ """
126
+ return [self.run(call) for call in tool_calls]
127
+
128
+ @property
129
+ def ledger(self) -> AuditLedger:
130
+ return self._ledger
131
+
132
+
133
+ def make_swarms_executor(server_path: str, *, transport: str = "streamable-http") -> Executor:
134
+ """Build an executor backed by Swarms' real MCP tool dispatcher.
135
+
136
+ This is the production path: it wraps ``swarms.tools.execute_tool_call_simple``
137
+ (verified against the Swarms MCP API) so a guarded call that passes screening
138
+ is executed against a real MCP server. Swarms' function is async, so it is run
139
+ to completion here for the simple synchronous executor shape. Importing swarms
140
+ happens only when this is called, so the rest of this module stays import-free.
141
+ """
142
+ import asyncio
143
+
144
+ from swarms.tools import execute_tool_call_simple # imported lazily on purpose
145
+
146
+ def _executor(tool_call: ToolCall) -> str:
147
+ result = asyncio.run(
148
+ execute_tool_call_simple(
149
+ response=tool_call,
150
+ server_path=server_path,
151
+ output_type="str",
152
+ transport=transport,
153
+ )
154
+ )
155
+ return result if isinstance(result, str) else str(result)
156
+
157
+ return _executor
158
+
159
+
160
+ def _stub_executor(tool_call: ToolCall) -> str:
161
+ """Offline stand-in for a real Swarms tool dispatcher. Runs no real tool."""
162
+ name = tool_call.get("function", {}).get("name", "")
163
+ return f"[stub] executed {name} and returned a canned result"
164
+
165
+
166
+ def _demo() -> None:
167
+ ledger = AuditLedger()
168
+ guarded = GuardedToolExecutor(_stub_executor, ledger=ledger, agent_name="swarms-agent")
169
+
170
+ # a batch as Swarms might dispatch in one step: one of them is dangerous
171
+ calls: list[ToolCall] = [
172
+ {"function": {"name": "get_weather", "arguments": {"city": "Berlin"}}},
173
+ {"function": {"name": "run_command", "arguments": {"cmd": "rm -rf /"}}},
174
+ {"function": {"name": "send_email", "arguments": {"to": "a@b.com", "subject": "hi"}}},
175
+ ]
176
+
177
+ for call, result in zip(calls, guarded.run_many(calls)):
178
+ name = call["function"]["name"]
179
+ print(f"{name:14} -> {result}")
180
+
181
+ ok, detail = ledger.verify()
182
+ print(f"\nledger: {'OK' if ok else 'FAIL'} - {detail}")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ _demo()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wardproof"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Local-first, verifiable defensive AI agent swarms that protect other AI agent systems."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,7 +16,7 @@ from wardproof.sandbox.executor import SandboxExecutor, ToolRegistry
16
16
  from wardproof.sandbox.permissions import PermissionBroker, ToolGrant
17
17
  from wardproof.schema import Decision, Event, Finding, Severity, Verdict
18
18
 
19
- __version__ = "0.3.2"
19
+ __version__ = "0.3.4"
20
20
  __all__ = [
21
21
  "Event",
22
22
  "Decision",
@@ -101,6 +101,9 @@ def main(argv: list[str] | None = None) -> int:
101
101
  cp.add_argument("--source", default="agent", help="who originated the event")
102
102
  cp.add_argument("--args", default=None, help="tool-call arguments as a JSON string")
103
103
  cp.add_argument("--json", action="store_true", help="print a JSON object instead of text")
104
+ rp = sub.add_parser("serve", help="run a local HTTP service that screens one event per request")
105
+ rp.add_argument("--host", default="127.0.0.1", help="bind address (default localhost)")
106
+ rp.add_argument("--port", type=int, default=8787, help="bind port (default 8787)")
104
107
  args = parser.parse_args(argv)
105
108
  if args.cmd == "verify-ledger":
106
109
  return _verify_file(args.path, args.pubkey)
@@ -108,6 +111,11 @@ def main(argv: list[str] | None = None) -> int:
108
111
  return _export_stix(args.path, args.out)
109
112
  if args.cmd == "check":
110
113
  return _check(args.kind, args.content, args.source, args.args, args.json)
114
+ if args.cmd == "serve":
115
+ from wardproof.server import serve
116
+
117
+ serve(host=args.host, port=args.port)
118
+ return 0
111
119
  return 1
112
120
 
113
121
 
@@ -0,0 +1,119 @@
1
+ """A small local HTTP service that screens one event per request.
2
+
3
+ Stdlib only, no third-party dependencies. It builds the default swarm once at
4
+ startup and reuses it, so a host (an agent, a bot, any language) can gate a tool
5
+ call or input with a single HTTP request instead of spawning a process and
6
+ re-importing on every call.
7
+
8
+ The service binds to localhost by default and is meant to run next to the agent
9
+ it guards, not to be exposed to the public internet.
10
+
11
+ Endpoints:
12
+ GET /health -> {"status": "ok", "version": "..."}
13
+ POST /check -> body: {"kind": "tool_call"|"input", "content": "...",
14
+ "source": "...", "args": {...}}
15
+ reply: {"verdict": "...", "allowed": true|false,
16
+ "risk": 0.0, "reasons": [...]}
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from typing import Any
24
+
25
+ from wardproof import __version__
26
+ from wardproof.orchestration.factory import build_default_swarm
27
+ from wardproof.schema import Event, Verdict
28
+
29
+
30
+ def screen_event(swarm: Any, payload: dict[str, Any]) -> dict[str, Any]:
31
+ """Screen one payload with the given swarm and return a JSON-able verdict.
32
+
33
+ Shared by the HTTP handler and the tests so both exercise identical logic.
34
+ """
35
+ kind = str(payload.get("kind", "tool_call"))
36
+ content = str(payload.get("content", ""))
37
+ source = str(payload.get("source", "agent"))
38
+ metadata: dict[str, Any] = {}
39
+ args = payload.get("args")
40
+ if isinstance(args, dict):
41
+ metadata["args"] = args
42
+
43
+ out = swarm.handle(Event(kind=kind, source=source, content=content, metadata=metadata))
44
+
45
+ seen: set[str] = set()
46
+ reasons: list[str] = []
47
+ for decision in (out.detector, out.verifier):
48
+ for finding in decision.findings:
49
+ if finding.triggered and finding.reason and finding.reason not in seen:
50
+ seen.add(finding.reason)
51
+ reasons.append(finding.reason)
52
+
53
+ return {
54
+ "verdict": out.verdict.value,
55
+ "allowed": out.verdict is Verdict.ALLOW,
56
+ "risk": round(out.risk, 3),
57
+ "reasons": reasons,
58
+ }
59
+
60
+
61
+ def _make_handler(swarm: Any) -> type[BaseHTTPRequestHandler]:
62
+ class Handler(BaseHTTPRequestHandler):
63
+ # one shared swarm for every request
64
+ _swarm = swarm
65
+
66
+ def _send(self, code: int, body: dict[str, Any]) -> None:
67
+ raw = json.dumps(body).encode("utf-8")
68
+ self.send_response(code)
69
+ self.send_header("Content-Type", "application/json")
70
+ self.send_header("Content-Length", str(len(raw)))
71
+ self.end_headers()
72
+ self.wfile.write(raw)
73
+
74
+ def do_GET(self) -> None: # noqa: N802 (stdlib name)
75
+ if self.path.rstrip("/") == "/health":
76
+ self._send(200, {"status": "ok", "version": __version__})
77
+ else:
78
+ self._send(404, {"error": "not found"})
79
+
80
+ def do_POST(self) -> None: # noqa: N802 (stdlib name)
81
+ if self.path.rstrip("/") != "/check":
82
+ self._send(404, {"error": "not found"})
83
+ return
84
+ try:
85
+ length = int(self.headers.get("Content-Length", 0))
86
+ except (TypeError, ValueError):
87
+ length = 0
88
+ raw = self.rfile.read(length) if length else b""
89
+ try:
90
+ payload = json.loads(raw or b"{}")
91
+ if not isinstance(payload, dict):
92
+ raise ValueError("body must be a JSON object")
93
+ except (json.JSONDecodeError, ValueError) as exc:
94
+ self._send(400, {"error": f"invalid JSON body: {exc}"})
95
+ return
96
+ try:
97
+ self._send(200, screen_event(self._swarm, payload))
98
+ except Exception as exc: # fail closed: report, do not 200 a non-screen
99
+ self._send(500, {"error": f"screen failed: {exc}"})
100
+
101
+ def log_message(self, *_args: Any) -> None:
102
+ # stay quiet by default; the host decides what to log
103
+ return
104
+
105
+ return Handler
106
+
107
+
108
+ def serve(host: str = "127.0.0.1", port: int = 8787) -> None:
109
+ """Build the swarm once and serve until interrupted."""
110
+ swarm = build_default_swarm()
111
+ handler = _make_handler(swarm)
112
+ httpd = ThreadingHTTPServer((host, port), handler)
113
+ print(f"wardproof serve: screening on http://{host}:{port} (POST /check, GET /health)")
114
+ try:
115
+ httpd.serve_forever()
116
+ except KeyboardInterrupt:
117
+ pass
118
+ finally:
119
+ httpd.server_close()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes