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.

Start here: three steps, copy and paste#

CAIN-42 sits between your AI agent and the things it can do. Before the agent runs a tool (send an email, delete a file, move money), it asks CAIN. CAIN answers ALLOWED, REQUIRE_APPROVAL (a human says yes first) or BLOCKED, and keeps a signed record. You need a terminal with curl and python3. Nothing to install.

Step 1 -- get your keys. One key is yours (you approve with it); the other is your agent's (it asks with it). A key can never approve its own request.

export CAIN_API_KEY=$(curl -s -X POST https://cainstudio.online/subscribe/free/signup \
  -H 'content-type: application/json' -d '{}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["api_key"])')
export AGENT_KEY=$(curl -s https://cainstudio.online/fabric/agent-keys \
  -H "X-API-Key: $CAIN_API_KEY" -H 'content-type: application/json' -d '{"name":"my-agent"}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["agent_key"])')
echo "save these: $CAIN_API_KEY $AGENT_KEY"

Keys are shown once, so save them. (Prefer a web account with sign-in and recovery codes? Use /signup and copy the key from the dashboard.)

Step 2 -- your agent asks before it acts.

curl -s --max-time 60 https://cainstudio.online/fabric/decisions \
  -H "X-API-Key: $AGENT_KEY" -H 'content-type: application/json' \
  -d '{"path":"/tools/send_email","agent_id":"my-agent","payload":{"to":"a@example.com"}}' \
  | python3 -c 'import json,sys; t=sys.stdin.read(); d=json.loads(t) if t.strip() else {}; print(d.get("verdict", "NO ANSWER YET: run this box again"), d.get("approval_id") or "")'

A new agent has no track record, so its first call comes back REQUIRE_APPROVAL ap_.... That is the product working: nothing ran.

example.com is a reserved test name, which is why this call is not refused. An agent key reaches only destinations you have allowed, so with a real recipient (bob@acme.io, a #channel, an account number) the answer is BLOCKED until you add an allow rule with your own key:

curl -s https://cainstudio.online/fabric/egress/rules \
  -H "X-API-Key: $CAIN_API_KEY" -H 'content-type: application/json' -d '{"target":"acme.io"}'

A target is a host (acme.io covers bob@acme.io), a .suffix, a CIDR, or the exact recipient name. Once one rule exists, every destination without a rule is refused for every key.

Step 3 -- you approve, the agent goes ahead.

curl -s -X POST https://cainstudio.online/fabric/approvals/<the ap_... id>/approve -H "X-API-Key: $CAIN_API_KEY"

Send the same call from step 2 again: it now answers ALLOWED. **The one rule for your code: run the tool only when the verdict starts with ALLOWED.** Anything else (including an error or a timeout) means do not run it. An approved retry waits for a consensus round (measured 0.5-2 s on 13 October 2026), so still give it a generous timeout (the --max-time 60 above). An approval is used once: the next identical call is held again until the agent has a track record.

Some actions always need a human, however good the agent's record: rm -rf /, DROP TABLE, curl ... | sh, transfers of 10,000 or more. See every decision with curl -s https://cainstudio.online/fabric/decisions?limit=20 -H "X-API-Key: $CAIN_API_KEY".

New to all this? Start here walks through the same steps in plain words.

That is the whole integration. Everything below is detail: the same steps in Python, rules you can set, signatures you can verify, and the SDK.

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.

How an agent earns autonomy#

Give each agent its own key, so the agent asks and you approve (a key can never approve its own request):

curl -s https://cainstudio.online/fabric/agent-keys \
  -H "X-API-Key: $CAIN_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"support-bot"}'
# -> {"agent_key_id":"ak_...","agent_key":"agt_...", ...}   (the key is shown once)

The agent sends its decisions with its agt_... key. A held action is not a dead end: every REQUIRE_APPROVAL lands in your review queue with an approval_id (the approval stage carries it), and you resolve it with your own key:

cainstudio approvals                 # what is waiting
cainstudio approve <approval_id>     # approve it (with a key other than the agent's own)

Resend the same call and it runs, once. The approval covers that exact action, tool and arguments: the same tool with different arguments (another amount, another recipient) is held again. Each approved, completed action is evidence for the agent:

Agent's recordLow-risk actionsMedium (writes, email)High (shell, payments, deletes)Critical
New (unknown)held for youheld for youheld for youheld for you
Mostly approved actions (normal)runrunheld for youheld for you
Long clean record (trusted: 50+ approved, 95%+)runrunrun, monitoredheld for you
Blocked attempts on record (degraded, suspended)held for youheld for youheld for youheld for you
Revokeddenieddenieddenieddenied

A payload the injection screen flags (for example "ignore all previous instructions") is denied outright for a new or degraded agent and held for a normal one.

Critical actions (rm -rf /, DROP TABLE, curl ... | sh, transfers of 10,000 or more) always need a human, however good the record. Trust is tracked per agent and can never exceed the trust of the API key it runs under, so a new agent under an established key still starts held. Each decision's trust stage shows the agent's state and why (data.action_risk.reasons).

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)

Every endpoint you need to get started is on this page and in the Python SDK page. The customer API is published as OpenAPI 3.1 at /fabric/openapi.json -- import it into Postman, Insomnia or an SDK generator; every call takes your key in X-API-Key. (The full internal schema at /openapi.json is operator-only.)

*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.

How many decisions have I used?#

curl -s https://cainstudio.online/fabric/usage -H "X-API-Key: $CAIN_API_KEY"

Answers with this calendar month's decisions (UTC) against your plan's monthly allowance: used, limit, remaining, percent_used, a count per verdict, and when it resets. It counts the same records GET /fabric/decisions lists; dry runs ("record": false) are not counted.

The Python SDK#

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

# From the CAIN package index (PyPI publication is pending). cainstudio has no dependencies, so this index
# alone is enough and pip never asks PyPI for a package with the same name. Pins and upgrades work as usual:
pip install --index-url https://cainstudio.online/simple cainstudio
# requirements.txt: pin the exact wheel by URL and hash (every /explore/recipes page prints the current line):
#   cainstudio @ https://cainstudio.online/cainstudio-<version>-py3-none-any.whl#sha256=<hash>
# Do not use --extra-index-url: pip would also take a higher-versioned "cainstudio" from PyPI if one appeared.

# Or the wheel directly:
pip install https://cainstudio.online/cainstudio-0.6.1-py3-none-any.whl

# LangChain extra:
pip install "cainstudio[langchain] @ https://cainstudio.online/cainstudio-0.6.1-py3-none-any.whl"

The wheel's SHA-256 is published at https://cainstudio.online/cainstudio-0.6.1-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#