AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Cruxial

mcp-cruxial-ai-cruxial · by cruxial-ai

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

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add mcp-cruxial-ai-cruxial

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-cruxial-ai-cruxial)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Cruxial? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

pip install cruxial

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

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()

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

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:

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.

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/runvsown_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:

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:

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:

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

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:

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

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.