diff --git a/contributing/samples/vaid_identity/README.md b/contributing/samples/vaid_identity/README.md new file mode 100644 index 0000000..89f1a31 --- /dev/null +++ b/contributing/samples/vaid_identity/README.md @@ -0,0 +1,93 @@ +# VAID identity plugin for Google ADK + +An ADK plugin that verifies each agent's signed identity (VAID) and +delegation chain before the agent runs, before it is delegated to as a +sub-agent, and before every tool call, using +[vaid-adk](https://github.com/solara-associates/vaid/tree/main/python/vaid-adk) +(Apache-2.0 licensed). + +## Install + +```bash +pip install google-adk-community[vaid] +``` + +## Usage + +```python +from google.adk.agents import Agent +from google.adk.runners import Runner +from google.adk_community.plugins import VaidIdentityPlugin +from vaid_adk import ToolAuthorization, TrustedIssuers, VaidPresentation + +plugin = VaidIdentityPlugin( + keys=TrustedIssuers.from_raw_keys([("vaid.example", issuer_kernel_public_key)]), + revocation=my_revocation_check, + tool_authorizations={"lookup_order": ToolAuthorization(capability="read")}, +) +plugin.register("sub_agent", VaidPresentation(leaf=child_vaid, chain=(parent_vaid,))) + +agent = Agent(name="worker", model="gemini-2.0-flash", tools=[my_tool]) +runner = Runner(agent=agent, plugins=[plugin], app_name="my-app") +``` + +## How it works + +Each identity is a signed document (VAID) that can only narrow as it is +delegated (scope, capabilities, expiry, tenant), verified offline with a +public key, with pluggable revocation. `VaidIdentityPlugin` is a thin +wrapper over `vaid_adk.VaidAuthPlugin`: it delegates `before_agent_callback` +and `before_tool_callback` to it and performs no verification itself. + +Registered once on the `Runner`, it covers every agent and tool in the +invocation tree, including sub-agents reached through ADK's native +delegation (`transfer_to_agent` / `sub_agents`), with no further code at the +call site. + +## Fail-closed by default + +- An agent with no registered presentation is denied, never treated as + exempt. +- A tool with no entry in `tool_authorizations` is denied, never allowed by + omission. +- `keys` and `revocation` are required constructor arguments; there is no + default trust configuration and no fail-open revocation posture. + +## This sample + +`main.py` mints a root VAID for a `coordinator` agent and an attenuated +child VAID for a `worker` sub-agent (scoped to `data.orders`, capability +`read`; the coordinator's own VAID also covers `data.payments` and +`write`). It registers both with `VaidIdentityPlugin` and drives them +through a real `Runner`: + +- `worker` calls `lookup_order` against `data.orders`: **ALLOWED**, in + scope and capability. +- `worker` calls `issue_refund` against `data.payments`: **DENIED + (out_of_scope)**, before the tool itself runs — the attenuated child VAID + never carried `data.payments`. + +The two agents are plain `BaseAgent` subclasses that act without an LLM, so +the sample runs with no model or API key, while still exercising +enforcement through the genuine ADK extension points (the real +`plugin_manager`, not a mock of it). + +Run: + +```bash +python contributing/samples/vaid_identity/main.py +``` + +Expected output: + +``` +[coordinator] delegating to worker +[worker] lookup_order: ALLOWED +[worker] issue_refund: DENIED (out_of_scope) +``` + +## Links + +- [vaid-adk on PyPI](https://pypi.org/project/vaid-adk/) +- [vaid-adk source, tests and a two-agent example](https://github.com/solara-associates/vaid/tree/main/python/vaid-adk) +- [ADK Plugin docs](https://google.github.io/adk-docs/plugins/) diff --git a/contributing/samples/vaid_identity/main.py b/contributing/samples/vaid_identity/main.py new file mode 100644 index 0000000..e8176d1 --- /dev/null +++ b/contributing/samples/vaid_identity/main.py @@ -0,0 +1,205 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Example: a coordinator ADK agent delegates to a worker holding an +attenuated VAID, driven through a real ADK Runner with VaidIdentityPlugin +registered. + +Offline: no model/API key. The two agents are plain BaseAgent subclasses +that act without an LLM, so this proves the plugin's enforcement through +the genuine ADK extension points (before_tool_callback via the real +plugin manager) rather than through a mock of them. + +Run: python contributing/samples/vaid_identity/main.py +""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from google.adk.agents import BaseAgent +from google.adk.agents.invocation_context import InvocationContext +from google.adk.events import Event +from google.adk.runners import Runner +from google.adk.sessions import InMemorySessionService +from google.genai import types as gtypes +from vaid_adk import ToolAuthorization, TrustedIssuers, VaidPresentation +from vaid_mint import ( + InMemoryAudit, + MintService, + ReferenceIssuer, + VaidSeed, + prefix_entry, +) +from vaid_mint.mint_types import MintPop, build_mint_pop_payload +from vaid_mint.revocation import InMemoryRevocationList +from vaid_pop import canonical_request_signing_bytes + +from google.adk_community.plugins import VaidIdentityPlugin + +APP_NAME = "vaid-identity-sample" +USER_ID = "demo-user" + +# RFC 2606 reserved domain, named once because it has to agree in two +# places: the issuer stamps it into every document it mints, and the +# plugin's trust configuration binds the kernel key to it. +TRUST_DOMAIN = "vaid.example" + + +class _FakeTool: + """Carries only what VaidIdentityPlugin.before_tool_callback reads: .name. + Avoids needing a real FunctionTool + LLM round trip for this offline + example.""" + + def __init__(self, name: str) -> None: + self.name = name + + +class _FakeToolContext: + """Carries only what VaidIdentityPlugin.before_tool_callback reads: + .agent_name, the identity of the agent making the call.""" + + def __init__(self, agent_name: str) -> None: + self.agent_name = agent_name + + +class WorkerAgent(BaseAgent): + """A worker agent that attempts two tool calls through the real plugin + manager: lookup_order (within its attenuated scope) and issue_refund + (outside it). No LLM involved; the calls are issued directly, where + ADK's own LLM flow would issue them.""" + + async def _run_async_impl(self, ctx: InvocationContext): + for tool_name, resource in ( + ("lookup_order", "data.orders"), + ("issue_refund", "data.payments"), + ): + denial = await ctx.plugin_manager.run_before_tool_callback( + tool=_FakeTool(tool_name), + tool_args={"resource": resource}, + tool_context=_FakeToolContext(self.name), + ) + outcome = "ALLOWED" if denial is None else f"DENIED ({denial['code']})" + yield Event( + invocation_id=ctx.invocation_id, + author=self.name, + content=gtypes.Content( + role="model", + parts=[gtypes.Part(text=f"{tool_name}: {outcome}")], + ), + ) + + +class CoordinatorAgent(BaseAgent): + """Drives ADK's native in-process delegation (sub_agent.run_async).""" + + async def _run_async_impl(self, ctx: InvocationContext): + yield Event( + invocation_id=ctx.invocation_id, + author=self.name, + content=gtypes.Content( + role="model", parts=[gtypes.Part(text="delegating to worker")] + ), + ) + for sub in self.sub_agents: + async for event in sub.run_async(ctx): + yield event + + +async def main() -> None: + # 1. Mint a root VAID for the coordinator, then an attenuated child for + # the worker. Ordinary vaid_mint usage, no ADK involved yet. + kernel_key = Ed25519PrivateKey.generate() + issuer = ReferenceIssuer.from_seed( + kernel_key.private_bytes_raw(), vaid_ttl_hours=1, trust_domain=TRUST_DOMAIN + ) + revocation = InMemoryRevocationList.assume_nothing_revoked() + mint = MintService(issuer, InMemoryAudit()) + + root = mint.mint_root( + VaidSeed( + agent_class="coordinator", + version="1.0.0", + tenant_id="acme", + scope_boundary=[prefix_entry("data.orders"), prefix_entry("data.payments")], + capability_set=["read", "write"], + ) + ) + + worker_key = Ed25519PrivateKey.generate() + worker_public = worker_key.public_key().public_bytes_raw() + worker_seed = VaidSeed( + agent_class="worker", + version="1.0.0", + tenant_id="acme", + parent_vaid=root["vaid_id"], + scope_boundary=[prefix_entry("data.orders")], # attenuated: no data.payments + capability_set=["read"], # attenuated: no write + public_key_der=worker_public, + ) + pop_nonce = "sample-nonce-1" + pop_issued_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + pop_payload = build_mint_pop_payload( + worker_seed, public_key_der=worker_public, nonce=pop_nonce, issued_at=pop_issued_at + ) + pop = MintPop( + nonce=pop_nonce, + issued_at=pop_issued_at, + signature=worker_key.sign(canonical_request_signing_bytes(pop_payload)), + ) + worker_child = mint.mint_child(worker_seed, root, pop).vaid + + # 2. Wire the plugin: one trusted kernel key, a vouching revocation store + # (demo only), and the explicit tool -> capability map. + plugin = VaidIdentityPlugin( + keys=TrustedIssuers.from_raw_keys([(TRUST_DOMAIN, issuer.kernel_public_key())]), + revocation=revocation, + tool_authorizations={ + "lookup_order": ToolAuthorization(capability="read", resource="data.orders"), + "issue_refund": ToolAuthorization(capability="write", resource="data.payments"), + }, + ) + plugin.register("coordinator", VaidPresentation(leaf=root)) + plugin.register("worker", VaidPresentation(leaf=worker_child, chain=(root,))) + + # 3. Build the agents and run through a real ADK Runner. + worker = WorkerAgent(name="worker") + coordinator = CoordinatorAgent(name="coordinator", sub_agents=[worker]) + + session_service = InMemorySessionService() + await session_service.create_session( + app_name=APP_NAME, user_id=USER_ID, session_id="s1" + ) + runner = Runner( + app_name=APP_NAME, + agent=coordinator, + session_service=session_service, + plugins=[plugin], + ) + + async for event in runner.run_async( + user_id=USER_ID, + session_id="s1", + new_message=gtypes.Content(role="user", parts=[gtypes.Part(text="go")]), + ): + if event.content and event.content.parts: + text = event.content.parts[0].text + if text: + print(f"[{event.author}] {text}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index a03bdca..27d3ebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ hitl = [ "fastapi>=0.110.0", "sqlalchemy>=2.0.0", ] +vaid = [ + "vaid-adk>=0.2.0,<0.3", +] [tool.pyink] diff --git a/src/google/adk_community/plugins/__init__.py b/src/google/adk_community/plugins/__init__.py index 2e4b2ee..dbad2a6 100644 --- a/src/google/adk_community/plugins/__init__.py +++ b/src/google/adk_community/plugins/__init__.py @@ -12,18 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from google.adk_community.plugins.agent_governance_plugin import ( - AgentGovernancePlugin, -) -from google.adk_community.plugins.taxonomy import ( - DefaultSkillPolicy, - SkillPolicy, - TaxonomyPipeline, - TaxonomyPlugin, - TaxonomyRegistry, - TaxonomyResolver, - TaxonomyTerm, -) +from google.adk_community.plugins.agent_governance_plugin import AgentGovernancePlugin +from google.adk_community.plugins.taxonomy import DefaultSkillPolicy +from google.adk_community.plugins.taxonomy import SkillPolicy +from google.adk_community.plugins.taxonomy import TaxonomyPipeline +from google.adk_community.plugins.taxonomy import TaxonomyPlugin +from google.adk_community.plugins.taxonomy import TaxonomyRegistry +from google.adk_community.plugins.taxonomy import TaxonomyResolver +from google.adk_community.plugins.taxonomy import TaxonomyTerm +from google.adk_community.plugins.vaid_identity_plugin import VaidIdentityPlugin __all__ = [ "AgentGovernancePlugin", @@ -34,4 +31,5 @@ "TaxonomyRegistry", "TaxonomyResolver", "TaxonomyTerm", + "VaidIdentityPlugin", ] diff --git a/src/google/adk_community/plugins/vaid_identity_plugin.py b/src/google/adk_community/plugins/vaid_identity_plugin.py new file mode 100644 index 0000000..8f069cf --- /dev/null +++ b/src/google/adk_community/plugins/vaid_identity_plugin.py @@ -0,0 +1,131 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ADK plugin for VAID agent-identity and delegation verification. + +Verifies each agent's signed identity (VAID) and delegation chain before the +agent runs, before it is delegated to as a sub-agent, and before every tool +call, denying fail-closed whenever any of that cannot be positively +confirmed. Built on vaid-adk (https://github.com/solara-associates/vaid). + +Requires: ``pip install google-adk-community[vaid]`` +""" + +from __future__ import annotations + +from typing import Any +from typing import Optional + +from google.adk.plugins.base_plugin import BasePlugin +from google.adk.tools.base_tool import BaseTool +from google.adk.tools.tool_context import ToolContext +from google.genai import types as gtypes + + +class _VaidIdentityUnavailableError(ImportError): + """Raised when vaid-adk is not installed.""" + + +try: + from vaid_adk import ToolAuthorization + from vaid_adk import TrustedIssuers + from vaid_adk import VaidAuthPlugin + from vaid_adk import VaidPresentation + from vaid_mint.revocation import RevocationCheck +except ImportError as exc: + raise _VaidIdentityUnavailableError( + "vaid-adk is required for VaidIdentityPlugin. Install with: " + "pip install google-adk-community[vaid]" + ) from exc + + +class VaidIdentityPlugin(BasePlugin): + """ADK plugin that verifies a VAID before an agent runs, before it is + delegated to as a sub-agent, and before every tool call. + + A thin wrapper over :class:`vaid_adk.VaidAuthPlugin`: this class delegates + every verification decision to it and adds no verification logic of its + own. See that class's docstring for the fail-closed semantics this plugin + inherits (an agent with no registered presentation is denied, never + treated as exempt; a tool with no entry in ``tool_authorizations`` is + denied, never allowed by omission). + + Args: + keys: Trusted kernel keys, each bound to the one trust domain it is + trusted for. Required; there is no default trust configuration. + revocation: The revocation check to consult for every verified + lineage. Required; there is no fail-open default. + tool_authorizations: Maps a tool name to the capability (and, + optionally, resource) a VAID must carry to call it. + name: The plugin's registered name on the ``Runner``/``App``. + + Example:: + + from google.adk_community.plugins import VaidIdentityPlugin + from vaid_adk import ToolAuthorization, TrustedIssuers, VaidPresentation + + plugin = VaidIdentityPlugin( + keys=TrustedIssuers.from_raw_keys([("vaid.example", issuer_public_key)]), + revocation=my_revocation_check, + tool_authorizations={"lookup_order": ToolAuthorization(capability="read")}, + ) + plugin.register("worker", VaidPresentation(leaf=child_vaid, chain=(root_vaid,))) + runner = Runner(agent=my_agent, plugins=[plugin], ...) + """ + + def __init__( + self, + *, + keys: "TrustedIssuers", + revocation: "RevocationCheck", + tool_authorizations: Optional[dict[str, "ToolAuthorization"]] = None, + name: str = "vaid_identity", + ) -> None: + super().__init__(name=name) + self._delegate = VaidAuthPlugin( + keys=keys, + revocation=revocation, + tool_authorizations=tool_authorizations, + name=name, + ) + + def register(self, agent_name: str, presentation: "VaidPresentation") -> None: + """Register the VAID presentation for one agent, by its ADK agent + ``name``. See :meth:`vaid_adk.VaidAuthPlugin.register`.""" + self._delegate.register(agent_name, presentation) + + def unregister(self, agent_name: str) -> None: + """Drop a registered presentation. See + :meth:`vaid_adk.VaidAuthPlugin.unregister`.""" + self._delegate.unregister(agent_name) + + async def before_agent_callback( + self, *, agent: Any, callback_context: Any + ) -> Optional[gtypes.Content]: + """Delegates to :meth:`vaid_adk.VaidAuthPlugin.before_agent_callback`.""" + return await self._delegate.before_agent_callback( + agent=agent, callback_context=callback_context + ) + + async def before_tool_callback( + self, + *, + tool: BaseTool, + tool_args: dict[str, Any], + tool_context: ToolContext, + ) -> Optional[dict]: + """Delegates to :meth:`vaid_adk.VaidAuthPlugin.before_tool_callback`.""" + return await self._delegate.before_tool_callback( + tool=tool, tool_args=tool_args, tool_context=tool_context + ) diff --git a/tests/plugins/test_vaid_identity_plugin.py b/tests/plugins/test_vaid_identity_plugin.py new file mode 100644 index 0000000..6a7ad9d --- /dev/null +++ b/tests/plugins/test_vaid_identity_plugin.py @@ -0,0 +1,224 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for VaidIdentityPlugin. + +VaidIdentityPlugin is a thin delegate over vaid_adk.VaidAuthPlugin, so these +tests exercise it the same way vaid-adk's own test_plugin.py does: minimal +duck-typed stand-ins for ADK's Agent/Tool/ToolContext, and real VAID +documents minted in-test with an Ed25519 key (cheap, no network). The +ImportError path is tested separately by faking the absence of vaid_adk in +sys.modules. +""" + +from __future__ import annotations + +import uuid + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +import pytest +from vaid_adk import ToolAuthorization +from vaid_adk import TrustedIssuers +from vaid_adk import VaidPresentation +from vaid_mint.document import build_unsigned_vaid_document +from vaid_mint.document import canonical_vaid_signing_bytes +from vaid_mint.document import compute_lineage_hash +from vaid_mint.document import prefix_entry +from vaid_mint.issuer_identity import kernel_key_thumbprint +from vaid_mint.revocation import InMemoryRevocationList + +from google.adk_community.plugins.vaid_identity_plugin import VaidIdentityPlugin + +_KEY = Ed25519PrivateKey.from_private_bytes(bytes([0x33]) * 32) +_PUBLIC = _KEY.public_key().public_bytes_raw() +_THUMBPRINT = kernel_key_thumbprint(_PUBLIC) + + +class _Agent: + + def __init__(self, name: str) -> None: + self.name = name + + +class _Tool: + + def __init__(self, name: str) -> None: + self.name = name + + +class _ToolContext: + + def __init__(self, agent_name: str) -> None: + self.agent_name = agent_name + + +def _root_vaid(scope: list[str], caps: list[str]) -> dict: + agent_id = str(uuid.uuid4()) + unsigned = build_unsigned_vaid_document( + vaid_id=agent_id, + agent_id=agent_id, + agent_class="test", + version="1.0.0", + tenant_id="acme", + issued_at="2026-06-01T00:00:00Z", + expires_at="2999-01-01T00:00:00Z", + public_key_der=list(range(32)), + parent_vaid=None, + scope_boundary=[prefix_entry(v) for v in scope], + lineage_hash=compute_lineage_hash(None, agent_id), + capability_set=caps, + trust_domain="vaid.example", + kernel_key_thumbprint=_THUMBPRINT, + ) + signature = _KEY.sign(canonical_vaid_signing_bytes(unsigned)) + return {**unsigned, "kernel_signature": list(signature)} + + +def _plugin( + tool_authorizations: dict[str, ToolAuthorization] | None = None, +) -> VaidIdentityPlugin: + return VaidIdentityPlugin( + keys=TrustedIssuers.from_raw_keys([("vaid.example", _PUBLIC)]), + revocation=InMemoryRevocationList.assume_nothing_revoked(), + tool_authorizations=tool_authorizations or {}, + ) + + +@pytest.mark.asyncio +async def test_plugin_name() -> None: + plugin = _plugin() + assert plugin.name == "vaid_identity" + + +@pytest.mark.asyncio +async def test_before_agent_callback_allows_registered_identity() -> None: + plugin = _plugin() + plugin.register( + "worker", VaidPresentation(leaf=_root_vaid(["data.x"], ["read"])) + ) + result = await plugin.before_agent_callback( + agent=_Agent("worker"), callback_context=None + ) + assert result is None + + +@pytest.mark.asyncio +async def test_before_agent_callback_denies_unregistered_agent() -> None: + plugin = _plugin() + result = await plugin.before_agent_callback( + agent=_Agent("ghost"), callback_context=None + ) + assert result is not None + assert "ghost" in result.parts[0].text + + +@pytest.mark.asyncio +async def test_before_tool_callback_allows_in_scope_call() -> None: + plugin = _plugin({ + "lookup_order": ToolAuthorization( + capability="read", resource="data.orders" + ) + }) + plugin.register( + "worker", + VaidPresentation(leaf=_root_vaid(["data.orders"], ["read"])), + ) + result = await plugin.before_tool_callback( + tool=_Tool("lookup_order"), + tool_args={}, + tool_context=_ToolContext("worker"), + ) + assert result is None + + +@pytest.mark.asyncio +async def test_before_tool_callback_denies_out_of_scope_call() -> None: + # Capability is authorized; the resource is outside the leaf's + # scope_boundary. Denied before the tool runs — before_tool_callback + # returns a dict, which ADK uses as the tool's function response directly, + # so the tool itself is never called. + plugin = _plugin({ + "issue_refund": ToolAuthorization( + capability="read", resource="data.payments" + ) + }) + plugin.register( + "worker", + VaidPresentation(leaf=_root_vaid(["data.orders"], ["read"])), + ) + result = await plugin.before_tool_callback( + tool=_Tool("issue_refund"), + tool_args={}, + tool_context=_ToolContext("worker"), + ) + assert result is not None + assert result["code"] == "out_of_scope" + + +@pytest.mark.asyncio +async def test_before_tool_callback_denies_unregistered_agent() -> None: + # "worker" never registered: no VAID at all, denied rather than exempt. + plugin = _plugin({ + "lookup_order": ToolAuthorization( + capability="read", resource="data.orders" + ) + }) + result = await plugin.before_tool_callback( + tool=_Tool("lookup_order"), + tool_args={}, + tool_context=_ToolContext("worker"), + ) + assert result is not None + assert result["code"] == "no_vaid" + + +@pytest.mark.asyncio +async def test_before_tool_callback_denies_unmapped_tool_never_default_allow() -> ( + None +): + plugin = _plugin(tool_authorizations={}) # nothing mapped + plugin.register( + "worker", VaidPresentation(leaf=_root_vaid(["data.x"], ["read"])) + ) + result = await plugin.before_tool_callback( + tool=_Tool("anything"), tool_args={}, tool_context=_ToolContext("worker") + ) + assert result is not None + assert result["code"] == "unmapped_tool" + + +def test_raises_import_error_when_vaid_adk_missing(monkeypatch) -> None: + import builtins + import importlib + import sys + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name == "vaid_adk" or name.startswith("vaid_adk."): + raise ImportError("no module named vaid_adk") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _fake_import) + for mod_name in list(sys.modules): + if mod_name == "vaid_adk" or mod_name.startswith("vaid_adk."): + monkeypatch.delitem(sys.modules, mod_name, raising=False) + monkeypatch.delitem( + sys.modules, + "google.adk_community.plugins.vaid_identity_plugin", + raising=False, + ) + + with pytest.raises(ImportError): + importlib.import_module("google.adk_community.plugins.vaid_identity_plugin")