Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/google/adk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,27 @@
from .runners import Runner
from .workflow import Workflow

# Taxonomy Policy & Security Engine
from .plugins.taxonomy.policy import DefaultSkillPolicy
from .plugins.taxonomy.policy import SkillPolicy
from .plugins.taxonomy.policy import TaxonomyPipeline
from .plugins.taxonomy.policy import TaxonomyResolver
from .plugins.taxonomy.taxonomy_config import TaxonomyRegistry
from .plugins.taxonomy.taxonomy_config import TaxonomyTerm
from .plugins.taxonomy.taxonomy_plugin import TaxonomyPlugin

__version__ = version.__version__
__all__ = ["Agent", "Context", "Event", "Runner", "Workflow"]
__all__ = [
"Agent",
"Context",
"DefaultSkillPolicy",
"Event",
"Runner",
"SkillPolicy",
"TaxonomyPipeline",
"TaxonomyPlugin",
"TaxonomyRegistry",
"TaxonomyResolver",
"TaxonomyTerm",
"Workflow",
]
5 changes: 5 additions & 0 deletions src/google/adk/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
from .debug_logging_plugin import DebugLoggingPlugin
from .logging_plugin import LoggingPlugin
from .reflect_retry_tool_plugin import ReflectAndRetryToolPlugin
from .taxonomy import TaxonomyPlugin

__all__ = [
'BasePlugin',
'DebugLoggingPlugin',
'LoggingPlugin',
'PluginManager',
'ReflectAndRetryToolPlugin',
'TaxonomyPlugin',
]

_LAZY_MEMBERS: dict[str, str] = {
Expand All @@ -43,4 +45,7 @@ def __getattr__(name: str):
if name in _LAZY_MEMBERS:
module = importlib.import_module(f'{__name__}.{_LAZY_MEMBERS[name]}')
return vars(module)[name]
if name == 'TaxonomyPlugin':
from .taxonomy import TaxonomyPlugin
return TaxonomyPlugin
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
33 changes: 33 additions & 0 deletions src/google/adk/plugins/taxonomy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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.

"""Pluggable Policy & Taxonomy Security Engine for ADK."""

from .policy import DefaultSkillPolicy
from .policy import SkillPolicy
from .policy import TaxonomyPipeline
from .policy import TaxonomyResolver
from .taxonomy_config import TaxonomyRegistry
from .taxonomy_config import TaxonomyTerm
from .taxonomy_plugin import TaxonomyPlugin

__all__ = [
"DefaultSkillPolicy",
"SkillPolicy",
"TaxonomyPipeline",
"TaxonomyPlugin",
"TaxonomyRegistry",
"TaxonomyResolver",
"TaxonomyTerm",
]
150 changes: 150 additions & 0 deletions src/google/adk/plugins/taxonomy/policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# 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.

"""Abstract interfaces for taxonomy resolution and skill policy enforcement.

This module defines the pluggable contracts that developers implement:

- ``TaxonomyResolver``: Classifies the active security/regulatory domains
from runtime context and LLM conversation history.
- ``TaxonomyPipeline``: Chains multiple resolvers into a multi-step pipeline.
- ``SkillPolicy``: Gates skill access and shapes instructions dynamically.
- ``DefaultSkillPolicy``: Reference implementation using taxonomy-bind matching.
"""

from __future__ import annotations

from abc import ABC
from abc import abstractmethod

from ...agents.readonly_context import ReadonlyContext
from ...models.llm_request import LlmRequest
from ...skills.models import Skill


class TaxonomyResolver(ABC):
"""Abstract base class for taxonomy resolution.

Resolvers can be chained to form multi-step pipelines via ``TaxonomyPipeline``.

Example use cases:
- Semantic classification: Analyze past agent interactions to classify
the active security domain (e.g. ``urn:adk:domain:compliance``).
- Entitlements verification: Gate access using feature flags.
- DB-backed RBAC: Query database records for user permissions.
"""

@abstractmethod
async def resolve_taxonomies(
self, context: ReadonlyContext, llm_request: LlmRequest
) -> list[str]:
"""Resolves active taxonomy domain URIs from the runtime context and LLM history.

Args:
context: The session runtime context. Provides access to
``user_content``, ``user_id``, ``state``, and ``session``.
llm_request: Outgoing LLM request containing conversation history,
agent-to-agent dialogues, and reasoning blocks.

Returns:
List of active taxonomy domain URI strings
(e.g. ``["urn:adk:domain:compliance", "urn:adk:domain:medical"]``).
"""
pass


class TaxonomyPipeline(TaxonomyResolver):
"""Executes a sequence of taxonomy resolvers in order (multi-step pipeline)."""

def __init__(self, resolvers: list[TaxonomyResolver]):
self.resolvers = resolvers

async def resolve_taxonomies(
self, context: ReadonlyContext, llm_request: LlmRequest
) -> list[str]:
active_domains: set[str] = set()
for resolver in self.resolvers:
domains = await resolver.resolve_taxonomies(context, llm_request)
if domains:
active_domains.update(domains)
return list(active_domains)


class SkillPolicy(ABC):
"""Abstract policy engine determining skill execution permissions and instruction shaping."""

@abstractmethod
def is_skill_allowed(
self,
skill: Skill,
context: ReadonlyContext,
active_taxonomies: list[str],
) -> bool:
"""Determines if a skill can be loaded/used under the active taxonomies and context."""
pass

@abstractmethod
def shape_instructions(
self,
skill: Skill,
context: ReadonlyContext,
original_instructions: str,
) -> str:
"""Applies dynamic instruction shaping/guardrails to a skill's instructions.

Called after a skill is loaded but before instructions are returned to the model.
Use this to append compliance disclaimers, restrict tool usage, inject
role-specific constraints, etc.

Args:
skill: The skill being loaded.
context: The session runtime context.
original_instructions: The original instruction text from SKILL.md.

Returns:
The shaped/modified instruction text.
"""
pass


class DefaultSkillPolicy(SkillPolicy):
"""Default skill policy using taxonomy-bind set-intersection matching.

A skill is allowed if:
- It has no ``taxonomy-binds`` in its frontmatter (unrestricted), OR
- At least one of its ``taxonomy-binds`` matches an active taxonomy domain.

Instructions are returned unmodified. Subclass and override
``shape_instructions`` to add custom guardrails.
"""

def is_skill_allowed(
self,
skill: Skill,
context: ReadonlyContext,
active_taxonomies: list[str],
) -> bool:
binds = skill.frontmatter.taxonomy_binds
if not binds:
return True
# At least one bind must match an active taxonomy
return bool(set(binds) & set(active_taxonomies))

def shape_instructions(
self,
skill: Skill,
context: ReadonlyContext,
original_instructions: str,
) -> str:
return original_instructions
Loading