# Defense-in-Depth Score: LangGraph

**Repo:** https://github.com/langchain-ai/langgraph · **Commit:** `7dc9195e4141c8fbd8118581b3dd61d158628aa8` · **Reviewed:** 2026-10-03
**What it is:** Graph-based orchestration runtime for stateful, durable agents
**Category:** Agent Frameworks
**Scored configuration:** The langgraph library with default arguments: StateGraph.compile() and the prebuilt create_react_agent/ToolNode with no checkpointer, store, interrupts or timeouts; the CLI and checkpointers are footnoted where relevant.
**Agent surface (default):** code execution no · filesystem write no · network egress yes · external credentials yes · persistent memory opt-in · untrusted input yes · third party extensions no · sub agents yes · external communication opt-in

## Score: 4.2 / 10.0 (Minimal)

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

Controls where a risk surface exists: 2.15 / 8.0 (27%); 2 criteria scored SA (surface absent).

LangGraph is a low-level runtime: it ships no shell, code-execution or plugin features of its own, which is why two criteria score full marks. Everything else is left to the developer. By default every tool call the model makes runs immediately with the host process's full credentials, tool output flows back to the model unmarked, and the only limit is a 10,007-step cap. The dominant risk is prompt injection through tool results driving the developer's tools; human-in-the-loop interrupts and checkpoint history exist but are opt-in, and the default checkpoint deserializer will execute code stored in a tampered database.

## Critical gaps
- No identity or authorization layer: every tool runs with the host process's full ambient credentials, so a hijacked agent holds all of them. (ASI03, T3; C1) — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958); searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.)
- Tool output loops back to the model unmarked and no approval runs by default, so a prompt injection can leak data and take irreversible actions unattended. (ASI01, LLM01; C5) — [libs/prebuilt/langgraph/prebuilt/tool_node.py:1441-1443](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L1441-L1443); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303)

## Criterion details

### C1 Identity & least privilege — 0.00 (high)

LangGraph has no identity or authorization layer of its own. Tools and nodes run inside the developer's process with whatever credentials that process holds, and nothing checks a tool call against a policy or against the user who asked for it. The Python SDK includes an Auth decorator API for the hosted LangGraph Server, but its enforcement lives in the closed-source server, not in this repository. If an agent is hijacked, the attacker gets the full authority of the host process.

- **S L0:** No credential scoping: the runtime passes nothing but state and config to tools, which use the host process's ambient credentials. — searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.); [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
  - *To reach the next level:* No dedicated or scoped identity for agent tool calls; L1 needs at least a dedicated identity.
