# Defense-in-Depth Score: Google ADK (Python)

**Repo:** https://github.com/google/adk-python · **Commit:** `e94c2e726a269e0f04e2e4b202f5c131c80c20de` (2.11.0) · **Reviewed:** 2026-10-03
**What it is:** Code-first toolkit for building, evaluating, deploying agents
**Category:** Agent Frameworks
**Scored configuration:** Library defaults: Agent/LlmAgent with default constructor arguments (no tools, code_executor=None) run by Runner/InMemoryRunner with default RunConfig; adk run/web dev CLI footnoted.
**Agent surface (default):** code execution opt-in · filesystem write opt-in · network egress opt-in · external credentials opt-in · persistent memory opt-in · untrusted input opt-in · third party extensions opt-in · sub agents opt-in · external communication opt-in

## Score: 3.5 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L2 | L1 | L0 | L2 | 0.33 | G1 | **0.33** (alt) | Medium |
| C2 | Approval gates | L3 | L0 | L0 | L1 | 0.28 | G1 | **0.28** | High |
| C3 | Tool & action scoping | L2 | L2 | L3 | L1 | 0.50 | — | **0.50** | High |
| C4 | Code-execution isolation | L4 | L1 | L0 | L2 | 0.47 | G1 | **0.47** (alt) | High |
| C5 | Untrusted input blast radius | L1 | L2 | L2 | L0 | 0.33 | C5-WORSTCASE | **0.25** | Medium |
| C6 | Memory, context & configuration integrity | L2 | L1 | L2 | L1 | 0.38 | — | **0.38** | Medium |
| C7 | Third-party extensions | L1 | L0 | L2 | L1 | 0.23 | — | **0.23** | High |
| C8 | Secrets & sensitive-data protection | L1 | L1 | L2 | L0 | 0.25 | — | **0.25** | High |
| C9 | Audit & traceability | L2 | L2 | L1 | L1 | 0.40 | — | **0.40** | Medium |
| C10 | Limits & kill switch | L2 | L2 | L1 | L1 | 0.40 | — | **0.40** | High |


ADK starts every agent with no tools and no code execution, and it ships genuinely good building blocks: an exact-call confirmation flow, an SSRF-hardened web loader, a read-only-by-default BigQuery tool and a gVisor code executor. Almost none of them are on by default, though. Tools run without approval unless each one opts in, the local executors give model-written code the full environment with its keys, and nothing limits what an agent can do after reading a malicious page or MCP result. Developers who enable code execution or shell tools have to choose isolation and approval themselves.

