Skip to main content

Drop-in permission middleware for AI agents.

Project description

Custos

Drop-in permission middleware for AI agents.

Python License Version


What is Custos?

Modern AI agents make tool calls — they read your files, send emails, query databases, run shell commands, and call APIs. A single misdirected prompt can cause real damage: data exfiltration, unintended writes, or privilege escalation.

Custos sits between your agent and the tools it calls, intercepting every invocation and deciding whether to allow, deny, or ask you — based on a declarative policy you control.

Think of it as OAuth-style consent, but for autonomous LLM agents.

  ┌───────┐      ┌────────────────────┐      ┌──────────┐
  │ Agent │─────>│      Custos        │─────>│  Tools   │
  └───────┘      │                    │      └──────────┘
                 │  policy evaluator  │
                 │  + AI assistant    │
                 │  + your approval   │
                 │  + audit trail     │
                 └────────────────────┘

Key principles:

  • Default-deny. Anything not explicitly allowed in policy is blocked.
  • Policy is the floor. An AI assistant can escalate strictness, but can never override a deny.
  • Transparent wrapping. The agent sees the same tool signatures it always did. Gating is invisible to the agent code.
  • Zero runtime deps beyond a JSON-schema validator. Every framework adapter, LLM backend, and eval suite is an optional extra.

How it works

Every tool call flows through a single decision pipeline:

    Tool call
       │
  ┌────▼─────┐
  │ 1. Policy│  Deterministic. Evaluates your YAML rules.
  │   engine │  First-match-wins. Returns allow/deny/assist/prompt.
  └────┬─────┘
       │
  ┌────▼─────┐
  │ 2. AI    │  (optional) If policy says "assist:<name>", an LLM-driven
  │   asst.  │  assistant scores risk and returns a recommendation.
  └────┬─────┘
       │
  ┌────▼─────┐
  │ 3. User  │  (optional) If still unresolved, prompts you via CLI,
  │   prompt │  Slack, web widget, or webhook. You decide.
  └────┬─────┘
       │
  ┌────▼─────┐
  │ 4. Fatigue│  Deduplicates repeated prompts, batches similar calls,
  │   layer  │  enforces rate limits, supports "ask me later".
  └────┬─────┘
       │
  ┌────▼─────┐
  │ 5. Audit │  Every decision is logged — what, who, why, how long.
  │   + return│  Hash-chained, tamper-evident, verifiable.
  └────┬─────┘
       │
  Decision: allow / allow_once / allow_and_persist / deny / prompt / defer

Decision types

Decision Meaning
allow Matched a standing policy rule. Execute immediately.
allow_once Execute this one time. Future identical calls will be re-evaluated.
allow_and_persist Execute and save a new policy rule for future calls.
deny Blocked. Final — no assistant or responder can override.
prompt Hand to the responder. Ask the user what to do.
defer Rate-limited or batched. Try again later.

Quick example

from custos import Gateway, Policy
from custos.assistants import RulePolicy
from custos.responders import CLIResponder
from custos.audit import FileAuditSink

# 1. Load your rules
policy = Policy.from_yaml("policy.yaml")

# 2. Assemble the gateway
gw = Gateway(
    policy=policy,
    assistant=RulePolicy,                # deterministic fast path (A7)
    responder=CLIResponder(timeout=30),  # ask you in the terminal
    audit_sink=FileAuditSink("audit.jsonl"),
)

# 3. Wrap your tools — they keep the same signatures
gated_read, gated_write, gated_send = gw.wrap([read_file, write_file, send_email])

# 4. Agent code calls tools normally; Custos gates transparently
gated_read("/etc/hosts")   # policy match -> allowed, silently audited
gated_send(  "/hi")        # no policy match -> prompt: y/N/a/A/l/d

A minimal policy.yaml:

version: 1
default: deny                    # everything blocked by default

overlays:
  - id: base
    rules:
      - match: { tool: "fs.read*" }
        action: allow_and_audit

      - match: { tool: "fs.write*", side_effects: [write] }
        action: assist:risk-assessment    # hand to AI risk scorer

      - match: { tool: "shell.*", risk_tier: [4, 5] }
        action: prompt                    # ask the user

      - match: { tool: "payment.*" }
        action: prompt
        options: [allow_once, deny]       # no standing allows

      - match: { tool: "email.send" }
        action: assist:constitution       # check against your constitution doc

