Metadata-Version: 2.4
Name: dome-sdk
Version: 0.3.0
Summary: Dome Platform Python SDK — AI agent governance
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,audit,authorization,authz,cedar,dome,llm
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: cedarpy>=4.0
Requires-Dist: httpx>=0.27
Requires-Dist: pyjwt[crypto]>=2.0
Provides-Extra: dev
Requires-Dist: anthropic==0.112.0; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: openai==2.44.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# Dome Python SDK

The official Python SDK for Dome agent governance. Use `dome.Client` for
gateway-backed tools, LLM calls, audit reads, activity correlation, and optional
local policy evaluation from one SDK surface.

## Install

```bash
pip install dome-sdk
```

Provider SDKs are optional. `client.gateway.openai_client()` lazy-imports
`openai`; `client.gateway.anthropic_client()` lazy-imports `anthropic`. Install
those packages only in applications that use those factories.

## Gateway Quick Start

```python
import dome

client = dome.Client(
    token="dome_...",
    gateway_url="https://gateway.example.com/gateways/<gateway-id>",
    act_as_method="none",
)
client.connect()

tools = client.gateway.tools.list(
    act_as=dome.PlainActAs(email="alice@example.com"),
)
print([tool.name for tool in tools])

result = client.gateway.tools.call(
    "github/list_issues",
    {"repo": "dome"},
    act_as=dome.PlainActAs(email="alice@example.com"),
)
print(result.content)

response = client.gateway.llm.chat(
    model="prod-gpt",
    messages=[{"role": "user", "content": "Summarize the open incidents"}],
    act_as=dome.PlainActAs(email="alice@example.com"),
)

client.close()
```

The gateway URL names the one Gateway this agent is granted
(`/gateways/{uuid}`). There is no root endpoint — a bare-root URL (no `/gateways/{id}`
segment) fails closed with `DomeGatewayConfigurationError` at `connect()`.
When `control_plane_url` is configured, token exchange returns this complete
URL. Pass `gateway_id` to select a Gateway explicitly. Omitting it succeeds
only when the agent can access exactly one Gateway; a default marker does not
resolve an otherwise ambiguous selection.

Gateway calls are authorized, routed, credential-checked, and audited by Dome.
The SDK does not require provider API keys in agent code.

## Client Configuration

All URLs are named by plane:

| Argument | Description |
| --- | --- |
| `token` | Agent API key or exchanged token. |
| `control_plane_url` | Dome control-plane URL, used for token exchange, gateway discovery, local policy sync, and audit reads. |
| `gateway_url` | Complete Dome gateway data-plane URL, including the `/gateways/{id}` prefix. Required for gateway-only use; bare roots and surface URLs ending in `/mcp` or `/v1` fail closed. |
| `gateway_id` | Optional Gateway UUID selection sent during token exchange. Omission succeeds only when exactly one Gateway is accessible. If `gateway_url` is also supplied, its path must name the same UUID. |
| `gateway_auth_mode` | `auto` (default), `raw`, or `exchange`. With an explicit `gateway_url`, `auto` uses the raw `dome_*` key. |
| `act_as_method` | Gateway act-as method: `none`, `hmac`, `oidc`, or `bound`. Required when the SDK cannot discover it from the control plane. |
| `tools_list_cache_ttl` | In-memory tools/list cache TTL, default 30 seconds. |

`gateway_url` and `gateway_id` are validated at `connect()`. The SDK does not
compose customer URLs from an unscoped data-plane base: explicit URLs must
already be complete, and token-exchange responses are authoritative.

`connect()` prepares token/gateway transport state. It does not block on local
Cedar bundle sync. Call `start_policy_sync()` only when you want local
self-enforced checks.

## Gateway Readiness

`client.gateway.wait_ready()` is an explicit diagnostic/bootstrap helper, not a
startup requirement.

```python
client.gateway.wait_ready(timeout=30)
```

It polls authenticated `GET /gateways/{id}/ready` for the selected Gateway. For newly created raw
keys, use `dome.wait_for_agent_key(...)` or `dome.bootstrap.ensure_agent(...,
wait_gateway=True)` in setup scripts when you need to wait for the gateway's
synced API-key snapshot.

## Tools

`tools/list` can do real gateway work: upstream discovery, per-user
authorization, audit emission, and credential-link generation. The SDK never
does an implicit list-before-call.

```python
catalog = client.gateway.tools.list(refresh=True, act_as=user)
cached = client.gateway.tools.list_cached(act_as=user)
client.gateway.tools.invalidate_cache(act_as=user)
```

The cache is in-memory, bounded, keyed by gateway, agent, act-as method, and an
act-as header hash. Cache hits do not call the gateway and do not emit gateway
audit. Non-blocking credential advisories from `tools/list` are returned on the
fresh response and are not cached by default.

