SDK & Framework Integration

Prismor guards production framework agents at the tool-execution boundary. An adapter wraps the one function a framework calls to run a tool, and routes that call through the local Warden runtime before the tool body executes.

See also: docs/connecting-to-the-platform.md (the runtime — not the SDK — is what connects to the platform) · docs/frameworks-overview.md.

The one rule: the SDK talks to the LOCAL RUNTIME, never the control plane

Every adapter funnels into warden.runtime.evaluate_tool_call(...)Decision{allow, findings, reason, subject}. Python adapters import and call it in-process; non-Python adapters POST to a local sidecar (warden/eval_server.py) that calls the same function. The runtime evaluates policy locally and is the only component that phones home (telemetry, heartbeat, signed-policy pull). The SDK has no control-plane credentials and no control-plane URL.

  ┌─────────────────────────────────────────────┐
  │ Your process (framework agent)               │
  │  LLM ──tool call──► Adapter wrap point         │
  │                       │                        │
  │   (Python) in-process │  evaluate_tool_call()   │  (TS / any lang)
  │   ──────────────────► │ ◄──────────────────────  POST /v1/evaluate
  │              warden.runtime.evaluate_tool_call  │   (eval-server, same box)
  │                       │ Decision                │
  │             allow ────┴──► run tool             │
  │             deny  ───────► blocked / WardenBlocked
  └───────────────────────┬─────────────────────────┘
                          │  (runtime only)
                          ▼   redacted telemetry · signed-policy pull · heartbeat
                 Prismor control plane

Always honor Decision.allow. The runtime already folds the requested mode and any org per-agent control (kill-switch / forced-enforce) into Decision.allow. Adapters must block whenever allow is False — they must not re-derive the verdict from a locally-passed mode, or an org override can be bypassed.

The canonical event every adapter builds is identical in shape (warden/hooks.py): { ts, session_id, agent, agent_event, type, command|path| url|content, metadata{tool_name, framework, args, kwargs} }. type selects the matched field: shell→command, file_read|file_write→path, network→url, prompt|tool_result→content.

Multi-tenant subjects

One deployed agent serves many users. Attribute each call with use_subject (a contextvar — async/thread-safe; warden/principal.py):

from prismor.warden.openai import use_subject
with use_subject("user:alice"):       # also "user=alice;team=data;org=acme"
    Runner.run_sync(agent, prompt)

Priority: explicit subject= arg → use_subject context → WARDEN_SUBJECT env → enrolled device identity → anonymous.

Per framework

FrameworkWrap point (patched)Guard call
OpenAI AgentsFunctionTool.on_invoke_toolguard_agent(agent, subject=...)
LangChain/LangGraphtool.func / tool.coroutine (+ WardenCallbackHandler)guard_tools([...], subject=...)
CrewAItool.func / _run / runguard_tools([...], subject=...)
browser-useRegistry.execute_actionguard_controller(controller, subject=...)
Vercel AI (TS)tool.execute → HTTPwardenTools({...}, { subject })

OpenAI Agents

from agents import Agent, Runner
from prismor.warden.openai import guard_agent, use_subject
agent = Agent(name="ops", tools=[run_shell])
guard_agent(agent, subject="user:alice")     # wraps every FunctionTool
with use_subject("user:alice"):
    Runner.run_sync(agent, "…")

LangChain / LangGraph

from prismor.warden.langchain import guard_tools
tools = guard_tools([run_shell, fetch_url], subject="user:alice")
# Observability-only: config={"callbacks": [WardenCallbackHandler()]}

CrewAI

from prismor.warden.crewai import guard_tools
tools = guard_tools([run_shell], subject="user:alice")

browser-use

from prismor.warden.browser_use import guard_controller, use_subject
controller = Controller(); guard_controller(controller)   # patch once at startup
with use_subject("user:alice"):
    await Agent(task="…", llm=llm, controller=controller).run()

Vercel AI SDK / any language (HTTP)

Run the sidecar: prismor eval-server --port 7071 --workspace .

import { wardenTools } from "prismor-warden";
const tools = wardenTools({ run_shell, search_web }, { subject: `user:${userId}` });
await generateText({ model, tools, prompt });

POST /v1/evaluate:

{ "tool_name":"run_shell", "arguments":{"command":"rm -rf /"},
  "event_type":"shell", "agent":"vercel-ai", "mode":"enforce",
  "session_id":"req-1", "subject":"user:alice", "workspace":"/srv/app" }

200 {"allow":false,"reason":"[HIGH] …","findings":[…],"subject":{…}}. Subject and agent name may also be sent as X-Warden-Subject / X-Warden-Agent-Name headers.

Failure behavior

The eval-server is local; if it is unreachable the HTTP client treats the call as allowed so an infrastructure fault never breaks the agent. Note: failure handling is not yet uniform across all adapters — a known open item, with an intended single policy. For non-loopback binds of the eval-server, an auth follow-up is also planned.

Licensing

The adapters are MIT (adapters/LICENSE) — the most permissive terms, so the ecosystem can integrate and vendor them freely. The Warden runtime they call into is Apache-2.0 (repo root LICENSE).