Metadata-Version: 2.4
Name: cainstudio
Version: 0.6.2
Summary: Authorize every consequential action your AI agents take: the CAIN Trust Fabric client (guard decorator, LangChain tools, approvals, CLI).
Author-email: FIRME AI <security@cainstudio.online>
License-Expression: Apache-2.0
Project-URL: Homepage, https://cainstudio.online
Project-URL: Documentation, https://cainstudio.online/docs/quickstart
Project-URL: Changelog, https://cainstudio.online/changelog
Project-URL: Signup, https://cainstudio.online/signup
Keywords: ai-agents,agent-security,authorization,langchain,mcp,guardrails,human-in-the-loop,audit
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Security
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: crypto
Requires-Dist: cryptography>=41; extra == "crypto"
Provides-Extra: agi
Requires-Dist: anthropic>=1.0; extra == "agi"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
Provides-Extra: crewai
Requires-Dist: crewai>=0.100; extra == "crewai"
Provides-Extra: llamaindex
Requires-Dist: llama-index-core>=0.11; extra == "llamaindex"
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai>=0.0.20; extra == "pydantic-ai"
Provides-Extra: autogen
Requires-Dist: autogen-agentchat>=0.4; extra == "autogen"
Provides-Extra: google-adk
Requires-Dist: google-adk>=0.1; extra == "google-adk"
Provides-Extra: claude-agent-sdk
Requires-Dist: claude-agent-sdk>=0.1; extra == "claude-agent-sdk"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
Provides-Extra: all
Requires-Dist: autogen-agentchat>=0.4; extra == "all"
Requires-Dist: claude-agent-sdk>=0.1; extra == "all"
Requires-Dist: crewai>=0.100; extra == "all"
Requires-Dist: google-adk>=0.1; extra == "all"
Requires-Dist: langchain-core>=0.3; extra == "all"
Requires-Dist: llama-index-core>=0.11; extra == "all"
Requires-Dist: mcp>=1.0; extra == "all"
Requires-Dist: openai-agents>=0.1; extra == "all"
Requires-Dist: opentelemetry-api>=1.20; extra == "all"
Requires-Dist: pydantic-ai>=0.0.20; extra == "all"
Dynamic: license-file

# cainstudio

**Authorize every consequential action your AI agents take, before it runs.**

