CAIN-42 CAIN Studio

Developer documentation

Quickstart

Last reviewed 31 August 2026

All docs

Five minutes from nothing to a protected agent. Nothing below is aspirational -- every command exists and every output block was captured from a real run.

Fastest path: the HTTP API (nothing to install)#

Everything the SDK does goes through one HTTP call, so you can protect an agent today with curl or any HTTP client. The SDK and CLI further down are conveniences on top of it.

See a decision without an account#

curl -s -X POST 'https://cainstudio.online/fabric/try?scenario=safe-read'
{"scenario":{"id":"safe-read", ...},
 "verdict":"REQUIRE_APPROVAL","blocked":false,
 "decision_id":"fd_2a0da78b47d34964b049851c",
 "chain":[{"stage":"identity","verdict":"allow",...},
          {"stage":"trust","verdict":"require_approval",
           "detail":"trust=unknown, risk=low, decision=require_approval"}, ...]}

GET /fabric/try lists the other fixed scenarios (prompt injection, malformed plan, ...). They run against a throwaway tenant.

Ask before every consequential tool call#

Get an API key at /signup, then send the tool call *before* you run it:

export CAIN_API_KEY=...
curl -s https://cainstudio.online/fabric/decisions \
  -H "X-API-Key: $CAIN_API_KEY" -H 'content-type: application/json' \
  -d '{"path":"/tools/send_email","agent_id":"support-bot",
       "payload":{"to":"a@example.com","subject":"hi"}}'
{
  "decision_id": "fd_aeffae8d45cb4cf79576f823",
  "verdict": "REQUIRE_APPROVAL",
  "blocked": false,
  "enforcing": true,
  "mode": "enforce",
  "path": "/tools/send_email",
  "stages": [ {"stage": "identity", "verdict": "allow", ...},
              {"stage": "trust", "verdict": "require_approval",
               "detail": "trust=unknown, risk=low, decision=require_approval"}, ... ]
}

Run the tool only when verdict starts with ALLOWED and blocked is false. Treat every other verdict, a timeout and a non-200 response as "do not run". A new agent has no trust history, so its first actions come back REQUIRE_APPROVAL. Unknown trust never turns into ALLOWED by itself.

No key gives 401 {"detail":"missing X-API-Key"}, which is never a permit.

Add a rule and watch it bite#

curl -s https://cainstudio.online/fabric/tool-rules \
  -H "X-API-Key: $CAIN_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"no outbound email","effect":"deny","match_path":"/tools/send_email"}'

Send the same decision request again:

"verdict": "BLOCKED",
"blocked": true,
...
{"stage": "policy", "verdict": "deny", "enforcing": true,
 "detail": "tool rule 'no outbound email' (tr_66a83083d50c46b8 v1) denies this action ..."}

Rules can also allow or require_approval. You can manage them in the console at /dashboard under *Tools & rules*.

Check the record afterwards#

curl -s https://cainstudio.online/fabric/decisions?limit=20 -H "X-API-Key: $CAIN_API_KEY"
curl -s https://cainstudio.online/fabric/decisions/<decision_id>/signature -H "X-API-Key: $CAIN_API_KEY"

The signature covers the decision id, tenant, time, verdict and every stage's verdict. It shows the record hasn't changed since it was signed. It is not a third-party notarisation.

The same thing in Python, standard library only#

import json, os, urllib.request

def cain_allows(tool, payload, agent_id="my-agent"):
    req = urllib.request.Request(
        "https://cainstudio.online/fabric/decisions",
        data=json.dumps({"path": f"/tools/{tool}", "payload": payload,
                         "agent_id": agent_id}).encode(),
        headers={"X-API-Key": os.environ["CAIN_API_KEY"],
                 "content-type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=5) as r:
            d = json.load(r)
        # ALLOWED, ALLOWED_DEGRADED, ALLOWED_WITH_DENIALS (shadow mode) permit; nothing else does
        return d.get("blocked") is False and str(d.get("verdict", "")).startswith("ALLOWED")
    except Exception:
        return False          # unreachable or error: do not run the tool

if cain_allows("send_email", {"to": to, "subject": subject}):
    send_email(to, subject, body)

The full API is in openapi.json.

*How the output above was captured:* the /fabric/try output came from the live site. The /fabric/decisions and /fabric/tool-rules output came from the production gateway code run locally with a test key. On the hosted service, responses also include a consensus stage, and ids and timestamps will differ.

The Python SDK#

The same calls, wrapped: a fail-closed decorator, a LangChain integration, human approval that can wait, and traces. No runtime dependencies.

# 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"

The wheel's SHA-256 is published at https://cainstudio.online/cainstudio-0.2.0-py3-none-any.whl.sha256; verify it before installing. The SDK calls exactly the HTTP path above and has no runtime dependencies (the langchain extra adds langchain-core only).

1. See a decision, no key needed#

cainstudio try

2. Guard a tool#

import cainstudio                        # reads CAIN_API_KEY

@cainstudio.guard()
def send_email(to: str, subject: str, body: str):
    ...                                  # unchanged

Not allowed, not executed: the decorator raises ActionBlocked, ApprovalRequired or CainUnavailable before the body runs. A timeout or an outage is never an allow.

3. LangChain / LangGraph#

from cainstudio.langchain import protect

tools = protect([search, send_email], agent_id="support-agent")
with cainstudio.run():                   # one trace per agent run
    agent.invoke({"messages": [...]})

4. See what happened#

cainstudio decisions
cainstudio explain <decision-id>
cainstudio approvals
cainstudio approve <approval-id> --note "checked"

Or open the console at /dashboard: decisions, traces, the approval queue, tool rules and the kill switch.

What next#