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.
Files changed (32) hide show
  1. dome_langchain-0.1.0/.gitignore +48 -0
  2. dome_langchain-0.1.0/PKG-INFO +196 -0
  3. dome_langchain-0.1.0/README.md +155 -0
  4. dome_langchain-0.1.0/examples/README.md +231 -0
  5. dome_langchain-0.1.0/examples/bootstrap.py +359 -0
  6. dome_langchain-0.1.0/examples/bootstrap.sh +39 -0
  7. dome_langchain-0.1.0/examples/chain_e2e.py +320 -0
  8. dome_langchain-0.1.0/examples/lib/__init__.py +1 -0
  9. dome_langchain-0.1.0/examples/lib/config.py +164 -0
  10. dome_langchain-0.1.0/examples/spawn_e2e.py +219 -0
  11. dome_langchain-0.1.0/pyproject.toml +80 -0
  12. dome_langchain-0.1.0/src/dome_langchain/__init__.py +135 -0
  13. dome_langchain-0.1.0/src/dome_langchain/broker.py +201 -0
  14. dome_langchain-0.1.0/src/dome_langchain/chat.py +184 -0
  15. dome_langchain-0.1.0/src/dome_langchain/chat_anthropic.py +201 -0
  16. dome_langchain-0.1.0/src/dome_langchain/compose.py +584 -0
  17. dome_langchain-0.1.0/src/dome_langchain/governed_tool.py +206 -0
  18. dome_langchain-0.1.0/src/dome_langchain/graph.py +206 -0
  19. dome_langchain-0.1.0/src/dome_langchain/py.typed +0 -0
  20. dome_langchain-0.1.0/tests/README.md +200 -0
  21. dome_langchain-0.1.0/tests/__init__.py +0 -0
  22. dome_langchain-0.1.0/tests/_helpers.py +264 -0
  23. dome_langchain-0.1.0/tests/conftest.py +31 -0
  24. dome_langchain-0.1.0/tests/integration/conftest.py +84 -0
  25. dome_langchain-0.1.0/tests/integration/test_broker_e2e.py +48 -0
  26. dome_langchain-0.1.0/tests/integration/test_spawn_e2e.py +71 -0
  27. dome_langchain-0.1.0/tests/test_act_as.py +223 -0
  28. dome_langchain-0.1.0/tests/test_chain.py +301 -0
  29. dome_langchain-0.1.0/tests/test_chat.py +213 -0
  30. dome_langchain-0.1.0/tests/test_compose.py +415 -0
  31. dome_langchain-0.1.0/tests/test_governed_tool.py +363 -0
  32. 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.