On a deny or defer, the wrapper raises custos.exceptions.PermissionDenied. For async frameworks (OpenAI Agents, Anthropic, MCP, AutoGen), use AsyncGateway — same pipeline, native-async.


Custos vs. what exists

Approach Autonomy Safety User fatigue How Custos compares
No guardrails Full None Zero Custos adds a safety floor without breaking tool signatures
Manual review None Maximum Severe Custos automates the routine; prompts only on high-risk calls
System prompts only Full Brittle Zero Prompts are advice, not enforcement. Custos is a hard gate
Janus Configurable Configurable Configurable Academic reference. Custos is a production-grade reimplementation
Custos Configurable Configurable Tunable (fatigue layer) Policy floor + AI assistant + user prompt + audit, all in one

Install

The runtime has zero hard dependencies beyond jsonschema. Pick the extra that matches what you need:

# Start here — runtime-only, policy via Python dicts or YAML
pip install "custos-middleware[yaml]"       # + PyYAML (recommended)
pip install custos-middleware               # programmatic policies only

# LLM-backed AI assistants (A3-A6, A9)
pip install "custos-middleware[llm]"        # + LiteLLM

# Framework adapters — pick your agent framework
pip install "custos-middleware[langchain]"   # LangChain
pip install "custos-middleware[mcp]"         # MCP in-process
pip install "custos-middleware[openai-agents]"   # OpenAI Agents SDK
pip install "custos-middleware[anthropic]"   # Anthropic messages API

# Eval harness + CI test suites
pip install "custos-middleware[eval]"        # google-adk, litellm (dev/test)

# Local development
pip install "custos-middleware[dev]"         # pytest, ruff, mypy

All extras are additive: pip install "custos-middleware[yaml,llm,langchain]".

Which one do I need?

You want to... Install
Try it out, read the docs custos-middleware[yaml]
Add LLM-powered risk scoring add [llm]
Protect a LangChain agent add [langchain]
Run the adversarial eval suite in CI add [eval]
Develop Custos itself add [dev]

Components

Custos is assembled from interchangeable pieces. Wire what you need, skip what you don't.

Policy engine

A deterministic, pure-function rules engine. Write rules in YAML or construct them programmatically. First-match-wins, default-deny. Hot-reloadable. Supports tool-name globs, arg predicates, risk tiers, delegation depth, and side-effect matching. Docs →

Permission assistants (A1–A11)

Pluggable AI assistants that handle policy escalations. A1–A6 reproduce the Janus reference designs; A7–A11 are Custos extensions:

ID Name Strategy Needs LLM?
A1 Auto-approve Always yes. Baseline. No
A2 User confirmation Always ask. Maximum safety. No
A3 Constitution Check against a written constitution doc. Yes
A4 Policy suggestion Draft generalized ABAC rules for you. Yes
A5 Risk assessment Score risk against task goals; escalate to user. Yes
A6 Risk assessment autonomous Same as A5, but deny instead of prompting. Yes
A7 Rule policy Pure deterministic rules. Fast path. No
A8 Summarize batch Batch similar calls into one prompt. No
A9 Context adaptive Choose prompt granularity by sensitivity. Yes
A10 Learned policy Learn from your past decisions. No
A11 Delegation aware Stricter rules for deeper delegation chains. No

Docs →

Responders

User-facing prompt backends. One gateway can route to different responders per tool or risk tier.

  • CLI — yn/A/a/L/d prompt in the terminal
  • Noop — silent deny (for CI/headless)
  • Web — SSE-backed web widget, embeddable
  • Webhook — HMAC-signed HTTP callbacks with nonce replay protection
  • Slack — Block Kit messages with signed interactions
  • Multi-approver — Quorum: requires N distinct approvers from disjoint roles

Docs →

Fatigue layer

Prevents prompt storms. Deduplicates repeated tool calls (SHA-256 args hash), batches similar calls into a single prompt, enforces per-minute rate limits, and supports "ask me later" via defer. Docs →

Audit

Structured, append-only JSONL decision log. Every event records: what tool was called, by whom, what the policy said, what the assistant recommended, what you decided, and how long it took. Hash-chained mode adds tamper evidence with custos audit verify. Secret/PII fields in tool args are redacted before they reach the audit log or any responder. Docs →

Framework adapters

