Skip to main content
The Runlayer Python SDK wires custom agent runtimes into Runlayer’s hook enforcement and telemetry pipeline. It is for agents that execute tools directly and need Runlayer to observe, rewrite, or block tool calls.
This SDK is different from an MCP client SDK. To connect an agent framework to Runlayer-hosted MCP servers, use the framework integration docs for Vercel AI SDK, OpenAI Agents SDK, or Google ADK. For Claude Agent SDK MCP server setup, see Claude Agent SDK.

Capabilities

  • lifecycle telemetry for prompt, stop, and synthetic preflight session events
  • pre-tool enforcement with optional argument rewriting
  • post-tool output scanning and blocking
  • failed tool telemetry
  • direct MCP source enforcement
  • tool enforcement filters for Runlayer-owned MCP tools and proxy URLs
  • duck-typed tool adapters for Vercel-style and custom tool dictionaries
  • preflight session emission for ingestion checks
  • transcript-bearing Stop events for assistant reasoning extraction

Installation

uv add runlayer-sdk==0.1.0
For Claude Agent SDK hooks, also install:
uv add claude-agent-sdk==0.1.80

Configuration

Create a Runlayer user API key, then set:
export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
export RUNLAYER_API_KEY="rl_..."
To authenticate as an agent account instead, set:
export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
export RUNLAYER_AGENT_CLIENT_ID="client_..."
export RUNLAYER_AGENT_CLIENT_SECRET="..."
The SDK exchanges these credentials for a Runlayer bearer token with client_credentials, then caches and refreshes that token internally. Do not configure or pass a pre-minted bearer token. For OBO agent tokens, also set:
export RUNLAYER_AGENT_SUBJECT_TOKEN="user@example.com"
export RUNLAYER_AGENT_SUBJECT_TOKEN_TYPE="urn:runlayer:token-type:user-email"
The SDK defaults to client="python-sdk" so events are attributed to the first-party Python SDK in Runlayer. Ensure your workspace hook-client allowlist includes python-sdk before testing. If your deployment’s settings UI does not expose that client yet, ask Runlayer support to enable it for your workspace. Optional environment variables:
VariablePurpose
RUNLAYER_AGENT_CLIENT_IDAgent account Client ID for client_credentials token exchange.
RUNLAYER_AGENT_CLIENT_SECRETAgent account Client Secret for client_credentials token exchange.
RUNLAYER_AGENT_SUBJECT_TOKENOptional user identity for OBO agent token exchange.
RUNLAYER_AGENT_SUBJECT_TOKEN_TYPEToken type for RUNLAYER_AGENT_SUBJECT_TOKEN; use urn:runlayer:token-type:user-id, urn:runlayer:token-type:user-email, or urn:ietf:params:oauth:token-type:access_token.
RUNLAYER_HOOK_CLIENTOverride the client name sent to Runlayer. Only use this when your deployment supports a dedicated SDK client name.
RUNLAYER_HOOK_DEBUG=1Log non-strict hook event failures to stderr.
RUNLAYER_HOOK_TIMEOUT_MSOptional timeout override, in milliseconds, for Runlayer hook calls.
RUNLAYER_HOOK_MAX_TOOL_OUTPUT_BYTESMaximum serialized tool output sent to Runlayer. Defaults to 65536; larger values are truncated with a marker.
RUNLAYER_HOOK_ENFORCEMENT_FAILURE_MODEEnforcement outage behavior. Defaults to closed; set to open only if your agent should continue when Runlayer cannot be reached.
RUNLAYER_ALLOW_INSECURE_TRANSPORT=1Allow a non-HTTPS RUNLAYER_BASE_URL for local development only.
RUNLAYER_DIRECT_MCP_SOURCE_ENFORCEMENT_PATHEndpoint path for direct MCP source validation, when enabled by your deployment.

Runtime Safety

