agentapprovalprotocol 0.1.0__tar.gz → 0.3.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 (31) hide show
  1. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/.gitignore +2 -0
  2. agentapprovalprotocol-0.3.0/PKG-INFO +237 -0
  3. agentapprovalprotocol-0.3.0/README.md +206 -0
  4. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/pyproject.toml +21 -1
  5. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/__init__.py +5 -2
  6. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/_client.py +17 -0
  7. agentapprovalprotocol-0.3.0/src/agentapprovalprotocol/adk.py +262 -0
  8. agentapprovalprotocol-0.3.0/src/agentapprovalprotocol/claude_agent_sdk.py +344 -0
  9. agentapprovalprotocol-0.3.0/src/agentapprovalprotocol/crewai.py +273 -0
  10. agentapprovalprotocol-0.3.0/src/agentapprovalprotocol/openai_agents.py +364 -0
  11. agentapprovalprotocol-0.3.0/src/agentapprovalprotocol/receiver.py +340 -0
  12. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/testing.py +65 -2
  13. agentapprovalprotocol-0.3.0/tests/test_adk.py +346 -0
  14. agentapprovalprotocol-0.3.0/tests/test_claude_agent_sdk.py +385 -0
  15. agentapprovalprotocol-0.3.0/tests/test_crewai.py +358 -0
  16. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/tests/test_langgraph.py +2 -0
  17. agentapprovalprotocol-0.3.0/tests/test_openai_agents.py +359 -0
  18. agentapprovalprotocol-0.3.0/tests/test_wire_and_receiver.py +285 -0
  19. agentapprovalprotocol-0.3.0/uv.lock +6760 -0
  20. agentapprovalprotocol-0.1.0/PKG-INFO +0 -96
  21. agentapprovalprotocol-0.1.0/README.md +0 -77
  22. agentapprovalprotocol-0.1.0/src/agentapprovalprotocol/receiver.py +0 -168
  23. agentapprovalprotocol-0.1.0/tests/test_wire_and_receiver.py +0 -121
  24. agentapprovalprotocol-0.1.0/uv.lock +0 -2184
  25. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/_gate.py +0 -0
  26. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/_wire.py +0 -0
  27. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/langgraph.py +0 -0
  28. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/py.typed +0 -0
  29. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/src/agentapprovalprotocol/pydantic_ai.py +0 -0
  30. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/tests/test_gate.py +0 -0
  31. {agentapprovalprotocol-0.1.0 → agentapprovalprotocol-0.3.0}/tests/test_pydantic_ai.py +0 -0
@@ -14,5 +14,7 @@ playwright-report/
14
14
  /sdk/python/.venv/
15
15
  /sdk/python/dist/
16
16
  /sdk/typescript/dist/
17
+ /sdk/typescript-ai-sdk/dist/
18
+ /sdk/typescript-mastra/dist/
17
19
  __pycache__/
