Skip to main content

vaid-adk

VAID verification for Google's Agent Development Kit (ADK): a BasePlugin that checks a presented VAID's signature, expiry, delegation-chain attenuation and revocation status before an agent runs, before it is delegated to as a sub-agent, and before it calls a tool. It denies fail-closed whenever any of that cannot be positively confirmed.

ADK has no built-in mechanism for this (google/adk-python#4992, #6551). This package adds it for an agent that already carries a VAID (https://github.com/solara-associates/vaid), using only vaid_mint's existing signature, chain and revocation primitives. It does not mint credentials; minting an attenuated child VAID for a sub-agent is the caller's job, via vaid_mint.mint.MintService.mint_child, exactly what examples/two_agent_delegation.py does before wiring the plugin.

Why a plugin, not a per-agent callback

ADK's BaseAgent/LlmAgent accept before_agent_callback and before_tool_callback directly, and that is the hook an ADK adapter built in June 2026 (forge-agents commit 85fdaa8, synthera commit dd0503c) used for its governed-delegation gate: before_agent_callback closing over one sub-agent's mint_child call. That shape, enforce before an agent runs and before a tool is called, is reused here, but vaid-adk registers through BasePlugin instead of per-agent fields, for one reason: a per-agent callback is opt-in per agent, so an agent constructed without one runs ungoverned by omission. A BasePlugin is registered once on the Runner/App, and ADK's plugin_manager consults it for every agent and every tool call in the invocation tree, including a sub-agent added after the gate was wired, and including ADK's own native delegation (transfer_to_agent / sub_agents) with no further code at the call site.

Confirmed directly against the ADK source (google-adk==2.11.0):

  • BaseAgent._run_with_lifecycle calls self._handle_before_agent_callback(ctx) on every run_async, which runs ctx.plugin_manager.run_before_agent_callback before the agent's own canonical callbacks; returning Content sets ctx.end_invocation, skipping the agent entirely, including a sub-agent reached through ADK's native delegation loop.
  • _caller.py's _call_tool_async runs invocation_context.plugin_manager.run_before_tool_callback(tool=tool, tool_args=function_args, tool_context=tool_context) before any canonical callback; a non-None return becomes the tool's function response directly, and the tool itself is never called.

Usage

from vaid_adk import (
    ToolAuthorization,
    TrustedIssuers,
    VaidAuthPlugin,
    VaidPresentation,
)

plugin = VaidAuthPlugin(
    # Each trusted kernel key is bound to the ONE trust domain it is trusted
    # for. Read from an `AgentCard`-shaped config instead with
    # `TrustedIssuers([{"trustDomain": ..., "kernelPublicKey": "<base64url>"}])`.
    keys=TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)]),
    revocation=my_revocation_check,                   # see "Production note" below
    tool_authorizations={
        "lookup_order": ToolAuthorization(capability="read", resource="data.orders"),
        "issue_refund": ToolAuthorization(capability="write", resource="data.payments"),
    },
)

# Register the VAID each agent presents: its own signed document (`leaf`)
# plus the ancestor chain back to a trusted root (`chain`), not including
# the leaf itself.
plugin.register("coordinator", VaidPresentation(leaf=root_vaid))
plugin.register("worker", VaidPresentation(leaf=worker_child_vaid, chain=(root_vaid,)))

# runner = Runner(..., plugins=[plugin])

A tool with no entry in tool_authorizations is denied, never allowed by omission. An agent with no registered presentation is denied the same way: it is indistinguishable from one that was simply never given an identity.

Run the example

pip install -e python/vaid-mint
pip install -e python/vaid-adk[test]
python python/vaid-adk/examples/two_agent_delegation.py

A coordinator mints an attenuated child VAID for a worker sub-agent (scope data.orders only, capability read only, narrower than the coordinator's own data.orders + data.payments, read + write), registers both agents' presentations with the plugin, and runs them through a real ADK Runner. The worker then attempts two tool calls:

[coordinator] delegating to worker
[worker] lookup_order: ALLOWED
[worker] issue_refund: DENIED (out_of_scope)

lookup_order is within the worker's attenuated data.orders/read bounds; issue_refund needs data.payments/write, which the worker's VAID never held, so the plugin denies it before the tool runs.

Trust configuration: a key is trusted for one domain

keys is a TrustedIssuers, and each kernel key in it is bound to the one trust domain it is trusted for. A document signed by a trusted key is accepted only if it also claims that key's domain.

Binding matters because without it a trusted key vouches for any domain a document cares to claim. Given two legitimately trusted issuers, A and B, a document signed by B's key while claiming A's trust_domain verifies perfectly against B's key and was accepted: B's key vouching for A's identity, with no forged signature and no compromised key anywhere in the attack (GHSA-hfjh-v2x2-r58v). A key is trusted for the domain it is paired with, never for a domain a document asserts.

Two spellings of the same configuration:

# From an AgentCard-shaped config: the VAID A2A extension's `trustedIssuers`
# shape (docs/a2a/v1/extension.md section 2.1), so one agent's trust
# configuration reads the same whether it serves A2A or runs under ADK.
keys = TrustedIssuers([
    {"trustDomain": "vaid.example",  "kernelPublicKey": "IVL40Zt5HSRF..."},
    {"trustDomain": "other.example", "kernelPublicKey": "My6-jSfLcyOz..."},
])

# From key bytes already held in the process (a local issuer, a key just
# fetched from a KMS).
keys = TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)])