Runlayer enforcement is fail-closed by default. If pre-tool, post-tool, or direct MCP source enforcement cannot reach Runlayer, the SDK treats the call as blocked. Lifecycle telemetry is best-effort unless a helper documents strict behavior, such as preflight and transcript Stop emission. Every hook request uses a timeout, and tool output is capped before upload. When no timeout override is set, enforcement and tool lifecycle hooks use 30 seconds, lifecycle event hooks use 5 seconds, and direct endpoint calls use 10 seconds. Failed tool outputs include bounded stdout, stderr, and output fields with truncation metadata when available, so Runlayer can scan failure context without the SDK uploading unbounded logs. The SDK skips pre-tool and post-tool enforcement for Runlayer’s own MCP server names (runlayer, runlayer-plugin, and onelayer) and for MCP calls whose tool_url points at a Runlayer proxy URL. Third-party MCP tools remain enforced by default. For custom runtimes, use the exported should_enforce_tool(...) helper or the client’s tool_enforcement option so new adapters make the same decision consistently. Additional ignored_mcp_server_names are added to the default Runlayer self-MCP skip list; set skip_runlayer_self_mcp=False only if you need to enforce those self-MCP names. If you call should_enforce_tool(...) directly with absolute self-hosted Runlayer MCP proxy URLs, pass runlayer_base_url; RunlayerClient does this automatically. base_url must use https:// unless allow_insecure_transport or RUNLAYER_ALLOW_INSECURE_TRANSPORT=1 is set. Only enable insecure transport for a trusted local development endpoint.

Preflight

Use preflight to verify that Runlayer receives hook events before invoking a real model.
from runlayer_sdk import RunlayerClient, send_runlayer_preflight

runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

result = send_runlayer_preflight(
    runlayer,
    prompt="Runlayer ingestion preflight",
)

print(result.session_id)
Preflight sends a strict SessionStart, UserPromptSubmit, and Stop sequence for a synthetic session. If any request fails, the helper throws.

Claude Agent SDK Hooks

Pass Runlayer hooks into Claude Agent SDK’s query options.
import anyio
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, SystemMessage, query
from runlayer_sdk import RunlayerClient
from runlayer_sdk.claude_agent_sdk import (
    claude_agent_sdk_assistant_message_to_transcript_line,
    create_claude_agent_sdk_hooks,
    emit_claude_agent_sdk_transcript_stop,
)


async def main() -> None:
    runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")
    model = "claude-sonnet-4-6"
    transcript_lines: list[str] = []
    session_id: str | None = None

    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Glob", "Grep", "Bash"],
        hooks=create_claude_agent_sdk_hooks(runlayer, include_stop=False),
        max_turns=4,
        model=model,
        thinking={"type": "adaptive"},
    )

    async for message in query(
        prompt="Inspect this workspace and summarize the active project.",
        options=options,
    ):
        if isinstance(message, SystemMessage) and message.subtype == "init":
            session_id = message.session_id

        if isinstance(message, AssistantMessage):
            transcript_lines.append(
                claude_agent_sdk_assistant_message_to_transcript_line(message.message)
            )

    if session_id:
        emit_claude_agent_sdk_transcript_stop(
            runlayer,
            model=model,
            session_id=session_id,
            transcript_lines=transcript_lines,
        )


anyio.run(main)
Use include_stop=False when you emit a transcript-bearing Stop manually. That lets Runlayer extract assistant thinking or reasoning blocks from the completed transcript instead of receiving a bare lifecycle stop. The generated hook map covers UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, and Stop by default. It does not register Claude SessionStart or SessionEnd hooks; use preflight when you need a strict synthetic session sequence.

Tool Enforcement

If your runtime executes tools outside Claude Agent SDK hooks, wrap those calls with run_tool.
from runlayer_sdk import RunlayerClient

runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")


def run_local_tool(tool_input: dict[str, object]) -> str:
    return "tool output"


output = runlayer.run_tool(
    execute=run_local_tool,
    session_id="session-id",
    tool_input={"command": "cat README.md"},
    tool_name="Bash",
    tool_type="shell",
)
Runlayer can return modified arguments from the pre-tool hook. The SDK passes those modified arguments to execute. If Runlayer denies the tool call or blocks the tool output, the SDK throws RunlayerBlockedError. To avoid recursively enforcing Runlayer-owned tools, configure tool_enforcement when constructing the client:
runlayer = RunlayerClient.from_env(
    tool_enforcement={
        "ignored_mcp_server_names": ["github-preview"],
        "ignored_tool_names": ["localHealthcheck"],
        "skip_runlayer_mcp_proxy_urls": True,
        "skip_runlayer_self_mcp": True,
    }
)
The default behavior skips Runlayer MCP servers (runlayer, runlayer-plugin, onelayer) and Runlayer MCP proxy URLs.

Framework Tool Adapters

