Drop-in permission middleware for AI agents.
Project description
Custos
Drop-in permission middleware for AI agents.
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, ask you, or quarantine the call when indirect prompt injection is detected — based on a declarative policy you control.
Think of it as OAuth-style consent, but for autonomous LLM agents.
┌───────┐ ┌────────────────────┐ ┌──────────┐
│ Agent │─────>│ Custos │─────>│ Tools │
└───────┘ │ │ └──────────┘
│ policy evaluator │
│ + IPI defence │
│ + 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 or context inspector 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/inspect.
└────┬─────┘
│
┌────▼─────┐
│ 2. Context│ (optional) If policy says "inspect:<name>", an inspector
│ insp. │ scans full agent context for indirect prompt injection.
└────┬─────┘
│
┌────▼─────┐
│ 3. AI │ (optional) If policy says "assist:<name>", an LLM-driven
│ asst. │ assistant scores risk and returns a recommendation.
└────┬─────┘
│
┌────▼─────┐
│ 4. User │ (optional) If still unresolved, prompts you via CLI,
│ prompt │ Slack, web widget, or webhook. You decide.
└────┬─────┘
│
┌────▼─────┐
│ 5. Fatigue│ Deduplicates repeated prompts, batches similar calls,
│ layer │ enforces rate limits, supports "ask me later".
└────┬─────┘
│
┌────▼─────┐
│ 6. 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
+ quarantine (injection detected — block + memory wipe)
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. |
quarantine |
Injection detected. Block call + trigger SDK memory wipe. |
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: inspect:ipi-defender # scan for prompt injection first
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–A12)
Pluggable AI assistants that handle policy escalations. A1–A6 reproduce the Janus reference designs; A7–A12 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 |
| A12 | IPI defender | Context inspector. Detects indirect prompt injection via pattern matching + leave-one-out causal attribution. | configurable |
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
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.
- LangChain —
wrap_langchain_tools(gw, agent.tools) - MCP —
gated_tooldecorator andwrap_mcp_toolsfor FastMCP servers - OpenAI Agents SDK —
gated_function_tooldecorator - Anthropic —
gated_anthropic_toolfor the messages-API dispatch loop - AutoGen, Google ADK, LlamaIndex — v1.0 carry-forward adapters
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, IPI detection, 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. - 6 additional assistants — A7 (rule-policy), A8 (summarize-batch), A9 (context-adaptive), A10 (learned-policy), A11 (delegation-aware), A12 (ipi-defender).
- 7 decision types — Janus has 3 (approve_once / create_policy / reject).
Custos adds
allow,prompt,defer, andquarantine. - 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-A12 with exfiltrates_args flags |
| Context inspectors | A12 IPI defence + leave-one-out attribution |
| 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file custos_middleware-1.1.1.tar.gz.
File metadata
- Download URL: custos_middleware-1.1.1.tar.gz
- Upload date:
- Size: 428.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4eef4c2a4540bd4a5aa8551f8ccd557f4d53622946fe5519b5863b4b61c06d7e
|
|
| MD5 |
751c7603dd6a184de3de0895a40ae433
|
|
| BLAKE2b-256 |
1bf15205a8f8f6cecce9287f34a80bcc74f6373e58aa8d5e279f0903752d4763
|
Provenance
The following attestation bundles were made for custos_middleware-1.1.1.tar.gz:
Publisher:
release.yml on Taki-chiasf/Custos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
custos_middleware-1.1.1.tar.gz -
Subject digest:
4eef4c2a4540bd4a5aa8551f8ccd557f4d53622946fe5519b5863b4b61c06d7e - Sigstore transparency entry: 2312791120
- Sigstore integration time:
-
Permalink:
Taki-chiasf/Custos@4111b9fc49b9d1b7be12c8c649814d52ce26da2c -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/Taki-chiasf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4111b9fc49b9d1b7be12c8c649814d52ce26da2c -
Trigger Event:
push
-
Statement type:
File details
Details for the file custos_middleware-1.1.1-py3-none-any.whl.
File metadata
- Download URL: custos_middleware-1.1.1-py3-none-any.whl
- Upload date:
- Size: 270.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
916e798f57f062c6825d17b9b74c773a8f2ae97075396afd5df2e0246fc1470f
|
|
| MD5 |
a8816555c01a80ece80d01b34ccb9513
|
|
| BLAKE2b-256 |
7ad1b78a51543924b98523bb652fab408d94fa42c9d73342cce13d40992d6bc8
|
Provenance
The following attestation bundles were made for custos_middleware-1.1.1-py3-none-any.whl:
Publisher:
release.yml on Taki-chiasf/Custos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
custos_middleware-1.1.1-py3-none-any.whl -
Subject digest:
916e798f57f062c6825d17b9b74c773a8f2ae97075396afd5df2e0246fc1470f - Sigstore transparency entry: 2312791123
- Sigstore integration time:
-
Permalink:
Taki-chiasf/Custos@4111b9fc49b9d1b7be12c8c649814d52ce26da2c -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/Taki-chiasf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4111b9fc49b9d1b7be12c8c649814d52ce26da2c -
Trigger Event:
push
-
Statement type: