Metadata-Version: 2.4
Name: dome-langchain
Version: 0.3.0
Summary: Dome Platform LangChain adapter — govern LangChain tools with Dome authorization
Project-URL: Homepage, https://domesystems.ai
Project-URL: Documentation, https://docs.domesystems.ai
Project-URL: Repository, https://github.com/dome-systems/sdk-dome-python
Project-URL: Issues, https://github.com/dome-systems/sdk-dome-python/issues
Author-email: Dome Systems <eng@domesystems.ai>
License: Proprietary
Keywords: agent-governance,ai-agents,authorization,dome,langchain,langgraph,llm,tools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: dome-sdk>=0.3.0
Requires-Dist: langchain-core>=0.3
Provides-Extra: anthropic
Requires-Dist: langchain-anthropic>=0.2; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: langchain-anthropic>=0.2; extra == 'dev'
Requires-Dist: langchain-openai>=0.2; extra == 'dev'
Requires-Dist: langgraph>=0.2; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
Provides-Extra: openai
Requires-Dist: langchain-openai>=0.2; extra == 'openai'
Description-Content-Type: text/markdown

# Dome LangChain Adapter

LangChain / LangGraph integration for the [Dome Platform](https://domesystems.ai).
It injects Dome **identity** and **authorization** into the seams of an
existing LangChain app — it is *additive*, not a replacement for LangChain.

There are three surfaces, each opt-in:

| Surface | What it governs | Key symbols |
|---------|-----------------|-------------|
| Governed tools | tool calls | `DomeGovernedTool`, `govern_tools` |
| Governed chat models | LLM calls | `DomeChatOpenAI`, `DomeChatAnthropic`, `broker_chat`, `broker_chat_for` |
| Compose primitives | agent-to-agent handoffs & spawned fleets | `Gate`, `gate_edge`, `gate_command`, `session`, `mint_ephemeral`, `fleet_send`, `causal` |

## Install

```bash
pip install dome-langchain                 # core adapter: tools + gates + spawn
pip install 'dome-langchain[openai]'       # + DomeChatOpenAI (langchain-openai)
pip install 'dome-langchain[anthropic]'    # + DomeChatAnthropic (langchain-anthropic)
pip install 'dome-langchain[langgraph]'    # + the LangGraph shims
```

The core install needs only `langchain-core`. The chat-model surfaces pull in a
provider client; importing `DomeChatOpenAI` / `DomeChatAnthropic` without the
matching extra raises an `ImportError` with the install hint.

## 1. Governed tools

`DomeGovernedTool` wraps any LangChain `BaseTool`. Before each execution it
calls `dome.Client.check()`; on deny it returns a denial message instead of
running, and audit events are emitted automatically.

```python
import dome
from dome_langchain import DomeGovernedTool, govern_tools

client = dome.Client(token="dome_...", control_plane_url="https://api.domesystems.ai")
client.start_policy_sync()

governed = DomeGovernedTool(tool=search_tool, dome_client=client)
# or wrap several at once:
tools = govern_tools(client, [search_tool, db_tool, email_tool])

# Wrapping real Dome MCP tools: pass the connection UUID so the local check
# addresses them the way the gateway does (see dome.Client.check).
tools = govern_tools(client, mcp_tools, connection_id="<connection-uuid>")

client.close()
```

`check()` runs in this process, so a governed tool is a **pre-check plus local
enforcement** — the gateway is not in the path and cannot enforce a call it never
sees. Rules gating on facts only the gateway holds (a tool's Gateway
memberships beyond the client's own, an LLM routing pool, connection attributes)
have no local answer and fail closed. Use `DomeGatewayTool` / `gateway_tool`, which
call through the gateway, when the gateway should be the one deciding.

## 2. Governed chat models

`DomeChatOpenAI` (the name mirrors its `ChatOpenAI` base; the older name
`DomeChatModel` still works as an alias) is a `ChatOpenAI` subclass whose
requests flow through the Dome Broker (the LLM gateway) under a specific
agent's identity. The Broker
authenticates the call as that agent, evaluates Cedar for the model action,
audits it, then forwards to the upstream provider. Because it *is* a
`ChatOpenAI`, streaming, tool-calling, and structured output work unchanged.

```python
import dome
from dome_langchain import AgentIdentity, DomeChatOpenAI

# `for_agent` accepts any agent identity (anything with `token` +
# `gateway_url`) — an `EphemeralAgent` from the spawn API, or an
# `AgentIdentity` for an ordinary long-lived agent. The endpoint must name
# the agent's Gateway (`{root}/gateways/{id}`); enrollment/bootstrap/token
# exchange return the complete call-ready URL:
llm = DomeChatOpenAI.for_agent(
    AgentIdentity(
        token="dome_...",
        gateway_url="https://gw.example.com/gateways/<gateway-id>",
    )
)
llm.invoke("Summarise this ticket.")          # called as that agent

# End-user delegation (act-as) — per call, or bound once:
llm.invoke("Summarise this ticket.", act_as=user)   # ActAs or OIDC-JWT string
per_user = llm.with_act_as(user)
per_user.invoke("Summarise this ticket.")
```

`act_as` is encoded into an `X-Dome-Act-As` header on that request only; the
Broker exposes it to Cedar as `principal.act_as` and does not forward it
upstream. `broker_chat` / `broker_chat_for` are factory shims that return a
`DomeChatOpenAI` (with a construction-time act-as default).

Every call also carries `X-Dome-Caller-Surface: sdk` so Dome audit attributes it
to the SDK — attribution telemetry only, never an authorization input.

For Claude-native features (prompt caching, extended thinking, citations) use
the Anthropic ingress instead:

```python
from dome_langchain import DomeChatAnthropic
llm = DomeChatAnthropic.for_agent(agent, model="claude-haiku-4-5")
```

## 3. Compose primitives (chains & spawn)

### Gated handoffs

A `Gate` turns a Dome authorization check into a routing decision. `gate_edge`
adapts it to a LangGraph conditional edge (`(state) -> next_node`); `gate_command`
is the newer node-returns-`Command` idiom. `Gate` is also a `Runnable` **guard**:
in an LCEL chain, `gate | next` forwards the payload on allow and raises
`GateDenied` on deny. (Use `gate.evaluate()` when you want the `GateDecision`
value instead.)

> **What a gate checks — and what it doesn't.** A gate evaluates Cedar with the
> agent backing `dome_client` (typically the orchestrator/parent) as the
> **principal** — *not* the downstream node. The `from_node`/`to_node` labels
> and any extra context are **application-asserted, not verified**: a gate is a
> client-side routing decision, not a cryptographic boundary, so it governs hops
> only insofar as your nodes assert that context honestly. Verified call-chain
> identity is a planned platform feature (S.PRP.008).

```python
from dome_langchain import gate_edge

g.add_conditional_edges(
    "classifier",
    gate_edge(dome_client=client, from_node="classifier",
              to_node="retriever", resource="chain.hop_allowed"),
    {"retriever": "retriever"},
)
```

On allow the hop proceeds; on deny it routes to an `on_deny` node or raises
`GateDenied`.

### Spawned ephemeral fleets

`session` + `mint_ephemeral` register real, short-lived child agents (each with
its own Dome identity and token) and tear them down on exit. `fleet_send` fans
them out across a LangGraph node via `Send`.

```python
from dome_langchain import session, fleet_send, DomeChatOpenAI

# `gateway_id` selects the Gateway every child is granted. Key creation and
# token exchange return each child's complete `/gateways/{id}` URL.
with session(platform, parent_agent_id=parent.agent_id,
             gateway_id=gateway_id) as s:
    fleet = s.spawn(count=4, template="researcher")
    sends = fleet_send(node="research", fleet=fleet,
                       payload_fn=lambda agent, i: {"agent": agent, "task": tasks[i]})
    # inside the "research" node, each worker calls the LLM as itself:
    #   llm = DomeChatOpenAI.for_agent(state["agent"])
# session exit revokes + hard-deletes the whole fleet
```

`mint_ephemeral` / `session` require a `dome.DomeAdminClient` (a workspace-admin
client), not the per-agent `DomeClient`.

## Examples

Runnable end-to-end demos live in [`examples/`](./examples) (chain, spawn,
broker). They require a provisioned Dome workspace — see
[`examples/README.md`](./examples/README.md).

## Documentation

- [Dome Platform Docs](https://docs.domesystems.ai)
- [Core Python SDK](https://github.com/dome-systems/sdk-dome-python)

## License

Proprietary. See LICENSE for details.