`tools/call` distinguishes JSON-RPC errors from successful MCP tool errors.
If an upstream tool returns `isError=true`, the SDK raises
`DomeToolExecutionError` — a failed tool call is not a success-shaped return
value. Pass `raise_on_tool_error=False` to get `ToolCallResult(is_error=True)`
back instead, for callers that want to read the partial `content` on a failed
call; the raised error carries the same payload on `.raw`.

## LLMs

The low-level SDK-owned LLM helpers return provider-shaped response dictionaries
and decode structured Dome gateway errors:

```python
model = client.gateway.model("prod-claude", provider="anthropic", act_as=user)
message = model.messages.create(
    messages=[{"role": "user", "content": "Draft a status update"}],
    max_tokens=512,
)
```

Stock provider clients are available for teams that want provider-native APIs:

```python
openai_client = client.gateway.openai_client(act_as=user)
completion = openai_client.chat.completions.create(
    model="prod-gpt",
    messages=[{"role": "user", "content": "Hello"}],
)
```

Provider factories return provider-shaped clients and do not promise typed Dome
exceptions unless Dome owns the transport/subclass for that path. Use
`client.gateway.llm.*` or `client.gateway.model(...)` when you want SDK-owned
typed error decoding.

## Act-As

The gateway act-as trust model is method-driven by Dome configuration, not by
the client choosing a header shape.

| SDK value | Gateway method | Header behavior |
| --- | --- | --- |
| `PlainActAs(...)` or legacy `ActAs(...)` | `none` | Canonical JSON, standard-base64 encoded. |
| `HMACActAs(secret=..., ...)` | `hmac` | Signed, timestamped, base64 encoded. |
| `OIDCActAs(jwt=...)` or raw JWT string | `oidc` | Raw JWT evidence. |
| `BoundActAs()` or no act-as | `bound` | No client act-as header; server-bound identity is used. |

For `bound`, the SDK fails closed if caller code tries to send an act-as
header.

## Activity Correlation

Direct calls carry no activity ID. Use an explicit activity context to correlate
a run across gateway calls and control-plane audit reads:

```python
with client.activity(metadata={"case": "incident-123"}) as activity:
    client.gateway.tools.call("github/list_issues", {"repo": "dome"})
    page = client.audit.query(event_types=("mcp.tool_call.completed",))
    print(activity.activity_id)
```

The ID is an opaque UUID by default. Put human labels in metadata, not in
`X-Dome-Activity-Id`.

Local `check()` decisions made inside an activity are reported to that activity
too, so the agent's own decisions land on the same chain as the gateway calls
they guard — including checks made on a worker thread, since the activity is
captured when the decision is made rather than when the audit batch flushes.
This is the correlator to reach for: it spans gateway calls, control-plane
reads, local decisions, the `openai`/`anthropic` clients from
`gateway.openai_client()` / `anthropic_client()`, and the LangChain adapter's
chat models. All of them resolve the activity per request, so a client or chat
model built once at startup and reused across turns lands each call on the right
chain.

`dome.current_activity_id()` returns the enclosing activity's id (`None` outside
one) for integrations that issue their own requests and need to stamp
`dome.ACTIVITY_ID_HEADER` themselves.

The one boundary the activity does not cross is a thread: contextvars are
per-thread, so work handed to a worker thread inside an activity is outside it
unless you re-enter the activity there. Asyncio tasks inherit it.

`check(trace_id=...)` is the other axis, and it points outward. It stamps an id
you already own — an HTTP request id, a job id — onto the emitted
`device.decision`, so the decision joins a trace in *your* system:

```python
with client.activity(metadata={"case": "incident-123"}):
    client.check(tool="github/list_issues", trace_id=http_request_id, ...)
```

`trace_id` is correlation only and never reaches rule evaluation. The SDK has no
public way to put that same id on a gateway call, so it links the decision to
your system rather than to Dome-side gateway events — use the activity for that.

## Caller-Surface Attribution

Every request this SDK sends to Dome — control plane, gateway, and the
`openai`/`anthropic` clients it builds — carries `X-Dome-Caller-Surface: sdk`, so
audit can answer "which application did this?" without guessing. Nothing to
configure; first shipped in `dome-sdk` 0.1.0.

Attribution is **telemetry only**: it never participates in authentication or
authorization, and Dome bounds the value to its own enum, so a wrong or forged
one cannot widen access.

Request origin has two independent axes on a returned event:

```python
event = client.audit.query(event_types=("mcp.tool_call.completed",)).events[0]

event.request_surface.surface         # "INITIATOR_SURFACE_GATEWAY_MCP" — verified transport
event.request_surface.caller_surface  # "CALLER_SURFACE_SDK" — asserted calling application
```

## Audit Reads

