axonflow-google-adk-plugin 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- axonflow_google_adk_plugin-1.0.0/LICENSE +21 -0
- axonflow_google_adk_plugin-1.0.0/PKG-INFO +227 -0
- axonflow_google_adk_plugin-1.0.0/README.md +194 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_adk/__init__.py +34 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_adk/_version.py +4 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_adk/mcp_helper.py +90 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_adk/plugin.py +1148 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_google_adk_plugin.egg-info/PKG-INFO +227 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_google_adk_plugin.egg-info/SOURCES.txt +13 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_google_adk_plugin.egg-info/dependency_links.txt +1 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_google_adk_plugin.egg-info/requires.txt +7 -0
- axonflow_google_adk_plugin-1.0.0/axonflow_google_adk_plugin.egg-info/top_level.txt +1 -0
- axonflow_google_adk_plugin-1.0.0/pyproject.toml +53 -0
- axonflow_google_adk_plugin-1.0.0/setup.cfg +4 -0
- axonflow_google_adk_plugin-1.0.0/tests/test_plugin.py +1033 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 getaxonflow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axonflow-google-adk-plugin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AxonFlow governance plugin for Google Agent Development Kit (ADK)
|
|
5
|
+
Author-email: AxonFlow <hello@getaxonflow.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://getaxonflow.com
|
|
8
|
+
Project-URL: Documentation, https://docs.getaxonflow.com/docs/integration/google-adk
|
|
9
|
+
Project-URL: Repository, https://github.com/getaxonflow/axonflow-google-adk-plugin
|
|
10
|
+
Project-URL: Changelog, https://github.com/getaxonflow/axonflow-google-adk-plugin/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/getaxonflow/axonflow-google-adk-plugin/issues
|
|
12
|
+
Keywords: axonflow,google-adk,adk,ai-governance,llm,policy,compliance
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: google-adk>=2.0.0
|
|
27
|
+
Requires-Dist: axonflow>=8.2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# axonflow-google-adk-plugin
|
|
35
|
+
|
|
36
|
+
AxonFlow governance plugin for [Google Agent Development Kit (ADK)](https://adk.dev/).
|
|
37
|
+
|
|
38
|
+
Register `AxonFlowPlugin` once on a `Runner` and **every model call and every
|
|
39
|
+
tool call across every agent on that Runner** is governed by AxonFlow
|
|
40
|
+
policies: pre-check, HITL approval, deny short-circuit, audit trail, PII
|
|
41
|
+
redaction on tool I/O.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install axonflow-google-adk-plugin
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires `google-adk>=2.0` and `axonflow>=8.2.0` (AxonFlow Python SDK).
|
|
50
|
+
|
|
51
|
+
## Quickstart (5 lines)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from google.adk.runners import InMemoryRunner
|
|
55
|
+
from google.adk.agents import LlmAgent
|
|
56
|
+
from axonflow_adk import AxonFlowPlugin
|
|
57
|
+
|
|
58
|
+
agent = LlmAgent(model="gemini-2.0-flash", name="loan_desk", instruction="...")
|
|
59
|
+
runner = InMemoryRunner(
|
|
60
|
+
agent=agent,
|
|
61
|
+
app_name="loan_desk",
|
|
62
|
+
plugins=[AxonFlowPlugin(
|
|
63
|
+
endpoint="http://localhost:8080",
|
|
64
|
+
client_id="loan-desk",
|
|
65
|
+
client_secret="secret-from-axonflow",
|
|
66
|
+
)],
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Hook → AxonFlow endpoint mapping
|
|
71
|
+
|
|
72
|
+
| ADK hook | AxonFlow call | Deny shape |
|
|
73
|
+
|------------------------------|---------------------------|------------------------------------------------|
|
|
74
|
+
| `before_model_callback` | `pre_check` | `LlmResponse` with policy-denial text |
|
|
75
|
+
| `after_model_callback` | `audit_llm_call` | never blocks (audit only) |
|
|
76
|
+
| `before_tool_callback` | `check_tool_input` | `{"error": "[AxonFlow] <reason>"}` |
|
|
77
|
+
| `after_tool_callback` | `check_tool_output` | redacted dict OR `{"error": ...}` on hard deny |
|
|
78
|
+
| `on_tool_error_callback` | `audit_tool_call` | never blocks (audit only) |
|
|
79
|
+
| `on_user_message_callback` | no-op (v1) | n/a |
|
|
80
|
+
|
|
81
|
+
The `on_user_message_callback` hook is intentionally a no-op in v1 — returning
|
|
82
|
+
non-None Content there would silently **replace** the user's message, which is
|
|
83
|
+
the wrong tool for governance.
|
|
84
|
+
|
|
85
|
+
## HITL approval flow — 4-step
|
|
86
|
+
|
|
87
|
+
When AxonFlow policy evaluates to `require_approval`, the plugin runs the
|
|
88
|
+
full **4-step HITL flow** by default (`enable_hitl_polling=True`):
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
before_model_callback / before_tool_callback
|
|
92
|
+
│
|
|
93
|
+
├─ STEP 1 — gate (pre_check / check_tool_input)
|
|
94
|
+
│ returns blocked, BlockReason == "require_approval"
|
|
95
|
+
│
|
|
96
|
+
├─ STEP 2 — POST /api/v1/hitl/queue
|
|
97
|
+
│ plugin calls client.create_hitl_request(request=HITLCreateInput(...))
|
|
98
|
+
│ returns approval_id (uuid)
|
|
99
|
+
│
|
|
100
|
+
├─ STEP 3 — GET /api/v1/hitl/queue/{approval_id}
|
|
101
|
+
│ polled every approval_poll_interval_seconds (default 2s);
|
|
102
|
+
│ local consecutive-failure counter (NOT the shared
|
|
103
|
+
│ breaker) so a polling outage can't disable governance
|
|
104
|
+
│ for other in-flight calls
|
|
105
|
+
│
|
|
106
|
+
└─ STEP 4 — terminal state:
|
|
107
|
+
├─ "approved" → return None (let LLM / tool proceed)
|
|
108
|
+
├─ "rejected" | "expired" → return deny short-circuit
|
|
109
|
+
├─ N consecutive poll failures → deny
|
|
110
|
+
└─ time > approval_max_wait_seconds → deny
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The plugin's `before_model_callback` and `before_tool_callback` both run
|
|
114
|
+
this flow. Detection is an exact-string match against the platform's
|
|
115
|
+
`require_approval` sentinel. Substring matching previously false-positived
|
|
116
|
+
on any policy whose reason text contained the word "approval".
|
|
117
|
+
|
|
118
|
+
The 4-step flow is the only **fail-closed** path in the plugin —
|
|
119
|
+
everything else fails open. Approvals are safety-critical; defaulting to
|
|
120
|
+
"allow" on an AxonFlow outage during an approval gate would defeat the
|
|
121
|
+
gate.
|
|
122
|
+
|
|
123
|
+
### Approving / rejecting out-of-band
|
|
124
|
+
|
|
125
|
+
When step 2 returns an `approval_id`, the plugin emits a single INFO log:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
axonflow hitl AWAITING APPROVAL: request_id=<uuid>; approve via
|
|
129
|
+
POST /api/v1/hitl/queue/<uuid>/{approve|reject}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The reviewer (UI, Slack bot, internal portal) posts the decision via:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Approve
|
|
136
|
+
curl -X POST $AXONFLOW_ENDPOINT/api/v1/hitl/queue/<approval_id>/approve \
|
|
137
|
+
-H 'Content-Type: application/json' \
|
|
138
|
+
-d '{"reviewer_id":"compliance","reviewer_email":"compliance@bank.example"}'
|
|
139
|
+
|
|
140
|
+
# Reject (same shape)
|
|
141
|
+
curl -X POST $AXONFLOW_ENDPOINT/api/v1/hitl/queue/<approval_id>/reject \
|
|
142
|
+
-H 'Content-Type: application/json' \
|
|
143
|
+
-d '{"reviewer_id":"compliance","reviewer_email":"compliance@bank.example"}'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Opting out — deny-fast mode
|
|
147
|
+
|
|
148
|
+
Set `enable_hitl_polling=False` on the config to short-circuit
|
|
149
|
+
`require_approval` immediately without enqueuing a row. The host app
|
|
150
|
+
then drives its own approval workflow.
|
|
151
|
+
|
|
152
|
+
## Authenticating in enterprise mode
|
|
153
|
+
|
|
154
|
+
ADK does not carry a first-class `user_token` concept. To propagate the
|
|
155
|
+
end-user identity AxonFlow's enterprise-mode policy enforcement requires,
|
|
156
|
+
set `state["axonflow_user_token"]` to a valid JWT on the session BEFORE
|
|
157
|
+
calling `runner.run_async(...)`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
session = runner.session_service.create_session(
|
|
161
|
+
app_name="loan_desk", user_id="cust-001", session_id="sess-A",
|
|
162
|
+
)
|
|
163
|
+
session.state["axonflow_user_token"] = generate_axonflow_jwt(user_id="cust-001")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For **community mode** (no tenant signing key), leave the state key
|
|
167
|
+
unset; the plugin will use `config.default_user_token` (default
|
|
168
|
+
`"anonymous"`).
|
|
169
|
+
|
|
170
|
+
## Failure semantics
|
|
171
|
+
|
|
172
|
+
A buggy or unreachable AxonFlow **must not** break the agent. The plugin
|
|
173
|
+
ships with:
|
|
174
|
+
|
|
175
|
+
- **Per-hook timeout** (default 5s, configurable via `call_timeout_seconds`)
|
|
176
|
+
- **Half-open circuit breaker** (default open after 5 consecutive failures,
|
|
177
|
+
recover after 30s). HALF_OPEN admits exactly one probe; concurrent
|
|
178
|
+
hooks during recovery are skipped without leaking a thundering herd.
|
|
179
|
+
- **Fail-open default** — every hook except `_await_hitl_decision`
|
|
180
|
+
returns `None` on error/timeout/open-circuit, letting the model or
|
|
181
|
+
tool call proceed.
|
|
182
|
+
|
|
183
|
+
## MCP toolset helper
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from google.adk.agents import LlmAgent
|
|
187
|
+
from axonflow_adk import axonflow_mcp_toolset
|
|
188
|
+
|
|
189
|
+
agent = LlmAgent(
|
|
190
|
+
model="gemini-2.0-flash",
|
|
191
|
+
name="postgres_governed",
|
|
192
|
+
instruction="Answer questions about the production DB.",
|
|
193
|
+
tools=[axonflow_mcp_toolset(
|
|
194
|
+
endpoint="http://localhost:8080",
|
|
195
|
+
client_id="my-app",
|
|
196
|
+
client_secret="secret",
|
|
197
|
+
)],
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Run the example
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pip install axonflow-google-adk-plugin
|
|
205
|
+
export GOOGLE_API_KEY=...
|
|
206
|
+
export AXONFLOW_ENDPOINT=http://localhost:8080
|
|
207
|
+
export AXONFLOW_CLIENT_ID=loan-desk
|
|
208
|
+
export AXONFLOW_CLIENT_SECRET=...
|
|
209
|
+
|
|
210
|
+
python -m examples.loan_disbursement_agent
|
|
211
|
+
# or: python examples/loan_disbursement_agent.py
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Tests
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pip install -e ".[dev]"
|
|
218
|
+
pytest tests/ -v
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Documentation
|
|
222
|
+
|
|
223
|
+
Full integration guide: [docs.getaxonflow.com/docs/integration/google-adk](https://docs.getaxonflow.com/docs/integration/google-adk/)
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# axonflow-google-adk-plugin
|
|
2
|
+
|
|
3
|
+
AxonFlow governance plugin for [Google Agent Development Kit (ADK)](https://adk.dev/).
|
|
4
|
+
|
|
5
|
+
Register `AxonFlowPlugin` once on a `Runner` and **every model call and every
|
|
6
|
+
tool call across every agent on that Runner** is governed by AxonFlow
|
|
7
|
+
policies: pre-check, HITL approval, deny short-circuit, audit trail, PII
|
|
8
|
+
redaction on tool I/O.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install axonflow-google-adk-plugin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires `google-adk>=2.0` and `axonflow>=8.2.0` (AxonFlow Python SDK).
|
|
17
|
+
|
|
18
|
+
## Quickstart (5 lines)
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from google.adk.runners import InMemoryRunner
|
|
22
|
+
from google.adk.agents import LlmAgent
|
|
23
|
+
from axonflow_adk import AxonFlowPlugin
|
|
24
|
+
|
|
25
|
+
agent = LlmAgent(model="gemini-2.0-flash", name="loan_desk", instruction="...")
|
|
26
|
+
runner = InMemoryRunner(
|
|
27
|
+
agent=agent,
|
|
28
|
+
app_name="loan_desk",
|
|
29
|
+
plugins=[AxonFlowPlugin(
|
|
30
|
+
endpoint="http://localhost:8080",
|
|
31
|
+
client_id="loan-desk",
|
|
32
|
+
client_secret="secret-from-axonflow",
|
|
33
|
+
)],
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Hook → AxonFlow endpoint mapping
|
|
38
|
+
|
|
39
|
+
| ADK hook | AxonFlow call | Deny shape |
|
|
40
|
+
|------------------------------|---------------------------|------------------------------------------------|
|
|
41
|
+
| `before_model_callback` | `pre_check` | `LlmResponse` with policy-denial text |
|
|
42
|
+
| `after_model_callback` | `audit_llm_call` | never blocks (audit only) |
|
|
43
|
+
| `before_tool_callback` | `check_tool_input` | `{"error": "[AxonFlow] <reason>"}` |
|
|
44
|
+
| `after_tool_callback` | `check_tool_output` | redacted dict OR `{"error": ...}` on hard deny |
|
|
45
|
+
| `on_tool_error_callback` | `audit_tool_call` | never blocks (audit only) |
|
|
46
|
+
| `on_user_message_callback` | no-op (v1) | n/a |
|
|
47
|
+
|
|
48
|
+
The `on_user_message_callback` hook is intentionally a no-op in v1 — returning
|
|
49
|
+
non-None Content there would silently **replace** the user's message, which is
|
|
50
|
+
the wrong tool for governance.
|
|
51
|
+
|
|
52
|
+
## HITL approval flow — 4-step
|
|
53
|
+
|
|
54
|
+
When AxonFlow policy evaluates to `require_approval`, the plugin runs the
|
|
55
|
+
full **4-step HITL flow** by default (`enable_hitl_polling=True`):
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
before_model_callback / before_tool_callback
|
|
59
|
+
│
|
|
60
|
+
├─ STEP 1 — gate (pre_check / check_tool_input)
|
|
61
|
+
│ returns blocked, BlockReason == "require_approval"
|
|
62
|
+
│
|
|
63
|
+
├─ STEP 2 — POST /api/v1/hitl/queue
|
|
64
|
+
│ plugin calls client.create_hitl_request(request=HITLCreateInput(...))
|
|
65
|
+
│ returns approval_id (uuid)
|
|
66
|
+
│
|
|
67
|
+
├─ STEP 3 — GET /api/v1/hitl/queue/{approval_id}
|
|
68
|
+
│ polled every approval_poll_interval_seconds (default 2s);
|
|
69
|
+
│ local consecutive-failure counter (NOT the shared
|
|
70
|
+
│ breaker) so a polling outage can't disable governance
|
|
71
|
+
│ for other in-flight calls
|
|
72
|
+
│
|
|
73
|
+
└─ STEP 4 — terminal state:
|
|
74
|
+
├─ "approved" → return None (let LLM / tool proceed)
|
|
75
|
+
├─ "rejected" | "expired" → return deny short-circuit
|
|
76
|
+
├─ N consecutive poll failures → deny
|
|
77
|
+
└─ time > approval_max_wait_seconds → deny
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The plugin's `before_model_callback` and `before_tool_callback` both run
|
|
81
|
+
this flow. Detection is an exact-string match against the platform's
|
|
82
|
+
`require_approval` sentinel. Substring matching previously false-positived
|
|
83
|
+
on any policy whose reason text contained the word "approval".
|
|
84
|
+
|
|
85
|
+
The 4-step flow is the only **fail-closed** path in the plugin —
|
|
86
|
+
everything else fails open. Approvals are safety-critical; defaulting to
|
|
87
|
+
"allow" on an AxonFlow outage during an approval gate would defeat the
|
|
88
|
+
gate.
|
|
89
|
+
|
|
90
|
+
### Approving / rejecting out-of-band
|
|
91
|
+
|
|
92
|
+
When step 2 returns an `approval_id`, the plugin emits a single INFO log:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
axonflow hitl AWAITING APPROVAL: request_id=<uuid>; approve via
|
|
96
|
+
POST /api/v1/hitl/queue/<uuid>/{approve|reject}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The reviewer (UI, Slack bot, internal portal) posts the decision via:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Approve
|
|
103
|
+
curl -X POST $AXONFLOW_ENDPOINT/api/v1/hitl/queue/<approval_id>/approve \
|
|
104
|
+
-H 'Content-Type: application/json' \
|
|
105
|
+
-d '{"reviewer_id":"compliance","reviewer_email":"compliance@bank.example"}'
|
|
106
|
+
|
|
107
|
+
# Reject (same shape)
|
|
108
|
+
curl -X POST $AXONFLOW_ENDPOINT/api/v1/hitl/queue/<approval_id>/reject \
|
|
109
|
+
-H 'Content-Type: application/json' \
|
|
110
|
+
-d '{"reviewer_id":"compliance","reviewer_email":"compliance@bank.example"}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Opting out — deny-fast mode
|
|
114
|
+
|
|
115
|
+
Set `enable_hitl_polling=False` on the config to short-circuit
|
|
116
|
+
`require_approval` immediately without enqueuing a row. The host app
|
|
117
|
+
then drives its own approval workflow.
|
|
118
|
+
|
|
119
|
+
## Authenticating in enterprise mode
|
|
120
|
+
|
|
121
|
+
ADK does not carry a first-class `user_token` concept. To propagate the
|
|
122
|
+
end-user identity AxonFlow's enterprise-mode policy enforcement requires,
|
|
123
|
+
set `state["axonflow_user_token"]` to a valid JWT on the session BEFORE
|
|
124
|
+
calling `runner.run_async(...)`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
session = runner.session_service.create_session(
|
|
128
|
+
app_name="loan_desk", user_id="cust-001", session_id="sess-A",
|
|
129
|
+
)
|
|
130
|
+
session.state["axonflow_user_token"] = generate_axonflow_jwt(user_id="cust-001")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For **community mode** (no tenant signing key), leave the state key
|
|
134
|
+
unset; the plugin will use `config.default_user_token` (default
|
|
135
|
+
`"anonymous"`).
|
|
136
|
+
|
|
137
|
+
## Failure semantics
|
|
138
|
+
|
|
139
|
+
A buggy or unreachable AxonFlow **must not** break the agent. The plugin
|
|
140
|
+
ships with:
|
|
141
|
+
|
|
142
|
+
- **Per-hook timeout** (default 5s, configurable via `call_timeout_seconds`)
|
|
143
|
+
- **Half-open circuit breaker** (default open after 5 consecutive failures,
|
|
144
|
+
recover after 30s). HALF_OPEN admits exactly one probe; concurrent
|
|
145
|
+
hooks during recovery are skipped without leaking a thundering herd.
|
|
146
|
+
- **Fail-open default** — every hook except `_await_hitl_decision`
|
|
147
|
+
returns `None` on error/timeout/open-circuit, letting the model or
|
|
148
|
+
tool call proceed.
|
|
149
|
+
|
|
150
|
+
## MCP toolset helper
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from google.adk.agents import LlmAgent
|
|
154
|
+
from axonflow_adk import axonflow_mcp_toolset
|
|
155
|
+
|
|
156
|
+
agent = LlmAgent(
|
|
157
|
+
model="gemini-2.0-flash",
|
|
158
|
+
name="postgres_governed",
|
|
159
|
+
instruction="Answer questions about the production DB.",
|
|
160
|
+
tools=[axonflow_mcp_toolset(
|
|
161
|
+
endpoint="http://localhost:8080",
|
|
162
|
+
client_id="my-app",
|
|
163
|
+
client_secret="secret",
|
|
164
|
+
)],
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Run the example
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install axonflow-google-adk-plugin
|
|
172
|
+
export GOOGLE_API_KEY=...
|
|
173
|
+
export AXONFLOW_ENDPOINT=http://localhost:8080
|
|
174
|
+
export AXONFLOW_CLIENT_ID=loan-desk
|
|
175
|
+
export AXONFLOW_CLIENT_SECRET=...
|
|
176
|
+
|
|
177
|
+
python -m examples.loan_disbursement_agent
|
|
178
|
+
# or: python examples/loan_disbursement_agent.py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Tests
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install -e ".[dev]"
|
|
185
|
+
pytest tests/ -v
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
Full integration guide: [docs.getaxonflow.com/docs/integration/google-adk](https://docs.getaxonflow.com/docs/integration/google-adk/)
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright 2026 AxonFlow
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""AxonFlow governance plugin for Google Agent Development Kit (ADK).
|
|
5
|
+
|
|
6
|
+
Register `AxonFlowPlugin` on a `Runner` and every model + tool call across
|
|
7
|
+
every agent on that runner is governed by AxonFlow policies, with HITL
|
|
8
|
+
approval, denial short-circuits, and an audit trail.
|
|
9
|
+
|
|
10
|
+
from google.adk.runners import InMemoryRunner
|
|
11
|
+
from axonflow_adk import AxonFlowPlugin
|
|
12
|
+
|
|
13
|
+
runner = InMemoryRunner(
|
|
14
|
+
agent=root_agent,
|
|
15
|
+
app_name="loan_desk",
|
|
16
|
+
plugins=[AxonFlowPlugin(
|
|
17
|
+
endpoint="http://localhost:8080",
|
|
18
|
+
client_id="loan-desk",
|
|
19
|
+
client_secret="...",
|
|
20
|
+
)],
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from axonflow_adk._version import __version__
|
|
25
|
+
from axonflow_adk.plugin import AxonFlowPlugin, ApprovalTimeout, ApprovalRejected
|
|
26
|
+
from axonflow_adk.mcp_helper import axonflow_mcp_toolset
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"ApprovalRejected",
|
|
31
|
+
"ApprovalTimeout",
|
|
32
|
+
"AxonFlowPlugin",
|
|
33
|
+
"axonflow_mcp_toolset",
|
|
34
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Copyright 2026 AxonFlow
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""ADK `MCPToolset` helper for the AxonFlow agent's MCP server.
|
|
5
|
+
|
|
6
|
+
The AxonFlow agent ships an MCP endpoint at `/mcp/` on the same host as
|
|
7
|
+
its REST API. This module returns an `McpToolset` configured against that
|
|
8
|
+
endpoint over Streamable HTTP, so ADK callers can register AxonFlow's
|
|
9
|
+
governed MCP tools (e.g. PostgreSQL, Snowflake, GCS) with one line:
|
|
10
|
+
|
|
11
|
+
from axonflow_adk import axonflow_mcp_toolset
|
|
12
|
+
|
|
13
|
+
toolset = axonflow_mcp_toolset(
|
|
14
|
+
endpoint="http://localhost:8080",
|
|
15
|
+
client_id="my-app",
|
|
16
|
+
client_secret="...",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Reference: https://adk.dev/tools-custom/mcp-tools/
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def axonflow_mcp_toolset(
|
|
29
|
+
endpoint: str,
|
|
30
|
+
*,
|
|
31
|
+
client_id: str | None = None,
|
|
32
|
+
client_secret: str | None = None,
|
|
33
|
+
bearer_token: str | None = None,
|
|
34
|
+
mcp_path: str = "/mcp/",
|
|
35
|
+
extra_headers: dict[str, str] | None = None,
|
|
36
|
+
) -> Any:
|
|
37
|
+
"""Return an ADK `McpToolset` pointed at AxonFlow's MCP server.
|
|
38
|
+
|
|
39
|
+
Authentication shape (R3 HIGH-3 — the platform's MCP server expects
|
|
40
|
+
one of):
|
|
41
|
+
|
|
42
|
+
* `Authorization: Basic <base64(client_id:client_secret)>` when
|
|
43
|
+
`client_id` AND `client_secret` are provided.
|
|
44
|
+
* `Authorization: Bearer <token>` when `bearer_token` is provided.
|
|
45
|
+
* Anonymous (no header) when none are provided — community-mode.
|
|
46
|
+
|
|
47
|
+
Custom `X-AxonFlow-*` headers (a prior shape) are NOT recognized by
|
|
48
|
+
the platform and would result in silently-anonymous calls bypassing
|
|
49
|
+
per-client / per-tenant policy scoping. Use the canonical
|
|
50
|
+
Authorization header instead.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
endpoint: AxonFlow agent base URL (e.g. `http://localhost:8080`).
|
|
54
|
+
Trailing slash is stripped; `mcp_path` is appended.
|
|
55
|
+
client_id: AxonFlow client identifier (community/enterprise).
|
|
56
|
+
client_secret: AxonFlow client secret (community/enterprise).
|
|
57
|
+
bearer_token: Pre-issued bearer token (overrides client_id/secret
|
|
58
|
+
when set).
|
|
59
|
+
mcp_path: Path component of the AxonFlow MCP endpoint. Defaults
|
|
60
|
+
to `/mcp/` which is the agent's canonical path.
|
|
61
|
+
extra_headers: Optional additional headers (tenant scoping,
|
|
62
|
+
tracing context) merged into the connection params.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
`McpToolset` ready to drop into `LlmAgent(tools=[...])`.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ImportError: if `google-adk` is not installed.
|
|
69
|
+
"""
|
|
70
|
+
from google.adk.tools.mcp_tool import McpToolset
|
|
71
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import (
|
|
72
|
+
StreamableHTTPConnectionParams,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
headers: dict[str, str] = {}
|
|
76
|
+
if bearer_token:
|
|
77
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
78
|
+
elif client_id and client_secret:
|
|
79
|
+
raw = f"{client_id}:{client_secret}".encode()
|
|
80
|
+
headers["Authorization"] = "Basic " + base64.b64encode(raw).decode("ascii")
|
|
81
|
+
if extra_headers:
|
|
82
|
+
headers.update(extra_headers)
|
|
83
|
+
|
|
84
|
+
url = endpoint.rstrip("/") + mcp_path
|
|
85
|
+
return McpToolset(
|
|
86
|
+
connection_params=StreamableHTTPConnectionParams(
|
|
87
|
+
url=url,
|
|
88
|
+
headers=headers,
|
|
89
|
+
)
|
|
90
|
+
)
|