Your agent decides to send an email, move money, or run SQL. `cainstudio` asks
[CAIN](https://cainstudio.online) first, with the exact arguments the model chose.
The call runs only when CAIN allows it. A refused or held call never executes, and
every decision leaves a signed record you can inspect later.

- **One decorator**, sync or async, for any framework, or `protect([...])` for LangChain tools
- **Tool rules without redeploying**: deny `/tools/send_email`, or hold transfers over $1,000 for a human
- **Human approval built in**: a held call can wait for a person, then run exactly once
- **Traces**: every decision in one agent run is grouped under a run id
- **Fail-closed**: a timeout, an outage or an unknown verdict never becomes "allowed"
- **Zero dependencies**: the Python standard library only; `langchain-core` is optional

```
# Hosted index (works today; PyPI publication is pending):
pip install --index-url https://cainstudio.online/simple cainstudio
pip install "cryptography>=41"   # optional: OpenSSL-backed (constant-time) agent signing
```

## See it work in 30 seconds (no account)

```
$ cainstudio try
BLOCKED  scenario=prompt-injection  decision=fd_82fcd19ed7a74184a9011b7e
  identity       allow             enforcing  resolved service principal
  risk           deny              recorded   matched injection technique: instruction override, ...
  trust          deny              enforcing  trust=unknown, risk=high, decision=deny
  ...
  execution      deny              enforcing  execution prevented: an enforcing stage denied this action
```

That is a real decision from the live pipeline, stage by stage, for a throwaway demo
tenant. `cainstudio try --list` shows the other scenarios.

## Agent security: identity, sandbox, memory, evidence

```python
from cainstudio import Cain
cain = Cain()                                             # CAIN_API_KEY from the environment

# Identity (RIG): the agent's private key is generated here and never leaves this machine
bot = cain.identity.register("billing-bot", actions=["read"], scopes=["/invoices"])
token = bot.token("read", "/invoices/2026-10.pdf")       # single use, 30 s by default
assert bot.use(token).verified                           # proof-of-possession, not a bearer secret

# Sandbox (OpenShell): ask before running a command
assert not cain.sandbox.allowed("billing-bot", "cat /etc/shadow")

# Memory (MemVault): injections are quarantined, agents cannot read each other's memory
cain.memory.write("billing-bot", "terms", "Invoices are due in 30 days.")
cain.memory.search("billing-bot", "when are invoices due")

# Evidence (SCITT): receipts you can check offline, without trusting CAIN
receipt = cain.evidence.record("billing-bot", "invoice.read", {"file": "2026-10.pdf"})
assert cain.evidence.verify(receipt, body={"file": "2026-10.pdf"}).ok
```

What each one does and does not do:

| Product | Guarantees | Limits |
|---|---|---|
| Identity | per-agent Ed25519 keys; tokens single use, 1-300 s, bound to action + resource (+ exact request); delegation only narrows; revoking an agent revokes its descendants | no X.509/JWT SVIDs, no cross-domain federation |
| Sandbox | policy decision per command (program, arguments, paths) | the decision is advisory unless you run commands only after ALLOW |
| Memory | rules + classifier: 61.7% recall, 100% precision, 0% false positives on the held-out deepset test split (n=116); per-agent isolation; quarantine; provenance; tamper-checked | misses many paraphrased injections; lexical (not semantic) search unless you pass vectors |
| Evidence | RFC 9162 Merkle log, Ed25519-signed checkpoints, inclusion + consistency proofs, offline verification | not the IETF SCRAPI API; no external witnesses yet |

### Check that it holds: self-run conformance

```bash
export CAIN_API_KEY=...                        # an owner or member key of the tenant to test
export CAIN_SECOND_KEY=...                     # optional: a key of ANOTHER tenant, for cross-tenant checks
cainstudio conform --second-key-env CAIN_SECOND_KEY --out conformance.json
```

32 checks, each a real operation or a real attack through the public API:
- **RIG:** delegation cannot widen; tokens are single use and bound to the request; a stolen token is useless without
  the key; revocation cascades.
- **OpenShell:** `;` `&&` `|` `$(` backtick, newline, `>` `<` `&` and `$VAR` are refused, as are forbidden paths,
  traversal and paths outside the policy.
- **MemVault:** injected text is quarantined and never returned by search; agents cannot read each other's memory.
- **SCITT:** receipts and an append-only consistency proof are verified locally.
- **Cross-tenant:** another tenant can neither read your data nor tell it exists.

Each product gets CAIN-CONFORMANT, CAIN-PARTIAL, CAIN-INCOMPATIBLE or CAIN-UNASSESSED. The report carries a SHA-256
digest. Exit codes: 0 conformant, 2 any failure, 1 error.

This is a test you run yourself, not a certification. Test agents and memories are revoked afterwards. Evidence
statements stay, because the log is append-only. From Python: `from cainstudio.conformance import run`.

## Protect your first tool

Get a free key at <https://cainstudio.online/signup> (no card), then:

```
export CAIN_API_KEY=...
```

```python
import cainstudio

@cainstudio.guard()
def transfer(amount_usd: float, to: str) -> str:
    ...  # runs only if CAIN allows this call, with these arguments

try:
    transfer(5000, "acme")
except cainstudio.ApprovalRequired as e:
    print("held for a human:", e.approval_id)
except cainstudio.ActionBlocked as e:
    print("refused:", e.decision.reasons)
except cainstudio.CainUnavailable:
    print("CAIN unreachable: not run")        # fail-closed
```

A brand-new agent has no trust history, so its first actions usually come back
`REQUIRE_APPROVAL`: unknown trust is never promoted to "allowed" on its own.

## LangChain and LangGraph

```
pip install "cainstudio[langchain] @ https://cainstudio.online/cainstudio-0.3.0-py3-none-any.whl"
```

```python
import cainstudio
from cainstudio.langchain import protect

tools = protect([search, send_email, transfer_funds], agent_id="support-agent")
agent = create_agent(model, tools)          # names, descriptions and schemas are unchanged

with cainstudio.run():                      # optional: one trace per agent run
    agent.invoke({"messages": [...]})
```

A refused call is returned to the model as the tool's error, so the agent can explain
itself or take another route. Pass `on_block="raise"` to raise instead.

## Other agent frameworks

> The integrations below, the coding-agent hooks, MCP pinning, content inspection, discovery, red team,
> credential broker and OpenTelemetry need **0.3.0** or later (the hosted wheel above).

Each adapter uses the framework's own pre-execution hook, so the framework still runs the tool, and only
after CAIN allowed that exact call. A refused, held or unanswered call never runs the tool body.

| Framework | Use | Extra |
|---|---|---|
| OpenAI Agents SDK | `from cainstudio.integrations.openai_agents import protect` then `Agent(tools=protect([...]))` | `openai-agents` |
| CrewAI | `from cainstudio.integrations.crewai import protect` | `crewai` |
| LlamaIndex | `from cainstudio.integrations.llamaindex import protect` | `llamaindex` |
| Pydantic AI | `Agent(..., capabilities=[CainCapability(agent_id=...)])` from `cainstudio.integrations.pydantic_ai` | `pydantic-ai` |
| AutoGen (agentchat) | `from cainstudio.integrations.autogen import protect` | `autogen` |
| Google ADK | `from cainstudio.integrations.google_adk import protect` | `google-adk` |
| Claude Agent SDK | `ClaudeAgentOptions(hooks=cain_hooks(agent_id=...))` from `cainstudio.integrations.claude_agent_sdk` | `claude-agent-sdk` |
| Any MCP client | `CainMCPProxy(session, pin_tools=True, scan_tools=True)` from `cainstudio.integrations.mcp` | `mcp` |
| Anything else | `@cainstudio.guard()` on the function, under the framework's own decorator | |

`cainstudio.integrations.available()` shows which of these import in your environment.

## Claude Code and Cursor

CAIN can decide on every tool call, shell command and MCP call a coding agent makes:

```
cainstudio hook config claude-code   # paste the printed "hooks" block into .claude/settings.json
cainstudio hook config cursor > .cursor/hooks.json
```

The editor runs `cainstudio hook claude-code` (or `cursor`) before each call. A refused call is denied with
CAIN's reason. A call CAIN holds for approval becomes the editor's own "ask" prompt (for Cursor shell and MCP
calls; other Cursor events deny). Anything that goes wrong in the hook denies: no key, CAIN unreachable, or
unreadable input. Cursor's file reads send only the path, never the contents. Set `CAIN_HOOK_AGENT_ID` to
name the agent, and `CAIN_HOOK_WAIT` to wait for an approval instead of asking.

## MCP tool pinning and poisoning checks

```python
from cainstudio.integrations.mcp import CainMCPProxy

session = CainMCPProxy(session, agent_id="research-agent", pins_file="mcp-pins.json", scan_tools=True)
tools = await session.list_tools()      # changed or poisoned tools are removed from this list
```

With pinning on, the first `list_tools` records a SHA-256 of each tool's name, description and schema. A tool
that later changes (a "rug pull") is dropped from what the model sees, and every call to it is refused. A tool
whose description hides instructions is dropped and refused the same way. Hidden instructions include
`<IMPORTANT>` blocks, "ignore previous instructions", "do not tell the user", credential-file paths and
invisible Unicode. Calls are also refused until this session has listed the tools, and for any tool that
was never listed. These refusals happen on your machine, before CAIN or the server is asked.

## Inspecting what tools return

`check` decides whether an agent may act. Indirect prompt injection arrives the other way, in what a tool
returns: a web page, an email, a file. `cainstudio.inspection` looks for injected instructions, secrets and
personal data, locally, with no network call:

```python
from cainstudio.inspection import inspect_text, redact
report = inspect_text(page)        # report.injection / report.secrets / report.pii / report.rules()
safe = redact(page)                # secrets and personal data -> [REDACTED:<kind>]

session = CainMCPProxy(session, inspect_results=True, redact_results=True)   # every MCP tool result
```

```
cainstudio inspect < page.txt      # exit 2 when it finds injected instructions
```

Secret and card/IBAN findings are precise: documented key formats plus Luhn and mod-97 checks. Injection
detection is pattern-based. On the held-out test split of the public deepset/prompt-injections dataset it
had precision 1.0 but recall 0.30, and it flagged 0 of 130 benign package READMEs (see
`benchmarks/inspect_benchmark.json`). So it catches the blunt forms and misses paraphrases. Pass
`classifier=` to add your own model; a classifier that raises counts as a detection, never as clean.

## Calling APIs without giving the agent the key

Store the credential in CAIN once (owner or admin key; agent keys cannot), scoped to hosts and methods:

```
curl -s https://cainstudio.online/fabric/broker/credentials -H "X-API-Key: $CAIN_ROOT_KEY" \
  -H 'content-type: application/json' -d '{"name":"github","kind":"bearer","secret":"ghp_...",
  "allowed_hosts":["api.github.com"],"allowed_methods":["GET","POST"]}'
```

The agent then calls through CAIN. CAIN decides on the exact call (tool `http_<method>`, so your rules,
the risk model and approvals apply), and only then sends it with the credential added:

```python
resp = cain.broker_call("github", "https://api.github.com/repos/acme/app/issues", method="POST",
                        json_body={"title": "Flaky test"}, wait_for_approval=120)
```

The secret never reaches the agent's process, prompt or logs, and it is scrubbed from responses that echo
it. It is only sent to the credential's own hosts and methods, and redirects are not followed.
Revocation is immediate. The stored credential itself is long-lived: CAIN does not mint per-call downstream
tokens.

## Find what is ungoverned, then attack your own rules

```
cainstudio discover ~/src            # MCP servers in Claude Code/Desktop, Cursor, VS Code, Windsurf, Gemini, Codex;
                                     # whether the CAIN hook is installed; agent frameworks in code without cainstudio
cainstudio redteam --agent my-agent  # 22 dangerous calls (rm -rf variants, base64, reverse shells, mass deletes,
                                     # credential reads, exfiltration, wildcard IAM, look-alike tool names) as
                                     # dry runs against your live rules; exit 2 if any would run with no human
```

`discover` is local and read-only. It never starts a server and never prints a secret's value, only that
one is inline in a config. `redteam` sends one dry-run decision per case: no evidence is written and nothing
is queued, but each case counts against your quota.

## OpenTelemetry

```python
from cainstudio.integrations.otel import instrument
cain = instrument(Cain())       # every decision becomes an "execute_tool <tool>" span
```

Spans nest under your current agent span and carry `gen_ai.tool.name`, `gen_ai.agent.id`, `cain.verdict`,
`cain.decision_id` and `cain.reasons`, so they show up in any OTLP backend you already export to.
Arguments are left out unless you pass `record_arguments=True`. Needs `pip install "cainstudio[otel]"`.

## Rules, approvals and the kill switch

Rules live in CAIN, not in your code, so changing them needs no redeploy. Add them in
the console at <https://cainstudio.online/dashboard>, or over HTTP:

```
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"}'
```

Wait for a human instead of failing:

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

An approval is single-use: it lets that one call through, and the next identical call is
held again. An agent key cannot approve its own actions.

## The client

```python
from cainstudio import Cain

cain = Cain()                                   # CAIN_API_KEY, CAIN_BASE_URL
d = cain.decide("send_email", {"to": "a@example.com"}, agent_id="support-bot")
d.allowed, d.held, d.blocked, d.verdict, d.reasons, d.stages

cain.decide("run_sql", {"query": "DROP TABLE t"}, dry_run=True)   # evaluate, record nothing
cain.decisions(20); cain.get_decision(d.id); cain.explain(d.id)
cain.approvals(); cain.approve("ap_..."); cain.deny("ap_...", note="not today")
await cain.adecide(...); await cain.acheck(...)                    # async
```

`d.allowed` is true only for `ALLOWED`, `ALLOWED_DEGRADED` or `ALLOWED_WITH_DENIALS`
with `blocked` false. Every other verdict means "do not run".

Label where argument values came from, so a recipient or URL that the agent copied out
of a tool result cannot be used:

```python
cain.decide("send_email", {"to": addr}, provenance={"to": "tool:read_inbox"})   # BLOCKED
```

## Verify a decision yourself

```python
v = cain.verify(d)
v.status      # VERIFIED, FAILED, PENDING (certificate not issued yet) or NOT_ORDERED
v.checks      # [("COMMITMENT", True, ...), ("DIGEST", ...), ("QUORUM", ...), ...]
```

`verify` fetches the decision's quorum certificate and checks it in your process:
- the commitment is recomputed from the decision you received;
- the certificate is for that commitment;
- at least 3 of the 4 replicas signed it, checked against replica keys pinned in this
  package, never keys the certificate carries;
- an allowed verdict was already allowed before consensus.

It needs no extra packages (pure-Python Ed25519, or `cryptography` when installed).
Stages after consensus (egress, tool arguments, execution) are not in the certificate;
`cainstudio.verify_decision(decision_json, certificate_json)` does the same check fully offline.

## Command line

```
cainstudio try [scenario] [--list]           # no key needed
cainstudio decide send_email --args '{"to": "a@example.com"}' --agent support-bot
cainstudio decisions --limit 20
cainstudio explain fd_...
cainstudio approvals
cainstudio approve ap_... --note "checked with finance"
```

`decide` exits 0 allowed, 2 blocked, 3 held, 1 error, so it drops into shell scripts and CI.

## Configuration

| Variable | Default | |
|---|---|---|
| `CAIN_API_KEY` | none | required, except for `cainstudio try` |
| `CAIN_BASE_URL` | `https://cainstudio.online` | https only, except for localhost |

## What this package is

A thin HTTP client for the CAIN decision API. It contains no decision logic: every
verdict comes from the CAIN service. The API it calls is documented at
<https://cainstudio.online/docs> and in [openapi.json](https://cainstudio.online/openapi.json).
On the hosted service not every pipeline stage enforces yet. Each stage in a decision
says whether it enforced, and <https://cainstudio.online/fabric/status> shows the current state.

- Docs: <https://cainstudio.online/docs/quickstart>
- Book a demo: <https://cainstudio.online/book-demo>
- Security: security@cainstudio.online

Apache-2.0
