dome-langchain 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dome_langchain-0.1.0/.gitignore +48 -0
- dome_langchain-0.1.0/PKG-INFO +196 -0
- dome_langchain-0.1.0/README.md +155 -0
- dome_langchain-0.1.0/examples/README.md +231 -0
- dome_langchain-0.1.0/examples/bootstrap.py +359 -0
- dome_langchain-0.1.0/examples/bootstrap.sh +39 -0
- dome_langchain-0.1.0/examples/chain_e2e.py +320 -0
- dome_langchain-0.1.0/examples/lib/__init__.py +1 -0
- dome_langchain-0.1.0/examples/lib/config.py +164 -0
- dome_langchain-0.1.0/examples/spawn_e2e.py +219 -0
- dome_langchain-0.1.0/pyproject.toml +80 -0
- dome_langchain-0.1.0/src/dome_langchain/__init__.py +135 -0
- dome_langchain-0.1.0/src/dome_langchain/broker.py +201 -0
- dome_langchain-0.1.0/src/dome_langchain/chat.py +184 -0
- dome_langchain-0.1.0/src/dome_langchain/chat_anthropic.py +201 -0
- dome_langchain-0.1.0/src/dome_langchain/compose.py +584 -0
- dome_langchain-0.1.0/src/dome_langchain/governed_tool.py +206 -0
- dome_langchain-0.1.0/src/dome_langchain/graph.py +206 -0
- dome_langchain-0.1.0/src/dome_langchain/py.typed +0 -0
- dome_langchain-0.1.0/tests/README.md +200 -0
- dome_langchain-0.1.0/tests/__init__.py +0 -0
- dome_langchain-0.1.0/tests/_helpers.py +264 -0
- dome_langchain-0.1.0/tests/conftest.py +31 -0
- dome_langchain-0.1.0/tests/integration/conftest.py +84 -0
- dome_langchain-0.1.0/tests/integration/test_broker_e2e.py +48 -0
- dome_langchain-0.1.0/tests/integration/test_spawn_e2e.py +71 -0
- dome_langchain-0.1.0/tests/test_act_as.py +223 -0
- dome_langchain-0.1.0/tests/test_chain.py +301 -0
- dome_langchain-0.1.0/tests/test_chat.py +213 -0
- dome_langchain-0.1.0/tests/test_compose.py +415 -0
- dome_langchain-0.1.0/tests/test_governed_tool.py +363 -0
- dome_langchain-0.1.0/tests/test_spawn.py +169 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
ENV/
|
|
16
|
+
|
|
17
|
+
# Testing
|
|
18
|
+
.coverage
|
|
19
|
+
coverage.xml
|
|
20
|
+
coverage.html
|
|
21
|
+
htmlcov/
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
|
|
24
|
+
# Type checking
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
*~
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
37
|
+
|
|
38
|
+
# Environment
|
|
39
|
+
.env
|
|
40
|
+
.env.local
|
|
41
|
+
.env.*.local
|
|
42
|
+
.dome-demo.env
|
|
43
|
+
|
|
44
|
+
# Claude
|
|
45
|
+
.claude/settings.local.json
|
|
46
|
+
|
|
47
|
+
# Build artifacts
|
|
48
|
+
tmp/
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dome-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dome Platform LangChain adapter — govern LangChain tools with Dome authorization
|
|
5
|
+
Project-URL: Homepage, https://domesystems.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.domesystems.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/dome-systems/sdk-dome-python
|
|
8
|
+
Project-URL: Issues, https://github.com/dome-systems/sdk-dome-python/issues
|
|
9
|
+
Author-email: Dome Systems <eng@domesystems.ai>
|
|
10
|
+
License: Proprietary
|
|
11
|
+
Keywords: agent-governance,ai-agents,authorization,dome,langchain,langgraph,llm,tools
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: Other/Proprietary License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: dome-sdk>=0.1.0
|
|
24
|
+
Requires-Dist: langchain-core>=0.3
|
|
25
|
+
Provides-Extra: anthropic
|
|
26
|
+
Requires-Dist: langchain-anthropic>=0.2; extra == 'anthropic'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: langchain-anthropic>=0.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: langchain-openai>=0.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: langgraph>=0.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
36
|
+
Provides-Extra: langgraph
|
|
37
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
38
|
+
Provides-Extra: openai
|
|
39
|
+
Requires-Dist: langchain-openai>=0.2; extra == 'openai'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Dome LangChain Adapter
|
|
43
|
+
|
|
44
|
+
LangChain / LangGraph integration for the [Dome Platform](https://domesystems.ai).
|
|
45
|
+
It injects Dome **identity** and **authorization** into the seams of an
|
|
46
|
+
existing LangChain app — it is *additive*, not a replacement for LangChain.
|
|
47
|
+
|
|
48
|
+
There are three surfaces, each opt-in:
|
|
49
|
+
|
|
50
|
+
| Surface | What it governs | Key symbols |
|
|
51
|
+
|---------|-----------------|-------------|
|
|
52
|
+
| Governed tools | tool calls | `DomeGovernedTool`, `govern_tools` |
|
|
53
|
+
| Governed chat models | LLM calls | `DomeChatOpenAI`, `DomeChatAnthropic`, `broker_chat`, `broker_chat_for` |
|
|
54
|
+
| Compose primitives | agent-to-agent handoffs & spawned fleets | `Gate`, `gate_edge`, `gate_command`, `session`, `mint_ephemeral`, `fleet_send`, `causal` |
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install dome-langchain # core adapter: tools + gates + spawn
|
|
60
|
+
pip install 'dome-langchain[openai]' # + DomeChatOpenAI (langchain-openai)
|
|
61
|
+
pip install 'dome-langchain[anthropic]' # + DomeChatAnthropic (langchain-anthropic)
|
|
62
|
+
pip install 'dome-langchain[langgraph]' # + the LangGraph shims
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The core install needs only `langchain-core`. The chat-model surfaces pull in a
|
|
66
|
+
provider client; importing `DomeChatOpenAI` / `DomeChatAnthropic` without the
|
|
67
|
+
matching extra raises an `ImportError` with the install hint.
|
|
68
|
+
|
|
69
|
+
## 1. Governed tools
|
|
70
|
+
|
|
71
|
+
`DomeGovernedTool` wraps any LangChain `BaseTool`. Before each execution it
|
|
72
|
+
calls `dome.DomeClient.check()`; on deny it returns a denial message instead of
|
|
73
|
+
running, and audit events are emitted automatically.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import dome
|
|
77
|
+
from dome_langchain import DomeGovernedTool, govern_tools
|
|
78
|
+
|
|
79
|
+
client = dome.DomeClient(dome.DomeConfig(base_url="https://api.domesystems.ai", token="dome_..."))
|
|
80
|
+
client.start()
|
|
81
|
+
|
|
82
|
+
governed = DomeGovernedTool(tool=search_tool, dome_client=client)
|
|
83
|
+
# or wrap several at once:
|
|
84
|
+
tools = govern_tools(client, [search_tool, db_tool, email_tool])
|
|
85
|
+
|
|
86
|
+
client.close()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 2. Governed chat models
|
|
90
|
+
|
|
91
|
+
`DomeChatOpenAI` (the name mirrors its `ChatOpenAI` base; the older name
|
|
92
|
+
`DomeChatModel` still works as an alias) is a `ChatOpenAI` subclass whose
|
|
93
|
+
requests flow through the Dome Broker (the LLM gateway) under a specific
|
|
94
|
+
agent's identity. The Broker
|
|
95
|
+
authenticates the call as that agent, evaluates Cedar for the model action,
|
|
96
|
+
audits it, then forwards to the upstream provider. Because it *is* a
|
|
97
|
+
`ChatOpenAI`, streaming, tool-calling, and structured output work unchanged.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from dome_langchain import AgentIdentity, DomeChatOpenAI
|
|
101
|
+
|
|
102
|
+
# `for_agent` accepts any agent identity (anything with `token` +
|
|
103
|
+
# `gateway_endpoint`) — an `EphemeralAgent` from the spawn API, or an
|
|
104
|
+
# `AgentIdentity` for an ordinary long-lived agent:
|
|
105
|
+
llm = DomeChatOpenAI.for_agent(
|
|
106
|
+
AgentIdentity(token="dome_...", gateway_endpoint="https://gw.example.com")
|
|
107
|
+
)
|
|
108
|
+
llm.invoke("Summarise this ticket.") # called as that agent
|
|
109
|
+
|
|
110
|
+
# End-user delegation (act-as) — per call, or bound once:
|
|
111
|
+
llm.invoke("Summarise this ticket.", act_as=user) # ActAs or OIDC-JWT string
|
|
112
|
+
per_user = llm.with_act_as(user)
|
|
113
|
+
per_user.invoke("Summarise this ticket.")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`act_as` is encoded into an `X-Dome-Act-As` header on that request only; the
|
|
117
|
+
Broker exposes it to Cedar as `principal.act_as` and does not forward it
|
|
118
|
+
upstream. `broker_chat` / `broker_chat_for` are factory shims that return a
|
|
119
|
+
`DomeChatOpenAI` (with a construction-time act-as default).
|
|
120
|
+
|
|
121
|
+
For Claude-native features (prompt caching, extended thinking, citations) use
|
|
122
|
+
the Anthropic ingress instead:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from dome_langchain import DomeChatAnthropic
|
|
126
|
+
llm = DomeChatAnthropic.for_agent(agent, model="claude-haiku-4-5")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 3. Compose primitives (chains & spawn)
|
|
130
|
+
|
|
131
|
+
### Gated handoffs
|
|
132
|
+
|
|
133
|
+
A `Gate` turns a Dome authorization check into a routing decision. `gate_edge`
|
|
134
|
+
adapts it to a LangGraph conditional edge (`(state) -> next_node`); `gate_command`
|
|
135
|
+
is the newer node-returns-`Command` idiom. `Gate` is also a `Runnable` **guard**:
|
|
136
|
+
in an LCEL chain, `gate | next` forwards the payload on allow and raises
|
|
137
|
+
`GateDenied` on deny. (Use `gate.evaluate()` when you want the `GateDecision`
|
|
138
|
+
value instead.)
|
|
139
|
+
|
|
140
|
+
> **What a gate checks — and what it doesn't.** A gate evaluates Cedar with the
|
|
141
|
+
> agent backing `dome_client` (typically the orchestrator/parent) as the
|
|
142
|
+
> **principal** — *not* the downstream node. The `from_node`/`to_node` labels
|
|
143
|
+
> and any extra context are **application-asserted, not verified**: a gate is a
|
|
144
|
+
> client-side routing decision, not a cryptographic boundary, so it governs hops
|
|
145
|
+
> only insofar as your nodes assert that context honestly. Verified call-chain
|
|
146
|
+
> identity is a planned platform feature (S.PRP.008).
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from dome_langchain import gate_edge
|
|
150
|
+
|
|
151
|
+
g.add_conditional_edges(
|
|
152
|
+
"classifier",
|
|
153
|
+
gate_edge(dome_client=client, from_node="classifier",
|
|
154
|
+
to_node="retriever", resource="chain.hop_allowed"),
|
|
155
|
+
{"retriever": "retriever"},
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
On allow the hop proceeds; on deny it routes to an `on_deny` node or raises
|
|
160
|
+
`GateDenied`.
|
|
161
|
+
|
|
162
|
+
### Spawned ephemeral fleets
|
|
163
|
+
|
|
164
|
+
`session` + `mint_ephemeral` register real, short-lived child agents (each with
|
|
165
|
+
its own Dome identity and token) and tear them down on exit. `fleet_send` fans
|
|
166
|
+
them out across a LangGraph node via `Send`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from dome_langchain import session, fleet_send, DomeChatOpenAI
|
|
170
|
+
|
|
171
|
+
with session(platform, parent_agent_id=parent.agent_id, gateway_endpoint=gw) as s:
|
|
172
|
+
fleet = s.spawn(count=4, template="researcher")
|
|
173
|
+
sends = fleet_send(node="research", fleet=fleet,
|
|
174
|
+
payload_fn=lambda agent, i: {"agent": agent, "task": tasks[i]})
|
|
175
|
+
# inside the "research" node, each worker calls the LLM as itself:
|
|
176
|
+
# llm = DomeChatOpenAI.for_agent(state["agent"])
|
|
177
|
+
# session exit revokes + hard-deletes the whole fleet
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`mint_ephemeral` / `session` require a `dome.DomeAdminClient` (a workspace-admin
|
|
181
|
+
client), not the per-agent `DomeClient`.
|
|
182
|
+
|
|
183
|
+
## Examples
|
|
184
|
+
|
|
185
|
+
Runnable end-to-end demos live in [`examples/`](./examples) (chain, spawn,
|
|
186
|
+
broker). They require a provisioned Dome workspace — see
|
|
187
|
+
[`examples/README.md`](./examples/README.md).
|
|
188
|
+
|
|
189
|
+
## Documentation
|
|
190
|
+
|
|
191
|
+
- [Dome Platform Docs](https://docs.domesystems.ai)
|
|
192
|
+
- [Core Python SDK](https://github.com/dome-systems/sdk-dome-python)
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
Proprietary. See LICENSE for details.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Dome LangChain Adapter
|
|
2
|
+
|
|
3
|
+
LangChain / LangGraph integration for the [Dome Platform](https://domesystems.ai).
|
|
4
|
+
It injects Dome **identity** and **authorization** into the seams of an
|
|
5
|
+
existing LangChain app — it is *additive*, not a replacement for LangChain.
|
|
6
|
+
|
|
7
|
+
There are three surfaces, each opt-in:
|
|
8
|
+
|
|
9
|
+
| Surface | What it governs | Key symbols |
|
|
10
|
+
|---------|-----------------|-------------|
|
|
11
|
+
| Governed tools | tool calls | `DomeGovernedTool`, `govern_tools` |
|
|
12
|
+
| Governed chat models | LLM calls | `DomeChatOpenAI`, `DomeChatAnthropic`, `broker_chat`, `broker_chat_for` |
|
|
13
|
+
| Compose primitives | agent-to-agent handoffs & spawned fleets | `Gate`, `gate_edge`, `gate_command`, `session`, `mint_ephemeral`, `fleet_send`, `causal` |
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install dome-langchain # core adapter: tools + gates + spawn
|
|
19
|
+
pip install 'dome-langchain[openai]' # + DomeChatOpenAI (langchain-openai)
|
|
20
|
+
pip install 'dome-langchain[anthropic]' # + DomeChatAnthropic (langchain-anthropic)
|
|
21
|
+
pip install 'dome-langchain[langgraph]' # + the LangGraph shims
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The core install needs only `langchain-core`. The chat-model surfaces pull in a
|
|
25
|
+
provider client; importing `DomeChatOpenAI` / `DomeChatAnthropic` without the
|
|
26
|
+
matching extra raises an `ImportError` with the install hint.
|
|
27
|
+
|
|
28
|
+
## 1. Governed tools
|
|
29
|
+
|
|
30
|
+
`DomeGovernedTool` wraps any LangChain `BaseTool`. Before each execution it
|
|
31
|
+
calls `dome.DomeClient.check()`; on deny it returns a denial message instead of
|
|
32
|
+
running, and audit events are emitted automatically.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import dome
|
|
36
|
+
from dome_langchain import DomeGovernedTool, govern_tools
|
|
37
|
+
|
|
38
|
+
client = dome.DomeClient(dome.DomeConfig(base_url="https://api.domesystems.ai", token="dome_..."))
|
|
39
|
+
client.start()
|
|
40
|
+
|
|
41
|
+
governed = DomeGovernedTool(tool=search_tool, dome_client=client)
|
|
42
|
+
# or wrap several at once:
|
|
43
|
+
tools = govern_tools(client, [search_tool, db_tool, email_tool])
|
|
44
|
+
|
|
45
|
+
client.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2. Governed chat models
|
|
49
|
+
|
|
50
|
+
`DomeChatOpenAI` (the name mirrors its `ChatOpenAI` base; the older name
|
|
51
|
+
`DomeChatModel` still works as an alias) is a `ChatOpenAI` subclass whose
|
|
52
|
+
requests flow through the Dome Broker (the LLM gateway) under a specific
|
|
53
|
+
agent's identity. The Broker
|
|
54
|
+
authenticates the call as that agent, evaluates Cedar for the model action,
|
|
55
|
+
audits it, then forwards to the upstream provider. Because it *is* a
|
|
56
|
+
`ChatOpenAI`, streaming, tool-calling, and structured output work unchanged.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from dome_langchain import AgentIdentity, DomeChatOpenAI
|
|
60
|
+
|
|
61
|
+
# `for_agent` accepts any agent identity (anything with `token` +
|
|
62
|
+
# `gateway_endpoint`) — an `EphemeralAgent` from the spawn API, or an
|
|
63
|
+
# `AgentIdentity` for an ordinary long-lived agent:
|
|
64
|
+
llm = DomeChatOpenAI.for_agent(
|
|
65
|
+
AgentIdentity(token="dome_...", gateway_endpoint="https://gw.example.com")
|
|
66
|
+
)
|
|
67
|
+
llm.invoke("Summarise this ticket.") # called as that agent
|
|
68
|
+
|
|
69
|
+
# End-user delegation (act-as) — per call, or bound once:
|
|
70
|
+
llm.invoke("Summarise this ticket.", act_as=user) # ActAs or OIDC-JWT string
|
|
71
|
+
per_user = llm.with_act_as(user)
|
|
72
|
+
per_user.invoke("Summarise this ticket.")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`act_as` is encoded into an `X-Dome-Act-As` header on that request only; the
|
|
76
|
+
Broker exposes it to Cedar as `principal.act_as` and does not forward it
|
|
77
|
+
upstream. `broker_chat` / `broker_chat_for` are factory shims that return a
|
|
78
|
+
`DomeChatOpenAI` (with a construction-time act-as default).
|
|
79
|
+
|
|
80
|
+
For Claude-native features (prompt caching, extended thinking, citations) use
|
|
81
|
+
the Anthropic ingress instead:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from dome_langchain import DomeChatAnthropic
|
|
85
|
+
llm = DomeChatAnthropic.for_agent(agent, model="claude-haiku-4-5")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 3. Compose primitives (chains & spawn)
|
|
89
|
+
|
|
90
|
+
### Gated handoffs
|
|
91
|
+
|
|
92
|
+
A `Gate` turns a Dome authorization check into a routing decision. `gate_edge`
|
|
93
|
+
adapts it to a LangGraph conditional edge (`(state) -> next_node`); `gate_command`
|
|
94
|
+
is the newer node-returns-`Command` idiom. `Gate` is also a `Runnable` **guard**:
|
|
95
|
+
in an LCEL chain, `gate | next` forwards the payload on allow and raises
|
|
96
|
+
`GateDenied` on deny. (Use `gate.evaluate()` when you want the `GateDecision`
|
|
97
|
+
value instead.)
|
|
98
|
+
|
|
99
|
+
> **What a gate checks — and what it doesn't.** A gate evaluates Cedar with the
|
|
100
|
+
> agent backing `dome_client` (typically the orchestrator/parent) as the
|
|
101
|
+
> **principal** — *not* the downstream node. The `from_node`/`to_node` labels
|
|
102
|
+
> and any extra context are **application-asserted, not verified**: a gate is a
|
|
103
|
+
> client-side routing decision, not a cryptographic boundary, so it governs hops
|
|
104
|
+
> only insofar as your nodes assert that context honestly. Verified call-chain
|
|
105
|
+
> identity is a planned platform feature (S.PRP.008).
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from dome_langchain import gate_edge
|
|
109
|
+
|
|
110
|
+
g.add_conditional_edges(
|
|
111
|
+
"classifier",
|
|
112
|
+
gate_edge(dome_client=client, from_node="classifier",
|
|
113
|
+
to_node="retriever", resource="chain.hop_allowed"),
|
|
114
|
+
{"retriever": "retriever"},
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
On allow the hop proceeds; on deny it routes to an `on_deny` node or raises
|
|
119
|
+
`GateDenied`.
|
|
120
|
+
|
|
121
|
+
### Spawned ephemeral fleets
|
|
122
|
+
|
|
123
|
+
`session` + `mint_ephemeral` register real, short-lived child agents (each with
|
|
124
|
+
its own Dome identity and token) and tear them down on exit. `fleet_send` fans
|
|
125
|
+
them out across a LangGraph node via `Send`.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from dome_langchain import session, fleet_send, DomeChatOpenAI
|
|
129
|
+
|
|
130
|
+
with session(platform, parent_agent_id=parent.agent_id, gateway_endpoint=gw) as s:
|
|
131
|
+
fleet = s.spawn(count=4, template="researcher")
|
|
132
|
+
sends = fleet_send(node="research", fleet=fleet,
|
|
133
|
+
payload_fn=lambda agent, i: {"agent": agent, "task": tasks[i]})
|
|
134
|
+
# inside the "research" node, each worker calls the LLM as itself:
|
|
135
|
+
# llm = DomeChatOpenAI.for_agent(state["agent"])
|
|
136
|
+
# session exit revokes + hard-deletes the whole fleet
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`mint_ephemeral` / `session` require a `dome.DomeAdminClient` (a workspace-admin
|
|
140
|
+
client), not the per-agent `DomeClient`.
|
|
141
|
+
|
|
142
|
+
## Examples
|
|
143
|
+
|
|
144
|
+
Runnable end-to-end demos live in [`examples/`](./examples) (chain, spawn,
|
|
145
|
+
broker). They require a provisioned Dome workspace — see
|
|
146
|
+
[`examples/README.md`](./examples/README.md).
|
|
147
|
+
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
- [Dome Platform Docs](https://docs.domesystems.ai)
|
|
151
|
+
- [Core Python SDK](https://github.com/dome-systems/sdk-dome-python)
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
Proprietary. See LICENSE for details.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# dome-langchain end-to-end demos
|
|
2
|
+
|
|
3
|
+
Runnable demos that show LangChain + Dome composing real multi-agent topologies against a live workspace. Each demo registers agents in Dome, runs them through the Broker for real LLM calls (Anthropic via OpenAI-compatible ingress), evaluates Cedar at every chain hop and every model call, and tears the agents down on exit.
|
|
4
|
+
|
|
5
|
+
Two patterns are demonstrated:
|
|
6
|
+
|
|
7
|
+
- **Chain** (`chain_e2e.py`) — three sequential agents with a Dome-evaluated gate between every hop.
|
|
8
|
+
- **Spawn** (`spawn_e2e.py`) — a parent agent mints N ephemeral children at runtime and fans work out via LangGraph `Send`.
|
|
9
|
+
|
|
10
|
+
Both run against staging (`api.staging.domesystems.ai`) by default and use the SDK Demo workspace.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
| Need | Why |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Access to the SDK Demo workspace on staging | Where the agents register, the Cedar bundle lands, and the Broker routes |
|
|
17
|
+
| A workspace platform API key (`dome_pk_...`) with the permissions in step 2 | Bootstrap and the demos use this to provision children, deploy rules, and configure the Broker |
|
|
18
|
+
| The workspace's UUID | Required in every Connect RPC body (see step 3 for how to find it) |
|
|
19
|
+
| `ANTHROPIC_API_KEY` in your shell | Bootstrap writes it into the workspace's Broker (server-side Vault); the SDK never reads it back |
|
|
20
|
+
| Python 3.13 + `uv` | The repo's tooling |
|
|
21
|
+
|
|
22
|
+
## 1 · Install (one-time)
|
|
23
|
+
|
|
24
|
+
From the SDK repo root:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uv venv --python 3.13 .venv
|
|
28
|
+
source .venv/bin/activate
|
|
29
|
+
uv pip install -e ".[dev]"
|
|
30
|
+
uv pip install -e "packages/dome-langchain[dev]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 2 · Create a workspace platform API key
|
|
34
|
+
|
|
35
|
+
In the staging dashboard, generate a workspace-scoped platform API key (`dome_pk_...`) with these permissions:
|
|
36
|
+
|
|
37
|
+
- `workspace:agent:register`
|
|
38
|
+
- `workspace:agent:revoke`
|
|
39
|
+
- `workspace:agent:delete`
|
|
40
|
+
- `workspace:agent:key:manage`
|
|
41
|
+
- `workspace:rules:deploy`
|
|
42
|
+
- `workspace:gateway:manage`
|
|
43
|
+
|
|
44
|
+
Copy the secret — staging only shows it once.
|
|
45
|
+
|
|
46
|
+
## 3 · Find the workspace UUID
|
|
47
|
+
|
|
48
|
+
The dashboard URL may use a slug rather than the UUID, in which case:
|
|
49
|
+
|
|
50
|
+
1. Open DevTools → Network tab.
|
|
51
|
+
2. Click anything in the SDK Demo workspace that loads data (Agents, Audit, anything).
|
|
52
|
+
3. Click any request to `api.staging.domesystems.ai`, inspect the request payload — `workspaceId` will be present. Copy it.
|
|
53
|
+
|
|
54
|
+
## 4 · Set environment variables
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
export DOME_BASE_URL=https://api.staging.domesystems.ai
|
|
58
|
+
export DOME_DASHBOARD_URL=https://app.staging.domesystems.ai
|
|
59
|
+
export DOME_PLATFORM_KEY=dome_pk_... # from step 2
|
|
60
|
+
export DOME_WORKSPACE_ID=<uuid> # from step 3
|
|
61
|
+
# ANTHROPIC_API_KEY already in your shell — used by bootstrap only
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 5 · Provision the workspace
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
cd packages/dome-langchain/examples
|
|
68
|
+
./bootstrap.sh provision
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Idempotent — re-running is safe. The script:
|
|
72
|
+
|
|
73
|
+
1. Registers `sdk-demo-parent` (the audit-hierarchy root for everything).
|
|
74
|
+
2. Issues / rotates its API key.
|
|
75
|
+
3. Creates an Anthropic LLM connection in the Broker (your `ANTHROPIC_API_KEY` is stored server-side in Vault).
|
|
76
|
+
4. Creates the default LLM pool and binds the Anthropic connection to it.
|
|
77
|
+
5. Deploys a workspace-scoped Cedar bundle containing the chain gate rule.
|
|
78
|
+
|
|
79
|
+
State is written to `.dome-demo.env` at the package root; the demos source it automatically.
|
|
80
|
+
|
|
81
|
+
Sanity-check what landed:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
./bootstrap.sh status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 6 · Run the chain demo
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
python chain_e2e.py # happy path
|
|
91
|
+
python chain_e2e.py --deny # context-driven deny
|
|
92
|
+
python chain_e2e.py --act-as alice@example.com # attach end-user identity
|
|
93
|
+
python chain_e2e.py --act-as alice@deny.example.com # identity-driven deny
|
|
94
|
+
python chain_e2e.py --act-as alice@deny.example.com --act-as-roles admin
|
|
95
|
+
python chain_e2e.py --keep # skip teardown
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
What happens, observable in the terminal and the dashboard:
|
|
99
|
+
|
|
100
|
+
| Step | Terminal | Dashboard |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Three children register | `agents registered:` with three UUIDs | Three new agents appear under `sdk-demo-parent` |
|
|
103
|
+
| Wait for gateway sync | `mint_ephemeral: waiting up to 20.0s for gateway sync` | (nothing — internal poll on `/v1/models`) |
|
|
104
|
+
| Classify | `[classifier] <output>` | `llm.called` event attributed to the classifier agent |
|
|
105
|
+
| Gate hop 1 | (no error means allowed) | `tool.called`, `action=chain:gate`, `from_node=classifier` |
|
|
106
|
+
| Retrieve | `[retriever] <output>` | `llm.called` attributed to the retriever agent |
|
|
107
|
+
| Gate hop 2 | (no error → allowed; `--deny` → `[CHAIN HALTED]`) | `tool.called` or `tool.denied` |
|
|
108
|
+
| Summarize (skipped on `--deny`) | `[summarizer] <output>` | `llm.called` attributed to the summarizer agent |
|
|
109
|
+
| Teardown | `tore down: revoked=3 deleted=3` | The three agents disappear |
|
|
110
|
+
|
|
111
|
+
### End-user identity (`--act-as`)
|
|
112
|
+
|
|
113
|
+
`--act-as <email>` attaches an end-user identity to every gate evaluation and every LLM call as `X-Dome-Act-As`. Cedar sees it as `principal.act_as.email` / `.roles` / `.groups` during evaluation, so policies can gate on *who the agent is acting for*, not just which agent is calling.
|
|
114
|
+
|
|
115
|
+
The deployed Cedar bundle ships with one identity-keyed rule:
|
|
116
|
+
|
|
117
|
+
```cedar
|
|
118
|
+
forbid (
|
|
119
|
+
principal,
|
|
120
|
+
action == Dome::Action::"chain:gate",
|
|
121
|
+
resource
|
|
122
|
+
) when {
|
|
123
|
+
principal has act_as
|
|
124
|
+
&& principal.act_as has email
|
|
125
|
+
&& principal.act_as.email like "*@deny.example.com"
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
So:
|
|
130
|
+
|
|
131
|
+
| Command | Outcome |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `chain_e2e.py --act-as alice@example.com` | All gates allow; X-Dome-Act-As decorated audit events |
|
|
134
|
+
| `chain_e2e.py --act-as alice@deny.example.com` | First gate denies; `[CHAIN HALTED]`; one `tool.denied` event with act-as attached |
|
|
135
|
+
| `chain_e2e.py --act-as alice@example.com --act-as-roles admin,billing` | Roles flow through; visible in Cedar context and audit |
|
|
136
|
+
|
|
137
|
+
The demo uses `method=none` act-as (plaintext base64-JSON). For production, switch the workspace's act-as policy to `method=oidc` and pass a verified OIDC JWT instead of an `ActAs` instance — the SDK accepts both. See the security section below.
|
|
138
|
+
|
|
139
|
+
**One subtlety the platform enforces and you should know about**: the Broker explicitly does *not* propagate `X-Dome-Act-As` to the upstream LLM provider (`internal/gateway/egress/llm/headers.go`). Anthropic never sees the user identity. The header is only used for Dome's own Cedar evaluation of the model call. That's the intentional contract — per-user identity isn't part of the LLM provider's interface.
|
|
140
|
+
|
|
141
|
+
## 7 · Run the spawn demo
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
python spawn_e2e.py --count 4
|
|
145
|
+
python spawn_e2e.py --count 8 --topic "..."
|
|
146
|
+
python spawn_e2e.py --keep
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
What happens:
|
|
150
|
+
|
|
151
|
+
| Step | Terminal | Dashboard |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| Mint N ephemeral researchers | `spawned 4 researchers: <ids>` | Four new agents under `sdk-demo-parent`, each with `template=researcher` and `spawn_run_id=<id>` in metadata |
|
|
154
|
+
| Gateway sync wait | `waiting up to 20.0s for gateway sync` | — |
|
|
155
|
+
| Fan-out via LangGraph `Send` | Four `[<agent-id>] <output>` lines, interleaved | Four parallel `llm.called` events |
|
|
156
|
+
| Aggregate findings | `[1] … [2] …` etc. | — |
|
|
157
|
+
| Teardown | `tore down: revoked=4 deleted=4` | All four agents disappear |
|
|
158
|
+
|
|
159
|
+
## 8 · Reset (when you're done)
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
./bootstrap.sh reset
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Revokes + hard-deletes every demo agent, deactivates the Cedar bundle, removes `.dome-demo.env`. The Broker connection + pool are left in place (workspace-level state you don't want to churn between runs).
|
|
166
|
+
|
|
167
|
+
## Gateway sync window
|
|
168
|
+
|
|
169
|
+
Newly-registered agents become callable on the workspace gateway **after the gateway's next sync tick**, not instantly. Default sync interval is **10 seconds**. During that window any gateway request from the new agent — `/v1/chat/completions`, `/v1/models`, anything — returns:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
HTTP 403 agent act-as configuration unavailable
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This is fail-closed behaviour by design (`internal/authorization/actas/middleware.go:65`). `mint_ephemeral` handles this transparently: after registering and issuing keys, it polls `GET /v1/models` (Cedar-filtered model listing, same auth path as the LLM call, no LLM tokens spent) until the gateway returns 200. Default timeout is 20s. Pass `wait_for_gateway_ready=False` to manage the wait yourself.
|
|
176
|
+
|
|
177
|
+
You'll see this log line on every spawn:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
INFO dome_langchain.compose: mint_ephemeral: waiting up to 20.0s for gateway sync (probing /v1/models)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If the probe times out you'll get a `TimeoutError` with the last status/body — that's a real signal something is wrong (sync loop stuck, wrong workspace, gateway misconfigured), not a normal flake.
|
|
184
|
+
|
|
185
|
+
## Common failure modes
|
|
186
|
+
|
|
187
|
+
| Symptom | Probable cause | Fix |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `CreateAgentKey -> 409 key name "sdk-demo" already exists` | A previous partial run left the parent's key behind | `ensure_agent_key` now create-or-rotates — should be transparent. If it still appears, `./bootstrap.sh reset` and retry. |
|
|
190
|
+
| `CreateLLMModelConnection -> 400 unknown auth_method` | You're on an older SDK — `auth_method` must be `api-key` (hyphen), `credential_type` must be `shared` | Pull the branch fresh |
|
|
191
|
+
| `CreateLLMPool -> 400 unknown cache_scope` | Same — must be `workspace`, `caller`, or omitted | Pull the branch fresh |
|
|
192
|
+
| `Request URL is missing an 'http://' or 'https://' protocol` | Child's `CreateAgentKey` response omitted `gateway_endpoint` | `mint_ephemeral`/`session.spawn` now accept a `gateway_endpoint` fallback that the demos pass through from the parent |
|
|
193
|
+
| `401 invalid or expired token` from Broker | Bearer is a raw API key, not a JWT | `mint_ephemeral` now exchanges API keys → JWTs internally |
|
|
194
|
+
| `403 agent act-as configuration unavailable` | Gateway hasn't synced the new agent yet | The 20s probe should handle this; if it times out, gateway sync may be stuck |
|
|
195
|
+
| `KeyError` in a LangGraph node | Demo bug (state schema), not Dome | See `chain_e2e.py` / `spawn_e2e.py` for the `TypedDict` pattern |
|
|
196
|
+
|
|
197
|
+
## Security posture — what the demos demonstrate, and what they don't
|
|
198
|
+
|
|
199
|
+
Honest evaluation of these patterns against a "production security best practice" bar.
|
|
200
|
+
|
|
201
|
+
### What's correctly modelled
|
|
202
|
+
|
|
203
|
+
| Property | How |
|
|
204
|
+
|---|---|
|
|
205
|
+
| **Identity per agent** | Every chain agent and every spawned worker is a distinct registry record with its own bearer token. No shared identities. |
|
|
206
|
+
| **Per-call audit attribution** | Every LLM call is audited under the calling agent's `agent_id`; you can reconstruct who did what when. |
|
|
207
|
+
| **Fail-closed authorization** | Cedar evaluates *both* at chain hops (gates) and at every model call (Broker, `S.AUT.011`). Unknown agents → 403. Missing rules → deny. |
|
|
208
|
+
| **Short-lived bearer tokens** | Agent API keys are exchanged for JWTs (default TTL ≈1h); the bearer presented to the Broker is the JWT, not the long-lived key. |
|
|
209
|
+
| **Provider-secret centralization** | The Anthropic API key lives in Vault behind the Broker. Children never see it. A compromised child cannot exfiltrate it. |
|
|
210
|
+
| **Lifecycle bookkeeping** | Ephemeral fleets are revoked *and* hard-deleted at session exit, even on exception. The revoked JWT is rejected immediately (`S.PRP.006`). |
|
|
211
|
+
| **Workspace isolation** | All agents, rules, audit, and provider config are scoped to the workspace (`M.ORG.003`). |
|
|
212
|
+
| **End-user identity via `X-Dome-Act-As`** | `--act-as <email>` attaches the user identity to every gate and every model call. Cedar can reason about it (`principal.act_as.email`, `.roles`, `.groups`). The deployed bundle includes a `*@deny.example.com` rule to demonstrate identity-driven deny. Uses `method=none` for the demo; production should configure `method=oidc` with a verified JWT. |
|
|
213
|
+
|
|
214
|
+
### Where these demos fall short of production best practice
|
|
215
|
+
|
|
216
|
+
| Gap | Why it matters | What closes it |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| **No delegation chain in identity** | The chain gate evaluates as the parent's principal, and downstream model calls carry the child's identity but not the chain (B → A → user). Cedar can't write rules that depend on the upstream chain. | `S.PRP.008` Agent-to-Agent Delegation (planned, not in code) — once landed, the SDK can swap the `caller_chain` context bag for a verified delegation claim with no public-API change. |
|
|
219
|
+
| **Act-as is `method=none` in the demo** | The demo uses plaintext base64-JSON for `X-Dome-Act-As`, which the workspace's gateway accepts as the agent's `actas_config.method=none`. A malicious caller could fabricate any user identity — Cedar would believe it. | Switch the workspace's act-as policy to `method=oidc` and require a verified OIDC JWT (the SDK already accepts JWTs through the same `act_as=` parameter). Or `method=hmac` and a workspace HMAC secret. Both verify the user identity cryptographically before Cedar sees it. |
|
|
220
|
+
| **Orchestrator holds a broad workspace key** | The script's `DOME_PLATFORM_KEY` has every permission needed to mint and tear down children. If the orchestrator process is compromised, an attacker can register or revoke arbitrary agents in the workspace. | Standard trade-off for an orchestrator service. Mitigations: scope the key to *just* the permissions needed; rotate often; store in a real secret manager (not `.dome-demo.env`); audit `agent.registered` events with alerting. |
|
|
221
|
+
| **No server-side scope clamp** | An orchestrator could mint children with capabilities or tier *higher than its own*. Today the SDK enforces "child ⊆ parent" client-side only. | Future: server-side Cedar rule on the `RegisterAgent` action that constrains child capabilities by the caller's tier. |
|
|
222
|
+
| **No classification firewall between chain steps** | A compromised middle agent could mislabel its output and slip data past a downstream gate. The demo treats every step's output as trusted input to the next. | The Atrium reference customer demonstrates the classification-firewall pattern: each step outputs a typed claim that's verified before becoming input. Worth borrowing for a chain that handles sensitive data. |
|
|
223
|
+
| **JWT refresh not modelled** | Long-running spawns (>1h) would see worker JWTs expire mid-run. Demos run in seconds, so it doesn't surface. | Production session manager that refreshes via `ExchangeToken` before the JWT's `exp`. |
|
|
224
|
+
| **`.dome-demo.env` is file-stored secrets** | OK for a local demo. Never production. | Production posture: read `DOME_PLATFORM_KEY` from a secret manager, never persist it on disk. |
|
|
225
|
+
| **Ephemeral identities are durable registry rows** | Every spawned worker creates a registry row that exists for the lifetime of the run. At high fan-out scale this means registry churn and audit-table growth. | Acceptable for governed scenarios — the audit trail is the whole point. For *truly* transient identities (no registry footprint) Dome doesn't currently have a primitive; that's a deliberate platform choice, not an oversight. |
|
|
226
|
+
|
|
227
|
+
### Net read
|
|
228
|
+
|
|
229
|
+
These patterns implement the **structural** security model Dome was built for — identity, authorization, audit per agent, fail-closed. That's the load-bearing 80%. The remaining 20% is application-level discipline (act-as for users, classification firewall for sensitive data, secret management) plus one genuine platform gap (`S.PRP.008` for verifiable delegation chains).
|
|
230
|
+
|
|
231
|
+
For a v0 "show me the platform works in my workspace" demo: it's correct. For a v1 production-shaped reference customer: switch act-as from `method=none` to `method=oidc`, swap `.dome-demo.env` for a secret manager, and consider the classification firewall pattern from Atrium.
|