google / google/adk-python

[Feature Request]: Add framework-neutral agent-hooks contract to govern ADK agents

Aperta
#6,602 1 commento 0 reazioni 1 assegnatario Rivendicata da @sanketpatil06 Vedi su GitHub
agent engine needs review
Lingua principale
Python
Stelle
21.5k
Fork
4k
Merge medio
1g 14h
PR unite (30g)
37

Descrizione

** Please make sure you read the contribution guide and file the issues in the right place. **
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)

## 🔴 Required Information

### Is your feature request related to a specific problem?
ADK exposes rich lifecycle seams through `BasePlugin`, but there is no
standard, framework-neutral way to plug governance and policy enforcement
(policy engines, content filters, PII redaction, egress guards, rate
limiters) into those seams. Today each team hand-writes bespoke callback
logic per agent. That is inconsistent, easy to accidentally "fail open,"
hard to audit, and not portable across agent frameworks. There is no
uniform allow / deny / transform decision contract, no fail-closed
guarantee, and no built-in, auditable record of *why* an action was
allowed, blocked, or rewritten.

### Describe the Solution You'd Like
Add `AgentHooksPlugin`: a `BasePlugin` that makes ADK a conformant *host*
for [agent-hooks](https://github.com/responsibleai/agent-hooks), a
framework-neutral control contract. You register one or more interceptors
once, and every governed ADK lifecycle point delegates its decision to the
agent-hooks emitter, which runs the interceptors, returns a `Verdict`
(`allow` / `deny` / `transform`), and records each decision as an auditable
`InterceptionRecord`.

API changes (all additive):
- New public export `google.adk.plugins.AgentHooksPlugin`.
- New optional extra: `pip install "google-adk[agent-hooks]"` (backed by
`agent-hooks-sdk`, a compiled-core optional dependency). The import is
deferred to plugin construction, so importing `google.adk.plugins` never
requires it.
- Interceptor contract: any object with `intercept(AgentContext) -> Verdict`
(sync or async). No ADK-specific base class required — the same
interceptor runs on other agent-hooks hosts (crewAI, ...).

Enforcement is **fail-closed**: `deny` blocks the guarded action,
`transform` rewrites the guarded value at `$target`, and any engine error,
malformed verdict, or interceptor timeout becomes a fail-closed deny — it
never fails open. `mode="evaluate_only"` records verdicts without acting.

### Impact on your work
This gives us one consistent, auditable governance layer across ADK agents
instead of per-agent callback code, and lets teams
enforce the *same* interceptor across ADK and other frameworks. It is
foundational for governed/regulated deployments where every tool call,
model call, input, and output must be policy-checked and logged.

### Willingness to contribute
Yes.

---

## 🟡 Recommended Information

### Describe Alternatives You've Considered
- **Framework-specific middleware.** Locks policy to one framework;
agent-hooks is framework-neutral, so the same interceptor is portable.
- **Hand-written `BasePlugin` callbacks per project.** Works, but is
reimplemented everywhere, has no shared verdict model or audit trail, and
is easy to accidentally fail open.
- **Provider/model-side guardrails only.** They don't uniformly cover tool
calls, tool results, user input, final output, and run lifecycle.

### Proposed API / Implementation
```python
from google.adk.apps.app import App
from google.adk.plugins import AgentHooksPlugin
from agent_hooks import AgentContext, Decision, Verdict

class ToolGovernance:
name = "tool_governance"

def intercept(self, ctx: AgentContext) -> Verdict:
if ctx["interception_point"] == "pre_tool_call":
if ctx["tool_call"]["name"] == "delete_account":
return Verdict.deny(reason="tool_denied",
message="disabled by policy")
return Verdict(decision=Decision.ALLOW)

records = []
plugin = AgentHooksPlugin(
interceptors=[ToolGovernance()],
mode="enforce", # or "evaluate_only" to observe without acting
record_sink=records.append, # one auditable InterceptionRecord per decision
)
app = App(name="demo", root_agent=root_agent, plugins=[plugin])
```
Constructor (defaults shown): `AgentHooksPlugin(interceptors, name="agent_hooks",
mode="enforce", timeout=5.0, composition=None, identity_provider="jcs-sha256",
record_sink=None, max_records=1000)`.

### Additional Context
Interception-point mapping (ADK callback → agent-hooks point):

| ADK plugin callback | agent-hooks point |
| ----------------------------- | ----------------- |
| `before_run_callback` | `agent_startup` |
| `on_user_message_callback` | `input` |
| `before_model_callback` | `pre_model_call` |
| `after_model_callback` | `post_model_call` |
| `before_tool_callback` | `pre_tool_call` |
| `after_tool_callback` | `post_tool_call` |
| `on_event_callback` (final) | `output` |
| `after_run_callback` | `agent_shutdown` |

Notes:
- `pre_model_call` supports `allow`/`deny` only; a `transform` there is
treated as a fail-closed deny, because rebuilding a provider-native
request from wire messages is not round-trip safe.
- **Trust model:** agent-hooks is a *cooperative* control contract, **not**
a security boundary. Interceptors run in-process with full data access and
the interception points do not guarantee complete mediation.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.