Developers
Three lines, and the refusal is a value.
No runtime to adopt and no framework to migrate to. The governed unit is the action, so this is a call you make before a consequential one — and the answer is recorded either way.
pip install rotascale
from rotascale import Rotascale rs = Rotascale() # reads ROTASCALE_URL and ROTASCALE_API_KEY # The agent appears here, the first time it speaks. Nobody registers it. agent = rs.agent("refund-assistant") # One episode of work, recorded as it happens. with rs.witness(agent, goal={"ticket": "TICKET-88123"}) as t: # Ask BEFORE acting. The answer is recorded either way. decision = t.authorize(grant_id, {"tools": ["issue_refund"]}, amount_minor=4_500, currency="EUR") if decision.allowed: ... # do the thing t.outcome(decision="refunded", outcome_amount_minor=4_500) else: # A refusal is not an exception. It is the product working. t.outcome(decision="refused", reason=decision.reason)
Works with the agents and models you already run
One middleware per framework, each duck-typed so it imports nothing —
from rotascale.middleware import * works with none of them
installed, and the SDK never drags a provider dependency into your
lockfile. Wrap the client once and every call lands on the trajectory in
scope.
-
OpenAI
watch_openai -
Anthropic
watch_anthropic -
Google Gemini
watch_gemini -
AWS Bedrock
watch_bedrock -
LangChain
watch_langchain -
LangGraph
watch_langgraph -
MCP
watch_mcp -
Google ADK
watch_adk -
CrewAI
watch_crewai -
AutoGen
watch_autogen -
AWS Strands
watch_strands
Names and marks belong to their owners and are shown to say what this works with, not to imply any endorsement. If the framework you run is not here, the SDK still records — a middleware only saves you writing the wrapper yourself.
Two rules the SDK will not let you break
- Capture fails open
- A recording problem must never break a production agent. Evidence is worth a lot; it is not worth an outage. Capture calls do not raise.
- Enforcement fails closed
- If the control plane is unreachable, the honest position is that the
action is ungoverned — and an ungoverned action is worse than a delayed
one. Teams who genuinely cannot accept that set
fail_open_enforcement=Trueand own the consequence explicitly, which is at least a recorded decision rather than an accident.
The refusal types name the remedy: Blocked means change the
grant, Exhausted means raise the budget or issue a new one,
Gated means the context was tainted and needs approval or a
declared sanitiser, ReviewRequired means park it, a person is
deciding. That is why they are distinct types rather than one
Refused.
Three ways in, and the last one needs no code
- The SDK
- A few lines, and middlewares capture the rest. Python and TypeScript. Fail open on capture, fail closed on enforcement — losing a record must never stop the work, and losing a decision must never let it through.
- The MCP proxy
- Zero code in the agent. It sits between the decision and the tool call, which makes it the one integration that is on the causal path rather than beside it: an agent cannot route around a check it never makes.
- OpenTelemetry
- If your estate already emits traces, point a collector at the receiver and the spans become governed records. No import, no proxy, nothing deployed alongside the agent — which is why it is usually the fastest way to find out which agents in an organisation are ungoverned. How it maps.
The convention is a proposal, not a standard we have declared. The mapping from this entity model to OTel spans belongs upstream at OpenTelemetry rather than in a vendor's specification, and the specification says so in the section listing what it declines to invent.
Where to go
Reference documentation lives in the console at /developers,
because it knows which deployment you are integrating against — whether
taint enforcement is on, which market profile resolves, and whether the
signing key is deployment-held or yours. A static page can only say "it
depends"; that one says which.