kiff-guard 1.0.0__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 (43) hide show
  1. kiff_guard-1.0.0/.gitignore +6 -0
  2. kiff_guard-1.0.0/LICENSE +21 -0
  3. kiff_guard-1.0.0/PKG-INFO +363 -0
  4. kiff_guard-1.0.0/README.md +314 -0
  5. kiff_guard-1.0.0/live_openai_check.py +140 -0
  6. kiff_guard-1.0.0/pyproject.toml +70 -0
  7. kiff_guard-1.0.0/src/kiff_guard/__init__.py +77 -0
  8. kiff_guard-1.0.0/src/kiff_guard/adapters/__init__.py +10 -0
  9. kiff_guard-1.0.0/src/kiff_guard/adapters/agno.py +40 -0
  10. kiff_guard-1.0.0/src/kiff_guard/adapters/google_adk.py +92 -0
  11. kiff_guard-1.0.0/src/kiff_guard/adapters/haystack.py +115 -0
  12. kiff_guard-1.0.0/src/kiff_guard/adapters/hermes.py +91 -0
  13. kiff_guard-1.0.0/src/kiff_guard/adapters/langgraph.py +89 -0
  14. kiff_guard-1.0.0/src/kiff_guard/adapters/llama_index.py +194 -0
  15. kiff_guard-1.0.0/src/kiff_guard/adapters/llama_index_core.py +78 -0
  16. kiff_guard-1.0.0/src/kiff_guard/adapters/microsoft_agent_framework.py +69 -0
  17. kiff_guard-1.0.0/src/kiff_guard/adapters/microsoft_agent_framework_core.py +87 -0
  18. kiff_guard-1.0.0/src/kiff_guard/adapters/openai_agents.py +72 -0
  19. kiff_guard-1.0.0/src/kiff_guard/adapters/openai_agents_core.py +78 -0
  20. kiff_guard-1.0.0/src/kiff_guard/adapters/pydantic_ai.py +102 -0
  21. kiff_guard-1.0.0/src/kiff_guard/adapters/strands.py +106 -0
  22. kiff_guard-1.0.0/src/kiff_guard/catalog.py +34 -0
  23. kiff_guard-1.0.0/src/kiff_guard/client.py +505 -0
  24. kiff_guard-1.0.0/src/kiff_guard/conformance.py +175 -0
  25. kiff_guard-1.0.0/src/kiff_guard/decision.py +86 -0
  26. kiff_guard-1.0.0/src/kiff_guard/draft.py +84 -0
  27. kiff_guard-1.0.0/src/kiff_guard/guard.py +279 -0
  28. kiff_guard-1.0.0/src/kiff_guard/py.typed +0 -0
  29. kiff_guard-1.0.0/tests/test_client_and_adapter.py +93 -0
  30. kiff_guard-1.0.0/tests/test_conformance.py +265 -0
  31. kiff_guard-1.0.0/tests/test_draft_save.py +133 -0
  32. kiff_guard-1.0.0/tests/test_draft_schema.py +102 -0
  33. kiff_guard-1.0.0/tests/test_google_adk_adapter.py +93 -0
  34. kiff_guard-1.0.0/tests/test_guard_core.py +101 -0
  35. kiff_guard-1.0.0/tests/test_haystack_adapter.py +101 -0
  36. kiff_guard-1.0.0/tests/test_hermes_adapter.py +153 -0
  37. kiff_guard-1.0.0/tests/test_langgraph_adapter.py +119 -0
  38. kiff_guard-1.0.0/tests/test_llama_index_adapter.py +114 -0
  39. kiff_guard-1.0.0/tests/test_microsoft_agent_framework_adapter.py +125 -0
  40. kiff_guard-1.0.0/tests/test_observe_push.py +252 -0
  41. kiff_guard-1.0.0/tests/test_openai_adapter.py +126 -0
  42. kiff_guard-1.0.0/tests/test_pydantic_ai_adapter.py +115 -0
  43. kiff_guard-1.0.0/tests/test_strands_adapter.py +98 -0
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KIFF contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,363 @@
1
+ Metadata-Version: 2.5
2
+ Name: kiff-guard
3
+ Version: 1.0.0
4
+ Summary: Drop-in guard that puts KIFF clearance in front of any agent's tool calls — observe to audit, enforce to govern.
5
+ Project-URL: Homepage, https://kiff.dev
6
+ Project-URL: Documentation, https://docs.kiff.dev
7
+ Project-URL: Source, https://github.com/kiff/kiff-guard
8
+ Project-URL: Changelog, https://github.com/kiff/kiff-guard/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/kiff/kiff-guard/issues
10
+ Author: KIFF
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,ai-agents,audit,governance,guardrails,kiff,llm,tool-calls
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Provides-Extra: agno
28
+ Requires-Dist: agno<3,>=2.6; extra == 'agno'
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: pyyaml; extra == 'dev'
32
+ Provides-Extra: google-adk
33
+ Requires-Dist: google-adk<3,>=2.0; extra == 'google-adk'
34
+ Provides-Extra: haystack
35
+ Requires-Dist: haystack-ai<3,>=2.0; extra == 'haystack'
36
+ Provides-Extra: langgraph
37
+ Requires-Dist: langchain<2,>=1.0; extra == 'langgraph'
38
+ Provides-Extra: llama-index
39
+ Requires-Dist: llama-index-core<0.15,>=0.14; extra == 'llama-index'
40
+ Provides-Extra: microsoft-agent-framework
41
+ Requires-Dist: agent-framework-core<2,>=1.0; extra == 'microsoft-agent-framework'
42
+ Provides-Extra: openai
43
+ Requires-Dist: openai-agents<0.18,>=0.17; extra == 'openai'
44
+ Provides-Extra: pydantic-ai
45
+ Requires-Dist: pydantic-ai<2,>=1.0; extra == 'pydantic-ai'
46
+ Provides-Extra: strands
47
+ Requires-Dist: strands-agents<2,>=1.0; extra == 'strands'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # kiff-guard
51
+
52
+ Drop-in KIFF clearance in front of any agent's tool calls. One guard,
53
+ two modes:
54
+
55
+ - **observe** — runs every tool, records an audit trail, and learns the
56
+ action catalog. **No KIFF account, no domain, no API call required.**
57
+ The fastest way to see what your agents actually do.
58
+ - **enforce** — asks KIFF to decide before each tool runs: `allowed`
59
+ proceeds, `approval_required` / `blocked` / `invalid` hold the call.
60
+
61
+ The same one-line integration that governs your agent at runtime also
62
+ **derives a starter KIFF domain** from real traffic — so you never start
63
+ from a blank `kiff.yaml`.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install kiff-guard # core, zero deps
69
+ pip install "kiff-guard[agno]" # + the Agno adapter's framework
70
+ ```
71
+
72
+ ## Quickstart — audit your agent in under 5 minutes (zero config)
73
+
74
+ ```python
75
+ from kiff_guard import Guard
76
+ from kiff_guard.adapters.agno import agno_hook
77
+
78
+ guard = Guard(mode="observe") # no client, no tenant needed
79
+
80
+ agent = Agent(model=..., tools=[refund_order, send_email],
81
+ tool_hooks=[agno_hook(guard)])
82
+
83
+ # ... run your agent as usual ...
84
+
85
+ for r in guard.receipts:
86
+ print(r.state, r.tool, r.outcome) # state == "observed"
87
+
88
+ from kiff_guard import export_yaml
89
+ print(export_yaml("my-domain", guard.catalog)) # your draft domain, free
90
+ ```
91
+
92
+ Observe never calls KIFF and never blocks a tool. You get a real audit
93
+ trail of your own agent and a derived domain draft — the draft you then
94
+ review and activate before turning on enforcement.
95
+
96
+ With a Cloud credential you can push that derived draft straight to the
97
+ authoring UI instead of pasting it:
98
+
99
+ ```python
100
+ guard = Guard(client=HTTPClient(api_key="kiff_live_...", tool_map=ToolMap()),
101
+ tenant="<tenant>", agent="support", mode="observe")
102
+ # ... run your agent ...
103
+ result = guard.save_draft("my-domain") # PUT /v1/me/domain/draft
104
+ print(result.valid, result.issues) # the draft now shows up in Studio
105
+ ```
106
+
107
+ `save_draft` renders the learned catalog with `export_yaml` and upserts it
108
+ to the cloud draft store. It's opt-in (separate from observe/enforce), so
109
+ zero-config audit stays local unless you explicitly call it.
110
+
111
+ ## Enforce — once you have a tenant and an active domain
112
+
113
+ ```python
114
+ from kiff_guard import Guard, HTTPClient, ToolMap
115
+ from kiff_guard.adapters.agno import agno_hook
116
+
117
+ client = HTTPClient(
118
+ api_key="kiff_live_...", # mint in the dashboard
119
+ tool_map=ToolMap().bind(
120
+ "refund_order", action="REFUND_ORDER",
121
+ entity_type="Order", entity_arg="order_id"),
122
+ )
123
+ guard = Guard(client=client, tenant="<tenant>", agent="support", mode="enforce")
124
+
125
+ agent = Agent(model=..., tools=[refund_order], tool_hooks=[agno_hook(guard)])
126
+ ```
127
+
128
+ In enforce mode a withheld decision raises `kiff_guard.Hold`, carrying the
129
+ decision so your app can route it to a human (approval_required) or
130
+ surface the refusal. The API key's roles govern authority server-side —
131
+ the guard never asserts roles, so it cannot weaken the trust boundary.
132
+
133
+ ## Custom agent? No adapter required
134
+
135
+ The adapters below are convenience glue for specific frameworks. They add
136
+ **no governance logic** — the guard logic lives in the core. If you run a
137
+ custom agent (your own loop, a framework with no adapter yet, Deno,
138
+ whatever), use the core directly. `HTTPClient` already speaks the hosted
139
+ decide route (`POST /v1/proposals/decide` against `api.kiff.dev`); there
140
+ is nothing extra to install or run.
141
+
142
+ **Observe — zero config, no KIFF account.** Call `observe` wherever your
143
+ loop is about to run a tool:
144
+
145
+ ```python
146
+ from kiff_guard import Guard
147
+
148
+ guard = Guard(mode="observe") # no client, no tenant
149
+
150
+ def run_tool(name, args):
151
+ guard.observe(name, args) # learn + record, never blocks
152
+ return tools[name](**args) # your agent runs the tool
153
+
154
+ # ... after the run:
155
+ for r in guard.receipts:
156
+ print(r.state, r.tool, r.outcome) # state == "observed"
157
+ ```
158
+
159
+ **Enforce — decide before you run.** Gate on `decision.withheld` (true for
160
+ anything that isn't an explicit `allowed`, so an unknown future outcome
161
+ fails safe), then record exactly one receipt:
162
+
163
+ ```python
164
+ from kiff_guard import Guard, HTTPClient, ToolMap
165
+
166
+ client = HTTPClient(
167
+ api_key="kiff_live_...",
168
+ tool_map=ToolMap().bind(
169
+ "refund_order", action="REFUND_ORDER",
170
+ entity_type="Order", entity_arg="order_id"),
171
+ )
172
+ guard = Guard(client=client, tenant="<tenant>", agent="support", mode="enforce")
173
+
174
+ def run_tool(name, args):
175
+ decision = guard.decide_only(name, args) # calls KIFF, does not run
176
+ if decision.withheld: # != "allowed" → withhold
177
+ guard.record_withheld(name, args, decision)
178
+ return f"withheld: {decision.outcome} — {decision.reason}"
179
+ result = tools[name](**args) # your agent runs the tool
180
+ guard.record_executed(name, args, decision) # one receipt per call
181
+ return result
182
+ ```
183
+
184
+ This is the same core the adapters call; an adapter just translates one
185
+ framework's pre-tool seam into these calls. You send `actor_id` (the
186
+ `agent`); you never send roles — the API key's roles govern authority
187
+ server-side, so your only integration responsibility is authenticating the
188
+ caller's identity, not granting it.
189
+
190
+ For stacks the SDKs don't cover (Ruby, Go, shell), a proposal is a single
191
+ HTTP POST — see
192
+ [`cookbook/custom-agent-http`](../../../cookbook/custom-agent-http/).
193
+
194
+ ## Architecture
195
+
196
+ A **framework-agnostic core** (`Guard.evaluate`) plus **thin adapters**,
197
+ one per framework, each translating that framework's pre-tool-execution
198
+ seam into a single `evaluate` call. The guard logic lives in the core,
199
+ once; an adapter adds no governance logic of its own.
200
+
201
+ | Framework | Adapter | Status |
202
+ |---|---|---|
203
+ | Agno | `kiff_guard.adapters.agno` | shipped |
204
+ | Hermes (Nous) | `kiff_guard.adapters.hermes` | shipped |
205
+ | LangGraph / LangChain | `kiff_guard.adapters.langgraph` | shipped |
206
+ | OpenAI Agents SDK | `kiff_guard.adapters.openai_agents` | shipped |
207
+ | Google ADK | `kiff_guard.adapters.google_adk` | shipped |
208
+ | Pydantic AI | `kiff_guard.adapters.pydantic_ai` | shipped |
209
+ | Strands Agents | `kiff_guard.adapters.strands` | shipped |
210
+ | Haystack Agents | `kiff_guard.adapters.haystack` | shipped |
211
+ | Microsoft Agent Framework | `kiff_guard.adapters.microsoft_agent_framework` | shipped |
212
+ | LlamaIndex | `kiff_guard.adapters.llama_index` | shipped |
213
+ | OpenClaw (TypeScript) | `@kiff/kiff-guard/adapters/openclaw` (`packages/js`) | shipped |
214
+
215
+ Each adapter documents its verified pre-tool-execution seam and block
216
+ contract in its module docstring. See the two adapter shapes below.
217
+
218
+ ### Two adapter shapes
219
+
220
+ - **Middleware** (Agno, LangGraph / LangChain, …): the guard runs the
221
+ tool via `Guard.evaluate(tool, args, run=...)`.
222
+ - **Inverted-control** (Hermes, OpenAI Agents SDK, …): the framework runs
223
+ the tool; the hook only votes. Adapters use `Guard.observe()` /
224
+ `Guard.decide_only()` and act on the returned `Decision` — no run
225
+ callback.
226
+
227
+ #### Hermes (Nous Research)
228
+
229
+ Ship a Hermes plugin (`~/.hermes/plugins/kiff-guard/`) whose
230
+ `__init__.py` wires the guard into Hermes' `pre_tool_call` hook:
231
+
232
+ ```python
233
+ from kiff_guard import Guard
234
+ from kiff_guard.adapters.hermes import register_kiff_guard
235
+
236
+ _GUARD = Guard(mode="observe") # zero-config audit; no KIFF account
237
+
238
+ def register(ctx):
239
+ register_kiff_guard(ctx, _GUARD)
240
+ ```
241
+
242
+ In observe mode the hook records + learns every tool call and never
243
+ blocks. In enforce mode (`Guard(client=..., mode="enforce")`) a withheld
244
+ KIFF decision returns Hermes' `{"action": "block", ...}` directive so the
245
+ tool never runs. Enforce fails closed on a guard error by default (a
246
+ control tower shouldn't wave traffic through when its decision path is
247
+ down); pass `fail_closed=False` to override.
248
+
249
+ #### OpenAI Agents SDK
250
+
251
+ Attach the guard as a **tool input guardrail** on a `function_tool`:
252
+
253
+ ```python
254
+ from agents import Agent, function_tool
255
+ from kiff_guard import Guard
256
+ from kiff_guard.adapters.openai_agents import kiff_tool_input_guardrail
257
+
258
+ guard = Guard(mode="observe") # zero-config audit; no KIFF account
259
+ kiff_gd = kiff_tool_input_guardrail(guard)
260
+
261
+ @function_tool(tool_input_guardrails=[kiff_gd])
262
+ def refund_order(order_id: str, amount_cents: int) -> str:
263
+ ...
264
+
265
+ agent = Agent(name="support", tools=[refund_order])
266
+ ```
267
+
268
+ The tool input guardrail runs **before** the tool executes (verified
269
+ against openai-agents v0.17.4). In observe mode it records + learns and
270
+ always allows. In enforce mode (`Guard(client=..., mode="enforce")`) a
271
+ withheld KIFF decision returns `ToolGuardrailFunctionOutput.reject_content(reason)`
272
+ so the SDK skips the tool and hands the reason to the model — without
273
+ running it. Enforce fails closed on a guard error by default. Install the
274
+ SDK with `pip install "kiff-guard[openai]"` (the `openai` extra maps to
275
+ the `openai-agents` package).
276
+
277
+ > The tool input guardrail — not `needs_approval` — is the synchronous
278
+ > policy seam. `needs_approval` is the heavyweight human-pause path (the
279
+ > run pauses and surfaces `interruptions`, resumed via RunState); KIFF's
280
+ > gate is a machine decision that belongs in the guardrail.
281
+
282
+ ### LangGraph / LangChain
283
+
284
+ Wrap the guard as `wrap_tool_call` middleware on a LangChain agent:
285
+
286
+ ```python
287
+ from langchain.agents import create_agent
288
+ from langchain.agents.middleware import wrap_tool_call
289
+ from kiff_guard import Guard
290
+ from kiff_guard.adapters.langgraph import kiff_wrap_tool_call
291
+
292
+ guard = Guard(mode="observe") # zero-config audit; no KIFF account
293
+ kiff_mw = wrap_tool_call(kiff_wrap_tool_call(guard))
294
+
295
+ agent = create_agent(model="...", tools=[...], middleware=[kiff_mw])
296
+ ```
297
+
298
+ In observe mode the middleware runs each tool via the handler, records +
299
+ learns, and never blocks. In enforce mode (`Guard(client=..., mode=
300
+ "enforce")`) a withheld KIFF decision returns a `ToolMessage`
301
+ (`status="error"`) carrying the reason **without** running the tool — the
302
+ same short-circuit pattern LangChain's built-in `ShellAllowListMiddleware`
303
+ uses. Install the framework with `pip install "kiff-guard[langgraph]"`.
304
+
305
+ #### LlamaIndex
306
+
307
+ Subclass `AgentWorkflow` via `GuardedAgentWorkflow`, which overrides the
308
+ `call_tool` step to insert the KIFF gate before `_call_tool` runs:
309
+
310
+ ```python
311
+ from llama_index.core.agent.workflow import FunctionAgent
312
+ from llama_index.llms.openai import OpenAI
313
+ from kiff_guard import Guard
314
+ from kiff_guard.adapters.llama_index import GuardedAgentWorkflow
315
+
316
+ guard = Guard(mode="observe") # zero-config audit; no KIFF account
317
+
318
+ workflow = GuardedAgentWorkflow(
319
+ agents=[FunctionAgent(tools=[...], llm=OpenAI(model="gpt-4o-mini"))],
320
+ guard=guard,
321
+ )
322
+ result = await workflow.run(user_msg="...")
323
+ ```
324
+
325
+ The seam is `AgentWorkflow.call_tool` — a `@step` that receives a
326
+ `ToolCall` event (`tool_name`, `tool_kwargs`, `tool_id`) before the tool
327
+ body runs. This is a **middleware shape**: the guard closure over
328
+ `_call_tool` is the continuation. In enforce mode a withheld decision
329
+ raises `Hold` so the tool never runs. Install with
330
+ `pip install "kiff-guard[llama-index]"`.
331
+
332
+ ## Conformance & verification
333
+
334
+ Every adapter must pass the **conformance suite** (`kiff_guard.conformance`)
335
+ — a `storetest`-style contract that pins the invariants all adapters
336
+ share, both shapes:
337
+
338
+ - observe never calls the client, always runs the tool, records exactly
339
+ one `observed` receipt, learns the catalog, and works with no client/
340
+ tenant;
341
+ - enforce allowed → tool runs + exactly one governed `executed=True`
342
+ receipt; enforce withheld → tool does not run + exactly one governed
343
+ `executed=False` receipt (the one-receipt rule);
344
+ - the guard never injects a `roles` field (trust boundary).
345
+
346
+ A new adapter is "done" when it has a `drive` shim in
347
+ `tests/test_conformance.py` and passes. This is the durability mechanism:
348
+ a community adapter can be accepted by passing conformance rather than a
349
+ line-by-line audit, and CI catches upstream framework drift.
350
+
351
+ ```bash
352
+ python -m pytest tests/ # full offline suite incl. conformance
353
+ ```
354
+
355
+ `live_openai_check.py` verifies the OpenAI Agents adapter against the
356
+ **real** `openai-agents` SDK + a live model call (the SDK accepts the
357
+ guardrail, `reject_content` genuinely skips the tool, one receipt per
358
+ call). It needs a 3.10+ env, `pip install openai-agents`, and
359
+ `OPENAI_API_KEY` in the environment; it is operator-run, not part of CI.
360
+
361
+ ## License
362
+
363
+ MIT.