- **C L0:** No authorization check on any tool path; ToolNode invokes any registered tool the model names. — searched `rg -n -S -i -e 'authoriz|permission|principal'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 1 hits (The only hit (pregel/main.py:4265) is a comment about the closed-source server's ProxyUser; no authorization layer exists in the runtime or prebuilt agent.); [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
  - *To reach the next level:* No authorization layer on the main tool path; L1 needs the main tool path checked.
- **D L0:** The default run carries the full authority of the developer's process; narrowing requires the developer to build it. — searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.) (verified)
  - *To reach the next level:* No narrower default; least privilege is entirely manual hardening.
- **B L0:** A hijacked agent can use every credential and system the host process can reach, including any cloud or SaaS keys in its environment. — searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.); [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
  - *To reach the next level:* Nothing bounds what a hijacked agent's tools can reach; L1 needs authority limited below the whole process.
- **Cap:** none
- **Notes:** The sdk-py Auth API (libs/sdk-py/langgraph_sdk/auth/__init__.py) lets developers register authentication and per-resource authorization handlers for the LangGraph Server; enforcement is in the closed-source langgraph-api and could not be verified, so it is not credited.

### C2 Approval gates — 0.30 (high)

By default a LangGraph agent executes every tool call the model emits with no human approval: create_react_agent and compile() both default to no interrupts. Developers can opt in to pausing before the tools node (interrupt_before=['tools']) or call interrupt() inside a tool, and the pending tool calls, with their exact arguments, sit in saved state where a person can inspect, edit, or replace them before resuming. The pause is all-or-nothing per node with no risk tiers, a nested sub-agent's own tool calls are not covered by the parent's pause, and anyone holding the thread id can resume. Nothing undoes actions already taken outside the graph.

- **default configuration** (default; raw 0.00 → 0.00)
  - **S L0:** No approval step runs by default; tools execute directly from the model's tool calls. — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303); [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
    - *To reach the next level:* No approval in the default configuration; L1 needs at least a blanket approval step.
  - **C L0:** With no gate, every tool path, including the most powerful developer tool, is ungated. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:990](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L990) (verified)
    - *To reach the next level:* No gate exists for any tool; L1 needs flagged tools gated.
  - **D L0:** interrupt_before/interrupt_after default to None in both create_react_agent and compile(). — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303); [libs/langgraph/langgraph/graph/state.py:1183](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1183) (verified)
    - *To reach the next level:* Approval is opt-in.
  - **B L0:** Developer tools can take irreversible external actions; LangGraph's checkpoint history can rewind graph state but not external effects. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
    - *To reach the next level:* No undo, preview, or dry-run for external actions; L1 needs some actions reversible.
- **opt-in interrupt_before=['tools'] breakpoint (requires a checkpointer)** (alt; raw 0.30, cap G1 → 0.30) ← counted
  - **S L2:** A node-level breakpoint pauses before every tool execution; the human sees the exact tool calls in state and can edit them via update_state or resume. — [libs/langgraph/langgraph/pregel/_algo.py:155-159](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_algo.py#L155-L159); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303) (verified)
    - *To reach the next level:* No risk tiers deciding which calls need a human, and reject is not a first-class outcome; L3 needs per-call tiering.
  - **C L2:** Every tool call of that agent passes through the paused tools node, but a sub-agent's internal tool calls run under its own graph without the parent's breakpoint. — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:872](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L872); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:990](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L990) (verified)
    - *To reach the next level:* Nested sub-agents' tool calls bypass the parent's breakpoint and unknown tools are not rejected by policy; L3 needs every path including sub-agents gated.
  - **D L0:** Off unless the developer passes interrupt_before. — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303); [libs/langgraph/langgraph/graph/state.py:1183](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1183) (verified)
    - *To reach the next level:* Approval is opt-in; L1 needs it on by default.
  - **B L0:** Approved or bypassed tool calls can take irreversible external actions; checkpoints only rewind graph state. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
    - *To reach the next level:* No undo or bounded quantities for external actions.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.
- **Notes:** The resume Command is accepted from whichever caller holds the thread config; the library does not authenticate the approver.

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

ToolNode only runs tools that were registered and returns an error for any other name, and tool arguments are type-checked against each tool's Pydantic schema before the tool runs. Values the framework injects (graph state, the store, runtime) overwrite anything the model supplies for those parameters. That is type validation, not an allowlist: LangGraph ships no path, URL, or quantity constraints, and every tool the developer passes is available to the model at every step.

- **S L2:** Unknown tool names are refused and arguments are validated against the tool's typed schema (ValidationError is caught and returned to the model); injected arguments overwrite model-supplied values. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:1268-1279](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L1268-L1279); [libs/prebuilt/langgraph/prebuilt/tool_node.py:957-959](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L957-L959); [libs/prebuilt/langgraph/prebuilt/tool_node.py:1323-1324](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L1323-L1324) (verified)
  - *To reach the next level:* No allowlist validation primitives (path containment, host allowlists, numeric bounds); L3 needs allowlist checks in code.
- **C L2:** Every ToolNode call goes through tool.invoke and its schema; provider-native dict tools bypass ToolNode entirely and run on the provider side. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:559-560](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L559-L560) (verified)
  - *To reach the next level:* No shared policy layer that wraps every tool; L3 needs a common validation layer for all tools.
- **D L2:** No tools are enabled unless the developer passes them, but every passed tool, including write and network tools, is bound to the model for every step. — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:586](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L586) (verified)
  - *To reach the next level:* No read-only default tier; L3 needs write/exec tools to require explicit enabling.
- **B L1:** A misused tool has whatever reach the developer's implementation has; the framework imposes no scope or quantity bounds. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958) (verified)
  - *To reach the next level:* No workspace scoping or quantity bounds from the framework; L2 needs tools scoped to a project.
- **Cap:** none

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

The LangGraph runtime and prebuilt agent contain no code-execution feature: no shell tool, no Python or JavaScript interpreter, no eval of model output. Tools are ordinary Python callables the developer writes; whether they run commands is the developer's choice, and isolating them is the developer's job. The CLI does run Docker commands, but only from the operator's own configuration. The checkpoint deserializer can import and call Python objects named in stored data, but that data is written by the framework, not by the model, and it is scored under memory integrity (C6).

- **Structural absence:** searched `rg -n -S -e 'subprocess|os\.system|os\.popen|\beval\(|\bexec\(|create_subprocess|pty\.spawn'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 0 hits (No process spawning or dynamic evaluation in the runtime or prebuilt agent.); searched `rg -n -S -e 'subprocess|create_subprocess'` in `libs/cli/langgraph_cli` → 7 hits (All hits are the CLI running docker/docker-compose commands from the operator's own langgraph.json (exec.py, docker.py); no model-influenced path reaches them.)
- **Notes:** Developers who add shell or code-execution tools get no isolation from LangGraph; that surface belongs to their tools (or to langchain/deepagents, scored separately).

### C5 Untrusted input blast radius — 0.00 (high)

Tool results, including web pages, files, API responses and the output of remote graphs, are wrapped as tool messages and fed straight back to the model on the next step. Nothing in LangGraph marks that content as untrusted, tracks taint, or disables state-changing or outbound tools once untrusted content has been read. Because LangGraph's whole purpose is to combine untrusted inputs, private data and outbound tools in one loop, a successful prompt injection can both leak data and take irreversible actions without a human in the default configuration.

- **S L0:** Tool output becomes a ToolMessage and loops back to the model with no provenance or restriction. — [libs/prebuilt/langgraph/prebuilt/tool_node.py:1441-1443](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L1441-L1443); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:990](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L990); searched `rg -n -S -i -e 'untrusted|taint|provenance|prompt.injection|quarantin'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 1 hits (The single hit is the serializer's docstring warning (jsonplus.py:88); nothing tracks or acts on untrusted content in agent context.) (verified)
  - *To reach the next level:* No structural limit on a hijacked agent; L1 needs at least detection or delimiting of untrusted content.
- **C L0:** No source is distinguished; tool, sub-agent, and remote-graph outputs all enter state with equal standing. — searched `rg -n -S -i -e 'untrusted|taint|provenance|prompt.injection|quarantin'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 1 hits (The single hit is the serializer's docstring warning (jsonplus.py:88); nothing tracks or acts on untrusted content in agent context.); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:990](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L990) (verified)
  - *To reach the next level:* Untrusted sources are not distinguished; L1 needs at least one source handled.
- **D L0:** No mechanism exists to be on by default. — searched `rg -n -S -i -e 'untrusted|taint|provenance|prompt.injection|quarantin'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 1 hits (The single hit is the serializer's docstring warning (jsonplus.py:88); nothing tracks or acts on untrusted content in agent context.) (verified)
  - *To reach the next level:* Nothing is on by default.
- **B L0:** With no approval by default, a hijacked agent can exfiltrate through any outbound tool and take irreversible actions with the host's credentials, unattended. — [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:302-303](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L302-L303); [libs/prebuilt/langgraph/prebuilt/tool_node.py:958](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/tool_node.py#L958); searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.) (verified)
  - *To reach the next level:* Leak and irreversible action both happen without a human; L1 needs at least one leg blocked.
- **Cap:** C5-WORSTCASE — B is L0: with no default approval, a hijack can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 0.30 (high)

By default a compiled graph keeps state only in memory for one run. When a developer adds a checkpointer (per conversation thread) or a store (shared across threads), everything the agent read, including injected tool output, is saved and reloaded verbatim on the next turn, with no validation, provenance, or expiry. The default checkpoint deserializer is permissive: its own module docstring says any Python callable stored in checkpoint data will be imported and executed on load, so anyone who can write to the checkpoint database can run code in the agent process; a strict allowlist exists but needs an environment variable. LangGraph loads no instruction files or .env files from a working directory.

- **S L1:** Persisted state is reloaded without validation or provenance, and the default deserializer imports and calls any callable named in checkpoint data. — [libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:112-114](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py#L112-L114); [libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py:3-5](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py#L3-L5); [libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:651](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py#L651); [libs/checkpoint/langgraph/checkpoint/base/__init__.py:210](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/base/__init__.py#L210) (verified)
  - *To reach the next level:* No provenance on persisted entries and no integrity check on loads; L2 needs memory presented as data with provenance.
- **C L1:** Only the legacy JSON path enforces a constructor allowlist; the default msgpack path, store items, and reloaded messages are uncontrolled. — [libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:233-242](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py#L233-L242); [libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:112-114](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py#L112-L114) (verified)
  - *To reach the next level:* The main checkpoint (msgpack) path and the store are uncontrolled; L2 needs the main memory store controlled.
- **D L2:** Checkpoints are keyed and queried per thread; store namespaces are whatever tuple the developer's code passes, in one shared map. — [libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py:240](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py#L240); [libs/checkpoint/langgraph/store/memory/__init__.py:186](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/store/memory/__init__.py#L186) (verified)
  - *To reach the next level:* Store namespaces are not enforced per user and the model's tools can write any namespace the developer exposes; L3 needs isolation the model cannot change.
- **B L1:** Once persistence is enabled, poisoned content persists across a thread's later turns (and across threads via the store) and can trigger tool use; tampered checkpoint rows execute code on load. — [libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py:3-5](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py#L3-L5); [libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:443-446](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py#L443-L446) (verified)
  - *To reach the next level:* Poison persists across the user's sessions and can trigger tools; L2 needs it limited to text output or gated actions.
- **Cap:** none
- **Notes:** The CLI's local deployment path does not fully protect checkpoint-store integrity.

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

LangGraph itself loads no third-party code at runtime: no plugin loader, MCP client, tool hub, model-file loading, or package installs the model can trigger. Tools come only from the developer's own code. RemoteGraph calls a remote LangGraph server over HTTP, so its outputs are untrusted input (C5) rather than code run locally. Developers who add MCP or hub tools through other libraries take on that risk outside LangGraph.

- **Structural absence:** searched `rg -n -S -i -e 'mcp|entry_points|trust_remote_code|torch\.load|from_pretrained|pip install|load_plugin'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 6 hits (All six hits are error or docstring hints telling the developer to pip install an optional dependency (langchain, pycryptodome, numpy); nothing loads extensions.)
- **Notes:** The checkpoint deserializer's ability to import arbitrary callables is persistence integrity, scored in C6. The CLI's `langgraph new` and `build` act on operator instructions, not the agent's.

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

LangGraph ships no telemetry or crash reporting, and debug printing is off by default. The SDK reads its server API key from environment variables and sends it only as a request header. There is no redaction anywhere: full conversation state, including tool output and anything secret in it, is stored in plaintext in the checkpoint database unless the developer opts in to the EncryptedSerializer, and the framework does nothing to keep secrets out of model-bound messages. Credentials in the host process are long-lived and reachable by every tool.

- **S L1:** API keys come from environment variables; no masking or redaction exists, and checkpoint values are written as plaintext serialized bytes by default. — [libs/sdk-py/langgraph_sdk/_shared/utilities.py:44-46](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/sdk-py/langgraph_sdk/_shared/utilities.py#L44-L46); [libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py:426](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py#L426); searched `rg -n -S -i -e 'redact|SecretStr|scrub'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 5 hits (Four hits are docstrings describing developer-written stream transformers that could redact; the fifth (types.py:549) says TracePolicy is not intended to redact secrets. No redaction ships.) (verified)
  - *To reach the next level:* No type-level masking or log/state redaction; L2 needs masking and filters on main paths.
- **C L1:** The API key is confined to a reserved header, but state persistence, model-bound messages, and debug output carry content unredacted. — [libs/sdk-py/langgraph_sdk/_shared/utilities.py:57-59](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/sdk-py/langgraph_sdk/_shared/utilities.py#L57-L59); [libs/langgraph/langgraph/types.py:549](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/types.py#L549) (verified)
  - *To reach the next level:* Only one path is protected; L2 needs logs and transcripts protected.
- **D L2:** No telemetry; debug printing defaults off; encryption at rest is opt-in. — searched `rg -n -S -i -e 'sentry|posthog|mixpanel|amplitude|telemetry'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 0 hits (No telemetry or crash reporting in the runtime, prebuilt agent, or checkpoint libraries.); [libs/langgraph/langgraph/graph/state.py:1185](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1185) (verified)
  - *To reach the next level:* No redaction exists to be always on; L3 needs redaction on by default.
- **B L1:** Long-lived developer keys (model provider, cloud, SaaS) sit in the process environment, reachable by every tool. — searched `rg -n -S -e 'boto3|google\.auth|DefaultAzureCredential|load_kube_config|os\.environ|getenv'` in `libs/langgraph/langgraph libs/prebuilt/langgraph` → 3 hits (All three hits are in _internal/_config.py and read recursion/snapshot tuning values; the framework neither loads nor scopes credentials, so tools use whatever the host process holds.) (verified)
  - *To reach the next level:* Keys are long-lived and unscoped by the framework; L2 needs scoped keys.
- **Cap:** none
- **Notes:** Opt-in EncryptedSerializer (libs/checkpoint/langgraph/checkpoint/serde/encrypted.py) encrypts checkpoint bytes with AES; it covers storage only and does not raise the criterion above the default. A credential-handling gap in the CLI's secondary deployment path is footnoted only.

### C9 Audit & traceability — 0.47 (high)

Without a checkpointer, the only record of a run is the state returned to the caller, which is lost if the process dies. With a checkpointer, LangGraph saves a versioned snapshot at every step plus each task's writes, including tool calls with their arguments and results, interrupts and resume values, with run ids and parent links across subgraphs, which makes replay and time-travel possible. Records are written in the background by default, carry no actor or approver identity, and live in a database the agent process can overwrite or delete.

- **default configuration** (default; raw 0.00 → 0.00)
  - **S L0:** No record is kept by default beyond the in-memory state handed back to the caller. — [libs/langgraph/langgraph/graph/state.py:1179](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1179) (verified)
    - *To reach the next level:* No structured record by default; L1 needs at least logs of actions.
  - **C L0:** Nothing is recorded by default. — [libs/langgraph/langgraph/graph/state.py:1179](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1179) (verified)
    - *To reach the next level:* Nothing recorded; L1 needs the main tool path recorded.
  - **D L0:** Recording requires passing a checkpointer. — [libs/langgraph/langgraph/graph/state.py:1179](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1179) (verified)
    - *To reach the next level:* Opt-in only.
  - **B L0:** With no record, nothing survives a crash. — [libs/langgraph/langgraph/graph/state.py:1179](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1179) (verified)
    - *To reach the next level:* Records are lost on crash; L1 needs best-effort persistence.
- **opt-in checkpointer (InMemorySaver / SqliteSaver / PostgresSaver)** (alt; raw 0.47, cap G1 → 0.47) ← counted
  - **S L2:** Every step saves a checkpoint with step, source, run_id and parent ids, plus per-task writes holding tool calls and results. — [libs/checkpoint/langgraph/checkpoint/base/__init__.py:57-63](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint/langgraph/checkpoint/base/__init__.py#L57-L63); [libs/langgraph/langgraph/pregel/_loop.py:467](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_loop.py#L467) (verified)
    - *To reach the next level:* No actor attribution (requesting principal, approver); L3 needs agent/human separation.
  - **C L3:** All tasks, including subgraph nodes (namespaced), interrupts and resume values, are written through put_writes. — [libs/langgraph/langgraph/pregel/_loop.py:900-907](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_loop.py#L900-L907); [libs/langgraph/langgraph/pregel/_loop.py:467](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_loop.py#L467) (verified)
    - *To reach the next level:* Configuration changes and credential use are not recorded; L4 needs them.
  - **D L0:** Off unless a checkpointer is passed; once on, rows are mutable (INSERT OR REPLACE) by the agent process. — [libs/langgraph/langgraph/graph/state.py:1179](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/graph/state.py#L1179); [libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py:463](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py#L463) (verified)
    - *To reach the next level:* Opt-in; L1 needs it on by default.
  - **B L2:** Writes are persisted per task, but under the default 'async' durability in a background executor while execution continues. — [libs/langgraph/langgraph/pregel/main.py:2552](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/main.py#L2552) (verified)
    - *To reach the next level:* Not durable before the next action under the default durability; L3 needs durable per-action records.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.
- **Notes:** LangSmith tracing (via langchain-core callbacks, env-var opt-in) is another opt-in record, shipped off-host; it is not part of this repository.

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

Every graph run has a step cap, but the default is 10,007 supersteps (overridable by an environment variable), so it bounds almost nothing. Per-step and per-node wall-clock timeouts exist but default to off, and there is no token or cost budget. Each subgraph or nested agent counts its own steps from zero, so delegation gets a fresh budget. Timeouts and cancellation cancel async work, but synchronous tool calls already running in threads cannot be stopped.

- **S L2:** Recursion limit enforced in code, plus opt-in step_timeout and per-node TimeoutPolicy run/idle timeouts; no token or cost cap. — [libs/langgraph/langgraph/_internal/_config.py:32](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/_internal/_config.py#L32); [libs/langgraph/langgraph/pregel/main.py:727](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/main.py#L727); [libs/langgraph/langgraph/types.py:476](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/types.py#L476); searched `rg -n -S -i -e 'max_cost|token_limit|budget|wall_clock'` in `libs/langgraph/langgraph libs/prebuilt/langgraph libs/checkpoint/langgraph` → 0 hits (No token, cost, or run-level wall-clock budget anywhere in the runtime.) (verified)
  - *To reach the next level:* No token/cost cap or wall-clock run limit; L3 needs step, time and cost caps together.
- **C L1:** The step counter is per graph: a subgraph starts from its own checkpoint step with a fresh limit. — [libs/langgraph/langgraph/pregel/_loop.py:1676-1677](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_loop.py#L1676-L1677) (verified)
  - *To reach the next level:* Sub-agents and subgraphs do not share the parent's budget and tool timeouts are off by default; L2 needs tool timeouts.
- **D L1:** The default limit is 10,007 steps and the environment can raise it; timeouts default to None. — [libs/langgraph/langgraph/_internal/_config.py:32](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/_internal/_config.py#L32); [libs/langgraph/langgraph/pregel/main.py:727](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/main.py#L727) (verified)
  - *To reach the next level:* Default ceiling is very large; L2 needs sensible defaults.
- **B L1:** A runaway can take thousands of steps; synchronous tasks already started cannot be cancelled. — [libs/langgraph/langgraph/_internal/_config.py:32](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/_internal/_config.py#L32); [libs/langgraph/langgraph/pregel/_executor.py:59](https://github.com/langchain-ai/langgraph/blob/7dc9195e4141c8fbd8118581b3dd61d158628aa8/libs/langgraph/langgraph/pregel/_executor.py#L59) (verified)
  - *To reach the next level:* Very large ceilings and in-flight sync work survives a stop; L2 needs moderate ceilings.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: tool results re-enter context as ToolMessages (prebuilt/tool_node.py:1441, chat_agent_executor.py:990) · [B] sensitive data/systems: developer tools and the host process's ambient credentials (tool_node.py:958); persisted state when a checkpointer/store is used · [C] state change / egress: any developer tool invoked without approval by default (tool_node.py:958, chat_agent_executor.py:302) · Same default session? Yes

## Highest-impact improvements
1. Ship the strict msgpack allowlist as the default deserializer (no import-and-call of unregistered types). — C6 C L1→L2, +0.075 before caps (Playbook 2, step 1)
2. Lower the default recursion limit to a task-sized value and set a default per-step timeout. — C10 D L1→L2, +0.050 before caps (Playbook 3, step 3)
3. Make subgraphs and nested agents draw on the parent's step budget. — C10 C L1→L2, +0.075 before caps (Playbook 3, step 3)
4. Add per-tool risk tiers to ToolNode so listed or unknown tools interrupt for approval by default. — C2 D L0→L1, +0.050 before caps (Playbook 5, step 1)
5. Tag tool messages with provenance and let ToolNode force approval for egress/state-changing tools after untrusted content is read. — C5 S L0→L2, +0.150 before caps (Playbook 1, step 2)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was installed, built, or run.
- The LangGraph Server runtime (langgraph-api) is closed source and not in this repository; server-side auth, encryption handlers, and its serializer configuration were not examined.
- langchain-core behaviour (tool argument validation, LangSmith tracing) is relied on as a dependency and was not reviewed here; create_react_agent is deprecated in favour of langchain's create_agent, scored separately.
- libs/sdk-js contains no source at this commit; the LangGraph.js library lives in another repository and is out of scope.
- AGENTS.md contains an instruction block for contributors' coding agents (Corridor analyzePlan); it is not aimed at reviewers and was treated as data. No reviewer-injection attempt was found.