18
20
  /sdk/python/.pytest_cache/
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentapprovalprotocol
3
+ Version: 0.3.0
4
+ Summary: Put an AI agent's tool calls behind an Agent Approval Protocol (AAP) approval provider.
5
+ Project-URL: Homepage, https://agentapprovalprotocol.io
6
+ Project-URL: Source, https://github.com/agentapprovalprotocol/agentapprovalprotocol
7
+ License-Expression: Apache-2.0
8
+ Keywords: aap,agents,ai,approval,human-in-the-loop
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Provides-Extra: adk
14
+ Requires-Dist: google-adk>=2.11; extra == 'adk'
15
+ Provides-Extra: claude-agent-sdk
16
+ Requires-Dist: claude-agent-sdk>=0.2.165; extra == 'claude-agent-sdk'
17
+ Provides-Extra: crewai
18
+ Requires-Dist: crewai>=1.15; (python_full_version < '3.14') and extra == 'crewai'
19
+ Provides-Extra: deep-agents
20
+ Requires-Dist: deepagents>=0.7.24; (python_full_version >= '3.11') and extra == 'deep-agents'
21
+ Requires-Dist: langchain>=1.0; extra == 'deep-agents'
22
+ Requires-Dist: langgraph>=1.0; extra == 'deep-agents'
23
+ Provides-Extra: langgraph
24
+ Requires-Dist: langchain>=1.0; extra == 'langgraph'
25
+ Requires-Dist: langgraph>=1.0; extra == 'langgraph'
26
+ Provides-Extra: openai-agents
27
+ Requires-Dist: openai-agents>=0.23; extra == 'openai-agents'
28
+ Provides-Extra: pydantic-ai
29
+ Requires-Dist: pydantic-ai-slim>=2.55; (python_full_version >= '3.11') and extra == 'pydantic-ai'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Agent Approval Protocol for Python
33
+
34
+ Put an AI agent's tool calls behind an [AAP](https://agentapprovalprotocol.io) approval provider. Each call waits for the provider's decision, runs only if approved, and runs at most once.
35
+
36
+ ```sh
37
+ pip install agentapprovalprotocol
38
+ ```
39
+
40
+ ## Check a tool call
41
+
42
+ ```python
43
+ from agentapprovalprotocol import Call, Client, Gate
44
+
45
+ gate = Gate(Client("https://approvals.example.com/api/aap", token))
46
+
47
+ with gate.guard(Call(key=f"{run_id}:{tool_call.id}", tool=tool_call.name, arguments=tool_call.args)) as verdict:
48
+ if verdict:
49
+ result = run_tool(tool_call)
50
+ else:
51
+ result = verdict.reason # return this to the model as the tool result
52
+ ```
53
+
54
+ `key` must identify the tool call within its run, and stay the same if the call is seen again after a retry or resume. `guard` reports whether the tool succeeded when the block exits.
55
+
56
+ `AsyncGate` and `AsyncClient` provide the same API for `asyncio`.
57
+
58
+ ## Pause instead of waiting
59
+
60
+ With `mode="async"`, `check` returns `Pending` when there is no decision yet, so the run can pause and the process can exit. Mount a `Receiver` where the provider can reach it. When a decision arrives it wakes the run, and the gate checks the decision again before anything runs.
61
+
62
+ Framework integrations handle pausing and resuming for you.
63
+
64
+ Start the receiver first, because the provider checks that it answers before registering it:
65
+
66
+ ```python
67
+ from fastapi import FastAPI
68
+ from agentapprovalprotocol import Receiver
69
+
70
+ receiver = Receiver(resumers={"langgraph": resumer(agent)})
71
+ app = FastAPI()
72
+ app.add_route("/aap/events", receiver.asgi(), methods=["POST"])
73
+ ```
74
+
75
+ Add it as a route, as shown. `app.mount("/aap/events", ...)` redirects `/aap/events` to `/aap/events/`, and providers do not follow redirects.
76
+
77
+ Once the server is running, register the receiver:
78
+
79
+ ```python
80
+ provisioned = await receiver.provision(
81
+ provider_url="https://approvals.example.com/api/aap",
82
+ provisioner_token=provisioner_token,
83
+ url="https://agent.example.com/aap/events",
84
+ )
85
+ ```
86
+
87
+ `provision` creates the signing secret and registration ID, and the receiver starts using the new instance credential. Save `token`, `secret`, `registration_id` and `instance_id` from the result. Nothing is saved for you. If it raises `WebhookVerificationFailed`, check the receiver is reachable at `url` and call `provision` again.
88
+
89
+ On later starts, pass the saved values. `client` can be a function, called the first time a run is woken:
90
+
91
+ ```python
92
+ receiver = Receiver(
93
+ secret=secret,
94
+ registration_id=registration_id,
95
+ instance_id=instance_id,
96
+ client=lambda: AsyncClient("https://approvals.example.com/api/aap", load_token()),
97
+ resumers={"langgraph": resumer(agent)},
98
+ )
99
+ ```
100
+
101
+ To register in your provider's app instead, start the receiver with the secret and registration ID you enter there. Without the registration ID, the receiver refuses the provider's check.
102
+
103
+ ## LangChain, LangGraph and Deep Agents
104
+
105
+ ```sh
106
+ pip install "agentapprovalprotocol[langgraph]"
107
+ ```
108
+
109
+ ```python
110
+ from agentapprovalprotocol.langgraph import ApprovalMiddleware
111
+
112
+ approvals = ApprovalMiddleware("https://approvals.example.com/api/aap", token)
113
+ agent = create_agent(model, tools=tools, middleware=[approvals])
114
+ ```
115
+
116
+ Every tool call the agent makes goes to the provider. A denied call returns the reason to the model as the tool result.
117
+
118
+ For Deep Agents (Python 3.11 or later), install `agentapprovalprotocol[deep-agents]` and add the middleware to every subagent:
119
+
120
+ ```python
121
+ from agentapprovalprotocol.langgraph import deep_agent_subagents
122
+
123
+ agent = create_deep_agent(model, tools=tools, middleware=[approvals], subagents=deep_agent_subagents(approvals, my_subagents))
124
+ ```
125
+
126
+ To pause instead of waiting, use `ApprovalMiddleware(..., mode="async")` with a checkpointer, and mount a `Receiver` with `resumers={"langgraph": resumer(agent)}`. On LangSmith Deployment, use `deployment_resumer(get_client())` in a custom route instead. Resuming a thread any other way cannot approve a call: the middleware asks the provider again.
127
+
128
+ ## Pydantic AI
129
+
130
+ ```sh
131
+ pip install "agentapprovalprotocol[pydantic-ai]"
132
+ ```
133
+
134
+ Needs Python 3.11 or later.
135
+
136
+ ```python
137
+ from agentapprovalprotocol.pydantic_ai import ApprovalCapability
138
+
139
+ approvals = ApprovalCapability("https://approvals.example.com/api/aap", token)
140
+ agent = Agent(model, tools=tools, capabilities=[approvals])
141
+ ```
142
+
143
+ Every tool call the agent makes goes to the provider, including MCP and other toolset tools. A denied call returns the reason to the model as a failed tool result. Add the same capability to every agent you run as a sub-agent.
144
+
145
+ Tools that run at the model provider, such as native web search or code execution, cannot be sent for approval. A run that includes one stops with an error. Use a local tool instead.
146
+
147
+ To pause instead of waiting, use `ApprovalCapability(..., mode="async")` and add `DeferredToolRequests` to the agent's `output_type`. When a run ends with `DeferredToolRequests`, save `result.conversation`. Mount a `Receiver` with `resumers={"pydantic-ai": resumer(agent, load=load, save=save)}`, where `load(conversation_id)` returns the saved conversation and `save(conversation)` stores the resumed one. Calls inside a sub-agent wait instead of pausing. Continuing a conversation any other way cannot approve a call: the capability asks the provider again, and an approval with edited arguments is refused.
148
+
149
+ ## Google Agent Development Kit
150
+
151
+ ```sh
152
+ pip install "agentapprovalprotocol[adk]"
153
+ ```
154
+
155
+ ```python
156
+ from agentapprovalprotocol.adk import ApprovalPlugin, check_coverage
157
+
158
+ approvals = ApprovalPlugin("https://approvals.example.com/api/aap", token)
159
+ app = App(name="support", root_agent=agent, plugins=[approvals])
160
+ check_coverage(app, approvals)
161
+ ```
162
+
163
+ Every tool call from every agent in the app goes to the provider, including sub-agents and agents used as tools. A denied call returns the reason to the model as the tool result. Register the plugin first, and don't change tool arguments in later plugins or `before_tool_callback`.
164
+
165
+ `check_coverage` raises an error for agents whose calls the plugin cannot see: code executors, remote agents, and agents used as tools with `include_plugins=False`. Gemini built-in tools such as `google_search` run on Google's servers, so the plugin stops the run when an agent uses one.
166
+
167
+ To pause instead of waiting, use `ApprovalPlugin(..., mode="async")`, set `resumability_config=ResumabilityConfig(is_resumable=True)` on the app, and mount a `Receiver` with `resumers={"adk": resumer(runner)}`. Calls inside an agent used as a tool, and calls in live (streaming audio or video) runs, wait instead of pausing. For a runner in another process, such as `adk api_server`, send `wake_message(session.events, request.id)` as the new message from your own resumer. Answering the confirmation any other way cannot approve a call: the plugin asks the provider again.
168
+
169
+ ## OpenAI Agents SDK
170
+
171
+ ```sh
172
+ pip install "agentapprovalprotocol[openai-agents]"
173
+ ```
174
+
175
+ ```python
176
+ from agentapprovalprotocol.openai_agents import Approvals
177
+
178
+ approvals = Approvals("https://approvals.example.com/api/aap", token)
179
+ agent = approvals.protect(Agent(name="Support", tools=tools, handoffs=handoffs))
180
+ result = await Runner.run(agent, "Refund the duplicate charge")
181
+ ```
182
+
183
+ `protect` covers the agent's tools, its local MCP servers, its handoff targets and agents it uses as tools. Call it after the agent is fully configured. A denied call returns the reason to the model as the tool result.
184
+
185
+ `protect` refuses tools that run inside OpenAI's response, such as web search, file search and code interpreter, because their calls cannot be stopped. It also refuses local shell, computer and apply patch tools: wrap the action in a function tool instead. Hosted MCP servers are supported, and their calls always wait for the decision.
186
+
187
+ To pause instead of waiting, use `Approvals(..., mode="async", save=save, load=load)`, where `save(run_id, state)` and `load(run_id)` keep each run's state on your server. Start runs with `await approvals.run(agent, input, run_id=run_id)` and mount a `Receiver` with `resumers={"openai-agents": approvals.resumer(agent)}`. Approving the run state any other way cannot run a call: only a decision from the provider can.
188
+
189
+ ## Claude Agent SDK
190
+
191
+ ```sh
192
+ pip install "agentapprovalprotocol[claude-agent-sdk]"
193
+ ```
194
+
195
+ ```python
196
+ from agentapprovalprotocol.claude_agent_sdk import ApprovalHooks
197
+
198
+ approvals = ApprovalHooks("https://approvals.example.com/api/aap", token)
199
+ options = approvals.options(ClaudeAgentOptions(mcp_servers=servers))
200
+ async for message in query(prompt=prompt, options=options):
201
+ ...
202
+ ```
203
+
204
+ Every tool call the agent makes goes to the provider, including built-in tools, MCP tools and calls made by subagents. A denied call returns the reason to the model.
205
+
206
+ `options()` does not load your user settings (`~/.claude/settings.json`) unless you set `setting_sources` yourself. If the Claude Code approval hook is installed there, loading them asks for approval twice for every call.
207
+
208
+ To pause instead of waiting, use `ApprovalHooks(..., mode="async")`. A call without a decision ends the run with `stop_reason="tool_deferred"`. Mount a `Receiver` with `resumers={"claude-agent-sdk": resumer(options)}` on the machine that stores the session, and it resumes the session when the decision arrives. Calls made by subagents, and a second call in the same turn, wait for their decision instead of pausing.
209
+
210
+ Tools that the Claude API runs itself, such as the advisor, never reach the hooks, so they are not sent for approval.
211
+
212
+ ## CrewAI
213
+
214
+ CrewAI and the OpenAI Agents SDK need different major versions of `openai`, so the `crewai` and `openai-agents` extras can't be installed together.
215
+
216
+ ```sh
217
+ pip install "agentapprovalprotocol[crewai]"
218
+ ```
219
+
220
+ CrewAI supports Python 3.10 to 3.13.
221
+
222
+ ```python
223
+ from agentapprovalprotocol.crewai import ApprovalHooks
224
+
225
+ ApprovalHooks("https://approvals.example.com/api/aap", token).install()
226
+ crew.kickoff()
227
+ ```
228
+
229
+ Every tool call any crew, agent or flow in the process makes goes to the provider, including delegation to coworkers. A denied call returns the reason to the model as the tool result.
230
+
231
+ CrewAI cannot pause a run at a tool call, so each call waits for the decision. In `akickoff` the wait blocks the event loop.
232
+
233
+ Limitations:
234
+
235
+ - Register any hook that changes tool arguments before `install()`. A hook registered later, including one from a `@CrewBase` crew, runs after the approval. If it changes the arguments, the call is reported to the provider as failed.
236
+ - Models with `builtin_tools` run those tools at the model provider, where they cannot be checked. Runs that use such a model stop with an error.
237
+ - Tools you run yourself with `llm.call(available_functions=...)` are not checked.
@@ -0,0 +1,206 @@
1
+ # Agent Approval Protocol for Python
2
+
3
+ Put an AI agent's tool calls behind an [AAP](https://agentapprovalprotocol.io) approval provider. Each call waits for the provider's decision, runs only if approved, and runs at most once.
4
+
5
+ ```sh
6
+ pip install agentapprovalprotocol
7
+ ```
8
+
9
+ ## Check a tool call
10
+
11
+ ```python
12
+ from agentapprovalprotocol import Call, Client, Gate
13
+
14
+ gate = Gate(Client("https://approvals.example.com/api/aap", token))
15
+
16
+ with gate.guard(Call(key=f"{run_id}:{tool_call.id}", tool=tool_call.name, arguments=tool_call.args)) as verdict:
17
+ if verdict:
18
+ result = run_tool(tool_call)
19
+ else:
20
+ result = verdict.reason # return this to the model as the tool result
21
+ ```
22
+
23
+ `key` must identify the tool call within its run, and stay the same if the call is seen again after a retry or resume. `guard` reports whether the tool succeeded when the block exits.
24
+
25
+ `AsyncGate` and `AsyncClient` provide the same API for `asyncio`.
26
+
27
+ ## Pause instead of waiting
28
+
29
+ With `mode="async"`, `check` returns `Pending` when there is no decision yet, so the run can pause and the process can exit. Mount a `Receiver` where the provider can reach it. When a decision arrives it wakes the run, and the gate checks the decision again before anything runs.
30
+
31
+ Framework integrations handle pausing and resuming for you.
32
+
33
+ Start the receiver first, because the provider checks that it answers before registering it:
34
+
35
+ ```python
36
+ from fastapi import FastAPI
37
+ from agentapprovalprotocol import Receiver
38
+
39
+ receiver = Receiver(resumers={"langgraph": resumer(agent)})
40
+ app = FastAPI()
41
+ app.add_route("/aap/events", receiver.asgi(), methods=["POST"])
42
+ ```
43
+
44
+ Add it as a route, as shown. `app.mount("/aap/events", ...)` redirects `/aap/events` to `/aap/events/`, and providers do not follow redirects.
45
+
46
+ Once the server is running, register the receiver:
47
+
48
+ ```python
49
+ provisioned = await receiver.provision(
50
+ provider_url="https://approvals.example.com/api/aap",
51
+ provisioner_token=provisioner_token,
52
+ url="https://agent.example.com/aap/events",
53
+ )
54
+ ```
55
+
56
+ `provision` creates the signing secret and registration ID, and the receiver starts using the new instance credential. Save `token`, `secret`, `registration_id` and `instance_id` from the result. Nothing is saved for you. If it raises `WebhookVerificationFailed`, check the receiver is reachable at `url` and call `provision` again.
57
+
58
+ On later starts, pass the saved values. `client` can be a function, called the first time a run is woken:
59
+
60
+ ```python
61
+ receiver = Receiver(
62
+ secret=secret,
63
+ registration_id=registration_id,
64
+ instance_id=instance_id,
65
+ client=lambda: AsyncClient("https://approvals.example.com/api/aap", load_token()),
66
+ resumers={"langgraph": resumer(agent)},
67
+ )
68
+ ```
69
+
70
+ To register in your provider's app instead, start the receiver with the secret and registration ID you enter there. Without the registration ID, the receiver refuses the provider's check.
71
+
72
+ ## LangChain, LangGraph and Deep Agents
73
+
74
+ ```sh
75
+ pip install "agentapprovalprotocol[langgraph]"
76
+ ```
77
+
78
+ ```python
79
+ from agentapprovalprotocol.langgraph import ApprovalMiddleware
80
+
81
+ approvals = ApprovalMiddleware("https://approvals.example.com/api/aap", token)
82
+ agent = create_agent(model, tools=tools, middleware=[approvals])
83
+ ```
84
+
85
+ Every tool call the agent makes goes to the provider. A denied call returns the reason to the model as the tool result.
86
+
87
+ For Deep Agents (Python 3.11 or later), install `agentapprovalprotocol[deep-agents]` and add the middleware to every subagent:
88
+
89
+ ```python
90
+ from agentapprovalprotocol.langgraph import deep_agent_subagents
91
+
92
+ agent = create_deep_agent(model, tools=tools, middleware=[approvals], subagents=deep_agent_subagents(approvals, my_subagents))
93
+ ```
94
+
95
+ To pause instead of waiting, use `ApprovalMiddleware(..., mode="async")` with a checkpointer, and mount a `Receiver` with `resumers={"langgraph": resumer(agent)}`. On LangSmith Deployment, use `deployment_resumer(get_client())` in a custom route instead. Resuming a thread any other way cannot approve a call: the middleware asks the provider again.
96
+
97
+ ## Pydantic AI
98
+
99
+ ```sh
100
+ pip install "agentapprovalprotocol[pydantic-ai]"
101
+ ```
102
+
103
+ Needs Python 3.11 or later.
104
+
105
+ ```python
106
+ from agentapprovalprotocol.pydantic_ai import ApprovalCapability
107
+
108
+ approvals = ApprovalCapability("https://approvals.example.com/api/aap", token)
109
+ agent = Agent(model, tools=tools, capabilities=[approvals])
110
+ ```
111
+
112
+ Every tool call the agent makes goes to the provider, including MCP and other toolset tools. A denied call returns the reason to the model as a failed tool result. Add the same capability to every agent you run as a sub-agent.
113
+
114
+ Tools that run at the model provider, such as native web search or code execution, cannot be sent for approval. A run that includes one stops with an error. Use a local tool instead.
115
+
116
+ To pause instead of waiting, use `ApprovalCapability(..., mode="async")` and add `DeferredToolRequests` to the agent's `output_type`. When a run ends with `DeferredToolRequests`, save `result.conversation`. Mount a `Receiver` with `resumers={"pydantic-ai": resumer(agent, load=load, save=save)}`, where `load(conversation_id)` returns the saved conversation and `save(conversation)` stores the resumed one. Calls inside a sub-agent wait instead of pausing. Continuing a conversation any other way cannot approve a call: the capability asks the provider again, and an approval with edited arguments is refused.
117
+
118
+ ## Google Agent Development Kit
119
+
120
+ ```sh
121
+ pip install "agentapprovalprotocol[adk]"
122
+ ```
123
+
124
+ ```python
125
+ from agentapprovalprotocol.adk import ApprovalPlugin, check_coverage
126
+
127
+ approvals = ApprovalPlugin("https://approvals.example.com/api/aap", token)
128
+ app = App(name="support", root_agent=agent, plugins=[approvals])
129
+ check_coverage(app, approvals)
130
+ ```
131
+
132
+ Every tool call from every agent in the app goes to the provider, including sub-agents and agents used as tools. A denied call returns the reason to the model as the tool result. Register the plugin first, and don't change tool arguments in later plugins or `before_tool_callback`.
133
+
134
+ `check_coverage` raises an error for agents whose calls the plugin cannot see: code executors, remote agents, and agents used as tools with `include_plugins=False`. Gemini built-in tools such as `google_search` run on Google's servers, so the plugin stops the run when an agent uses one.
135
+
136
+ To pause instead of waiting, use `ApprovalPlugin(..., mode="async")`, set `resumability_config=ResumabilityConfig(is_resumable=True)` on the app, and mount a `Receiver` with `resumers={"adk": resumer(runner)}`. Calls inside an agent used as a tool, and calls in live (streaming audio or video) runs, wait instead of pausing. For a runner in another process, such as `adk api_server`, send `wake_message(session.events, request.id)` as the new message from your own resumer. Answering the confirmation any other way cannot approve a call: the plugin asks the provider again.
137
+
138
+ ## OpenAI Agents SDK
139
+
140
+ ```sh
141
+ pip install "agentapprovalprotocol[openai-agents]"
142
+ ```
143
+
144
+ ```python
145
+ from agentapprovalprotocol.openai_agents import Approvals
146
+
147
+ approvals = Approvals("https://approvals.example.com/api/aap", token)
148
+ agent = approvals.protect(Agent(name="Support", tools=tools, handoffs=handoffs))
149
+ result = await Runner.run(agent, "Refund the duplicate charge")
150
+ ```
151
+
152
+ `protect` covers the agent's tools, its local MCP servers, its handoff targets and agents it uses as tools. Call it after the agent is fully configured. A denied call returns the reason to the model as the tool result.
153
+
154
+ `protect` refuses tools that run inside OpenAI's response, such as web search, file search and code interpreter, because their calls cannot be stopped. It also refuses local shell, computer and apply patch tools: wrap the action in a function tool instead. Hosted MCP servers are supported, and their calls always wait for the decision.
155
+
156
+ To pause instead of waiting, use `Approvals(..., mode="async", save=save, load=load)`, where `save(run_id, state)` and `load(run_id)` keep each run's state on your server. Start runs with `await approvals.run(agent, input, run_id=run_id)` and mount a `Receiver` with `resumers={"openai-agents": approvals.resumer(agent)}`. Approving the run state any other way cannot run a call: only a decision from the provider can.
157
+
158
+ ## Claude Agent SDK
159
+
160
+ ```sh
161
+ pip install "agentapprovalprotocol[claude-agent-sdk]"
162
+ ```
163
+
164
+ ```python
165
+ from agentapprovalprotocol.claude_agent_sdk import ApprovalHooks
166
+
167
+ approvals = ApprovalHooks("https://approvals.example.com/api/aap", token)
168
+ options = approvals.options(ClaudeAgentOptions(mcp_servers=servers))
169
+ async for message in query(prompt=prompt, options=options):
170
+ ...
171
+ ```
172
+
173
+ Every tool call the agent makes goes to the provider, including built-in tools, MCP tools and calls made by subagents. A denied call returns the reason to the model.
174
+
175
+ `options()` does not load your user settings (`~/.claude/settings.json`) unless you set `setting_sources` yourself. If the Claude Code approval hook is installed there, loading them asks for approval twice for every call.
176
+
177
+ To pause instead of waiting, use `ApprovalHooks(..., mode="async")`. A call without a decision ends the run with `stop_reason="tool_deferred"`. Mount a `Receiver` with `resumers={"claude-agent-sdk": resumer(options)}` on the machine that stores the session, and it resumes the session when the decision arrives. Calls made by subagents, and a second call in the same turn, wait for their decision instead of pausing.
178
+
179
+ Tools that the Claude API runs itself, such as the advisor, never reach the hooks, so they are not sent for approval.
180
+
181
+ ## CrewAI
182
+
183
+ CrewAI and the OpenAI Agents SDK need different major versions of `openai`, so the `crewai` and `openai-agents` extras can't be installed together.
184
+
185
+ ```sh
186
+ pip install "agentapprovalprotocol[crewai]"
187
+ ```
188
+
189
+ CrewAI supports Python 3.10 to 3.13.
190
+
191
+ ```python
192
+ from agentapprovalprotocol.crewai import ApprovalHooks
193
+
194
+ ApprovalHooks("https://approvals.example.com/api/aap", token).install()
195
+ crew.kickoff()
196
+ ```
197
+
198
+ Every tool call any crew, agent or flow in the process makes goes to the provider, including delegation to coworkers. A denied call returns the reason to the model as the tool result.
199
+
200
+ CrewAI cannot pause a run at a tool call, so each call waits for the decision. In `akickoff` the wait blocks the event loop.
201
+
202
+ Limitations:
203
+
204
+ - Register any hook that changes tool arguments before `install()`. A hook registered later, including one from a `@CrewBase` crew, runs after the approval. If it changes the arguments, the call is reported to the provider as failed.
205
+ - Models with `builtin_tools` run those tools at the model provider, where they cannot be checked. Runs that use such a model stop with an error.
206
+ - Tools you run yourself with `llm.call(available_functions=...)` are not checked.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentapprovalprotocol"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "Put an AI agent's tool calls behind an Agent Approval Protocol (AAP) approval provider."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -17,13 +17,26 @@ Homepage = "https://agentapprovalprotocol.io"
17
17
  Source = "https://github.com/agentapprovalprotocol/agentapprovalprotocol"