The Python SDK includes dependency-free wrappers for common tool dictionary shapes. These adapters call run_tool around the original execute function and preserve extra context arguments for custom runtimes that use OpenAI, Google ADK, or Vercel-style option dictionaries. These helpers do not wrap native Python FunctionTool or callable objects from the OpenAI Agents SDK or Google ADK. Use RunlayerClient.run_tool(...) directly inside those callables until native callable adapters are available.
from runlayer_sdk import RunlayerClient, with_runlayer_openai_agents_tool

runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

lookup_ticket = with_runlayer_openai_agents_tool(
    {
        "name": "lookup_ticket",
        "description": "Lookup a ticket",
        "parameters": {"type": "object"},
        "execute": lambda tool_input, context=None, details=None: {
            "ticket_id": tool_input["ticket_id"],
        },
    },
    {
        "client": runlayer,
        "session_id": "session-id",
    },
)
Available wrappers:
HelperPurpose
with_runlayer_vercel_ai_tool / with_runlayer_vercel_ai_toolsWraps Vercel AI-style tool dictionaries by tool name or tool-set key.
with_runlayer_openai_agents_toolWraps custom OpenAI Agents-style tool dictionaries.
with_runlayer_google_adk_toolWraps custom Google ADK-style tool dictionaries.
run_runlayer_adapter_toolLow-level adapter helper for custom framework wrappers.

Direct MCP Source Enforcement

Use direct MCP source enforcement when a custom agent can call MCP servers without going through the Runlayer proxy. Direct MCP source enforcement posts to the Cursor hook route and currently requires user API-key auth. Configure RUNLAYER_API_KEY for this client; agent account bearer auth is supported for lifecycle and tool hooks, but not this direct MCP source route.
from runlayer_sdk import RunlayerClient

runlayer = RunlayerClient.from_env(
    direct_mcp_source_enforcement_path="/api/v1/hooks/cursor",
)

runlayer.enforce_mcp_source(
    generation_id="generation-id",
    session_id="session-id",
    tool_name="mcp__github__list_repos",
    url="https://tenant.runlayer.com/api/v1/proxy/server-id/mcp",
)
If Runlayer denies the source, enforce_mcp_source throws RunlayerBlockedError.

API Reference

RunlayerClient.from_env(**options)

Creates a client from RUNLAYER_BASE_URL plus one auth source: RUNLAYER_API_KEY or RUNLAYER_AGENT_CLIENT_ID / RUNLAYER_AGENT_CLIENT_SECRET. When agent credentials are configured, the SDK exchanges them at /api/v1/oauth/token, caches the bearer token, and refreshes before expiry. The SDK does not accept pre-minted bearer tokens. Options:
OptionDescription
clientClient name sent to Runlayer. Defaults to RUNLAYER_HOOK_CLIENT or python-sdk.
client_versionVersion string for your agent runtime.
direct_mcp_source_enforcement_pathEndpoint path for direct MCP source enforcement.
timeout_msOptional per-request timeout override in milliseconds. If unset, enforcement and tool lifecycle hooks use 30000 ms, lifecycle event hooks use 5000 ms, and direct endpoint calls use 10000 ms.
max_tool_output_bytesMaximum serialized tool output bytes sent to Runlayer. Defaults to 65536.
enforcement_failure_modeclosed blocks on enforcement outages; open allows on outages. Defaults to closed.
tool_enforcementOptional filter config for ignored tools, ignored MCP server names, Runlayer proxy URLs, and custom predicates.
allow_insecure_transportAllows non-HTTPS base_url; local development only.

create_claude_agent_sdk_hooks(client, options=None, *, include_stop=None)

Returns a Claude Agent SDK hook map with prompt telemetry, pre-tool, post-tool, failed-tool, and optional stop hooks. Set include_stop=False as a keyword argument, or pass {"include_stop": False} / {"includeStop": False} as options, when you emit the transcript-bearing Stop event yourself.

send_runlayer_preflight(client, **options)

Sends a strict synthetic lifecycle sequence to confirm ingestion.

emit_claude_agent_sdk_transcript_stop(client, **options)

Sends a strict Stop event with newline-delimited transcript lines.

RunlayerClient.run_tool(**options)

Runs a custom tool through Runlayer pre-tool and post-tool enforcement.

with_runlayer_openai_agents_tool(tool, options)

Wraps a custom OpenAI Agents-style function tool dictionary.

with_runlayer_google_adk_tool(tool, options)

Wraps a custom Google ADK-style function tool dictionary.

with_runlayer_vercel_ai_tool(tool_name, tool, options)

Wraps a Vercel AI-style tool dictionary.

RunlayerClient.enforce_mcp_source(**options)

Validates a direct MCP source before execution.