rotascale

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=True and 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.