18
18
 
19
19
  [project.optional-dependencies]
20
+ claude-agent-sdk = ["claude-agent-sdk>=0.2.165"]
20
21
  langgraph = [
21
22
  "langchain>=1.0",
22
23
  "langgraph>=1.0",
23
24
  ]
25
+ deep-agents = [
26
+ "langchain>=1.0",
27
+ "langgraph>=1.0",
28
+ "deepagents>=0.7.24 ; python_full_version >= '3.11'",
29
+ ]
24
30
  pydantic-ai = [
25
31
  "pydantic-ai-slim>=2.55 ; python_full_version >= '3.11'",
26
32
  ]
33
+ adk = [
34
+ "google-adk>=2.11",
35
+ ]
36
+ openai-agents = [
37
+ "openai-agents>=0.23",
38
+ ]
39
+ crewai = ["crewai>=1.15 ; python_full_version < '3.14'"]
27
40
 
28
41
  [dependency-groups]
29
42
  dev = [
@@ -43,3 +56,10 @@ packages = ["src/agentapprovalprotocol"]
43
56
  [tool.pytest.ini_options]
44
57
  asyncio_mode = "auto"
45
58
  testpaths = ["tests"]
59
+
60
+ [tool.uv]
61
+ # CrewAI needs openai below 3 and the OpenAI Agents SDK needs openai 3, so
62
+ # the two extras resolve separately and cannot be installed together.
63
+ conflicts = [[{ extra = "crewai" }, { extra = "openai-agents" }]]
64
+ # chromadb (through crewai) allows onnxruntime releases without Python 3.10 wheels.
65
+ constraint-dependencies = ["onnxruntime<1.24 ; python_full_version < '3.11'"]
@@ -5,10 +5,10 @@ Put an agent's tool calls behind an AAP approval provider. ``Gate`` and
5
5
  on them. See https://agentapprovalprotocol.io.
6
6
  """
7
7
 
8
- from ._client import AAPError, AsyncClient, Client, ConflictError, CredentialRefused, NotFound
8
+ from ._client import AAPError, AsyncClient, Client, ConflictError, CredentialRefused, NotFound, ProvisionError, WebhookVerificationFailed
9
9
  from ._gate import Allow, AsyncGate, Call, Deny, Gate, Pending, Verdict
10
10
  from ._wire import Consumption, Decision, Outcome, ProtocolError, Request
11
- from .receiver import Receiver
11
+ from .receiver import Provisioned, Receiver
12
12
 
13
13
  __all__ = [
14
14
  "AAPError",
@@ -27,7 +27,10 @@ __all__ = [
27
27
  "Outcome",
28
28
  "Pending",
29
29
  "ProtocolError",
30
+ "Provisioned",
31
+ "ProvisionError",
30
32
  "Receiver",
31
33
  "Request",
32
34
  "Verdict",
35
+ "WebhookVerificationFailed",
33
36
  ]
@@ -42,6 +42,23 @@ class NotFound(AAPError):
42
42
  """The request is unknown or unavailable to this instance."""
43
43
 
44
44
 
45
+ class ProvisionError(AAPError):
46
+ """Provisioning an instance failed.
47
+
48
+ ``status`` is the HTTP status and ``code`` the provider's error code,
49
+ when the provider answered with them.
50
+ """
51
+
52
+ def __init__(self, message: str, *, status: int | None = None, code: str | None = None):
53
+ super().__init__(message)
54
+ self.status = status
55
+ self.code = code
56
+
57
+
58
+ class WebhookVerificationFailed(ProvisionError):
59
+ """The provider could not verify the receiver (HTTP 422, ``webhook_verification_failed``)."""
60
+
61
+
45
62
  def validate_base_url(base_url: str) -> str:
46
63
  parts = urlsplit(base_url)
47
64
  if not parts.netloc or parts.username or parts.password or parts.query or parts.fragment: