vaid-adk 0.1.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.
@@ -0,0 +1,30 @@
1
+ # Rust
2
+ /target
3
+ **/*.rs.bk
4
+ Cargo.lock.orig
5
+
6
+ # Backtraces
7
+ rustc-ice-*.txt
8
+
9
+ # Editor / OS
10
+ .DS_Store
11
+ *.swp
12
+ .idea/
13
+ .vscode/
14
+
15
+ # Python
16
+ __pycache__/
17
+ *.py[cod]
18
+ .pytest_cache/
19
+ *.egg-info/
20
+ dist/
21
+ build/
22
+ .venv/
23
+ .buildvenv/
24
+ .installtest/
25
+
26
+ # TypeScript / npm
27
+ node_modules/
28
+ typescript/*/dist/
29
+ typescript/*/test-dist/
30
+ *.tsbuildinfo
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.5
2
+ Name: vaid-adk
3
+ Version: 0.1.0
4
+ Summary: VAID verification for Google ADK: a BasePlugin that checks a presented VAID's signature, expiry, delegation-chain attenuation and revocation before an agent runs, is delegated to, or calls a tool, denying fail-closed.
5
+ Project-URL: Homepage, https://github.com/solara-associates/vaid
6
+ Project-URL: Repository, https://github.com/solara-associates/vaid
7
+ Project-URL: Source, https://github.com/solara-associates/vaid/tree/main/python/vaid-adk
8
+ Project-URL: Issues, https://github.com/solara-associates/vaid/issues
9
+ Author: solara.associates
10
+ Author-email: "solara.associates" <info@solara.associates>
11
+ License: Apache-2.0
12
+ Keywords: adk,agent-identity,attenuation,delegation,google-adk,vaid
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: google-adk<3,>=2.0
21
+ Requires-Dist: vaid-mint<0.10,>=0.9.0
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
24
+ Requires-Dist: pytest>=8.0; extra == 'test'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # vaid-adk
28
+
29
+ VAID verification for Google's Agent Development Kit (ADK): a `BasePlugin`
30
+ that checks a presented VAID's signature, expiry, delegation-chain
31
+ attenuation and revocation status before an agent runs, before it is
32
+ delegated to as a sub-agent, and before it calls a tool. It denies
33
+ fail-closed whenever any of that cannot be positively confirmed.
34
+
35
+ ADK has no built-in mechanism for this (google/adk-python#4992, #6551). This
36
+ package adds it for an agent that already carries a VAID
37
+ (https://github.com/solara-associates/vaid), using only `vaid_mint`'s
38
+ existing signature, chain and revocation primitives. It does not mint
39
+ credentials; minting an attenuated child VAID for a sub-agent is the
40
+ caller's job, via `vaid_mint.mint.MintService.mint_child`, exactly what
41
+ `examples/two_agent_delegation.py` does before wiring the plugin.
42
+
43
+ ## Why a plugin, not a per-agent callback
44
+
45
+ ADK's `BaseAgent`/`LlmAgent` accept `before_agent_callback` and
46
+ `before_tool_callback` directly, and that is the hook an ADK adapter built
47
+ in June 2026 (`forge-agents` commit `85fdaa8`, `synthera` commit `dd0503c`)
48
+ used for its governed-delegation gate: `before_agent_callback` closing over
49
+ one sub-agent's `mint_child` call. That shape, enforce before an agent
50
+ runs and before a tool is called, is reused here, but `vaid-adk` registers
51
+ through `BasePlugin` instead of per-agent fields, for one reason: a
52
+ per-agent callback is opt-in per agent, so an agent constructed without one
53
+ runs ungoverned by omission. A `BasePlugin` is registered once on the
54
+ `Runner`/`App`, and ADK's `plugin_manager` consults it for every agent and
55
+ every tool call in the invocation tree, including a sub-agent added after
56
+ the gate was wired, and including ADK's own native delegation
57
+ (`transfer_to_agent` / `sub_agents`) with no further code at the call site.
58
+
59
+ Confirmed directly against the ADK source (`google-adk==2.11.0`):
60
+
61
+ - `BaseAgent._run_with_lifecycle` calls `self._handle_before_agent_callback(ctx)`
62
+ on **every** `run_async`, which runs `ctx.plugin_manager.run_before_agent_callback`
63
+ before the agent's own canonical callbacks; returning `Content` sets
64
+ `ctx.end_invocation`, skipping the agent entirely, including a sub-agent
65
+ reached through ADK's native delegation loop.
66
+ - `_caller.py`'s `_call_tool_async` runs
67
+ `invocation_context.plugin_manager.run_before_tool_callback(tool=tool,
68
+ tool_args=function_args, tool_context=tool_context)` before any canonical
69
+ callback; a non-`None` return becomes the tool's function response
70
+ directly, and the tool itself is never called.
71
+
72
+ ## Usage
73
+
74
+ ```python
75
+ from vaid_adk import (
76
+ ToolAuthorization,
77
+ TrustedIssuers,
78
+ VaidAuthPlugin,
79
+ VaidPresentation,
80
+ )
81
+
82
+ plugin = VaidAuthPlugin(
83
+ # Each trusted kernel key is bound to the ONE trust domain it is trusted
84
+ # for. Read from an `AgentCard`-shaped config instead with
85
+ # `TrustedIssuers([{"trustDomain": ..., "kernelPublicKey": "<base64url>"}])`.
86
+ keys=TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)]),
87
+ revocation=my_revocation_check, # see "Production note" below
88
+ tool_authorizations={
89
+ "lookup_order": ToolAuthorization(capability="read", resource="data.orders"),
90
+ "issue_refund": ToolAuthorization(capability="write", resource="data.payments"),
91
+ },
92
+ )
93
+
94
+ # Register the VAID each agent presents: its own signed document (`leaf`)
95
+ # plus the ancestor chain back to a trusted root (`chain`), not including
96
+ # the leaf itself.
97
+ plugin.register("coordinator", VaidPresentation(leaf=root_vaid))
98
+ plugin.register("worker", VaidPresentation(leaf=worker_child_vaid, chain=(root_vaid,)))
99
+
100
+ # runner = Runner(..., plugins=[plugin])
101
+ ```
102
+
103
+ A tool with no entry in `tool_authorizations` is **denied**, never allowed
104
+ by omission. An agent with no registered presentation is denied the same
105
+ way: it is indistinguishable from one that was simply never given an
106
+ identity.
107
+
108
+ ## Run the example
109
+
110
+ ```
111
+ pip install -e python/vaid-mint
112
+ pip install -e python/vaid-adk[test]
113
+ python python/vaid-adk/examples/two_agent_delegation.py
114
+ ```
115
+
116
+ A coordinator mints an attenuated child VAID for a worker sub-agent (scope
117
+ `data.orders` only, capability `read` only, narrower than the coordinator's
118
+ own `data.orders` + `data.payments`, `read` + `write`), registers both
119
+ agents' presentations with the plugin, and runs them through a real ADK
120
+ `Runner`. The worker then attempts two tool calls:
121
+
122
+ ```
123
+ [coordinator] delegating to worker
124
+ [worker] lookup_order: ALLOWED
125
+ [worker] issue_refund: DENIED (out_of_scope)
126
+ ```
127
+
128
+ `lookup_order` is within the worker's attenuated `data.orders`/`read`
129
+ bounds; `issue_refund` needs `data.payments`/`write`, which the worker's
130
+ VAID never held, so the plugin denies it before the tool runs.
131
+
132
+ ## Trust configuration: a key is trusted for one domain
133
+
134
+ `keys` is a `TrustedIssuers`, and each kernel key in it is bound to the **one
135
+ trust domain it is trusted for**. A document signed by a trusted key is
136
+ accepted only if it also claims that key's domain.
137
+
138
+ Binding matters because without it a trusted key vouches for any domain a
139
+ document cares to claim. Given two legitimately trusted issuers, A and B, a
140
+ document signed by B's key while claiming A's `trust_domain` verifies
141
+ perfectly against B's key and was accepted: B's key vouching for A's
142
+ identity, with no forged signature and no compromised key anywhere in the
143
+ attack (GHSA-hfjh-v2x2-r58v). A key is trusted for the domain it is paired
144
+ with, never for a domain a document asserts.
145
+
146
+ Two spellings of the same configuration:
147
+
148
+ ```python
149
+ # From an AgentCard-shaped config: the VAID A2A extension's `trustedIssuers`
150
+ # shape (docs/a2a/v1/extension.md section 2.1), so one agent's trust
151
+ # configuration reads the same whether it serves A2A or runs under ADK.
152
+ keys = TrustedIssuers([
153
+ {"trustDomain": "vaid.example", "kernelPublicKey": "IVL40Zt5HSRF..."},
154
+ {"trustDomain": "other.example", "kernelPublicKey": "My6-jSfLcyOz..."},
155
+ ])
156
+
157
+ # From key bytes already held in the process (a local issuer, a key just
158
+ # fetched from a KMS).
159
+ keys = TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)])
160
+ ```
161
+
162
+ `kernelPublicKey` is decoded as **strict unpadded base64url to exactly 32
163
+ bytes**. A padded, non-canonical, out-of-alphabet, empty or wrong-length
164
+ value raises `TrustedIssuersConfigError` rather than being repaired or
165
+ skipped, as do a non-object entry, an entry with no `trustDomain`, and the
166
+ same key listed under two domains. One domain may hold several keys (an
167
+ issuer rotating); one key may not hold two domains.
168
+
169
+ A bare `vaid_mint.chain.KernelKeyMap` or `SingleKernelKey` is **not** accepted
170
+ as `keys`. Those resolve a key by thumbprint and cannot say which domain it is
171
+ trusted for, so the binding could not be checked at all; `VaidAuthPlugin`
172
+ refuses one in its constructor, so the gap cannot be left open by omission.
173
+ Any resolver answering both `resolve_key` and `trust_domain_for` is accepted.
174
+
175
+ ## What this package checks, in order
176
+
177
+ 1. The leaf VAID's own trust-domain binding — its claimed `trust_domain`
178
+ equals the domain its `kernel_key_thumbprint` is bound to in `keys` — and
179
+ only then its kernel signature, against the key that thumbprint resolved
180
+ to. The binding is checked first so an unrecognised or misbound issuer
181
+ (`issuer_mismatch`) stays distinguishable from a correctly-bound issuer
182
+ whose signature does not verify (`inauthentic`).
183
+ 2. The leaf's own `expires_at`.
184
+ 3. Each presented ancestor's own trust-domain binding, by the same rule and
185
+ reporting the same code, and then the presented chain's integrity and
186
+ attenuation back to a trusted root (`vaid_mint.chain.verify_chain_at`):
187
+ scope, capabilities, tenant and expiry contained at every hop.
188
+ 4. The specific tool call's mapped `resource`/`capability` against the
189
+ leaf's own `scope_boundary`/`capability_set`.
190
+ 5. Revocation of every VAID in the assembled lineage, via the
191
+ `RevocationCheck` passed to `VaidAuthPlugin`.
192
+
193
+ The order is load-bearing, not incidental. Step 1 reaches its verdict on the
194
+ leaf alone before step 3 examines any presented document, so a leaf whose own
195
+ signature does not verify is `inauthentic` regardless of what else was
196
+ presented alongside it — a defect found in the chain never pre-empts a verdict
197
+ step 1 would already have reached.
198
+
199
+ Any step that cannot be positively confirmed denies the call. "Could not
200
+ determine" (an unassembled chain, an unavailable revocation store) is never
201
+ read as "not revoked"; see `vaid_mint.revocation.RevocationStatus` and
202
+ `vaid_mint.verify.VaidVerdict`, whose fail-closed rule this package inherits
203
+ rather than re-deciding.
204
+
205
+ ## Production note: wire a real RevocationCheck
206
+
207
+ `VaidAuthPlugin` takes `revocation` as a **required** constructor argument
208
+ with no default. The example above uses
209
+ `InMemoryRevocationList.assume_nothing_revoked()` only because it has no
210
+ production revocation store to call. That store is non-durable and fails
211
+ *open* after a restart (a VAID revoked before the restart verifies clean
212
+ again), which is fine for a demo and wrong for anything that must survive
213
+ one. For production, inject a durable `RevocationCheck` backed by your own
214
+ revoked-credential store (see `vaid_mint.revocation.RevocationBackend` for
215
+ the reference shape: both the revoked-set half and the lineage-resolver half
216
+ durable together, never one without the other) and let it report
217
+ `RevocationStatus.UNAVAILABLE` whenever it cannot be reached; this plugin
218
+ already denies on that outcome.
219
+
220
+ ## What this package does not do
221
+
222
+ - **It does not mint.** Issuing a root VAID or an attenuated child VAID for
223
+ a sub-agent is ordinary `vaid_mint` usage (`MintService.mint_root` /
224
+ `mint_child`), done by the caller before `VaidAuthPlugin.register` is
225
+ called.
226
+ - **It does not depend on `forge-agents` or `synthera`.** The June 2026 ADK
227
+ adapters in those repos are prior art for the hook choice; this package
228
+ reuses only `vaid_mint`.
229
+ - **It does not depend on `vaid-a2a`.** The verification order in
230
+ `vaid_adk._verify` mirrors `vaid_a2a.verifier.verify_a2a_message`'s order
231
+ deliberately (both are "how do you check a presented VAID end to end"),
232
+ but each is self-contained against `vaid_mint` rather than one importing
233
+ the other.
234
+
235
+ ## Packaged conformance firewall
236
+
237
+ An installed copy can be checked without this repository:
238
+
239
+ ```
240
+ vaid-adk-conformance # exit 0 = PASS, 1 = BLOCKER
241
+ python -m vaid_adk.conformance # same
242
+ ```
243
+
244
+ It verifies the frozen three-hop delegation chain bundled in `vaid-mint`
245
+ (`chain_v1.json`), at the instant that vector names rather than the wall clock,
246
+ and asserts eight outcomes: one that must be **allowed**, and seven that must
247
+ each be **denied with a different code** (cross-domain key, out-of-scope
248
+ resource, missing capability, no VAID, withheld ancestry, revoked ancestor,
249
+ revocation unavailable). It also asserts that a trust resolver which cannot bind
250
+ a domain to a key is refused outright, which is the half of
251
+ GHSA-hfjh-v2x2-r58v no single request can demonstrate.
252
+
253
+ The allowed case is load-bearing: without it, a build that denied everything
254
+ would satisfy every other check. It pins **verdicts**, not bytes - this package
255
+ signs nothing of its own.
256
+
257
+ ## Install
258
+
259
+ **Local dev only**, from a repo checkout:
260
+
261
+ ```
262
+ pip install -e python/vaid-pop
263
+ pip install -e python/vaid-mint
264
+ pip install -e python/vaid-adk[test]
265
+ ```
@@ -0,0 +1,239 @@
1
+ # vaid-adk
2
+
3
+ VAID verification for Google's Agent Development Kit (ADK): a `BasePlugin`
4
+ that checks a presented VAID's signature, expiry, delegation-chain
5
+ attenuation and revocation status before an agent runs, before it is
6
+ delegated to as a sub-agent, and before it calls a tool. It denies
7
+ fail-closed whenever any of that cannot be positively confirmed.
8
+
9
+ ADK has no built-in mechanism for this (google/adk-python#4992, #6551). This
10
+ package adds it for an agent that already carries a VAID
11
+ (https://github.com/solara-associates/vaid), using only `vaid_mint`'s
12
+ existing signature, chain and revocation primitives. It does not mint
13
+ credentials; minting an attenuated child VAID for a sub-agent is the
14
+ caller's job, via `vaid_mint.mint.MintService.mint_child`, exactly what
15
+ `examples/two_agent_delegation.py` does before wiring the plugin.
16
+
17
+ ## Why a plugin, not a per-agent callback
18
+
19
+ ADK's `BaseAgent`/`LlmAgent` accept `before_agent_callback` and
20
+ `before_tool_callback` directly, and that is the hook an ADK adapter built
21
+ in June 2026 (`forge-agents` commit `85fdaa8`, `synthera` commit `dd0503c`)
22
+ used for its governed-delegation gate: `before_agent_callback` closing over
23
+ one sub-agent's `mint_child` call. That shape, enforce before an agent
24
+ runs and before a tool is called, is reused here, but `vaid-adk` registers
25
+ through `BasePlugin` instead of per-agent fields, for one reason: a
26
+ per-agent callback is opt-in per agent, so an agent constructed without one
27
+ runs ungoverned by omission. A `BasePlugin` is registered once on the
28
+ `Runner`/`App`, and ADK's `plugin_manager` consults it for every agent and
29
+ every tool call in the invocation tree, including a sub-agent added after
30
+ the gate was wired, and including ADK's own native delegation
31
+ (`transfer_to_agent` / `sub_agents`) with no further code at the call site.
32
+
33
+ Confirmed directly against the ADK source (`google-adk==2.11.0`):
34
+
35
+ - `BaseAgent._run_with_lifecycle` calls `self._handle_before_agent_callback(ctx)`
36
+ on **every** `run_async`, which runs `ctx.plugin_manager.run_before_agent_callback`
37
+ before the agent's own canonical callbacks; returning `Content` sets
38
+ `ctx.end_invocation`, skipping the agent entirely, including a sub-agent
39
+ reached through ADK's native delegation loop.
40
+ - `_caller.py`'s `_call_tool_async` runs
41
+ `invocation_context.plugin_manager.run_before_tool_callback(tool=tool,
42
+ tool_args=function_args, tool_context=tool_context)` before any canonical
43
+ callback; a non-`None` return becomes the tool's function response
44
+ directly, and the tool itself is never called.
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from vaid_adk import (
50
+ ToolAuthorization,
51
+ TrustedIssuers,
52
+ VaidAuthPlugin,
53
+ VaidPresentation,
54
+ )
55
+
56
+ plugin = VaidAuthPlugin(
57
+ # Each trusted kernel key is bound to the ONE trust domain it is trusted
58
+ # for. Read from an `AgentCard`-shaped config instead with
59
+ # `TrustedIssuers([{"trustDomain": ..., "kernelPublicKey": "<base64url>"}])`.
60
+ keys=TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)]),
61
+ revocation=my_revocation_check, # see "Production note" below
62
+ tool_authorizations={
63
+ "lookup_order": ToolAuthorization(capability="read", resource="data.orders"),
64
+ "issue_refund": ToolAuthorization(capability="write", resource="data.payments"),
65
+ },
66
+ )
67
+
68
+ # Register the VAID each agent presents: its own signed document (`leaf`)
69
+ # plus the ancestor chain back to a trusted root (`chain`), not including
70
+ # the leaf itself.
71
+ plugin.register("coordinator", VaidPresentation(leaf=root_vaid))
72
+ plugin.register("worker", VaidPresentation(leaf=worker_child_vaid, chain=(root_vaid,)))
73
+
74
+ # runner = Runner(..., plugins=[plugin])
75
+ ```
76
+
77
+ A tool with no entry in `tool_authorizations` is **denied**, never allowed
78
+ by omission. An agent with no registered presentation is denied the same
79
+ way: it is indistinguishable from one that was simply never given an
80
+ identity.
81
+
82
+ ## Run the example
83
+
84
+ ```
85
+ pip install -e python/vaid-mint
86
+ pip install -e python/vaid-adk[test]
87
+ python python/vaid-adk/examples/two_agent_delegation.py
88
+ ```
89
+
90
+ A coordinator mints an attenuated child VAID for a worker sub-agent (scope
91
+ `data.orders` only, capability `read` only, narrower than the coordinator's
92
+ own `data.orders` + `data.payments`, `read` + `write`), registers both
93
+ agents' presentations with the plugin, and runs them through a real ADK
94
+ `Runner`. The worker then attempts two tool calls:
95
+
96
+ ```
97
+ [coordinator] delegating to worker
98
+ [worker] lookup_order: ALLOWED
99
+ [worker] issue_refund: DENIED (out_of_scope)
100
+ ```
101
+
102
+ `lookup_order` is within the worker's attenuated `data.orders`/`read`
103
+ bounds; `issue_refund` needs `data.payments`/`write`, which the worker's
104
+ VAID never held, so the plugin denies it before the tool runs.
105
+
106
+ ## Trust configuration: a key is trusted for one domain
107
+
108
+ `keys` is a `TrustedIssuers`, and each kernel key in it is bound to the **one
109
+ trust domain it is trusted for**. A document signed by a trusted key is
110
+ accepted only if it also claims that key's domain.
111
+
112
+ Binding matters because without it a trusted key vouches for any domain a
113
+ document cares to claim. Given two legitimately trusted issuers, A and B, a
114
+ document signed by B's key while claiming A's `trust_domain` verifies
115
+ perfectly against B's key and was accepted: B's key vouching for A's
116
+ identity, with no forged signature and no compromised key anywhere in the
117
+ attack (GHSA-hfjh-v2x2-r58v). A key is trusted for the domain it is paired
118
+ with, never for a domain a document asserts.
119
+
120
+ Two spellings of the same configuration:
121
+
122
+ ```python
123
+ # From an AgentCard-shaped config: the VAID A2A extension's `trustedIssuers`
124
+ # shape (docs/a2a/v1/extension.md section 2.1), so one agent's trust
125
+ # configuration reads the same whether it serves A2A or runs under ADK.
126
+ keys = TrustedIssuers([
127
+ {"trustDomain": "vaid.example", "kernelPublicKey": "IVL40Zt5HSRF..."},
128
+ {"trustDomain": "other.example", "kernelPublicKey": "My6-jSfLcyOz..."},
129
+ ])
130
+
131
+ # From key bytes already held in the process (a local issuer, a key just
132
+ # fetched from a KMS).
133
+ keys = TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)])
134
+ ```
135
+
136
+ `kernelPublicKey` is decoded as **strict unpadded base64url to exactly 32
137
+ bytes**. A padded, non-canonical, out-of-alphabet, empty or wrong-length
138
+ value raises `TrustedIssuersConfigError` rather than being repaired or
139
+ skipped, as do a non-object entry, an entry with no `trustDomain`, and the
140
+ same key listed under two domains. One domain may hold several keys (an
141
+ issuer rotating); one key may not hold two domains.
142
+
143
+ A bare `vaid_mint.chain.KernelKeyMap` or `SingleKernelKey` is **not** accepted
144
+ as `keys`. Those resolve a key by thumbprint and cannot say which domain it is
145
+ trusted for, so the binding could not be checked at all; `VaidAuthPlugin`
146
+ refuses one in its constructor, so the gap cannot be left open by omission.
147
+ Any resolver answering both `resolve_key` and `trust_domain_for` is accepted.
148
+
149
+ ## What this package checks, in order
150
+
151
+ 1. The leaf VAID's own trust-domain binding — its claimed `trust_domain`
152
+ equals the domain its `kernel_key_thumbprint` is bound to in `keys` — and
153
+ only then its kernel signature, against the key that thumbprint resolved
154
+ to. The binding is checked first so an unrecognised or misbound issuer
155
+ (`issuer_mismatch`) stays distinguishable from a correctly-bound issuer
156
+ whose signature does not verify (`inauthentic`).
157
+ 2. The leaf's own `expires_at`.
158
+ 3. Each presented ancestor's own trust-domain binding, by the same rule and
159
+ reporting the same code, and then the presented chain's integrity and
160
+ attenuation back to a trusted root (`vaid_mint.chain.verify_chain_at`):
161
+ scope, capabilities, tenant and expiry contained at every hop.
162
+ 4. The specific tool call's mapped `resource`/`capability` against the
163
+ leaf's own `scope_boundary`/`capability_set`.
164
+ 5. Revocation of every VAID in the assembled lineage, via the
165
+ `RevocationCheck` passed to `VaidAuthPlugin`.
166
+
167
+ The order is load-bearing, not incidental. Step 1 reaches its verdict on the
168
+ leaf alone before step 3 examines any presented document, so a leaf whose own
169
+ signature does not verify is `inauthentic` regardless of what else was
170
+ presented alongside it — a defect found in the chain never pre-empts a verdict
171
+ step 1 would already have reached.
172
+
173
+ Any step that cannot be positively confirmed denies the call. "Could not
174
+ determine" (an unassembled chain, an unavailable revocation store) is never
175
+ read as "not revoked"; see `vaid_mint.revocation.RevocationStatus` and
176
+ `vaid_mint.verify.VaidVerdict`, whose fail-closed rule this package inherits
177
+ rather than re-deciding.
178
+
179
+ ## Production note: wire a real RevocationCheck
180
+
181
+ `VaidAuthPlugin` takes `revocation` as a **required** constructor argument
182
+ with no default. The example above uses
183
+ `InMemoryRevocationList.assume_nothing_revoked()` only because it has no
184
+ production revocation store to call. That store is non-durable and fails
185
+ *open* after a restart (a VAID revoked before the restart verifies clean
186
+ again), which is fine for a demo and wrong for anything that must survive
187
+ one. For production, inject a durable `RevocationCheck` backed by your own
188
+ revoked-credential store (see `vaid_mint.revocation.RevocationBackend` for
189
+ the reference shape: both the revoked-set half and the lineage-resolver half
190
+ durable together, never one without the other) and let it report
191
+ `RevocationStatus.UNAVAILABLE` whenever it cannot be reached; this plugin
192
+ already denies on that outcome.
193
+
194
+ ## What this package does not do
195
+
196
+ - **It does not mint.** Issuing a root VAID or an attenuated child VAID for
197
+ a sub-agent is ordinary `vaid_mint` usage (`MintService.mint_root` /
198
+ `mint_child`), done by the caller before `VaidAuthPlugin.register` is
199
+ called.
200
+ - **It does not depend on `forge-agents` or `synthera`.** The June 2026 ADK
201
+ adapters in those repos are prior art for the hook choice; this package
202
+ reuses only `vaid_mint`.
203
+ - **It does not depend on `vaid-a2a`.** The verification order in
204
+ `vaid_adk._verify` mirrors `vaid_a2a.verifier.verify_a2a_message`'s order
205
+ deliberately (both are "how do you check a presented VAID end to end"),
206
+ but each is self-contained against `vaid_mint` rather than one importing
207
+ the other.
208
+
209
+ ## Packaged conformance firewall
210
+
211
+ An installed copy can be checked without this repository:
212
+
213
+ ```
214
+ vaid-adk-conformance # exit 0 = PASS, 1 = BLOCKER
215
+ python -m vaid_adk.conformance # same
216
+ ```
217
+
218
+ It verifies the frozen three-hop delegation chain bundled in `vaid-mint`
219
+ (`chain_v1.json`), at the instant that vector names rather than the wall clock,
220
+ and asserts eight outcomes: one that must be **allowed**, and seven that must
221
+ each be **denied with a different code** (cross-domain key, out-of-scope
222
+ resource, missing capability, no VAID, withheld ancestry, revoked ancestor,
223
+ revocation unavailable). It also asserts that a trust resolver which cannot bind
224
+ a domain to a key is refused outright, which is the half of
225
+ GHSA-hfjh-v2x2-r58v no single request can demonstrate.
226
+
227
+ The allowed case is load-bearing: without it, a build that denied everything
228
+ would satisfy every other check. It pins **verdicts**, not bytes - this package
229
+ signs nothing of its own.
230
+
231
+ ## Install
232
+
233
+ **Local dev only**, from a repo checkout:
234
+
235
+ ```
236
+ pip install -e python/vaid-pop
237
+ pip install -e python/vaid-mint
238
+ pip install -e python/vaid-adk[test]
239
+ ```