In-process wrappers that make Custos transparent to the agent framework. The agent sees normal tool signatures; Custos sits inside as a gating layer.

  • LangChainwrap_langchain_tools(gw, agent.tools)
  • MCPgated_tool decorator and wrap_mcp_tools for FastMCP servers
  • OpenAI Agents SDKgated_function_tool decorator
  • Anthropicgated_anthropic_tool for the messages-API dispatch loop
  • AutoGen, Google ADK, LlamaIndex — v1.0 carry-forward adapters

Docs →

Eval harness

custos eval CLI for CI. Two suites: (1) a Janus v1 parity matrix reproducing the published 72-cell evaluation, and (2) a Custos-authored adversarial suite (53+ cells covering prompt injection, confused deputy, tool spoofing, delegation abuse, policy poisoning, and quorum bypass). Default backend is local Ollama — no API spend. Docs →


Relationship to Janus

Janus (arXiv:2607.01510, Brigham et al., U. Washington) is the academic reference implementation that established the permission-assistant design space. It introduces the concept of plug-and-play permission assistants and the six-assistant catalog (Auto-Approve, User Confirmation, Constitution, Policy Suggestion, Risk Assessment, Risk Assessment Autonomous).

Custos is an independent, production-grade reimplementation of the Janus concept. It is not affiliated with the Janus authors and does not vendor any Janus code (Apache-2.0). Key differences:

  • Production runtime — Zero hard deps beyond jsonschema. Janus depends on Google ADK and LiteLLM.
  • 5 additional assistants — A7 (rule-policy), A8 (summarize-batch), A9 (context-adaptive), A10 (learned-policy), A11 (delegation-aware).
  • 6 decision types — Janus has 3 (approve_once / create_policy / reject). Custos adds allow, prompt, and defer.
  • Cross-language — Python SDK, TypeScript SDK (@taqiy/custos-core), and a gRPC sidecar for out-of-process deployment.
  • Audit tamper evidence — Hash-chained JSONL with HMAC signing and custos audit verify.
  • Deterministic policy engine — Pure, no side effects, hot-reloadable. Janus policy is runtime-coupled to the ADK session.

License

Apache-2.0. See LICENSE.


Documentation

Document What it covers
Quickstart 5-line integration, policy YAML, audit log
Tutorial 20-30 minute walk from zero to gated agent
Policy schema Full YAML reference: match criteria, actions, overlays
Policy cookbook 5 runnable recipes for common patterns
Assistant catalog A1-A11 with exfiltrates_args flags
Responder reference CLI, web, Slack, webhook, multi-approver
Audit reference Sinks, hash-chaining, custos audit verify
Eval harness Janus-v1 parity + adversarial CI suites
Adapters Per-framework integration details
Sidecar (gRPC) Cross-language deployment surface
Threat model Normative: every mapped to a STRIDE threat
IR contract Cross-language pinning: Python-TS byte parity
Changelog Phased release history
Contributing Code style, runtime-dep discipline, test policy
Security Vulnerability disclosure policy

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

custos_middleware-1.0.1.tar.gz (407.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

custos_middleware-1.0.1-py3-none-any.whl (259.7 kB view details)

Uploaded Python 3

File details

Details for the file custos_middleware-1.0.1.tar.gz.

File metadata

  • Download URL: custos_middleware-1.0.1.tar.gz
  • Upload date:
  • Size: 407.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for custos_middleware-1.0.1.tar.gz
Algorithm Hash digest
SHA256 201b9ea121ff521c70d3d43f4883c41ffb18c0bd2e82aa0311e082e645242b70
MD5 bc206a16fdaa7aec8a371a93758812f1
BLAKE2b-256 24e20e59ffd00a8baa63f7ff7e60f2e85dd08d466d8a07e512d7c7a2d76a201e

See more details on using hashes here.

Provenance

The following attestation bundles were made for custos_middleware-1.0.1.tar.gz:

Publisher: release.yml on Taki-chiasf/Custos

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file custos_middleware-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for custos_middleware-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 054ba8c264868afec1bf145d8108e7dacd9012f013f1a61af49632b86a24ab49
MD5 b1261c2c66a9ff65b01a63a43898bc09
BLAKE2b-256 02b6e29ac5bf5789c6a47c31cc9c6e4c48d89d8ff909ddc8de0f26d31adfd3fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for custos_middleware-1.0.1-py3-none-any.whl:

Publisher: release.yml on Taki-chiasf/Custos

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page