## Critical gaps
- The bundled local code and shell executors run model-generated code as host subprocesses with the full process environment, including API keys and cloud credentials. (ASI05, T11; C4) — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139)
- Nothing in the framework breaks the untrusted-input, private-data and egress combination; with default (unconfirmed) tools a hijacked agent can leak data and act unattended. (ASI01, LLM01; C5) — [src/google/adk/tools/load_web_page.py:247](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/load_web_page.py#L247); [src/google/adk/tools/function_tool.py:111](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/function_tool.py#L111)

## Criterion details

### C1 Identity & least privilege — 0.33 (medium)

ADK has no agent identity or authorization layer of its own: function tools run in the developer's process with whatever credentials it holds, and the local code and shell executors hand model-generated code the full process environment. Google API toolsets can instead be configured with per-user OAuth (each toolset gets its own client and scopes and the end user consents), which is a real narrowing, but it is opt-in, defaults to broad scopes such as full BigQuery plus Dataplex read-write, and does not cover function tools or executors.

- **default configuration** (default; raw 0.05 → 0.05)
  - **S L0:** No scoping primitive on the default tool path; tools and executors use the host process's ambient authority. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); searched `rg -n -S 'authoriz|permission'` in `src/google/adk/tools/function_tool.py src/google/adk/flows/llm_flows/tools/_functions.py` → 2 hits (both hits are the Apache license header; no authorization in the tool executor) (verified)
    - *To reach the next level:* No dedicated or scoped identity by default; L1 needs at least a dedicated identity for agent tools.
  - **C L0:** Nothing checks authorization before a tool runs; executors pass the whole environment to model code. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
    - *To reach the next level:* No authorization layer; L1 needs the main tool path checked in code.
  - **D L0:** The default agent runs tools with the developer's full ambient credentials; narrowing needs manual configuration. — [src/google/adk/tools/_google_credentials.py:51](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/_google_credentials.py#L51); [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148) (verified)
    - *To reach the next level:* No narrower default identity; L1 needs a default narrower than the host's authority.
  - **B L1:** The framework neither narrows nor widens the developer's credentials; Google Cloud ADC in typical use reaches multiple systems. — [src/google/adk/integrations/bigquery/bigquery_credentials.py:21-24](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/integrations/bigquery/bigquery_credentials.py#L21-L24); [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148) (inferred)
    - *To reach the next level:* Developer credentials are fully reachable from tools and executors; L2 needs tools limited to one system.
- **opt-in per-user OAuth for Google API toolsets (BaseGoogleCredentialsConfig with client_id/secret)** (alt; raw 0.33, cap G1 → 0.33) ← counted
  - **S L2:** Each toolset obtains an end-user OAuth token with its own scope list, static for the run; read and write share it. — [src/google/adk/tools/_google_credentials.py:236-276](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/_google_credentials.py#L236-L276); [src/google/adk/integrations/bigquery/bigquery_credentials.py:38-39](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/integrations/bigquery/bigquery_credentials.py#L38-L39) (verified)
    - *To reach the next level:* Default scopes are broad and shared by read and write tools; L3 needs per-capability, minimally scoped credentials.
  - **C L1:** Only Google API/OpenAPI toolsets with auth configs use it; function tools, MCP and executors keep ambient authority. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
    - *To reach the next level:* Function tools, executors and MCP servers bypass it; L2 needs every built-in tool on the scoped identity.
  - **D L0:** Opt-in: the developer must supply OAuth client settings; nothing defaults to it. — [src/google/adk/tools/_google_credentials.py:116-119](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/_google_credentials.py#L116-L119) (verified)
    - *To reach the next level:* Off by default; L1+ needs it on by default.
  - **B L2:** A hijacked agent holds one user's token for one service with broad scopes (e.g. full BigQuery). — [src/google/adk/integrations/bigquery/bigquery_credentials.py:21-24](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/integrations/bigquery/bigquery_credentials.py#L21-L24) (inferred)
    - *To reach the next level:* Token carries write scope to the whole service; L3 needs project-scoped, mostly read authority.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C2 Approval gates — 0.28 (high)

ADK ships a well-built per-call confirmation flow: a tool can require approval statically or through a callable that inspects the arguments, the approval request carries the exact original call, only a user-authored event can approve, and the executed arguments must equal the approved ones. But it is off by default for every function, MCP and OpenAPI tool, and the code-executor path (model-written code run by a configured executor) and the experimental environment shell tool cannot be gated at all. Only the bash tool always asks. Nothing provides undo or dry-run for external actions; session rewind only restores ADK's own state and artifacts.

- **S L3:** Per-call approval carrying the exact original call, argument-aware risk decision via callable, approved args must equal executed args, reject is first-class. — [src/google/adk/tools/function_tool.py:399-424](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/function_tool.py#L399-L424); [src/google/adk/flows/llm_flows/tools/_confirmation.py:205-211](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/tools/_confirmation.py#L205-L211); [src/google/adk/flows/llm_flows/tools/_confirmation.py:281](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/tools/_confirmation.py#L281); [src/google/adk/flows/llm_flows/tools/_functions.py:226-236](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/tools/_functions.py#L226-L236) (verified)
  - *To reach the next level:* The callable can only escalate, not deny or rewrite; L4 needs allow/deny/escalate rules on parsed arguments.
- **C L0:** The code-executor path runs model code with no gate and no way to add one; EnvironmentToolset's shell tool has no confirmation. — [src/google/adk/flows/llm_flows/extensions/_code_execution.py:431-433](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/extensions/_code_execution.py#L431-L433); searched `rg -n -S -i 'confirm'` in `src/google/adk/code_executors src/google/adk/flows/llm_flows/extensions/_code_execution.py src/google/adk/tools/environment` → 0 hits (no confirmation anywhere on the code-executor or environment-tool paths) (verified)
  - *To reach the next level:* Code execution and the environment shell skip the gate; L1 needs the most powerful paths gateable and gated.
- **D L0:** require_confirmation defaults to False for function and MCP tools; BaseTool never asks. — [src/google/adk/tools/function_tool.py:111](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/function_tool.py#L111); [src/google/adk/tools/mcp_tool/mcp_toolset.py:161](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/mcp_tool/mcp_toolset.py#L161); [src/google/adk/tools/base_tool.py:194-198](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/base_tool.py#L194-L198) (verified)
  - *To reach the next level:* Approval is opt-in per tool; L1 needs it on by default.
- **B L1:** Session rewind restores ADK state and artifacts; external actions have no undo, preview or rate limit. — [src/google/adk/runners.py:1396-1404](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/runners.py#L1396-L1404); searched `rg -n -S 'checkpoint|rollback|undo|dry_run'` in `src/google/adk/tools/function_tool.py src/google/adk/tools/base_tool.py src/google/adk/flows/llm_flows/tools/_functions.py` → 0 hits (no checkpoint or dry-run in the generic tool path) (verified)
  - *To reach the next level:* No reversibility for the common case of tool side effects; L2 needs undo for typical actions.
- **Cap:** G1 — The confirmation flow is opt-in per tool in the default configuration.

### C3 Tool & action scoping — 0.50 (high)

Every function tool's arguments are type-checked against its schema before it runs, and several bundled tools are carefully bounded: the web loader pins the resolved IP, blocks internal addresses and refuses redirects; environment file tools enforce resolved-path containment; BigQuery blocks non-SELECT statements by default using the service's own dry-run. But the general-purpose tools stay general: the bash tool allows any command by default and the environment Execute tool runs any shell string, and MCP tools get no shared validation. A new agent starts with no tools at all, so every capability is an explicit developer choice.

- **S L2:** Typed schema validation for all function tools plus strong per-tool validation in a few built-ins; the shell tools take raw commands. — [src/google/adk/tools/function_tool.py:170](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/function_tool.py#L170); [src/google/adk/tools/load_web_page.py:182-188](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/load_web_page.py#L182-L188); [src/google/adk/environment/_local_environment.py:218-228](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L218-L228); [src/google/adk/integrations/bigquery/query_tool.py:217-227](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/integrations/bigquery/query_tool.py#L217-L227); [src/google/adk/tools/bash_tool.py:80](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/bash_tool.py#L80) (verified)
  - *To reach the next level:* General tools (bash with '*', Execute) accept raw commands; L3 needs allowlist validation on every built-in.
- **C L2:** Most built-in tools validate; MCP and other extension tools get only schema typing. — [src/google/adk/tools/environment/_execute_tool.py:97-101](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/environment/_execute_tool.py#L97-L101); [src/google/adk/tools/mcp_tool/mcp_session_manager.py:1328-1330](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/mcp_tool/mcp_session_manager.py#L1328-L1330) (verified)
  - *To reach the next level:* Extension tools have no shared validation layer; L3 needs one wrapping them.
- **D L3:** LlmAgent starts with an empty tool list and no code executor; every tool is added explicitly. — [src/google/adk/agents/llm_agent.py:399](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/llm_agent.py#L399); [src/google/adk/agents/llm_agent.py:484](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/llm_agent.py#L484) (verified)
  - *To reach the next level:* No per-task allowlist; skills can add pre-registered tools at the model's request; L4 needs the model unable to enable tools itself.
- **B L1:** When added, bash runs any command in the workspace with host reach; Execute runs any shell string. — [src/google/adk/tools/bash_tool.py:251-257](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/bash_tool.py#L251-L257); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
  - *To reach the next level:* General tools reach the whole host; L2 needs project/workspace scoping enforced.
- **Cap:** none

### C4 Code-execution isolation — 0.47 (high)

No code runs by default, but the execution options ADK ships without extras are unsandboxed: UnsafeLocalCodeExecutor runs model-written Python as a host subprocess with the full environment, and the bash tool and LocalEnvironment shell run on the host too. Strong isolation is available on request: the GKE executor runs each snippet in a fresh gVisor pod as a non-root user with dropped capabilities, no service-account token and resource limits, and the container executor disables networking and drops capabilities. Even then, shell tools, skill scripts on a local executor, and MCP stdio servers keep running on the host.

- **default configuration** (default; raw 0.00 → 0.00)
  - **S L0:** UnsafeLocalCodeExecutor, ExecuteBashTool and LocalEnvironment all run as same-user host subprocesses. — [src/google/adk/code_executors/unsafe_local_code_executor.py:158-168](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L158-L168); [src/google/adk/environment/_local_environment.py:134](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L134); [src/google/adk/tools/bash_tool.py:251](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/bash_tool.py#L251) (verified)
    - *To reach the next level:* No isolation in the bundled local executors; L1 needs at least filtering.
  - **C L0:** None of the host execution paths go through a sandbox. — [src/google/adk/flows/llm_flows/extensions/_code_execution.py:431-433](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/extensions/_code_execution.py#L431-L433); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
    - *To reach the next level:* Main exec paths unsandboxed; L1 needs the main exec tool sandboxed.
  - **D L0:** No executor is configured by default; choosing isolation is the developer's job. — [src/google/adk/agents/llm_agent.py:484](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/llm_agent.py#L484) (verified)
    - *To reach the next level:* No sandbox on by default; L1 needs one on.
  - **B L0:** Model code receives the full host environment (API keys, ADC paths) and host filesystem and network. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
    - *To reach the next level:* Credentials in the execution environment; L1 needs credentials kept out of reach.
- **opt-in GkeCodeExecutor (job mode, gVisor)** (alt; raw 0.47, cap G1 → 0.47) ← counted
  - **S L4:** Per-execution Kubernetes Job on the gVisor runtime, non-root, read-only root fs, all capabilities dropped. — [src/google/adk/code_executors/gke_code_executor.py:325](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/gke_code_executor.py#L325); [src/google/adk/code_executors/gke_code_executor.py:296-302](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/gke_code_executor.py#L296-L302) (verified)
  - **C L1:** Covers the code-executor path only; bash, Execute, and MCP stdio servers still run on the host. — [src/google/adk/tools/bash_tool.py:251](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/bash_tool.py#L251); [src/google/adk/environment/_local_environment.py:134](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L134) (verified)
    - *To reach the next level:* Shell tools and MCP stdio servers run on host; L2 needs most paths sandboxed.
  - **D L0:** Opt-in; LlmAgent.code_executor defaults to None. — [src/google/adk/agents/llm_agent.py:484](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/llm_agent.py#L484) (verified)
    - *To reach the next level:* Off by default; L1+ needs it on.
  - **B L2:** Ephemeral pod, no SA token, CPU/memory limits, no host env; network egress is not restricted. — [src/google/adk/code_executors/gke_code_executor.py:315](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/gke_code_executor.py#L315); [src/google/adk/code_executors/gke_code_executor.py:306](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/gke_code_executor.py#L306); searched `rg -n -S 'NetworkPolicy|network_policy|egress'` in `src/google/adk/code_executors/gke_code_executor.py` → 0 hits (no network restriction on the job pod) (verified)
    - *To reach the next level:* Pod egress unrestricted; L3 needs egress off or allowlisted.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C5 Untrusted input blast radius — 0.25 (medium)

ADK fences some untrusted text as data: MCP tool descriptions and messages from other agents are wrapped in markers with a preamble telling the model not to follow them. That is spotlighting, which raises the bar but does not stop a determined injection, and ordinary tool results (including fetched web pages) are not fenced at all. Nothing ties approval or egress to whether untrusted content has been read; Model Armor screening is an opt-in plugin. Under the framework rule, a hijacked agent can combine untrusted input, private data and egress with no human involved.

- **S L1:** Delimiters plus a do-not-follow preamble for MCP descriptions and other agents' turns; detection plugin opt-in. — [src/google/adk/flows/llm_flows/context/_fencing.py:98](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/context/_fencing.py#L98); [src/google/adk/flows/llm_flows/context/_fencing.py:45-52](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/context/_fencing.py#L45-L52); [src/google/adk/integrations/model_armor/_plugin.py:61](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/integrations/model_armor/_plugin.py#L61) (verified)
  - *To reach the next level:* Only spotlighting; L2 needs approval forced on dangerous capabilities after untrusted content is read.
- **C L2:** Covers MCP tool descriptions, other-agent and A2A peer messages; tool results are not fenced. — [src/google/adk/tools/mcp_tool/mcp_tool.py:440](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/mcp_tool/mcp_tool.py#L440); searched `rg -n -S 'fence|quote_untrusted|untrusted'` in `src/google/adk/flows/llm_flows/tools/_functions.py src/google/adk/tools/load_web_page.py` → 0 hits (tool results and fetched pages enter context unfenced) (verified)
  - *To reach the next level:* Tool results are not covered; L3 needs every source including tool results.
- **D L2:** Fencing is applied unconditionally in code; a developer can bypass it (e.g. non-str system instruction skips MCP fencing with an error log). — [src/google/adk/tools/mcp_tool/mcp_tool.py:418-428](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/mcp_tool/mcp_tool.py#L418-L428) (verified)
  - *To reach the next level:* Can be silently skipped by configuration; L3 needs disabling to be explicit and warned.
- **B L0:** Framework rule: nothing breaks the untrusted-input + private-data + egress combination; a hijack can leak and act unattended. — [src/google/adk/tools/load_web_page.py:247](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/load_web_page.py#L247); [src/google/adk/tools/function_tool.py:111](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/function_tool.py#L111) (inferred)
  - *To reach the next level:* Leak and irreversible action both possible unattended; L1 needs at least one of them blocked.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 0.38 (medium)

The model has no tool to write long-term memory directly: whole sessions are added to memory only when the developer's code or the dev server calls it, searches are keyed by app and user, and recalled memories come back with author and timestamp inside a 'past conversations' block. But nothing validates what is stored, so an injection inside a session is recalled in later sessions, and session state (including app-wide state shared by every user) can be templated straight into the system instruction. ADK does not auto-load instruction files from a workspace; the CLI loads the .env from the developer's own agent folder.

- **S L2:** Memory writes are developer-triggered, stored unvalidated, and recalled with author/time provenance as delimited context. — [src/google/adk/memory/in_memory_memory_service.py:95-105](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/memory/in_memory_memory_service.py#L95-L105); [src/google/adk/tools/preload_memory_tool.py:87-96](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/preload_memory_tool.py#L87-L96) (verified)
  - *To reach the next level:* No validation, gating or expiry on memory writes; L3 needs gated writes with expiry.
- **C L1:** Memory search is namespaced; persisted session state and compaction summaries are injected without control. — [src/google/adk/flows/llm_flows/prompt/_instructions_utils.py:171-172](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/flows/llm_flows/prompt/_instructions_utils.py#L171-L172) (verified)
  - *To reach the next level:* State templating and summaries are uncontrolled; L2 needs the main store plus retrieval controlled.
- **D L2:** Memory keyed by (app_name, user_id) in queries; app:-prefixed state is shared across all users. — [src/google/adk/memory/in_memory_memory_service.py:139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/memory/in_memory_memory_service.py#L139); [src/google/adk/sessions/state.py:75](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/sessions/state.py#L75) (verified)
  - *To reach the next level:* App-wide state is cross-user and tool-writable; L3 needs the model unable to write outside its namespace.
- **B L1:** Poisoned memory persists across the user's sessions and is re-presented to a model that can call tools. — [src/google/adk/tools/preload_memory_tool.py:94-96](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/preload_memory_tool.py#L94-L96) (inferred)
  - *To reach the next level:* Persists and can drive tool use; L2 needs it to influence only text or gated actions.
- **Cap:** none

### C7 Third-party extensions — 0.23 (high)

ADK loads no third-party code unless the developer configures it, and the developer writes the exact MCP launch command or skill source themselves. Nothing pins or verifies what is loaded: MCP server tool lists are refetched each session with no change detection, and skill revalidation is off by default and only re-states changed text. Skill scripts run through whichever executor is configured, which with the local executor means a host subprocess holding the full environment; MCP stdio servers get the MCP library's default environment.

- **S L1:** Developer-chosen sources, unpinned and unverified; no hash, signature or change detection. — searched `rg -n -S 'sha256|digest|checksum|signature'` in `src/google/adk/tools/mcp_tool src/google/adk/skills src/google/adk/integrations/skill_registry` → 8 hits (hits are call signatures, an md5 of headers for session caching, and a signed-URL comment; none verifies extension code); [src/google/adk/tools/skill_toolset.py:177](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/skill_toolset.py#L177) (verified)
  - *To reach the next level:* No version pinning or integrity check; L2 needs pinned versions.
- **C L0:** No extension type is verified. — [src/google/adk/tools/mcp_tool/mcp_session_manager.py:1328-1330](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/mcp_tool/mcp_session_manager.py#L1328-L1330) (verified)
  - *To reach the next level:* No type verified; L1 needs at least one.
- **D L2:** Explicit configuration by the developer; with a registry configured the model can load any registry skill on its own. — [src/google/adk/tools/skill_toolset.py:2027-2033](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/skill_toolset.py#L2027-L2033) (verified)
  - *To reach the next level:* No consent step showing what will run, and the model can load registry skills; L3 needs nothing third-party enabled without showing exact package and permissions.
- **B L1:** Skill scripts on the local executor run as the same user with the full environment. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/tools/skill_toolset.py:1284-1293](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/skill_toolset.py#L1284-L1293) (verified)
  - *To reach the next level:* Full environment reaches extension code; L2 needs a scrubbed environment.
- **Cap:** none

### C8 Secrets & sensitive-data protection — 0.25 (high)

Credentials come from environment variables or developer-supplied objects. The generic auth flow strips the OAuth client secret before storing credentials in temporary state, and CLI usage telemetry is content-free and only sent after the user opts in. Elsewhere protection is thin: the Google credentials helper caches the full OAuth credential JSON (refresh token and client secret) in persisted session state, OpenTelemetry spans capture message content by default, and the local executors pass every environment variable, API keys included, to model-generated code.

- **S L1:** Env-var secrets with masking on one path (client secret stripped in auth state); cached Google tokens stored as plaintext JSON in session state. — [src/google/adk/auth/auth_handler.py:105](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/auth/auth_handler.py#L105); [src/google/adk/tools/_google_credentials.py:216-219](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/_google_credentials.py#L216-L219) (verified)
  - *To reach the next level:* No type-level masking or log filters on main paths; L2 needs redacted reprs and log filters.
- **C L1:** Only the generic auth state path is protected; subprocess environments, spans and token cache are not. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); searched `rg -n -S 'redact|mask'` in `src/google/adk/code_executors src/google/adk/environment src/google/adk/tools/bash_tool.py src/google/adk/flows` → 0 hits (no redaction on execution or flow paths) (verified)
  - *To reach the next level:* Logs, spans and subprocess env unprotected; L2 needs logs and transcripts covered.
- **D L2:** CLI telemetry requires explicit consent and is content-free; span content capture is on by default but only leaves the host if the developer adds an exporter. — [src/google/adk/cli/cli_tools_click.py:375](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/cli/cli_tools_click.py#L375); [src/google/adk/telemetry/tracing.py:106-108](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/telemetry/tracing.py#L106-L108) (verified)
  - *To reach the next level:* Content capture in spans is on by default and no redaction is always-on; L3 needs always-on redaction.
- **B L0:** Long-lived keys in the process environment reach every subprocess and model-generated code on local executors. — [src/google/adk/code_executors/unsafe_local_code_executor.py:148](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/code_executors/unsafe_local_code_executor.py#L148); [src/google/adk/environment/_local_environment.py:130-139](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/environment/_local_environment.py#L130-L139) (verified)
  - *To reach the next level:* Long-lived keys reachable by model code; L1 needs them kept from subprocesses.
- **Cap:** none

### C9 Audit & traceability — 0.40 (medium)

Every tool call, tool result, code execution and approval is recorded as a structured session event with timestamp, author (agent name or 'user'), invocation id and branch, and the same steps are emitted as OpenTelemetry spans. That is a good transcript, but by default it lives in memory (or in a SQLite file inside the agent folder for the dev server), the agent's process can alter it, and calls made inside an agent wrapped as a tool go to a throwaway in-memory session. Nothing makes actions wait for their record.

- **S L2:** Structured events with args, results, timestamps, author and invocation id; OTel spans available. — [src/google/adk/events/event.py:102-104](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/events/event.py#L102-L104); [src/google/adk/events/event.py:150](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/events/event.py#L150) (verified)
  - *To reach the next level:* Sub-agent (AgentTool) correlation is lost and there is no approver/principal field; L3 needs full attribution and cross-sub-agent correlation.
- **C L2:** All tools including MCP are recorded; AgentTool sub-runs record into a discarded in-memory session. — [src/google/adk/tools/agent_tool.py:269](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/agent_tool.py#L269) (verified)
  - *To reach the next level:* Sub-agent tool calls missing from the parent record; L3 needs every call including sub-agents.
- **D L1:** On by default but in-memory for InMemoryRunner, or SQLite inside the agent folder for the dev server. — [src/google/adk/cli/utils/service_factory.py:263](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/cli/utils/service_factory.py#L263) (verified)
  - *To reach the next level:* Record is writable by the agent process and sits near the workspace; L2 needs storage outside the workspace.
- **B L1:** Database services append per event; the in-memory default loses everything on exit. — [src/google/adk/tools/agent_tool.py:269](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/agent_tool.py#L269) (inferred)
  - *To reach the next level:* Default store is not durable; L2 needs records flushed per action with errors surfaced.
- **Cap:** none

### C10 Limits & kill switch — 0.40 (high)

Each run is capped at 500 model calls by default, enforced in code and adjustable by the developer or an environment variable, and the bash, environment and container executors have per-execution timeouts that kill the process group. There is no wall-clock or token/cost cap per run, loop agents iterate without a limit by default, and an agent wrapped as a tool starts with a fresh 500-call count. An abort signal stops the run, but synchronous tools already running in the thread pool keep going.

- **S L2:** Iteration cap plus per-execution timeouts on exec tools; cooperative abort. — [src/google/adk/agents/invocation_context.py:76-85](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/invocation_context.py#L76-L85); [src/google/adk/tools/bash_tool.py:82](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/bash_tool.py#L82) (verified)
  - *To reach the next level:* No wall-clock or token/cost cap; L3 needs all three plus rate limits.
- **C L2:** Top-level loop plus exec-tool timeouts; AgentTool sub-runs get a fresh per-invocation count. — [src/google/adk/tools/agent_tool.py:296-297](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/tools/agent_tool.py#L296-L297) (verified)
  - *To reach the next level:* Delegation escapes the budget; L3 needs sub-agents to share it.
- **D L1:** 500 LLM calls per run by default with no time or cost bound; LoopAgent max_iterations=None. — [src/google/adk/agents/run_config.py:38](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/run_config.py#L38); [src/google/adk/agents/loop_agent.py:84](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/loop_agent.py#L84) (verified)
  - *To reach the next level:* Ceiling is large and resettable by delegation; L2 needs sensible time/cost defaults.
- **B L1:** Large ceilings; abort leaves started thread-pool tools running. — [src/google/adk/agents/run_config.py:230-232](https://github.com/google/adk-python/blob/e94c2e726a269e0f04e2e4b202f5c131c80c20de/src/google/adk/agents/run_config.py#L230-L232) (verified)
  - *To reach the next level:* Stop leaves work running and no spend ceiling; L2 needs moderate ceilings.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: load_web_page, MCP tool results, other agents (tools/load_web_page.py:247, tools/mcp_tool/mcp_session_manager.py:1328) · [B] sensitive data/systems: developer credentials and Google data via toolsets; full env in local executors (code_executors/unsafe_local_code_executor.py:148) · [C] state change / egress: arbitrary-URL GET, bash/Execute, MCP and API tools without confirmation by default (tools/function_tool.py:111) · Same default session? Yes

## Highest-impact improvements
1. Make a sandboxed executor the default whenever code execution is enabled, and stop passing os.environ to local executors. — C4 B L0→L2, +0.100 before caps (Playbook 3, step 1)
2. Add a runner-level confirmation policy (e.g. confirm every tool unless allowlisted) and route the code-executor and Execute paths through it. — C2 C L0→L2, +0.150 before caps (Playbook 5, step 1)
3. Fence tool results like MCP descriptions and force confirmation on egress/write tools once untrusted content has entered the session. — C5 S L1→L2, +0.075 before caps (Playbook 1, step 2)
4. Share the LLM-call budget with AgentTool sub-runs and add a per-run wall-clock and token cap. — C10 C L2→L3, +0.075 before caps (Playbook 3, step 3)
5. Store cached OAuth tokens in the credential service (temp: or encrypted) instead of plaintext session state, and redact span content by default. — C8 S L1→L2, +0.075 before caps (Playbook 4)

## Re-audit log
- No changes.

## Limitations
- Static source review of commit e94c2e7 only; nothing was installed, built, executed, or probed.
- Framework scored by its defaults: opt-in primitives are capped by G1 and absent ones score L0 even where a developer could add them.
- MCP stdio environment scrubbing and google-auth Credentials.to_json contents are inferred from third-party library behaviour, not read in this repo.
- Not examined in depth: the bundled web UI JavaScript, A2A server, deployers (Cloud Run/Agent Engine templates), E2B/Daytona/VMaaS integrations, and every OpenAPI/Google API toolset.
- Model behaviour is out of scope; only code-level controls are scored.
- No reviewer-directed prompt injection found in README.md, AGENTS.md, CONTRIBUTING.md, or llms.txt.
