shield-kya 0.17.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 (34) hide show
  1. shield_kya-0.17.0/PKG-INFO +265 -0
  2. shield_kya-0.17.0/README.md +232 -0
  3. shield_kya-0.17.0/pyproject.toml +43 -0
  4. shield_kya-0.17.0/setup.cfg +4 -0
  5. shield_kya-0.17.0/shield_kya/__init__.py +383 -0
  6. shield_kya-0.17.0/shield_kya/_never_list.py +94 -0
  7. shield_kya-0.17.0/shield_kya/integrations/__init__.py +109 -0
  8. shield_kya-0.17.0/shield_kya/integrations/adk.py +92 -0
  9. shield_kya-0.17.0/shield_kya/integrations/agent_framework.py +91 -0
  10. shield_kya-0.17.0/shield_kya/integrations/crewai.py +213 -0
  11. shield_kya-0.17.0/shield_kya/integrations/langchain.py +78 -0
  12. shield_kya-0.17.0/shield_kya/integrations/langgraph.py +65 -0
  13. shield_kya-0.17.0/shield_kya/integrations/openai_agents.py +173 -0
  14. shield_kya-0.17.0/shield_kya/integrations/pydantic_ai.py +129 -0
  15. shield_kya-0.17.0/shield_kya.egg-info/PKG-INFO +265 -0
  16. shield_kya-0.17.0/shield_kya.egg-info/SOURCES.txt +32 -0
  17. shield_kya-0.17.0/shield_kya.egg-info/dependency_links.txt +1 -0
  18. shield_kya-0.17.0/shield_kya.egg-info/requires.txt +21 -0
  19. shield_kya-0.17.0/shield_kya.egg-info/top_level.txt +1 -0
  20. shield_kya-0.17.0/tests/test_e2e_adk.py +153 -0
  21. shield_kya-0.17.0/tests/test_e2e_agent_framework.py +147 -0
  22. shield_kya-0.17.0/tests/test_e2e_crewai.py +200 -0
  23. shield_kya-0.17.0/tests/test_e2e_langchain.py +195 -0
  24. shield_kya-0.17.0/tests/test_e2e_openai_agents.py +145 -0
  25. shield_kya-0.17.0/tests/test_e2e_pydantic_ai.py +117 -0
  26. shield_kya-0.17.0/tests/test_integrations_adk.py +359 -0
  27. shield_kya-0.17.0/tests/test_integrations_agent_framework.py +326 -0
  28. shield_kya-0.17.0/tests/test_integrations_crewai.py +360 -0
  29. shield_kya-0.17.0/tests/test_integrations_langchain.py +104 -0
  30. shield_kya-0.17.0/tests/test_integrations_langgraph.py +143 -0
  31. shield_kya-0.17.0/tests/test_integrations_openai_agents.py +411 -0
  32. shield_kya-0.17.0/tests/test_integrations_pydantic_ai.py +256 -0
  33. shield_kya-0.17.0/tests/test_never_list.py +51 -0
  34. shield_kya-0.17.0/tests/test_shield_kya.py +578 -0
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.4
2
+ Name: shield-kya
3
+ Version: 0.17.0
4
+ Summary: Audit trail and traceability for AI agents. In-process kya shim for Python agent frameworks (offline evaluate via the kya CLI).
5
+ Author: MFR Limited
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/The-Pixel-Boys/shield-kya
8
+ Project-URL: Repository, https://github.com/The-Pixel-Boys/shield-kya
9
+ Keywords: kya,know-your-agent,governance,mcp,agents
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: langchain
20
+ Requires-Dist: langchain<2,>=1.4.4; extra == "langchain"
21
+ Provides-Extra: langgraph
22
+ Requires-Dist: langgraph; extra == "langgraph"
23
+ Provides-Extra: crewai
24
+ Requires-Dist: crewai<2,>=1.15.27; extra == "crewai"
25
+ Provides-Extra: pydantic-ai
26
+ Requires-Dist: pydantic-ai-slim<3,>=2.55.0; extra == "pydantic-ai"
27
+ Provides-Extra: adk
28
+ Requires-Dist: google-adk<3,>=2.11.0; extra == "adk"
29
+ Provides-Extra: agent-framework
30
+ Requires-Dist: agent-framework-core<2,>=1.21.0; extra == "agent-framework"
31
+ Provides-Extra: openai-agents
32
+ Requires-Dist: openai-agents<0.24,>=0.23.1; extra == "openai-agents"
33
+
34
+ # shield-kya
35
+
36
+ In-process [kya](https://github.com/The-Pixel-Boys/shield-kya) (Know Your Agent) governance shim for Python agent frameworks.
37
+
38
+ `shield_kya` delegates policy evaluation to the local kya CLI (`kya wrap --offline --json ...`), so Python tools share the same offline verdict path and trail as the TypeScript SDK. Stdlib only, no dependencies.
39
+
40
+ ## Requirements
41
+
42
+ - Python >= 3.9 (the framework hooks need what their framework needs: langchain 3.10+, pydantic-ai 3.11+, crewai 3.10 to 3.13, google-adk 3.10+, agent-framework-core 3.10+, openai-agents 3.10+)
43
+ - The kya CLI on `PATH` (`npm i -g @shield-agent/kya`), or set `KYA_CLI` to an absolute path (an executable, or a `.js` file run with `node`). `kya` is looked up in absolute PATH entries only, never the current directory; a relative `KYA_CLI` is ignored.
44
+
45
+ ## Install
46
+
47
+ Not yet published to PyPI: do not `pip install shield-kya` from PyPI until it is. Install from a checkout, in this directory:
48
+
49
+ ```sh
50
+ pip install .
51
+ pip install '.[langchain]' # LangChain / LangGraph middleware
52
+ pip install '.[pydantic-ai]' # Pydantic AI capability
53
+ pip install '.[crewai]' # CrewAI global hook
54
+ pip install '.[adk]' # Google ADK plugin
55
+ pip install '.[agent-framework]' # Microsoft Agent Framework middleware (agent-framework-core)
56
+ pip install '.[openai-agents]' # OpenAI Agents SDK tool input guardrail
57
+ ```
58
+
59
+ ## Semantics: advisory, fail-open
60
+
61
+ Same rules as `kya hook`:
62
+
63
+ | Situation | Default (OSS) | `fail_closed=True` / `KYA_FAIL_CLOSED=1` (hosted) |
64
+ |---|---|---|
65
+ | `ALLOW` | tool runs | tool runs |
66
+ | `REQUIRE_APPROVE` | tool runs, recorded (`strict=True` blocks) | same |
67
+ | `DENY` (local never-list) | blocked | blocked |
68
+ | CLI missing, timeout (4 s), non-JSON, unknown exit code, empty verdict, any shim error | tool runs; first event logged at WARNING, every event at DEBUG (logger `shield_kya`) | blocked |
69
+ | same, but the tool name is on the never-list | blocked (`NEVER_EVENT`) | blocked |
70
+
71
+ Without a CLI verdict the never-list is matched in Python (`shield_kya/_never_list.py`, a port of the CLI's name-only rule; `tests/test_never_list.py` and the TS suite assert the same vectors file). `KYA_FAIL_CLOSED` accepts 1/true/yes/on (any case). Hosted operators should not pass `fail_closed=False`, which overrides the env var.
72
+
73
+ On Windows the shim refuses to run `kya` through a `.cmd`/`.bat` shim (cmd.exe would re-parse the model-controlled arguments); set `KYA_CLI` to the absolute path of the package's `dist/cli.js` and it runs with `node`.
74
+
75
+ Arguments are passed to the CLI on argv. A tool call whose arguments exceed the OS argv limit (about 128 KiB per argument on Linux) gets no CLI verdict and no trail row: it fails open, and the never-list still applies by tool name. An empty tool id is denied (`EMPTY_TOOL`), like the CLI's offline evaluate.
76
+
77
+ "Hosted" here means only the fail-closed switch: the shim always evaluates offline (`kya wrap --offline`) and never consults a hosted policy plane.
78
+
79
+ Async callers use an asyncio subprocess, so the check never blocks the event loop.
80
+
81
+ ## LangChain / LangGraph (agent-wide)
82
+
83
+ ```python
84
+ from langchain.agents import create_agent
85
+ from shield_kya.integrations.langchain import KyaMiddleware
86
+
87
+ agent = create_agent(model, tools, middleware=[KyaMiddleware()])
88
+ ```
89
+
90
+ Each tool call this agent dispatches is checked before it runs (one trail row per call that the CLI evaluated). A `DENY` skips the tool and returns an error `ToolMessage` with the reason (`KYA denied postgres__drop_table: NEVER_EVENT`), so the model sees it and the run continues. It covers tool calls dispatched by that agent's ToolNode. Not covered: subagents or graphs built separately (add the middleware there too), direct `tool.invoke()` calls, tools the model provider executes server-side, and MCP tools whose names carry no server prefix, so the never-list cannot recognize them (`server=` applies one prefix to every tool of the agent). Put `KyaMiddleware` last in `middleware` (innermost) so it checks the call as it will execute, after any middleware that rewrites tool calls. Options: `server=` (tool id becomes `server__tool`), `irreversible=`, `strict=` (raise `KyaDenied`, and block `REQUIRE_APPROVE`), `fail_closed=`, `timeout=`.
91
+
92
+ Raw LangGraph graph:
93
+
94
+ ```python
95
+ kya = KyaMiddleware()
96
+ ToolNode(tools, wrap_tool_call=kya.wrap_tool_call, awrap_tool_call=kya.awrap_tool_call)
97
+ ```
98
+
99
+ ## Pydantic AI (agent-wide)
100
+
101
+ ```python
102
+ from pydantic_ai import Agent
103
+ from shield_kya.integrations.pydantic_ai import KyaCapability
104
+
105
+ agent = Agent(model, tools=[...], capabilities=[KyaCapability()])
106
+ ```
107
+
108
+ The capability hooks `before_tool_execute`, so every function tool the agent runs (toolsets and MCP servers included, but see Known limits on MCP names) is checked before it executes, with the CLI run as an asyncio subprocess (`run_sync` included, since Pydantic AI runs tools on its event loop). A `DENY` raises `SkipToolExecution` with the reason, so the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the tool result and the run continues. `strict=True` raises `KyaDenied` instead, which aborts the run, and also blocks `REQUIRE_APPROVE`. Output tools skip capability hooks and are not checked. The capability is per agent: add `KyaCapability()` to every agent, including agents another agent runs as a tool (subagents). Put `KyaCapability` last in `capabilities` so the trail records the args as they will execute. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
109
+
110
+ ## CrewAI (process-wide)
111
+
112
+ ```python
113
+ from shield_kya.integrations import crewai as kya_crewai
114
+
115
+ kya_crewai.register() # every tool call of every crew in this process
116
+ crew.kickoff()
117
+ kya_crewai.unregister() # optional
118
+ ```
119
+
120
+ `register()` adds a global `PRE_TOOL_CALL` hook and a matching `POST_TOOL_CALL` hook; calling it again replaces them. CrewAI swallows every hook exception except `HookAborted` and then runs the tool, so a deny is always a `HookAborted`: the tool does not run, and the post hook replaces CrewAI's fixed "Tool execution blocked by hook" text with the KYA reason, so the agent sees it and the crew continues. For the same reason `strict=True` blocks `REQUIRE_APPROVE` but never raises `KyaDenied`, and an unexpected error inside the hook still goes through the fail-open / never-list / fail-closed rules instead of reaching CrewAI. The tool id is the tool's own name: CrewAI's LLM-facing name is sanitized (`postgres__drop_table` becomes `postgres_drop_table`), which the never-list would not recognize. Call `register()` after your own pre-tool hooks so the check sees their input edits. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
121
+
122
+ ## Google ADK (Runner-wide)
123
+
124
+ ```python
125
+ from google.adk.apps import App
126
+ from shield_kya.integrations.adk import KyaPlugin
127
+
128
+ app = App(name="my_app", root_agent=agent, plugins=[KyaPlugin()])
129
+ runner = InMemoryRunner(app=app) # or Runner(app=app, ...)
130
+ ```
131
+
132
+ `KyaPlugin` is a `BasePlugin` whose `before_tool_callback` checks every tool call the Runner dispatches, with the CLI run as an asyncio subprocess (`Runner.run` included, since ADK runs tools on its event loop). A `DENY` returns `{"error": "KYA denied postgres__drop_table: NEVER_EVENT"}`, which ADK uses as the tool result without running the tool, so the model reads the reason and the run continues. ADK re-raises any exception from a plugin as a `RuntimeError` that aborts the run, so the plugin never lets one escape: a shim error goes through the fail-open / never-list / fail-closed rules. `strict=True` raises `KyaDenied` on purpose (ADK wraps it in that `RuntimeError`) and also blocks `REQUIRE_APPROVE`.
133
+
134
+ Covered (one plugin on the App): every agent the Runner runs, including `sub_agents` reached by `transfer_to_agent`, an `AgentTool` with its default `include_plugins=True`, MCP toolsets and long-running tools (the first call is checked), all tested; live (bidi) mode and workflow tool nodes call the same plugin hook in the google-adk 2.11.0 source (not tested here). ADK's own `transfer_to_agent` and `AgentTool` calls are checked too (UNKNOWN_TOOL, advisory; `strict=True` blocks them). Not covered: `AgentTool(include_plugins=False)`, a separate `Runner` or `App` without the plugin, tools the model provider runs server-side (`google_search`, built-in code execution), an agent `code_executor` running model-written code (not a tool call), and direct `tool.run_async()` calls. Plugins run in list order and an agent's own `before_tool_callback` runs after them, so put `KyaPlugin` last in `plugins` and do not rewrite tool args in agent callbacks. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`, `name=` (plugin name, default `shield_kya`).
135
+
136
+ ## Microsoft Agent Framework (agent-wide)
137
+
138
+ ```python
139
+ from agent_framework import Agent
140
+ from shield_kya.integrations.agent_framework import KyaMiddleware
141
+
142
+ agent = Agent(client=client, tools=[...], middleware=[KyaMiddleware()])
143
+ ```
144
+
145
+ `KyaMiddleware` is a `FunctionMiddleware` (package `agent-framework-core`, the AutoGen successor). Before a function runs it is checked with the CLI as an asyncio subprocess. A `DENY` sets `context.result` to the reason and does not call `call_next()`, so the function never runs, the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the function result and the run continues. Agent Framework turns an ordinary exception from middleware into a tool-error result (the tool does not run), so the middleware never raises one: a shim error goes through the fail-open / never-list / fail-closed rules instead of silently blocking a harmless tool, and a tool's own exception stays the tool's. `strict=True` raises `MiddlewareFailure` (from `KyaDenied`), Agent Framework's fail-closed signal that aborts the run, and also blocks `REQUIRE_APPROVE`. Hosted `fail_closed=True` / `KYA_FAIL_CLOSED=1` blocks the tool the same way as a deny and keeps the run going.
146
+
147
+ Covered: every function call of that agent's function-invocation loop (`run` and `run(stream=True)`), and MCP functions (tested); approved replays of `approval_mode="always_require"` tools go through the same middleware in the agent-framework-core 1.21.0 source (not tested here). Middleware on the chat client (`client = SomeClient(middleware=[KyaMiddleware()])`) covers every agent that uses the client. Not covered: an agent wrapped with `as_tool()` unless that agent (or its client) has its own `KyaMiddleware` (the `as_tool` call itself is checked), hosted tools the service runs (code interpreter, web search, hosted MCP), and direct `FunctionTool.invoke()` calls. The first middleware in the list is the outermost, so put `KyaMiddleware` last: argument-repair middleware must run before it (Agent Framework's own rule for security middleware). Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
148
+
149
+ ## OpenAI Agents SDK (every function tool present at attach() time, except SandboxAgent tools added at run time)
150
+
151
+ ```python
152
+ from agents import Agent, Runner
153
+ from shield_kya.integrations.openai_agents import attach
154
+
155
+ agent = attach(Agent(name="a", tools=[...], mcp_servers=[...], handoffs=[...]))
156
+ Runner.run_sync(agent, "...")
157
+ ```
158
+
159
+ The SDK has no agent-wide pre-tool hook that can skip a tool (`RunHooks.on_tool_start` can only abort the whole run), only per-tool `tool_input_guardrails`, so `attach()` puts one KYA guardrail (named `shield_kya`) on every `FunctionTool` in `agent.tools` and on every MCP server in `agent.mcp_servers`, and walks `handoffs` (an `Agent` or a `handoff(agent)`) and `agent.as_tool()` agents recursively. The SDK copies a server's guardrails onto every MCP tool it lists, so MCP tools a server adds later are covered without another call.
160
+
161
+ **Not covered.** Shell and file-editing tools: `ShellTool`, `LocalShellTool`, `ApplyPatchTool`, `ComputerTool`, `CustomTool`, and the shell, filesystem, apply-patch and image tools a `SandboxAgent` adds at run time (sandbox capabilities, created after `attach()`). An agent with them runs commands and edits files without a KYA check; for coding agents the shell and apply-patch tools are the dangerous ones, and this integration does not gate them. Also not covered: hosted tools the OpenAI service runs (`WebSearchTool`, `FileSearchTool`, `CodeInterpreterTool`, `ImageGenerationTool`, `HostedMCPTool`, `ToolSearchTool`), the handoff call itself (only the target agent's tools are checked), the commands Codex runs inside the experimental `codex_tool()` (the `codex` call itself is checked), function tools added to `agent.tools` after `attach()` (call it again), agents reached only through a custom `Handoff` without an agent reference, and direct `on_invoke_tool` calls. `ProgrammaticToolCallingTool` was not checked.
162
+
163
+ `attach()` returns the agent and is idempotent: a second call replaces the KYA guardrail and keeps your own guardrails. For one tool, `kya_tool_input_guardrail()` returns the guardrail to put in `tool_input_guardrails=[...]` yourself; for an MCP server use `attach([server])` or pass `mcp_server_name=server.name`, because the guardrail cannot read the server from the call (without it `drop_table` is checked alone and the never-list cannot tell it is `postgres__drop_table`).
164
+
165
+ Before a tool runs, the guardrail checks the call with the CLI as an asyncio subprocess (`Runner.run_sync` included, since the SDK runs guardrails on its event loop). A `DENY` returns `ToolGuardrailFunctionOutput.reject_content(reason)`: the tool does not run, the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the tool output and the run continues. The SDK aborts the run on any exception from a guardrail, so the guardrail never raises one: a shim error goes through the fail-open / never-list / fail-closed rules. `strict=True` returns `raise_exception` instead (the SDK raises `ToolInputGuardrailTripwireTriggered` and the run aborts) and also blocks `REQUIRE_APPROVE`. Hosted `fail_closed=True` / `KYA_FAIL_CLOSED=1` blocks the tool the same way as a deny and keeps the run going.
166
+
167
+ The tool id is the name the model called (`ToolContext.tool_name`), with the arguments the tool runs with. The never-list is also matched against `<namespace>.<name>` for a `tool_namespace()` tool and, for an MCP tool, against `<server name>__<tool name>`: an MCP server created with `name="postgres"` exposes `drop_table` (or `mcp_postgres__drop_table` with `mcp_config={"include_server_in_tool_names": True}`), and the check matches it as `postgres__drop_table` (the deny reason names both). Covered (tested): function tools of the agent, of handoff targets and of `as_tool()` agents, MCP server tools (both naming modes), namespaced tools, `Runner.run`, `Runner.run_sync` and `Runner.run_streamed`. Realtime agents run the same tool input guardrails in the openai-agents 0.23.1 source (not tested here). With `RunConfig(tool_execution=...)` `pre_approval_tool_input_guardrails=True` the guardrail runs before approval and again at execution, so an approved call has two trail rows. Options: `server=` (function tools only: an MCP tool already carries its server name), `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
168
+
169
+ ## Tested versions
170
+
171
+ | Framework | Entry point | Tested with |
172
+ |---|---|---|
173
+ | LangChain / LangGraph | `KyaMiddleware` | langchain 1.4.4 (langchain-core 1.6.9, langgraph 1.2.14, langgraph-prebuilt 1.1.0) |
174
+ | Pydantic AI | `KyaCapability` | pydantic-ai-slim 2.55.0 (Python 3.12, 3.13) |
175
+ | CrewAI | `register()` | crewai 1.15.27 (Python 3.12, 3.13; crewai needs Python < 3.14) |
176
+ | Google ADK | `KyaPlugin` | google-adk 2.11.0 (Python 3.12) |
177
+ | Microsoft Agent Framework | `KyaMiddleware` | agent-framework-core 1.21.0 (Python 3.12) |
178
+ | OpenAI Agents SDK | `attach()` | openai-agents 0.23.1 (Python 3.12) |
179
+
180
+ CI runs the unit tests of all six at the versions above, and the real-CLI e2e (`tests/test_e2e_*.py`) for Pydantic AI, CrewAI, Google ADK, Agent Framework and the OpenAI Agents SDK; the LangChain e2e is still a manual gate.
181
+
182
+ ## Known limits
183
+
184
+ - `KYA_CLI` set to an absolute path is trusted as is: the shim runs it. Its verdict cannot ALLOW a never-list tool (the never-list is matched in Python on every call), but the program itself still runs.
185
+ - crewai calls `load_dotenv()` at import. From `python -c`, a notebook or a debugger it reads the current directory's `.env`, otherwise the first `.env` above crewai's install directory (a `.venv` inside a repo reaches the repo's `.env`). A cloned repo can therefore set `KYA_CLI` for a CrewAI process (the never-list still blocks, see above). `load_dotenv()` does not replace variables that are already set, so exporting `KYA_CLI` yourself keeps a repo `.env` from choosing it. python-dotenv also expands `${PWD}` and other variables, so a `.env` can name absolute paths inside the repo. The child's `PATH` and `HOME` are only safe because crewai calls `load_dotenv()` without `override=True`, so an exported value wins; a `.env` that sets `HOME` or `PATH` for a process where they are unset is a risk the shim does not defend against.
186
+ - An empty tool id is denied (`EMPTY_TOOL`), like the CLI's offline evaluate; `kya hook` allows a hook payload that has no tool name.
187
+ - A grandchild that leaves the CLI's process group and keeps its output open can stretch a timeout past the 4 s budget. The real CLI's detached helpers do not hold its output.
188
+ - The child's PATH keeps only absolute entries, so a `#!/usr/bin/env node` launcher cannot pick up a repo-planted `./node`.
189
+ - CrewAI hooks are synchronous. Under `crew.akickoff()` the check blocks the event loop for up to `timeout` (4 s by default) per tool call; there is no async hook to use instead.
190
+ - CrewAI's `register()` is process-wide: it covers every crew and agent in the process, not one crew.
191
+ - The CrewAI reason hand-off from the pre hook to the post hook is per thread. If another pre-tool hook blocks a call before ours runs, CrewAI's own text stays (the tool is still blocked).
192
+ - Google ADK: `adk run`, `adk web` and `adk api_server` load the agent folder's `.env` with `load_dotenv(override=True)`, so under those commands a repo `.env` can replace `PATH`, `HOME`, `KYA_CLI` and `KYA_FAIL_CLOSED` (the allowlist above does not help: these are the values it forwards or reads). The never-list still blocks by name, but a planted `KYA_CLI` or `PATH` entry runs. In the google-adk 2.11.0 source only those CLI commands load a `.env`; a program that builds its own `Runner` does not (other libraries it imports may). agent-framework-core reads a `.env` only when you pass `env_file_path` (settings read it without changing `os.environ`; the observability setup loads it without `override`).
193
+ - The never-list only recognizes MCP tools named `server__tool` (or `server.tool`, `server/tool`, `mcp__server__tool`) with a known server. Google ADK and Agent Framework keep the original MCP name, so their plugin and middleware also check that name and `<prefix>__<original>`: an ADK toolset or Agent Framework `MCPTool` with `tool_name_prefix="postgres"` exposes `postgres_drop_table`, and the check matches it as `postgres__drop_table` (the deny reason names both). Without a prefix, an MCP tool is just `drop_table` in both frameworks and the never-list cannot tell which server it belongs to (`server=` applies one prefix to every tool). Agent Framework replaces characters outside `A-Za-z0-9_.-` with `-` in the exposed name; the raw remote name is checked as well. A prefix that names a known server can also block a harmless tool whose name contains a never-list verb (`github_transfer_issue` is checked as `github__transfer_issue`), the same over-block the never-list has for `server__tool` names. The other frameworks' own MCP names do not match: Pydantic AI `load_mcp_servers()` / `toolset.prefixed("postgres")` gives `postgres_drop_table` (one `_`), and CrewAI `mcps=` names tools `<server_name>_<tool>`, where a stdio `server_name` is the command line and an HTTP one the host, so CrewAI MCP tools are never matched. Those calls are still evaluated and recorded, but run (UNKNOWN_TOOL is advisory). What to do: name MCP tools in the `server__tool` form (Pydantic AI `toolset.prefixed("postgres_")` gives `postgres__drop_table`; LangChain `server=`), or rely on hosted enforcement.
194
+ - CrewAI MCP tool ids are the original tool names, which for stdio servers embed the full command line, including any DSN password (`postgresql://user:pass@...`). That id goes on the kya argv (visible in `ps`), into `~/.kya/trail.jsonl` and into the deny reason. Keep secrets out of MCP command lines (use the server's env instead) when you use `register()`.
195
+ - Pydantic AI output tools are not checked.
196
+ - Agent Framework: `KyaMiddleware` on the chat client always runs outside the agent's own function middleware (agent-framework-core 1.21.0 puts client middleware first), so agent-level middleware can rewrite the arguments after the check. With `KyaMiddleware` on the client, put argument-rewriting function middleware on the client too, before `KyaMiddleware`, not on the agent.
197
+ - `server=` on the agent-wide Google ADK, Agent Framework and OpenAI Agents SDK hooks prefixes the tool id sent to the CLI, but the never-list also matches every name without that prefix, so a prefix it does not know (`server="acme"`) cannot hide `postgres__drop_table`; the denied call is recorded under the unprefixed id.
198
+ - Google ADK: the original MCP name is read from `McpTool.raw_mcp_tool` (public in google-adk 2.11.0), else the private `_mcp_tool`. The tests use stand-in objects with one or the other attribute, not a real `McpTool`.
199
+ - Google ADK: `ManagedAgent` tools and `RemoteMcpServer` tools run on Google's backend (ADK never connects to that MCP server), so they never reach `before_tool_callback` and are not checked.
200
+ - OpenAI Agents SDK: the guardrail is per tool, so a function tool added to `agent.tools` after `attach()` is not checked until `attach()` runs again (MCP tools are, through the server). An MCP tool name the SDK shortened with a hash (server plus tool name over 64 characters) is not matched by its `<server>__<tool>` form. An MCP server name or a `tool_namespace()` name that is a known server can also block a harmless tool whose name contains a never-list verb (server `github` with `transfer_issue` is checked as `github__transfer_issue`), the same over-block as above. Options live on each tool and server object: when two agents share one, the last `attach()` wins for both (`attach(a, fail_closed=True)` then `attach(b)` makes a shared tool fail open for `a` too). Shell and file-editing tools (`ShellTool`, `LocalShellTool`, `ApplyPatchTool`, `ComputerTool`, `CustomTool`, `SandboxAgent` run-time tools) are not checked; see Not covered in the OpenAI Agents SDK section.
201
+ - The CLI child gets an allowlist of the environment, nothing else: an absolute-only `PATH`, `HOME`, `USERPROFILE`, `LANG`, `LC_*`, `TZ`, on Windows also `SYSTEMROOT`, `WINDIR`, `COMSPEC`, `PATHEXT`, `APPDATA`, `LOCALAPPDATA`, `TEMP` and `TMP`, plus `KYA_SKIP_NODE_CHECK=1` set by the shim (`TMPDIR` is left out). A framework can load a repo `.env` into `os.environ` (crewai does at import), and other variables would run repo code in node (`NODE_OPTIONS=--require ./x.js`, `OPENSSL_CONF` loading a provider library, `LD_PRELOAD`), move the trail (`KYA_HOME`) or send trail events to a webhook (`KYA_NOTIFY_WEBHOOK`, `KYA_OTLP_EXPORT_ENDPOINT`). A user-exported value looks the same, so CLI settings such as `KYA_HOME`, `KYA_SESSION_ID`, `KYA_TELEMETRY`, `KYA_NOTIFY_WEBHOOK`, proxy and CA variables are not supported through the shim: the trail always goes to `$HOME/.kya`. The privacy opt-outs `KYA_DIFF_PREVIEW` and `KYA_SEND_PREVIEWS` are forwarded only when set to an off value (0, false, no, off), so a planted value can only remove data. `KYA_CLI` and `KYA_FAIL_CLOSED` are read by the shim itself and still work.
202
+ - A committed repo `.kya/config.json` still applies: the CLI runs in the agent's working directory and reads that file, so a repo can add notify webhooks or an OTLP endpoint that receive trail events (every DENY and REQUIRE_APPROVE, with tool ids and summaries). This is CLI behaviour, the same for `kya hook`, not something the shim's environment allowlist can stop. CLI follow-up: do not honor repo-level webhook config in hook mode.
203
+ - CrewAI: when two tools sanitize to the same name (`postgres__drop_table` and `postgres_drop_table`), CrewAI can hand the hook one and run the other, so the hook denies the call if any tool with that sanitized name on the agent, the task, the agent's executor (`mcps=`, `apps=`, `execute_task(tools=)`) or a LiteAgent is on the never-list. The harmless twin is then blocked too, with a reason that names both: `KYA denied postgres_drop_table: a tool with the same sanitized name (postgres__drop_table) is on the never-list (NEVER_EVENT)`; the trail row is the DENY for `postgres__drop_table`.
204
+ - CrewAI `strict=True` also blocks CrewAI's own delegation tools (`delegate_work_to_coworker`, `ask_question_to_coworker`), which evaluate as REQUIRE_APPROVE UNKNOWN_TOOL.
205
+ - The legacy per-tool wrappers (`governed_tool`, `wrap_tool`, `wrap_tools`) still raise `KyaDenied` on a deny, which aborts a Pydantic AI run; prefer the agent-wide entry points.
206
+
207
+ ## Core API
208
+
209
+ ```python
210
+ import shield_kya
211
+
212
+ d = shield_kya.decide("web_search", {"q": "kya"}) # never raises; records the trail
213
+ d.allowed, d.verdict, d.reason_code, d.reason
214
+ d = await shield_kya.adecide("web_search", {"q": "kya"}) # async, off the event loop
215
+
216
+ shield_kya.evaluate("org.sample.data.write", args={"table": "users"}) # raw eval-tool dict; raises on CLI errors
217
+ ```
218
+
219
+ `@governed` gates a sync or async callable. A plain function has no other way to report a deny, so `DENY` raises `KyaDenied` before the function runs:
220
+
221
+ ```python
222
+ from shield_kya import governed, KyaDenied
223
+
224
+ @governed("org.sample.data.write", irreversible=True)
225
+ def write_users_table(rows): ...
226
+
227
+ try:
228
+ write_users_table(rows)
229
+ except KyaDenied as denied:
230
+ print(denied.verdict, denied.reason_code)
231
+ ```
232
+
233
+ ## Per-tool wrappers
234
+
235
+ Prefer the agent-wide entry points above. These wrap single tools and share the `governed` semantics (`DENY` raises `KyaDenied`). Framework imports are lazy.
236
+
237
+ ```python
238
+ from shield_kya.integrations.langgraph import governed_tool # LangGraph / LangChain @tool
239
+ tools = [governed_tool("web.fetch")(fetch_fn)]
240
+
241
+ from shield_kya.integrations.crewai import wrap_tool # CrewAI BaseTool or callable
242
+ safe_search = wrap_tool(search_tool, tool_id="search.web")
243
+
244
+ from shield_kya.integrations.pydantic_ai import governed_tool # Pydantic AI
245
+
246
+ @agent.tool_plain
247
+ @governed_tool("search.web")
248
+ def search_web(q: str) -> str: ...
249
+ ```
250
+
251
+ A raised `KyaDenied` aborts a Pydantic AI run (Pydantic AI does not turn tool exceptions into tool errors).
252
+
253
+ Extras (`pip install '.[name]'` from a checkout): `langchain`, `langgraph`, `crewai` (>=1.15.27,<2), `pydantic-ai` (pydantic-ai-slim >=2.55.0,<3), `adk` (google-adk >=2.11.0,<3), `agent-framework` (agent-framework-core >=1.21.0,<2), `openai-agents` (openai-agents >=0.23.1,<0.24).
254
+
255
+ ## Tests
256
+
257
+ ```sh
258
+ python -m unittest discover -s tests # stdlib suite; framework tests run when the framework is installed
259
+ KYA_E2E_BIN=/dir/with/kya python -m unittest discover -s tests -p 'test_e2e_*.py' # real-CLI e2e
260
+ KYA_E2E_BIN=/dir/with/kya python -m unittest discover -s tests -p 'test_shield_kya.py' -k RealCliEnvTest # real CLI: a planted OPENSSL_CONF never reaches node (POSIX, needs cc for the full check)
261
+ ```
262
+
263
+ ## License
264
+
265
+ MIT
@@ -0,0 +1,232 @@
1
+ # shield-kya
2
+
3
+ In-process [kya](https://github.com/The-Pixel-Boys/shield-kya) (Know Your Agent) governance shim for Python agent frameworks.
4
+
5
+ `shield_kya` delegates policy evaluation to the local kya CLI (`kya wrap --offline --json ...`), so Python tools share the same offline verdict path and trail as the TypeScript SDK. Stdlib only, no dependencies.
6
+
7
+ ## Requirements
8
+
9
+ - Python >= 3.9 (the framework hooks need what their framework needs: langchain 3.10+, pydantic-ai 3.11+, crewai 3.10 to 3.13, google-adk 3.10+, agent-framework-core 3.10+, openai-agents 3.10+)
10
+ - The kya CLI on `PATH` (`npm i -g @shield-agent/kya`), or set `KYA_CLI` to an absolute path (an executable, or a `.js` file run with `node`). `kya` is looked up in absolute PATH entries only, never the current directory; a relative `KYA_CLI` is ignored.
11
+
12
+ ## Install
13
+
14
+ Not yet published to PyPI: do not `pip install shield-kya` from PyPI until it is. Install from a checkout, in this directory:
15
+
16
+ ```sh
17
+ pip install .
18
+ pip install '.[langchain]' # LangChain / LangGraph middleware
19
+ pip install '.[pydantic-ai]' # Pydantic AI capability
20
+ pip install '.[crewai]' # CrewAI global hook
21
+ pip install '.[adk]' # Google ADK plugin
22
+ pip install '.[agent-framework]' # Microsoft Agent Framework middleware (agent-framework-core)
23
+ pip install '.[openai-agents]' # OpenAI Agents SDK tool input guardrail
24
+ ```
25
+
26
+ ## Semantics: advisory, fail-open
27
+
28
+ Same rules as `kya hook`:
29
+
30
+ | Situation | Default (OSS) | `fail_closed=True` / `KYA_FAIL_CLOSED=1` (hosted) |
31
+ |---|---|---|
32
+ | `ALLOW` | tool runs | tool runs |
33
+ | `REQUIRE_APPROVE` | tool runs, recorded (`strict=True` blocks) | same |
34
+ | `DENY` (local never-list) | blocked | blocked |
35
+ | CLI missing, timeout (4 s), non-JSON, unknown exit code, empty verdict, any shim error | tool runs; first event logged at WARNING, every event at DEBUG (logger `shield_kya`) | blocked |
36
+ | same, but the tool name is on the never-list | blocked (`NEVER_EVENT`) | blocked |
37
+
38
+ Without a CLI verdict the never-list is matched in Python (`shield_kya/_never_list.py`, a port of the CLI's name-only rule; `tests/test_never_list.py` and the TS suite assert the same vectors file). `KYA_FAIL_CLOSED` accepts 1/true/yes/on (any case). Hosted operators should not pass `fail_closed=False`, which overrides the env var.
39
+
40
+ On Windows the shim refuses to run `kya` through a `.cmd`/`.bat` shim (cmd.exe would re-parse the model-controlled arguments); set `KYA_CLI` to the absolute path of the package's `dist/cli.js` and it runs with `node`.
41
+
42
+ Arguments are passed to the CLI on argv. A tool call whose arguments exceed the OS argv limit (about 128 KiB per argument on Linux) gets no CLI verdict and no trail row: it fails open, and the never-list still applies by tool name. An empty tool id is denied (`EMPTY_TOOL`), like the CLI's offline evaluate.
43
+
44
+ "Hosted" here means only the fail-closed switch: the shim always evaluates offline (`kya wrap --offline`) and never consults a hosted policy plane.
45
+
46
+ Async callers use an asyncio subprocess, so the check never blocks the event loop.
47
+
48
+ ## LangChain / LangGraph (agent-wide)
49
+
50
+ ```python
51
+ from langchain.agents import create_agent
52
+ from shield_kya.integrations.langchain import KyaMiddleware
53
+
54
+ agent = create_agent(model, tools, middleware=[KyaMiddleware()])
55
+ ```
56
+
57
+ Each tool call this agent dispatches is checked before it runs (one trail row per call that the CLI evaluated). A `DENY` skips the tool and returns an error `ToolMessage` with the reason (`KYA denied postgres__drop_table: NEVER_EVENT`), so the model sees it and the run continues. It covers tool calls dispatched by that agent's ToolNode. Not covered: subagents or graphs built separately (add the middleware there too), direct `tool.invoke()` calls, tools the model provider executes server-side, and MCP tools whose names carry no server prefix, so the never-list cannot recognize them (`server=` applies one prefix to every tool of the agent). Put `KyaMiddleware` last in `middleware` (innermost) so it checks the call as it will execute, after any middleware that rewrites tool calls. Options: `server=` (tool id becomes `server__tool`), `irreversible=`, `strict=` (raise `KyaDenied`, and block `REQUIRE_APPROVE`), `fail_closed=`, `timeout=`.
58
+
59
+ Raw LangGraph graph:
60
+
61
+ ```python
62
+ kya = KyaMiddleware()
63
+ ToolNode(tools, wrap_tool_call=kya.wrap_tool_call, awrap_tool_call=kya.awrap_tool_call)
64
+ ```
65
+
66
+ ## Pydantic AI (agent-wide)
67
+
68
+ ```python
69
+ from pydantic_ai import Agent
70
+ from shield_kya.integrations.pydantic_ai import KyaCapability
71
+
72
+ agent = Agent(model, tools=[...], capabilities=[KyaCapability()])
73
+ ```
74
+
75
+ The capability hooks `before_tool_execute`, so every function tool the agent runs (toolsets and MCP servers included, but see Known limits on MCP names) is checked before it executes, with the CLI run as an asyncio subprocess (`run_sync` included, since Pydantic AI runs tools on its event loop). A `DENY` raises `SkipToolExecution` with the reason, so the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the tool result and the run continues. `strict=True` raises `KyaDenied` instead, which aborts the run, and also blocks `REQUIRE_APPROVE`. Output tools skip capability hooks and are not checked. The capability is per agent: add `KyaCapability()` to every agent, including agents another agent runs as a tool (subagents). Put `KyaCapability` last in `capabilities` so the trail records the args as they will execute. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
76
+
77
+ ## CrewAI (process-wide)
78
+
79
+ ```python
80
+ from shield_kya.integrations import crewai as kya_crewai
81
+
82
+ kya_crewai.register() # every tool call of every crew in this process
83
+ crew.kickoff()
84
+ kya_crewai.unregister() # optional
85
+ ```
86
+
87
+ `register()` adds a global `PRE_TOOL_CALL` hook and a matching `POST_TOOL_CALL` hook; calling it again replaces them. CrewAI swallows every hook exception except `HookAborted` and then runs the tool, so a deny is always a `HookAborted`: the tool does not run, and the post hook replaces CrewAI's fixed "Tool execution blocked by hook" text with the KYA reason, so the agent sees it and the crew continues. For the same reason `strict=True` blocks `REQUIRE_APPROVE` but never raises `KyaDenied`, and an unexpected error inside the hook still goes through the fail-open / never-list / fail-closed rules instead of reaching CrewAI. The tool id is the tool's own name: CrewAI's LLM-facing name is sanitized (`postgres__drop_table` becomes `postgres_drop_table`), which the never-list would not recognize. Call `register()` after your own pre-tool hooks so the check sees their input edits. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
88
+
89
+ ## Google ADK (Runner-wide)
90
+
91
+ ```python
92
+ from google.adk.apps import App
93
+ from shield_kya.integrations.adk import KyaPlugin
94
+
95
+ app = App(name="my_app", root_agent=agent, plugins=[KyaPlugin()])
96
+ runner = InMemoryRunner(app=app) # or Runner(app=app, ...)
97
+ ```
98
+
99
+ `KyaPlugin` is a `BasePlugin` whose `before_tool_callback` checks every tool call the Runner dispatches, with the CLI run as an asyncio subprocess (`Runner.run` included, since ADK runs tools on its event loop). A `DENY` returns `{"error": "KYA denied postgres__drop_table: NEVER_EVENT"}`, which ADK uses as the tool result without running the tool, so the model reads the reason and the run continues. ADK re-raises any exception from a plugin as a `RuntimeError` that aborts the run, so the plugin never lets one escape: a shim error goes through the fail-open / never-list / fail-closed rules. `strict=True` raises `KyaDenied` on purpose (ADK wraps it in that `RuntimeError`) and also blocks `REQUIRE_APPROVE`.
100
+
101
+ Covered (one plugin on the App): every agent the Runner runs, including `sub_agents` reached by `transfer_to_agent`, an `AgentTool` with its default `include_plugins=True`, MCP toolsets and long-running tools (the first call is checked), all tested; live (bidi) mode and workflow tool nodes call the same plugin hook in the google-adk 2.11.0 source (not tested here). ADK's own `transfer_to_agent` and `AgentTool` calls are checked too (UNKNOWN_TOOL, advisory; `strict=True` blocks them). Not covered: `AgentTool(include_plugins=False)`, a separate `Runner` or `App` without the plugin, tools the model provider runs server-side (`google_search`, built-in code execution), an agent `code_executor` running model-written code (not a tool call), and direct `tool.run_async()` calls. Plugins run in list order and an agent's own `before_tool_callback` runs after them, so put `KyaPlugin` last in `plugins` and do not rewrite tool args in agent callbacks. Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`, `name=` (plugin name, default `shield_kya`).
102
+
103
+ ## Microsoft Agent Framework (agent-wide)
104
+
105
+ ```python
106
+ from agent_framework import Agent
107
+ from shield_kya.integrations.agent_framework import KyaMiddleware
108
+
109
+ agent = Agent(client=client, tools=[...], middleware=[KyaMiddleware()])
110
+ ```
111
+
112
+ `KyaMiddleware` is a `FunctionMiddleware` (package `agent-framework-core`, the AutoGen successor). Before a function runs it is checked with the CLI as an asyncio subprocess. A `DENY` sets `context.result` to the reason and does not call `call_next()`, so the function never runs, the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the function result and the run continues. Agent Framework turns an ordinary exception from middleware into a tool-error result (the tool does not run), so the middleware never raises one: a shim error goes through the fail-open / never-list / fail-closed rules instead of silently blocking a harmless tool, and a tool's own exception stays the tool's. `strict=True` raises `MiddlewareFailure` (from `KyaDenied`), Agent Framework's fail-closed signal that aborts the run, and also blocks `REQUIRE_APPROVE`. Hosted `fail_closed=True` / `KYA_FAIL_CLOSED=1` blocks the tool the same way as a deny and keeps the run going.
113
+
114
+ Covered: every function call of that agent's function-invocation loop (`run` and `run(stream=True)`), and MCP functions (tested); approved replays of `approval_mode="always_require"` tools go through the same middleware in the agent-framework-core 1.21.0 source (not tested here). Middleware on the chat client (`client = SomeClient(middleware=[KyaMiddleware()])`) covers every agent that uses the client. Not covered: an agent wrapped with `as_tool()` unless that agent (or its client) has its own `KyaMiddleware` (the `as_tool` call itself is checked), hosted tools the service runs (code interpreter, web search, hosted MCP), and direct `FunctionTool.invoke()` calls. The first middleware in the list is the outermost, so put `KyaMiddleware` last: argument-repair middleware must run before it (Agent Framework's own rule for security middleware). Options: `server=`, `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
115
+
116
+ ## OpenAI Agents SDK (every function tool present at attach() time, except SandboxAgent tools added at run time)
117
+
118
+ ```python
119
+ from agents import Agent, Runner
120
+ from shield_kya.integrations.openai_agents import attach
121
+
122
+ agent = attach(Agent(name="a", tools=[...], mcp_servers=[...], handoffs=[...]))
123
+ Runner.run_sync(agent, "...")
124
+ ```
125
+
126
+ The SDK has no agent-wide pre-tool hook that can skip a tool (`RunHooks.on_tool_start` can only abort the whole run), only per-tool `tool_input_guardrails`, so `attach()` puts one KYA guardrail (named `shield_kya`) on every `FunctionTool` in `agent.tools` and on every MCP server in `agent.mcp_servers`, and walks `handoffs` (an `Agent` or a `handoff(agent)`) and `agent.as_tool()` agents recursively. The SDK copies a server's guardrails onto every MCP tool it lists, so MCP tools a server adds later are covered without another call.
127
+
128
+ **Not covered.** Shell and file-editing tools: `ShellTool`, `LocalShellTool`, `ApplyPatchTool`, `ComputerTool`, `CustomTool`, and the shell, filesystem, apply-patch and image tools a `SandboxAgent` adds at run time (sandbox capabilities, created after `attach()`). An agent with them runs commands and edits files without a KYA check; for coding agents the shell and apply-patch tools are the dangerous ones, and this integration does not gate them. Also not covered: hosted tools the OpenAI service runs (`WebSearchTool`, `FileSearchTool`, `CodeInterpreterTool`, `ImageGenerationTool`, `HostedMCPTool`, `ToolSearchTool`), the handoff call itself (only the target agent's tools are checked), the commands Codex runs inside the experimental `codex_tool()` (the `codex` call itself is checked), function tools added to `agent.tools` after `attach()` (call it again), agents reached only through a custom `Handoff` without an agent reference, and direct `on_invoke_tool` calls. `ProgrammaticToolCallingTool` was not checked.
129
+
130
+ `attach()` returns the agent and is idempotent: a second call replaces the KYA guardrail and keeps your own guardrails. For one tool, `kya_tool_input_guardrail()` returns the guardrail to put in `tool_input_guardrails=[...]` yourself; for an MCP server use `attach([server])` or pass `mcp_server_name=server.name`, because the guardrail cannot read the server from the call (without it `drop_table` is checked alone and the never-list cannot tell it is `postgres__drop_table`).
131
+
132
+ Before a tool runs, the guardrail checks the call with the CLI as an asyncio subprocess (`Runner.run_sync` included, since the SDK runs guardrails on its event loop). A `DENY` returns `ToolGuardrailFunctionOutput.reject_content(reason)`: the tool does not run, the model receives `KYA denied postgres__drop_table: NEVER_EVENT` as the tool output and the run continues. The SDK aborts the run on any exception from a guardrail, so the guardrail never raises one: a shim error goes through the fail-open / never-list / fail-closed rules. `strict=True` returns `raise_exception` instead (the SDK raises `ToolInputGuardrailTripwireTriggered` and the run aborts) and also blocks `REQUIRE_APPROVE`. Hosted `fail_closed=True` / `KYA_FAIL_CLOSED=1` blocks the tool the same way as a deny and keeps the run going.
133
+
134
+ The tool id is the name the model called (`ToolContext.tool_name`), with the arguments the tool runs with. The never-list is also matched against `<namespace>.<name>` for a `tool_namespace()` tool and, for an MCP tool, against `<server name>__<tool name>`: an MCP server created with `name="postgres"` exposes `drop_table` (or `mcp_postgres__drop_table` with `mcp_config={"include_server_in_tool_names": True}`), and the check matches it as `postgres__drop_table` (the deny reason names both). Covered (tested): function tools of the agent, of handoff targets and of `as_tool()` agents, MCP server tools (both naming modes), namespaced tools, `Runner.run`, `Runner.run_sync` and `Runner.run_streamed`. Realtime agents run the same tool input guardrails in the openai-agents 0.23.1 source (not tested here). With `RunConfig(tool_execution=...)` `pre_approval_tool_input_guardrails=True` the guardrail runs before approval and again at execution, so an approved call has two trail rows. Options: `server=` (function tools only: an MCP tool already carries its server name), `irreversible=`, `strict=`, `fail_closed=`, `timeout=`.
135
+
136
+ ## Tested versions
137
+
138
+ | Framework | Entry point | Tested with |
139
+ |---|---|---|
140
+ | LangChain / LangGraph | `KyaMiddleware` | langchain 1.4.4 (langchain-core 1.6.9, langgraph 1.2.14, langgraph-prebuilt 1.1.0) |
141
+ | Pydantic AI | `KyaCapability` | pydantic-ai-slim 2.55.0 (Python 3.12, 3.13) |
142
+ | CrewAI | `register()` | crewai 1.15.27 (Python 3.12, 3.13; crewai needs Python < 3.14) |
143
+ | Google ADK | `KyaPlugin` | google-adk 2.11.0 (Python 3.12) |
144
+ | Microsoft Agent Framework | `KyaMiddleware` | agent-framework-core 1.21.0 (Python 3.12) |
145
+ | OpenAI Agents SDK | `attach()` | openai-agents 0.23.1 (Python 3.12) |
146
+
147
+ CI runs the unit tests of all six at the versions above, and the real-CLI e2e (`tests/test_e2e_*.py`) for Pydantic AI, CrewAI, Google ADK, Agent Framework and the OpenAI Agents SDK; the LangChain e2e is still a manual gate.
148
+
149
+ ## Known limits
150
+
151
+ - `KYA_CLI` set to an absolute path is trusted as is: the shim runs it. Its verdict cannot ALLOW a never-list tool (the never-list is matched in Python on every call), but the program itself still runs.
152
+ - crewai calls `load_dotenv()` at import. From `python -c`, a notebook or a debugger it reads the current directory's `.env`, otherwise the first `.env` above crewai's install directory (a `.venv` inside a repo reaches the repo's `.env`). A cloned repo can therefore set `KYA_CLI` for a CrewAI process (the never-list still blocks, see above). `load_dotenv()` does not replace variables that are already set, so exporting `KYA_CLI` yourself keeps a repo `.env` from choosing it. python-dotenv also expands `${PWD}` and other variables, so a `.env` can name absolute paths inside the repo. The child's `PATH` and `HOME` are only safe because crewai calls `load_dotenv()` without `override=True`, so an exported value wins; a `.env` that sets `HOME` or `PATH` for a process where they are unset is a risk the shim does not defend against.
153
+ - An empty tool id is denied (`EMPTY_TOOL`), like the CLI's offline evaluate; `kya hook` allows a hook payload that has no tool name.
154
+ - A grandchild that leaves the CLI's process group and keeps its output open can stretch a timeout past the 4 s budget. The real CLI's detached helpers do not hold its output.
155
+ - The child's PATH keeps only absolute entries, so a `#!/usr/bin/env node` launcher cannot pick up a repo-planted `./node`.
156
+ - CrewAI hooks are synchronous. Under `crew.akickoff()` the check blocks the event loop for up to `timeout` (4 s by default) per tool call; there is no async hook to use instead.
157
+ - CrewAI's `register()` is process-wide: it covers every crew and agent in the process, not one crew.
158
+ - The CrewAI reason hand-off from the pre hook to the post hook is per thread. If another pre-tool hook blocks a call before ours runs, CrewAI's own text stays (the tool is still blocked).
159
+ - Google ADK: `adk run`, `adk web` and `adk api_server` load the agent folder's `.env` with `load_dotenv(override=True)`, so under those commands a repo `.env` can replace `PATH`, `HOME`, `KYA_CLI` and `KYA_FAIL_CLOSED` (the allowlist above does not help: these are the values it forwards or reads). The never-list still blocks by name, but a planted `KYA_CLI` or `PATH` entry runs. In the google-adk 2.11.0 source only those CLI commands load a `.env`; a program that builds its own `Runner` does not (other libraries it imports may). agent-framework-core reads a `.env` only when you pass `env_file_path` (settings read it without changing `os.environ`; the observability setup loads it without `override`).
160
+ - The never-list only recognizes MCP tools named `server__tool` (or `server.tool`, `server/tool`, `mcp__server__tool`) with a known server. Google ADK and Agent Framework keep the original MCP name, so their plugin and middleware also check that name and `<prefix>__<original>`: an ADK toolset or Agent Framework `MCPTool` with `tool_name_prefix="postgres"` exposes `postgres_drop_table`, and the check matches it as `postgres__drop_table` (the deny reason names both). Without a prefix, an MCP tool is just `drop_table` in both frameworks and the never-list cannot tell which server it belongs to (`server=` applies one prefix to every tool). Agent Framework replaces characters outside `A-Za-z0-9_.-` with `-` in the exposed name; the raw remote name is checked as well. A prefix that names a known server can also block a harmless tool whose name contains a never-list verb (`github_transfer_issue` is checked as `github__transfer_issue`), the same over-block the never-list has for `server__tool` names. The other frameworks' own MCP names do not match: Pydantic AI `load_mcp_servers()` / `toolset.prefixed("postgres")` gives `postgres_drop_table` (one `_`), and CrewAI `mcps=` names tools `<server_name>_<tool>`, where a stdio `server_name` is the command line and an HTTP one the host, so CrewAI MCP tools are never matched. Those calls are still evaluated and recorded, but run (UNKNOWN_TOOL is advisory). What to do: name MCP tools in the `server__tool` form (Pydantic AI `toolset.prefixed("postgres_")` gives `postgres__drop_table`; LangChain `server=`), or rely on hosted enforcement.
161
+ - CrewAI MCP tool ids are the original tool names, which for stdio servers embed the full command line, including any DSN password (`postgresql://user:pass@...`). That id goes on the kya argv (visible in `ps`), into `~/.kya/trail.jsonl` and into the deny reason. Keep secrets out of MCP command lines (use the server's env instead) when you use `register()`.
162
+ - Pydantic AI output tools are not checked.
163
+ - Agent Framework: `KyaMiddleware` on the chat client always runs outside the agent's own function middleware (agent-framework-core 1.21.0 puts client middleware first), so agent-level middleware can rewrite the arguments after the check. With `KyaMiddleware` on the client, put argument-rewriting function middleware on the client too, before `KyaMiddleware`, not on the agent.
164
+ - `server=` on the agent-wide Google ADK, Agent Framework and OpenAI Agents SDK hooks prefixes the tool id sent to the CLI, but the never-list also matches every name without that prefix, so a prefix it does not know (`server="acme"`) cannot hide `postgres__drop_table`; the denied call is recorded under the unprefixed id.
165
+ - Google ADK: the original MCP name is read from `McpTool.raw_mcp_tool` (public in google-adk 2.11.0), else the private `_mcp_tool`. The tests use stand-in objects with one or the other attribute, not a real `McpTool`.
166
+ - Google ADK: `ManagedAgent` tools and `RemoteMcpServer` tools run on Google's backend (ADK never connects to that MCP server), so they never reach `before_tool_callback` and are not checked.
167
+ - OpenAI Agents SDK: the guardrail is per tool, so a function tool added to `agent.tools` after `attach()` is not checked until `attach()` runs again (MCP tools are, through the server). An MCP tool name the SDK shortened with a hash (server plus tool name over 64 characters) is not matched by its `<server>__<tool>` form. An MCP server name or a `tool_namespace()` name that is a known server can also block a harmless tool whose name contains a never-list verb (server `github` with `transfer_issue` is checked as `github__transfer_issue`), the same over-block as above. Options live on each tool and server object: when two agents share one, the last `attach()` wins for both (`attach(a, fail_closed=True)` then `attach(b)` makes a shared tool fail open for `a` too). Shell and file-editing tools (`ShellTool`, `LocalShellTool`, `ApplyPatchTool`, `ComputerTool`, `CustomTool`, `SandboxAgent` run-time tools) are not checked; see Not covered in the OpenAI Agents SDK section.
168
+ - The CLI child gets an allowlist of the environment, nothing else: an absolute-only `PATH`, `HOME`, `USERPROFILE`, `LANG`, `LC_*`, `TZ`, on Windows also `SYSTEMROOT`, `WINDIR`, `COMSPEC`, `PATHEXT`, `APPDATA`, `LOCALAPPDATA`, `TEMP` and `TMP`, plus `KYA_SKIP_NODE_CHECK=1` set by the shim (`TMPDIR` is left out). A framework can load a repo `.env` into `os.environ` (crewai does at import), and other variables would run repo code in node (`NODE_OPTIONS=--require ./x.js`, `OPENSSL_CONF` loading a provider library, `LD_PRELOAD`), move the trail (`KYA_HOME`) or send trail events to a webhook (`KYA_NOTIFY_WEBHOOK`, `KYA_OTLP_EXPORT_ENDPOINT`). A user-exported value looks the same, so CLI settings such as `KYA_HOME`, `KYA_SESSION_ID`, `KYA_TELEMETRY`, `KYA_NOTIFY_WEBHOOK`, proxy and CA variables are not supported through the shim: the trail always goes to `$HOME/.kya`. The privacy opt-outs `KYA_DIFF_PREVIEW` and `KYA_SEND_PREVIEWS` are forwarded only when set to an off value (0, false, no, off), so a planted value can only remove data. `KYA_CLI` and `KYA_FAIL_CLOSED` are read by the shim itself and still work.
169
+ - A committed repo `.kya/config.json` still applies: the CLI runs in the agent's working directory and reads that file, so a repo can add notify webhooks or an OTLP endpoint that receive trail events (every DENY and REQUIRE_APPROVE, with tool ids and summaries). This is CLI behaviour, the same for `kya hook`, not something the shim's environment allowlist can stop. CLI follow-up: do not honor repo-level webhook config in hook mode.
170
+ - CrewAI: when two tools sanitize to the same name (`postgres__drop_table` and `postgres_drop_table`), CrewAI can hand the hook one and run the other, so the hook denies the call if any tool with that sanitized name on the agent, the task, the agent's executor (`mcps=`, `apps=`, `execute_task(tools=)`) or a LiteAgent is on the never-list. The harmless twin is then blocked too, with a reason that names both: `KYA denied postgres_drop_table: a tool with the same sanitized name (postgres__drop_table) is on the never-list (NEVER_EVENT)`; the trail row is the DENY for `postgres__drop_table`.
171
+ - CrewAI `strict=True` also blocks CrewAI's own delegation tools (`delegate_work_to_coworker`, `ask_question_to_coworker`), which evaluate as REQUIRE_APPROVE UNKNOWN_TOOL.
172
+ - The legacy per-tool wrappers (`governed_tool`, `wrap_tool`, `wrap_tools`) still raise `KyaDenied` on a deny, which aborts a Pydantic AI run; prefer the agent-wide entry points.
173
+
174
+ ## Core API
175
+
176
+ ```python
177
+ import shield_kya
178
+
179
+ d = shield_kya.decide("web_search", {"q": "kya"}) # never raises; records the trail
180
+ d.allowed, d.verdict, d.reason_code, d.reason
181
+ d = await shield_kya.adecide("web_search", {"q": "kya"}) # async, off the event loop
182
+
183
+ shield_kya.evaluate("org.sample.data.write", args={"table": "users"}) # raw eval-tool dict; raises on CLI errors
184
+ ```
185
+
186
+ `@governed` gates a sync or async callable. A plain function has no other way to report a deny, so `DENY` raises `KyaDenied` before the function runs:
187
+
188
+ ```python
189
+ from shield_kya import governed, KyaDenied
190
+
191
+ @governed("org.sample.data.write", irreversible=True)
192
+ def write_users_table(rows): ...
193
+
194
+ try:
195
+ write_users_table(rows)
196
+ except KyaDenied as denied:
197
+ print(denied.verdict, denied.reason_code)
198
+ ```
199
+
200
+ ## Per-tool wrappers
201
+
202
+ Prefer the agent-wide entry points above. These wrap single tools and share the `governed` semantics (`DENY` raises `KyaDenied`). Framework imports are lazy.
203
+
204
+ ```python
205
+ from shield_kya.integrations.langgraph import governed_tool # LangGraph / LangChain @tool
206
+ tools = [governed_tool("web.fetch")(fetch_fn)]
207
+
208
+ from shield_kya.integrations.crewai import wrap_tool # CrewAI BaseTool or callable
209
+ safe_search = wrap_tool(search_tool, tool_id="search.web")
210
+
211
+ from shield_kya.integrations.pydantic_ai import governed_tool # Pydantic AI
212
+
213
+ @agent.tool_plain
214
+ @governed_tool("search.web")
215
+ def search_web(q: str) -> str: ...
216
+ ```
217
+
218
+ A raised `KyaDenied` aborts a Pydantic AI run (Pydantic AI does not turn tool exceptions into tool errors).
219
+
220
+ Extras (`pip install '.[name]'` from a checkout): `langchain`, `langgraph`, `crewai` (>=1.15.27,<2), `pydantic-ai` (pydantic-ai-slim >=2.55.0,<3), `adk` (google-adk >=2.11.0,<3), `agent-framework` (agent-framework-core >=1.21.0,<2), `openai-agents` (openai-agents >=0.23.1,<0.24).
221
+
222
+ ## Tests
223
+
224
+ ```sh
225
+ python -m unittest discover -s tests # stdlib suite; framework tests run when the framework is installed
226
+ KYA_E2E_BIN=/dir/with/kya python -m unittest discover -s tests -p 'test_e2e_*.py' # real-CLI e2e
227
+ KYA_E2E_BIN=/dir/with/kya python -m unittest discover -s tests -p 'test_shield_kya.py' -k RealCliEnvTest # real CLI: a planted OPENSSL_CONF never reaches node (POSIX, needs cc for the full check)
228
+ ```
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shield-kya"
7
+ version = "0.17.0" # minor: OpenAI Agents SDK tool input guardrail (attach)
8
+ description = "Audit trail and traceability for AI agents. In-process kya shim for Python agent frameworks (offline evaluate via the kya CLI)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "MFR Limited" }]
13
+ keywords = ["kya", "know-your-agent", "governance", "mcp", "agents"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Security",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/The-Pixel-Boys/shield-kya"
26
+ Repository = "https://github.com/The-Pixel-Boys/shield-kya"
27
+
28
+ [project.optional-dependencies]
29
+ # Tested with langchain 1.4.4 (langchain-core 1.6.9, langgraph 1.2.14, langgraph-prebuilt 1.1.0).
30
+ langchain = ["langchain>=1.4.4,<2"]
31
+ langgraph = ["langgraph"]
32
+ # Tested with crewai 1.15.27 and pydantic-ai-slim 2.55.0.
33
+ crewai = ["crewai>=1.15.27,<2"]
34
+ pydantic-ai = ["pydantic-ai-slim>=2.55.0,<3"]
35
+ # Tested with google-adk 2.11.0 and agent-framework-core 1.21.0 (the agent-framework
36
+ # meta package installs every provider; core has FunctionMiddleware).
37
+ adk = ["google-adk>=2.11.0,<3"]
38
+ agent-framework = ["agent-framework-core>=1.21.0,<2"]
39
+ # Tested with openai-agents 0.23.1 (0.x: a minor release can change the guardrail API).
40
+ openai-agents = ["openai-agents>=0.23.1,<0.24"]
41
+
42
+ [tool.setuptools]
43
+ packages = ["shield_kya", "shield_kya.integrations"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+