kernelPublicKey is decoded as strict unpadded base64url to exactly 32 bytes. A padded, non-canonical, out-of-alphabet, empty or wrong-length value raises TrustedIssuersConfigError rather than being repaired or skipped, as do a non-object entry, an entry with no trustDomain, and the same key listed under two domains. One domain may hold several keys (an issuer rotating); one key may not hold two domains.

A bare vaid_mint.chain.KernelKeyMap or SingleKernelKey is not accepted as keys. Those resolve a key by thumbprint and cannot say which domain it is trusted for, so the binding could not be checked at all; VaidAuthPlugin refuses one in its constructor, so the gap cannot be left open by omission. Any resolver answering both resolve_key and trust_domain_for is accepted.

What this package checks, in order

  1. The leaf VAID's own trust-domain binding — its claimed trust_domain equals the domain its kernel_key_thumbprint is bound to in keys — and only then its kernel signature, against the key that thumbprint resolved to. The binding is checked first so an unrecognised or misbound issuer (issuer_mismatch) stays distinguishable from a correctly-bound issuer whose signature does not verify (inauthentic).
  2. The leaf's own expires_at.
  3. Each presented ancestor's own trust-domain binding, by the same rule and reporting the same code, and then the presented chain's integrity and attenuation back to a trusted root (vaid_mint.chain.verify_chain_at): scope, capabilities, tenant and expiry contained at every hop.
  4. The specific tool call's mapped resource/capability against the leaf's own scope_boundary/capability_set.
  5. Revocation of every VAID in the assembled lineage, via the RevocationCheck passed to VaidAuthPlugin.

The order is load-bearing, not incidental. Step 1 reaches its verdict on the leaf alone before step 3 examines any presented document, so a leaf whose own signature does not verify is inauthentic regardless of what else was presented alongside it — a defect found in the chain never pre-empts a verdict step 1 would already have reached.

Any step that cannot be positively confirmed denies the call. "Could not determine" (an unassembled chain, an unavailable revocation store) is never read as "not revoked"; see vaid_mint.revocation.RevocationStatus and vaid_mint.verify.VaidVerdict, whose fail-closed rule this package inherits rather than re-deciding.

Production note: wire a real RevocationCheck

VaidAuthPlugin takes revocation as a required constructor argument with no default. The example above uses InMemoryRevocationList.assume_nothing_revoked() only because it has no production revocation store to call. That store is non-durable and fails open after a restart (a VAID revoked before the restart verifies clean again), which is fine for a demo and wrong for anything that must survive one. For production, inject a durable RevocationCheck backed by your own revoked-credential store (see vaid_mint.revocation.RevocationBackend for the reference shape: both the revoked-set half and the lineage-resolver half durable together, never one without the other) and let it report RevocationStatus.UNAVAILABLE whenever it cannot be reached; this plugin already denies on that outcome.

What this package does not do

  • It does not mint. Issuing a root VAID or an attenuated child VAID for a sub-agent is ordinary vaid_mint usage (MintService.mint_root / mint_child), done by the caller before VaidAuthPlugin.register is called.
  • It does not depend on forge-agents or synthera. The June 2026 ADK adapters in those repos are prior art for the hook choice; this package reuses only vaid_mint.
  • It does not depend on vaid-a2a. The verification order in vaid_adk._verify mirrors vaid_a2a.verifier.verify_a2a_message's order deliberately (both are "how do you check a presented VAID end to end"), but each is self-contained against vaid_mint rather than one importing the other.

Packaged conformance firewall

An installed copy can be checked without this repository:

vaid-adk-conformance           # exit 0 = PASS, 1 = BLOCKER
python -m vaid_adk.conformance  # same

It verifies the frozen three-hop delegation chain bundled in vaid-mint (chain_v1.json), at the instant that vector names rather than the wall clock, and asserts eight outcomes: one that must be allowed, and seven that must each be denied with a different code (cross-domain key, out-of-scope resource, missing capability, no VAID, withheld ancestry, revoked ancestor, revocation unavailable). It also asserts that a trust resolver which cannot bind a domain to a key is refused outright, which is the half of GHSA-hfjh-v2x2-r58v no single request can demonstrate.

The allowed case is load-bearing: without it, a build that denied everything would satisfy every other check. It pins verdicts, not bytes - this package signs nothing of its own.

Install

Local dev only, from a repo checkout:

pip install -e python/vaid-pop
pip install -e python/vaid-mint
pip install -e python/vaid-adk[test]

Metadata

Release files for vaid-adk 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vaid-adk 0.2.1
File Size Uploaded Provenance
vaid_adk-0.2.1.tar.gz 31.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for vaid-adk 0.2.1
File Interpreter ABI Platform
vaid_adk-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 57.2 kB

Release files / vaid_adk-0.2.1.tar.gz

Download URL vaid_adk-0.2.1.tar.gz
Size 31.9 kB
Tags Source
SHA-256 checksum
How to use checksums
a997292a16275eedac7e9358f0c0f55945439a18fe95c5eca093838d0599d937
BLAKE2b-256 checksum
How to use checksums
d0f8c82d672cfd1ab7f07e724b197d3f8bd279c056ae1ef1eb45e3f7140b76aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.15

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / vaid_adk-0.2.1-py3-none-any.whl

Download URL vaid_adk-0.2.1-py3-none-any.whl
Size 25.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
779c1733724ae8ae2aa8f8dfa3c5247d0ec36066bd506bb99759ad75f50bba6a
BLAKE2b-256 checksum
How to use checksums
a6d6a5a6dece14e18bf6483f57ad5056b6cb58d458f4627ee01ea777b8eea1d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.15

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for vaid-adk 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for vaid_adk-0.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page