# LangGraph

LangGraph agents get their credentials the way any Python process does — from the
environment, through the provider client underneath `ChatOpenAI`. So the
zero-code shape works as-is, and the proxy shape is two changed arguments.

> **Note:** **Start here if seekrit is new:** [three commands](/docs/guides/frameworks#get-running-in-five-minutes) put the keys in an environment, mint a token bound to it, and export `SEEKRIT_TOKEN`. A token reads [everything in its environment](/docs/guides/frameworks#which-secrets-does-the-agent-get), so there is no per-key or per-framework setup to do before any of the below.

## 1. Wrap the process

```bash
seekrit run -- python agent.py
seekrit run -- langgraph dev
```

Every granted secret arrives as an environment variable in the child process:
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `TAVILY_API_KEY`, your database URL, all
of it. Delete the `.env` file afterwards — `seekrit run` overlays one if it's
there, and a file that still exists is a file an agent can still read.

## 2. Resolve in code

```python
import seekrit
from langchain.agents import create_agent

seekrit.Client().into_env()          # existing os.environ wins by default

agent = create_agent(
    model="gpt-5.6-terra",
    tools=[get_weather],
    system_prompt="You are a helpful assistant",
)
result = agent.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})
```

`into_env()` before you construct the model, since `ChatOpenAI` reads the key at
construction time. If you'd rather not touch `os.environ`:

```python
from langchain_openai import ChatOpenAI

secrets = seekrit.Client().resolve()
model = ChatOpenAI(model="gpt-5.6-terra", api_key=secrets["OPENAI_API_KEY"])
```

## 3. Never hold the key

Point the model at the [proxy](/docs/guides/agent-proxy) and pass a placeholder:

```python
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="gpt-5.6-terra",
    base_url="http://127.0.0.1:8080/openai/v1",
    api_key="{{seekrit:OPENAI_API_KEY}}",
)
```

```toml
# seekrit-proxy.toml
listen = "127.0.0.1:8080"

[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]
```

Now the graph can be built by anyone, including a code-generating node, and the
key still cannot leave toward anywhere but `api.openai.com`.

### Without running the proxy

`ChatOpenAI` takes an `http_client`, so the substitution can happen in your
process instead of a sidecar:

```python
import httpx
from langchain_openai import ChatOpenAI
from seekrit.transport import AsyncSeekritTransport, SeekritTransport

allow = {"api.openai.com": ["OPENAI_API_KEY"]}
model = ChatOpenAI(
    model="gpt-5.6-terra",
    api_key="{{seekrit:OPENAI_API_KEY}}",
    http_client=httpx.Client(transport=SeekritTransport(allow=allow)),
    http_async_client=httpx.AsyncClient(transport=AsyncSeekritTransport(allow=allow)),
)
```

Weaker than the proxy, since it runs in the same process as the graph.
[In-process injection](/docs/guides/agent-proxy/in-process) sets out the
trade-off — and on LangChain it unlocks the next shape, which nothing else can
express.

## 4. Scope a key to one tool

LangChain 1.x middleware wraps every model call and every tool call, so "which
credentials may *this tool* use" becomes a line of configuration:

```bash
pip install 'seekrit[langchain]'
```

```python
from langchain.agents import create_agent
from seekrit.langchain import SeekritCredentials
from seekrit.transport import SeekritTransport

transport = SeekritTransport(
    allow={
        "api.openai.com": ["OPENAI_API_KEY"],
        "api.stripe.com": ["STRIPE_SECRET_KEY"],
    },
    require_scope=True,
)

agent = create_agent(
    model=model,          # built with http_client=httpx.Client(transport=transport)
    tools=[refund, search],
    context_schema=Context,
    middleware=[
        SeekritCredentials(
            scope=lambda ctx: {"tenants": ctx.tenant},
            model=["OPENAI_API_KEY"],
            tools={"refund": ["STRIPE_SECRET_KEY"]},
        ),
    ],
)
```

The `refund` tool may substitute the Stripe key and nothing else. `search` may
substitute nothing at all — when `tools` is given it is exhaustive, so an
unlisted tool gets an empty allowlist and a prompt-injected `search` call cannot
reach a payment credential.

`scope` also chooses *which* secrets to resolve, off `runtime.context`. One
agent process can serve many tenants without holding two tenants' keys at once,
and without a per-tenant model instance: the middleware sets an ambient scope and
the transport resolves against it, so the model — which builds its HTTP client
once — stays a single object.

## Gotchas

- **LangSmith is a second credential.** `LANGSMITH_API_KEY` (or
  `LANGCHAIN_API_KEY`) belongs in seekrit alongside the provider keys. If you run
  the forward proxy with `unmatched_host_policy = "deny"`, tracing egress to
  `api.smith.langchain.com` is a separate host and needs its own rule, or traces
  silently stop.
- **`langgraph dev` reloads, `seekrit run` doesn't re-resolve.** The injected
  values are fixed for the life of the process. Rotate a secret and you restart
  the dev server — which is the honest behaviour, not a bug to work around.
- **Tool credentials are the interesting ones.** A provider key buys tokens; a
  Stripe or GitHub key bought by a tool call does something irreversible. Those
  are the ones to move to shape 3, with `methods` and `paths` set — or to shape
  4, scoped to the one tool that should have them.
- **An environment variable is in reach of a deserialization bug.**
  `CVE-2025-68664` (CVSS 9.3, December 2025) defaulted `secrets_from_env=True`
  in `langchain-core`'s `load`, so a crafted payload could name any environment
  variable and get its value back. It is patched in 1.2.5 / 0.3.81 — and it is
  also the argument for shapes 3 and 4, where there is nothing in the
  environment to name.