Gateway-owned audit is the source of truth for gateway calls. Query it through
the same gateway-first client when `control_plane_url` is configured:

```python
page = client.audit.query(
    event_types=("llm.called",),
    results=("EVENT_RESULT_SUCCEEDED",),
    page_size=50,
)

for event in page.events:
    print(event.type, event.correlation.activity_id, event.payload)
```

Audit read models expose `activity_id` and `activity_trust` on
`event.correlation`, and `surface` / `caller_surface` on
`event.request_surface` (see Caller-Surface Attribution above).

## Local Policy Checks

Local policy evaluation is available when an agent needs a fast, self-enforced
Cedar decision inside its own process.

```python
client = dome.Client(
    token="dome_...",
    control_plane_url="https://api.dome.example.com",
    gateway_id="<gateway-uuid>",
)
client.start_policy_sync()

decision = client.evaluate(
    tool="database/query",
    action="mcp:call",
    connection_id="<connection-uuid>",
)
if decision.allowed:
    run_query()
```

`gateway_id` selects the Gateway used during token exchange and the
`resource.gateways` fact available to local evaluation. You may omit it only
when the agent can access exactly one Gateway.

`check()` is callback-based for local self-enforcement:

```python
client.check(
    tool="database/query",
    action="mcp:call",
    connection_id="<connection-uuid>",
    on_allow=lambda result: run_query(),
    on_deny=lambda req, reason: log_denial(reason),
)
```

### What a local check is, and is not

A local check is a **pre-check**, not enforcement. The gateway is authoritative;
a local allow is not a promise that the gateway will allow the same call. Two
things follow from that:

**Address the tool the way the gateway does.** Dome stores rules against the MCP
connection's UUID — `Dome::MCPTool::"<connection-uuid>/<tool>"` — so that a
connection rename cannot silently change who can call what. Pass `connection_id=`
(the id `DomeAdminClient.create_mcp_connection` returns, or the connection's id in
the dashboard) and the SDK builds the same Cedar entity the gateway does: id
`<connection-uuid>/<tool>`, `resource.name` the human `<connection>/<tool>` you
passed as `tool`, plus `resource.connection_name` and `resource.tool_name`.
Without it the entity is keyed on the name, no stored rule can match, and the SDK
logs an ERROR saying so. There is no agent-scoped RPC that maps a tool name to
its connection UUID, so an agent has to be told the id it should use.

**Some rules cannot be evaluated in-process at all.** Rules that gate on facts
only the gateway holds — a tool's full Gateway memberships, an LLM
routing pool, customer-defined connection attributes — have no local answer.
The SDK stamps `resource.gateways` from the client's own configured Gateway
and omits what it cannot prove, so such a rule fails closed locally even where
the gateway would permit. Route the call through `client.gateway` when the
decision has to be the platform's.

## Bootstrap Helpers

Setup scripts can provision a development agent and issue a fresh key:

```python
agent = await dome.bootstrap.ensure_agent(
    name="incident-bot-dev",
    control_plane_url="https://api.dome.example.com",
    platform_key="dome_pk_...",
    workspace_id="...",
    capabilities=["mcp:call"],
    wait_gateway=True,
    gateway_id="<gateway-uuid>",
)

print(agent.agent_id, agent.token, agent.gateway_url)

# agent.gateway_url is the complete call-ready URL returned by the control plane.
```

For a newly created agent, `gateway_id` is also added to its
`allowed_gateway_ids` grant list (alongside any IDs supplied explicitly), so the
fresh key can use the selected Gateway. When an agent with the same name already
exists, bootstrap does not mutate its grants; the existing agent must already be
able to access `gateway_id`, or key creation/rotation fails closed.

## LangChain

`dome-langchain` stays an adapter over the SDK.

```python
from dome_langchain import DomeGatewayTool, DomeChatOpenAI, govern_tools

# Gateway-backed MCP tool execution.
search = DomeGatewayTool(
    dome_client=client,
    name="github/list_issues",
    description="List GitHub issues",
)

# Local Python tool with local pre-checks.
governed_local_tools = govern_tools(client, [python_tool])

# Provider-compatible chat through the Dome gateway.
llm = DomeChatOpenAI.for_agent(
    agent,
    model="prod-gpt",
    act_as=dome.ActAs(email="alice@example.com"),
)
```

## Error Model

SDK-owned gateway transports raise typed Dome errors from structured wire
contracts, including `DomeAuthorizationDenied`, `DomeCredentialRequired`,
`DomePolicyStale`, `DomeRateLimited`, and `DomeModelNotFound`. Ambiguous legacy
or provider-shaped responses remain generic `DomeGatewayError` or
provider-shaped errors rather than being parsed from brittle message strings.

## License

Proprietary. See LICENSE for details.
