CAIN-42 CAIN Studio

Developer documentation

Python SDK

Last reviewed 31 August 2026

All docs
# Hosted wheel (works today; PyPI publication is pending):
pip install https://cainstudio.online/cainstudio-0.2.0-py3-none-any.whl
# LangChain extra:
pip install "cainstudio[langchain] @ https://cainstudio.online/cainstudio-0.2.0-py3-none-any.whl"

Python 3.9+. No runtime dependencies. It is a thin client for the HTTP decision API: every verdict comes from CAIN, nothing is decided locally. Verify the wheel's SHA-256 (.../cainstudio-0.2.0-py3-none-any.whl.sha256) before installing; the HTTP API is exactly what the SDK calls and can be used directly.

See a decision without an account#

cainstudio try                  # a prompt injection, blocked, stage by stage
cainstudio try safe-read        # a normal call, held: a new agent has no trust history
cainstudio try --list

Guard a function#

import cainstudio                        # reads CAIN_API_KEY

@cainstudio.guard()
def transfer(amount_usd: float, to: str) -> str:
    ...                                  # unchanged

The function's arguments are sent as the action payload, so tool rules such as "hold transfers where amount_usd > 1000" see them. The decision happens before the body runs. If the call is not allowed, the body does not run and one of these is raised:

ExceptionMeaning
ActionBlockedCAIN refused it. e.decision.reasons says why.
ApprovalRequiredHeld for a human. e.approval_id is the review-queue entry.
CainUnavailableNo verdict (timeout, outage, malformed reply). Fail-closed: not run.
AuthenticationErrorMissing, unknown or unpaid key.

@cainstudio.guard("wire_transfer") names the tool explicitly; the default is the function name. Async functions work the same way.

Wait for a human#

@cainstudio.guard(wait_for_approval=300)     # seconds
def transfer(amount_usd: float, to: str): ...

A held call waits for an approve or deny in the console (or cainstudio approve), then is decided again and runs once. The approval is single-use and bound to that call. An agent key cannot approve its own actions.

Ask without guarding#

from cainstudio import Cain

cain = Cain()                            # or Cain(api_key=..., agent_id="support-bot")
d = cain.decide("send_email", {"to": "a@example.com"}, agent_id="support-bot")
if d.allowed:
    send_email(...)

Decision fields: verdict, allowed, held, blocked, reasons, stages, id, run_id, raw (the full response).

The server's verdicts are ALLOWED, ALLOWED_DEGRADED (allowed, a stage could not run), ALLOWED_WITH_DENIALS (shadow mode: a stage objected but was not enforcing), REQUIRE_APPROVAL and BLOCKED. d.allowed is true only for the three ALLOWED* verdicts with blocked false. Anything else, including a verdict this version does not know, is not allowed.

dry_run=True evaluates without writing an evidence record: use it for "how would this be decided?" in tests and policy work.

LangChain and LangGraph#

from cainstudio.langchain import protect

tools = protect([search, send_email, transfer_funds], agent_id="support-agent")
agent = create_agent(model, tools)

Names, descriptions and argument schemas are unchanged, so the model sees the same tools. A refused call is returned to the model as the tool error (on_block="raise" raises instead).

Other frameworks: put @cainstudio.guard() directly on the tool function, under the framework's own decorator.

Traces#

with cainstudio.run() as run_id:
    agent.invoke({"messages": [...]})

Every decision inside the block carries run_id, and the run appears under Traces in the console.

Evidence and approvals#

cain.decisions(20)                       # recent decisions
cain.get_decision("fd_...")              # one record
cain.explain("fd_...")                   # stage by stage, and which run it belongs to
cain.approvals()                         # pending review queue
cain.approve("ap_...", note="checked with finance")
cain.deny("ap_...")

Command line#

cainstudio decide send_email --args '{"to": "a@example.com"}' --agent support-bot
cainstudio decisions
cainstudio explain fd_...
cainstudio approvals
cainstudio approve ap_... --note "checked with finance"

decide exits 0 allowed, 2 blocked, 3 held, 1 error.

Configuration#

CAIN_API_KEY (required except for try). CAIN_BASE_URL (default https://cainstudio.online; must be https except for localhost, so a key is never sent in clear text).