entorin

Claude Agent SDK

Mount Entorin under the Claude Agent SDK via httpx transport, wrapped MCP sessions, and a per-run sandbox.

The SDK uses an internal httpx.AsyncClient to talk to Anthropic and uses mcp.ClientSession for any MCP servers it spawns. Two integration points:

import httpx
from pydantic import SecretStr

from adapters.claude_sdk import (
    EntorinAnthropicTransport, WrappedMCPSession, run_sandbox,
)
from entorin.auth import Capability, Principal
from entorin.budget import BudgetGate, MemoryLedger
from entorin.context import RunContext
from entorin.events import EventBus
from entorin.model.adapters.anthropic import CAPABILITY_KIND

# 1. The substrate primitives — one EventBus / Ledger / Gate per run.
bus    = EventBus()
ledger = MemoryLedger()
gate   = BudgetGate(ledger)
ledger.set_cap("alice", Decimal("5.00"))

# 2. Identity + capabilities. The API key is a Capability — substrate
#    never reads os.environ itself.
principal = Principal(
    user_id="alice",
    caps=(Capability(kind=CAPABILITY_KIND, value=SecretStr(...)),),
)
ctx = RunContext(run_id=str(uuid.uuid4()), principal=principal)

# 3. Mount the substrate transport on the SDK's httpx client.
transport = EntorinAnthropicTransport(
    ctx=ctx, bus=bus, ledger=ledger, gate=gate,
)
sdk_http_client = httpx.AsyncClient(
    transport=transport, base_url="https://api.anthropic.com",
)
# Pass `sdk_http_client` into the SDK in place of its default.

# 4. Wrap each MCP session before handing it to the SDK.
wrapped_fs = WrappedMCPSession(
    server_id="fs", session=raw_mcp_session, bus=bus, ctx=ctx,
)
# SDK uses `wrapped_fs` exactly like a raw mcp.ClientSession; the
# substrate sees and audits every call_tool, the SDK doesn't notice.

# 5. Confine spawned subprocesses to a per-run sandbox.
async with run_sandbox(ctx=ctx, bus=bus, artifacts_root="artifacts") as sb:
    # Generated files land in artifacts/{run_id}/ and survive the run.
    # On exit (normal or BudgetError mid-run), every spawned subprocess
    # is killed cleanly — no zombies leak across runs.
    ...

Outcomes

  • API key flows from Capability → request header. The SDK can keep reading ANTHROPIC_API_KEY from env all it likes; the substrate overwrites the header on every Messages call.
  • BudgetGate.check runs before any wire activity. A cap-exceeded run terminates with BudgetError and never contacts Anthropic.
  • tool.call.pre/post|error events fire for every tool the SDK invokes. Capability denial emits policy.violation and the wrapped session never proxies the call to the underlying server.