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.
- kiff_guard-1.0.0/.gitignore +6 -0
- kiff_guard-1.0.0/LICENSE +21 -0
- kiff_guard-1.0.0/PKG-INFO +363 -0
- kiff_guard-1.0.0/README.md +314 -0
- kiff_guard-1.0.0/live_openai_check.py +140 -0
- kiff_guard-1.0.0/pyproject.toml +70 -0
- kiff_guard-1.0.0/src/kiff_guard/__init__.py +77 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/__init__.py +10 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/agno.py +40 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/google_adk.py +92 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/haystack.py +115 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/hermes.py +91 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/langgraph.py +89 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/llama_index.py +194 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/llama_index_core.py +78 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/microsoft_agent_framework.py +69 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/microsoft_agent_framework_core.py +87 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/openai_agents.py +72 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/openai_agents_core.py +78 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/pydantic_ai.py +102 -0
- kiff_guard-1.0.0/src/kiff_guard/adapters/strands.py +106 -0
- kiff_guard-1.0.0/src/kiff_guard/catalog.py +34 -0
- kiff_guard-1.0.0/src/kiff_guard/client.py +505 -0
- kiff_guard-1.0.0/src/kiff_guard/conformance.py +175 -0
- kiff_guard-1.0.0/src/kiff_guard/decision.py +86 -0
- kiff_guard-1.0.0/src/kiff_guard/draft.py +84 -0
- kiff_guard-1.0.0/src/kiff_guard/guard.py +279 -0
- kiff_guard-1.0.0/src/kiff_guard/py.typed +0 -0
- kiff_guard-1.0.0/tests/test_client_and_adapter.py +93 -0
- kiff_guard-1.0.0/tests/test_conformance.py +265 -0
- kiff_guard-1.0.0/tests/test_draft_save.py +133 -0
- kiff_guard-1.0.0/tests/test_draft_schema.py +102 -0
- kiff_guard-1.0.0/tests/test_google_adk_adapter.py +93 -0
- kiff_guard-1.0.0/tests/test_guard_core.py +101 -0
- kiff_guard-1.0.0/tests/test_haystack_adapter.py +101 -0
- kiff_guard-1.0.0/tests/test_hermes_adapter.py +153 -0
- kiff_guard-1.0.0/tests/test_langgraph_adapter.py +119 -0
- kiff_guard-1.0.0/tests/test_llama_index_adapter.py +114 -0
- kiff_guard-1.0.0/tests/test_microsoft_agent_framework_adapter.py +125 -0
- kiff_guard-1.0.0/tests/test_observe_push.py +252 -0
- kiff_guard-1.0.0/tests/test_openai_adapter.py +126 -0
- kiff_guard-1.0.0/tests/test_pydantic_ai_adapter.py +115 -0
- kiff_guard-1.0.0/tests/test_strands_adapter.py +98 -0
kiff_guard-1.0.0/LICENSE
ADDED
|
@@ -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.
|