# Cruxial

> The action layer for AI agents. Proof of what your agent actually did, not what it claimed.

- **Type:** MCP server
- **Install:** `agentstack add mcp-cruxial-ai-cruxial`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cruxial-ai](https://agentstack.voostack.com/s/cruxial-ai)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cruxial-ai](https://github.com/cruxial-ai)
- **Source:** https://github.com/cruxial-ai/cruxial
- **Website:** https://cruxial.ai

## Install

```sh
agentstack add mcp-cruxial-ai-cruxial
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Cruxial

[](https://github.com/cruxial-ai/cruxial/actions/workflows/tests.yml)
[](https://pypi.org/project/cruxial/)
[](https://pypi.org/project/cruxial/)
[](LICENSE)
[](https://socket.dev/pypi/package/cruxial)

**The action layer for AI agents.**

Your agent said it sent the email. It didn't.

Cruxial proves what your agent actually did. Every side-effecting tool call resolves from a **receipt**: `posted` when there's proof, `unknown` when there isn't. Never the model's word for it.

It also validates each call against your schema and auto-repairs bad arguments before they run.

Drop-in for OpenAI and Anthropic. Under 1ms p99, all local, no extra network hop. Fails open by default: if Cruxial itself errors, your tool still runs.

```bash
pip install cruxial
```

Then watch it catch every failure category live, offline, no API key:

```bash
cruxial demo
```

## When not to use Cruxial yet

Cruxial is honest about its boundaries. Reach for it when an agent takes **side-effecting actions** (sends, charges, writes) and you need proof they happened. Some cases it does not cover yet:

- **You want proof the *right* thing happened, not just *something*.** A receipt with an id resolves to `posted`. Confirming the id is the right account, amount, or recipient is your `@verify` hook's job (it can read the raw tool output), not automatic. See [Guarantees and edge cases](#guarantees-and-edge-cases).
- **You own the loop but don't call `check_bypass()`.** The claimed-but-never-called catch ("said it sent it, never called the tool") is automatic under `run()`. In your own loop it runs only when you call `cx.check_bypass(text, called_tools=...)` on text-only turns — skip it and that one catch is silently off. `@action` alone can't cover it: a decorator can't fire for a call that never happened. A guard with `@action` tools that never engages the catch warns at `close()`. See [Own your loop](#own-your-loop--guard).
- **Read-only or pure-compute tools.** Nothing to prove. Cruxial validates the args and otherwise stays out of the way.
- **You need a runtime block on a bad action.** Cruxial records and surfaces (`unknown` / `needs_review`); it does not stop your tool from running. Acting on that state is your policy.
- **Non-Python stacks today.** Python only for now. TypeScript is on the roadmap.

If your agent only reads and reasons, you don't need this yet. The value shows up the moment it starts *doing* things.

## Two ways to use it, same guarantees

Pick by how much of the loop you want to own. Both paths validate every tool call, auto-repair bad args, read the receipt, and record every action to the same local ledger.

- **`cruxial.run()`** drives one model turn for you: call the model, validate, execute, auto-repair in one round-trip, record. The drop-in below.
- **`guard()`** keeps the loop yours, for streaming, a framework that owns the model call (LangGraph, CrewAI, an Assistants/Responses runtime), or a worker dispatching a single call. You call `.execute()` and `.check_bypass()` yourself ([Own your loop](#own-your-loop--guard)).

## Quickstart: `cruxial.run()`

```python
import cruxial

result = cruxial.run(
    client,                 # your OpenAI / AzureOpenAI / Anthropic client, or litellm.completion
    model="gpt-4o",
    messages=messages,
    tools=tools,            # the same tool defs you already pass the LLM
    executors=executors,    # {tool_name: your_function}
)

while not result.finished:  # your loop stays yours, one model call per turn
    result = cruxial.run(client, model="gpt-4o", messages=result.messages,
                         tools=tools, executors=executors)

print(result.text)          # the model's final answer
```

It reuses your configured client (Azure endpoint, `base_url`, timeouts all preserved), derives schemas from `tools`, and fails open. Deliberately **one turn, not a framework**: no streaming, no multi-turn ownership, you decide when to stop.

`result.operations` and `result.state(tool)` reflect the **latest turn**. Keep your own list across the loop if you want every turn's operations. The full history is always in the ledger (`cruxial view`).

## The action layer: did it actually happen?

Schema validation catches a *malformed* call. It can't catch the worse failure: your agent says **"I sent the email"** and never called the tool, or the call returned with no proof it worked.

Mark a side-effecting tool with `@action`, tell Cruxial how to read its **receipt**, and every call resolves from the receipt instead of the model's word for it. No receipt means `unknown`, never a silent "done".

```python
import cruxial

@cruxial.action                          # side-effecting → a receipt is required
def send_email(to, subject, body):
    return mailer.send(to=to, subject=subject, body=body)   # e.g. {"message_id": "..."}

@cruxial.receipt("send_email")           # how to read the proof (or: id_field("message_id"))
def _(raw):
    return cruxial.Receipt(ok=bool(raw.get("message_id")), id=raw.get("message_id"), kind="email")

@cruxial.verify("send_email")            # optional domain check → PASS / FLAG / HALT
def _(args, receipt):                    # add an optional 3rd arg to see the raw tool output
    # Runs only once a receipt exists. A send with no receipt is already `unknown`.
    # HALT a real send that needs a human look (here: an external recipient) → needs_review.
    return cruxial.HALT("external recipient") if not args["to"].endswith("@acme.com") else cruxial.PASS

result = cruxial.run(client, model=m, messages=msgs, tools=tools, executors=ex)
result.state("send_email")   # → "posted" | "unknown" | "needs_review" | "failed"
result.render()              # receipt-derived summary, never a bare "done"
```

A claimed-but-never-called action is caught **deterministically**, recorded as `unknown` with no extra model call. Then see what your agents actually did:

```bash
cruxial view           # terminal ledger
cruxial view --web     # the dashboard below: confirmed vs silent-failure (unknown), per-op receipts
```

> **Upgrading from 0.4:** the action layer is additive. Existing `guard()`/`run()` code is unchanged until you mark a tool `@action`. One breaking change: `run(bypass="on")` now *detects deterministically* and records the action as `unknown` instead of re-prompting the model (the old re-prompt remediation has been removed; what to do about an `unknown` is your policy). See the [CHANGELOG](CHANGELOG.md).

## Own your loop: `guard()`

Can't hand the turn to `run()`? Streaming, a framework that owns the model call, a worker dispatching one tool? Wrap your registry once and call the two primitives `run()` is built on. They land the **same** ledger.

```python
import json
from cruxial import guard

cx = guard(schemas=schemas, executors=executors)    # the same tool defs you pass the LLM

for tool_call in llm_response.tool_calls:
    args = json.loads(tool_call.arguments)           # OpenAI returns arguments as a JSON string
    result = cx.execute(tool_call.name, args)        # validate → run → receipt → record
    if not result.ok:
        result.raise_on_failure()                    # typed error (category on result.failure)
    if result.state == "needs_review":               # a verify HALT, surface it, don't blind-retry
        ...
    use(result.value)

# A text-only turn that CLAIMS an action but emitted no call → detect AND record it:
if not tool_calls_this_turn:
    cx.check_bypass(assistant_text, called_tools=tools_called_so_far)
```

`cx.execute()` records the full operation (receipt-derived `state`), so the healthy path **and** the called-but-no-proof failure are covered for free.

The one line not to skip is **`cx.check_bypass()`** on text-only turns. It is the safe, fused detect-and-record for the claimed-but-never-called catch. `run()` does this for you internally; when you own the loop, you call it. (The bare `cruxial.bypass.detect_bypass()` *detects* but doesn't record, silently dropping the catch.)

**Async?** `await cruxial.arun(...)` and `await cx.aexecute(...)`, same contract, everything awaited.

**See it run:** `python examples/action_layer.py` shows both `execute()` and `run()` offline (no key). [`examples/run_vs_own_loop.py`](examples/run_vs_own_loop.py) puts both wirings through a live model and prints a parity table: identical ledger either way, so the choice is ergonomic, not a safety trade-off.

## What it catches

Eight failure categories. Every interception is logged with the failure category, never the raw argument values.

| Category | What it catches |
|---|---|
| `missing_required` | Required field not in args |
| `type_mismatch` | Wrong type (`int` instead of `str`, etc.) |
| `enum_violation` | Value not in allowed enum |
| `format_violation` | Bad email / uri / date format, or a `pattern` mismatch |
| `constraint_violation` | maxLength / minimum / multipleOf / etc. |
| `extra_field` | Model invented a field that doesn't exist |
| `unknown_tool` | Tool name not in registry |
| `tool_bypass` | **Model *claimed* it did something but emitted no call** |

The first seven are schema-derivable. **`tool_bypass`** is the one validators structurally can't catch: there's no call to validate.

### tool_bypass: the silent failure

A model says *"I've sent the email"* and emits **no `send_email` call**. No call goes out, so no error, no log, nothing to grep for. `cruxial.run()` catches it:

```python
result = cruxial.run(client, model="gpt-4o", messages=messages,
                     tools=tools, executors=executors)   # bypass check is on by default

if result.bypass:        # the model claimed an action it never called → recorded as `unknown`
    print("caught a bypass:", result.bypass.tool)
```

The flag is recorded **deterministically as `unknown`** with no extra model call. The receipt's absence is the oracle. Owning your loop instead of `run()`? `cx.check_bypass(text, called_tools=...)` records the same catch. `bypass="off"` disables detection entirely.

## Auto-repair

`cruxial.run()` auto-repairs for you. If you use `guard()` directly, call the adapter helper with the failure context:

```python
from cruxial.adapters.openai import auto_repair

cruxial = guard(schemas=schemas, executors=executors)
result = cruxial.execute(name, args)

if not result.ok:
    # 1-attempt structured retry, feeds the failure back to the model
    new_args = auto_repair(
        client,
        model=model,
        messages=messages,            # conversation incl. the assistant tool-call turn
        tools=tools,                  # the same tool defs you sent the model
        failure=result.failure,
        failed_args=args,
        repair_prompt=cruxial.build_repair_prompt(result.failure, args),
    )
    result = cruxial.execute_repaired(name, new_args)
```

Roughly **90% of intercepted calls are fixed in a single repair round-trip** on the pooled live-MCP benchmark (87 to 94% per run on current code). Every number is sourced in [BENCHMARKS.md](BENCHMARKS.md).

## See your interception rate

Every interception is written to a local SQLite file. No data leaves your machine. To see your real rate:

```bash
cruxial stats
```

**Stats are project-local automatically.** Inside a project (a dir with `.git/`, `pyproject.toml`, `setup.py`, or `.cruxial/`) the DB lives at `./.cruxial/telemetry.sqlite`, so each app stays separate; elsewhere it falls back to `~/.cruxial/`. Override with `CRUXIAL_DB_PATH`; `cruxial diagnostic` shows the active path.

Output:

```
cruxial · last 24h
─────────────────────────────────────────
  total calls           1,247
  intercepted             184  (14.8%)
  auto-repaired           167  (90.8% of intercepted)
  passed through        1,063

top failing tools                    rate
  send_email                        23.1%
  create_calendar_event             18.4%
  search_web                         9.2%

top failure categories
  type_mismatch                       62
  missing_required                    44
  enum_violation                      38
  format_violation                    24
  constraint_violation                16
```

`cruxial stats` also shows the registry independently of traffic, handy for the "is it even on?" check after install:

```
  registry              6 registered  ·  3 fired  ·  1 intercepted
```

Traffic but 0 interceptions usually means a well-behaved model on a simple schema. `cruxial.testing.violation_payloads(schema)` fires a synthetic violation per category to verify end-to-end.

## How it works

```mermaid
sequenceDiagram
    participant M as LLM model
    participant C as Cruxial guard
    participant T as Your tool / executor
    participant L as Local action ledger

    M->>C: tool call (name, args)
    C->>C: validate args vs JSON Schema
    alt args valid
        C->>T: run tool
        T-->>C: result (+ receipt for an @action)
        C->>C: resolve state from the receipt
        C->>L: record operation: posted | unknown | needs_review | failed
        C-->>M: receipt-derived state, never a bare "done"
    else args invalid
        C->>L: record intercepted + failure category (hashes only)
        opt auto-repair enabled
            C-->>M: repair prompt (1-shot retry)
            M->>C: corrected tool call
        end
        C-->>M: typed failure (caller decides what to do)
    end
    Note over M,C: model claims an action but emits NO call, recorded as unknown (the silent-failure catch)
```

Cruxial wraps the **tool registry**, not the LLM client. No monkey-patching, no proxies, no framework lock-in.

## Guarantees and edge cases

The core path is above. These are the details that matter once you run Cruxial against a real agent.

A receipt proves something happened, not the right thing

`posted` means the tool returned proof a call landed (an id, a message_id, a job id). It does **not** by itself prove the call hit the right account, amount, or recipient. A shallow receipt (HTTP 200 with the wrong target) still resolves to `posted`.

Closing that gap is the `@verify` hook's job. Add an optional 3rd argument to read the **raw tool output** and assert the specifics:

```python
@cruxial.verify("charge")
def _(args, receipt, output):            # 3rd arg = the raw return value
    # HALT if the amount that came back isn't the amount we asked to charge
    return cruxial.HALT("amount drift") if output["amount"] != args["amount"] else cruxial.PASS
```

A receipt is evidence at a level. Name the level you can actually prove rather than overclaiming.

extra_field on open schemas

JSON Schema is open by default, so an invented field passes schema validation. Cruxial still catches it: when you execute, the field is checked against the executor's signature and blocked as `extra_field` before it can crash the call.

So a hallucinated field is caught either at the schema layer (`additionalProperties: false` or `GuardConfig(strict_properties=True)`) or at the executor boundary. An executor that declares `**kwargs` opts into extras and is never blocked.

tool_bypass precision and recall

Precision-first by design: the local pre-filter fires only on completion-form verbs (`"sent"`, not `"send"`), attributed to the assistant (not *"you"* / *"the scheduler"* / *"automatically"*), for a side-effecting tool that was never called, and that no tool which actually ran already satisfies. A 132-scenario adversarial set is in [BENCHMARKS.md](BENCHMARKS.md).

Because it is precision-first, terse claims (`"Done."`, `"Email's out."`) are a documented recall gap, a high-quality net rather than a complete guarantee. The durable guarantee is the **receipt**, not the prose read.

Schema source: if your LLM sees a trimmed schema

If you keep two views of a tool schema, a **canonical** one for execution and a **trimmed** one sent to the LLM, register the *trimmed* one. The LLM can only satisfy the schema it was shown; validating against canonical fields it never saw turns `missing_required` / `extra_field`

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cruxial-ai](https://github.com/cruxial-ai)
- **Source:** [cruxial-ai/cruxial](https://github.com/cruxial-ai/cruxial)
- **License:** MIT
- **Homepage:** https://cruxial.ai

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-cruxial-ai-cruxial
- Seller: https://agentstack.voostack.com/s/cruxial-ai
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
