Skip to Content

LangGraph

LangGraph decides how your agents hand work to each other. AgentValet decides what each of them is allowed to touch while they do it.

The interesting problem in a graph is not the supervisor. It is the workers: subagent nodes that inherit the supervisor’s process, and with it, in most setups, the supervisor’s full credentials. AgentValet closes that gap with child identities. The supervisor mints a child per worker, each child carries only the scope slice its worker needs, and the worker never sees the parent’s key at all.

pip install agentvalet # 0.2.0+

The supervisor: one governed identity

The supervisor node authenticates the normal way, with the agent identity you registered:

from agentvalet import AgentValet av = AgentValet.from_env() # AGENT_ID, OWNER_ID, AGENT_PRIVATE_KEY_PATH

If you have not registered an agent yet, it takes one command and the keypair is generated on your machine. The private half never leaves it:

agentvalet register --code <invite-or-enrollment-code>

See Child agents for the full mechanism this section relies on: the mint flow, the attenuation semantics, and the honest caveats on delegation depth and approval replay.

The workers: one child identity each

Before the supervisor routes work to a subagent node, it mints a child. The request names the slice; AgentValet intersects it with the supervisor’s own current grants, so a child can never hold a scope its parent does not:

child = av.issue_child( name="research-worker", ttl_seconds=600, grants=[{"platform": "github", "scopes": ["github:repo.read", "github:contents.read"]}], )

Put only child.bearer_token into the worker’s slice of graph state. The worker builds its own client from that single string:

worker_av = AgentValet.from_bearer(child.bearer_token) result = worker_av.call( platform="github", endpoint="/repos/your-org/your-repo", scope="github:repo.read" )

A minimal graph looks like this:

from langgraph.graph import StateGraph, START, END def supervisor(state): child = av.issue_child( name="research-worker", ttl_seconds=600, grants=[{"platform": "github", "scopes": ["github:repo.read"]}], ) return {"worker_bearer": child.bearer_token} def worker(state): worker_av = AgentValet.from_bearer(state["worker_bearer"]) repo = worker_av.call( platform="github", endpoint="/repos/your-org/your-repo", scope="github:repo.read" ) return {"summary": repo["description"]} g = StateGraph(dict) g.add_node("supervisor", supervisor) g.add_node("worker", worker) g.add_edge(START, "supervisor") g.add_edge("supervisor", "worker") g.add_edge("worker", END) app = g.compile()

A fuller two-worker example ships with the package: examples/05_langgraph_subagents.py.

What this buys you

Attenuation is enforced, not advisory. The child’s grants are written server-side at mint time as the intersection with the parent’s live grants, and re-checked on every call. A worker that asks for a scope outside its slice gets 403 scope_not_granted, even if the supervisor holds that scope.

Every call is attributed to the node that made it. The audit log records the child’s own agent id, so “which worker read that file” is a column, not a reconstruction from timestamps.

Revoke one worker without touching the rest. Suspend a child in the dashboard and its very next call is denied, bearer or no bearer, while its siblings keep running. Revoke or suspend the supervisor and every child is contained on its next call too. Children expire on their own (60 to 3600 seconds) and are garbage-collected.

Honest constraints

  • The bearer is the child’s whole credential. It lives in your graph state for the child’s lifetime. Keep TTLs short, keep slices narrow, and never log state that contains it.
  • Depth is one. A child cannot mint children. If a worker needs to delegate further, route back through the supervisor.
  • Static headers are fine here. Unlike the parent’s 60-second signed assertions, a child bearer is valid for the child’s full TTL, which is exactly what makes it safe to pin into a worker at construction. Enforcement does not depend on the token expiring: revocation is checked on every call server-side.
  • issue_child is a parent-only call. A bearer-mode client refuses it client-side; the server enforces the same rule.

Alternative: MCP tools instead of the SDK

If your LangGraph nodes consume tools via MCP (for example through langchain-mcp-adapters), the AgentValet stdio server exposes the same capability as a tool: issue_child_agent returns the child’s bearer for you to hand to a subagent. The SDK path above is the more natural fit when your nodes are plain Python.

Last updated on