Metadata-Version: 2.4
Name: anchor-gateway
Version: 0.3.1
Summary: ANCHOR middleware: one-line governance for agent tools (allow/deny/ask, every verdict with a receipt)
Author: Lauren Flipo
License-Expression: Apache-2.0
Keywords: ai-agents,governance,policy,guardrails,capability-security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: langchain
Requires-Dist: langchain-core; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents; extra == "openai-agents"
Provides-Extra: crewai
Requires-Dist: crewai; extra == "crewai"
Dynamic: license-file

# ANCHOR Middleware — Phase 3

One-line governance for agent tools: every tool call is judged by the real
ANCHOR v1 guardian before it runs. Verdicts: **allow / deny / ask**, every one
logged with a receipt. Native shims for LangChain, OpenAI Agents SDK, and
CrewAI; human approval over CLI or webhook; four versioned constitution packs;
a live verdict dashboard served by the daemon; ships as the `anchor-gateway`
package with an `anchor` CLI.

## Install

```bash
pip install anchor-gateway            # core: daemon + client (stdlib-only, zero deps)
pip install "anchor-gateway[langchain]"
pip install "anchor-gateway[openai-agents]"
pip install "anchor-gateway[crewai]"
```

(Not yet published to PyPI — see `PUBLISH_CHECKLIST.md`. Until then, install
from this directory: `pip install -e .`)

Phase 2 was proven against langchain-core 1.6.6, openai-agents 0.20.0,
crewai 1.15.23 — the pinned set in `REQUIREMENTS.md`
(`pip check` clean; do not blindly upgrade openai-agents/crewai).

## Constitution packs

| Pack | For | Allow | Ask | Deny |
|---|---|---|---|---|
| `code-agent` | coding agents in one project dir | reads, listings, read-only cmds | writes, deletes, other cmds | traversal, exfil binaries, shell metachars |
| `browser-agent` | web-browsing agents | allowlisted-domain nav, clicks, reads | unknown domains, downloads/uploads, forms, credential typing | private IPs, `169.254.169.254`, `file://`, traversal |
| `support-agent` | customer-support agents | ticket/order reads, drafts, canned-template sends | freeform sends, refunds > $100 | bulk export (unknown tool), non-numeric amounts |
| `data-agent` | data agents | aggregated PII-free queries, table reads | row-level or PII queries, capped exports | prod writes (`write_table`, `drop_table`), over-cap exports |

```bash
anchor run --constitution constitutions/browser-agent.yaml
# or: anchor-gateway --port 8471 --constitution constitutions/data-agent.yaml
# anchor init --constitution support-agent   (scaffolds a project with that pack)
```

Writing your own pack: `docs/CONSTITUTION_AUTHORING.md`. What the governor
guarantees (and doesn't): `docs/SECURITY_MODEL.md`.

## Verdict dashboard

The daemon serves a live dashboard — no new services, no new ports:

```bash
anchor run &                                   # daemon on :8471
# open http://127.0.0.1:8471/dashboard
```

Chain-status banner (the trust anchor), allow/deny/ask counts, a filterable
verdict table, and an "adversarial spotlight" of denied attacks. The page is
one self-contained file (`gateway/dashboard.html`); JS reads the existing
`GET /v1/verdicts`. Fleet/multi-daemon aggregation is explicitly out of scope
(needs daemon auth + a collector — a Phase 4 decision).

## Run it

```bash
anchor init                 # scaffolds ./anchor.yaml + ./constitutions/code-agent.yaml
anchor run                  # starts the policy daemon (localhost only, :8471)
# or: anchor-gateway --port 8471 --constitution constitutions/code-agent.yaml
```

## In your agent code

```python
import anchor
anchor.configure(agent_id="my-agent",
                 approval_callback=my_approver)  # (tool, params, reason, pending_id) -> bool

@anchor.govern(tool="read_file")
def read_file(path): ...

result = read_file("notes.txt")
# -> GovernedResult(value=..., capability_id=..., via="direct")
# Denied calls raise anchor.GovernedDeny and NEVER execute.
# Unreachable daemon raises GovernedDeny(fail_closed=True).
```

`anchor.wrap_tool(fn, tool=...)` wraps an existing callable without decorator
syntax. `anchor.decide_tool_call(tool, params)` judges without executing
(the path the framework shims use).

### Framework shims

```python
# LangChain — wrap any BaseTool; the agent sees the same name/description.
from anchor.integrations.langchain import govern_tool
tool = govern_tool(my_structured_tool)
tool.invoke({"path": "notes.txt"})   # allow -> runs; deny -> GovernedDeny

# OpenAI Agents SDK — a native tool input guardrail.
from agents import function_tool
from anchor.integrations.openai_agents import attach_anchor_guardrail, governed_function_tool

@function_tool
def read_file(path: str) -> str: ...
attach_anchor_guardrail(read_file)          # or: @governed_function_tool

# CrewAI — wrap any BaseTool.
from anchor.integrations.crewai import govern_tool
tool = govern_tool(my_crewai_tool)
tool.run(path="notes.txt")
```

Shim contract (all three): **allow** → the tool runs, receipt
(`capability_id`) attached to the decision; **deny** → the tool never runs;
**ask** → the configured approval callback runs (bounded by
`approval_timeout`), rejection or timeout denies fail-closed.

## ASK approval channels

`anchor/approvals.py` ships three callbacks:

- `cli_approver` — interactive terminal prompt (`approve? [y/N]`); EOF/Ctrl-C
  or any non-yes denies. Default priority; webhook is the option.
- `webhook_approver(url)` — POSTs `{pending_id, tool, params, reason}`.
  Sync mode: the webhook answers `200 {"approved": bool}`. Async mode: it
  answers `202` and resolves later through the daemon's `POST /v1/resolve`;
  the client polls `GET /v1/pending/<pending_id>` until resolved or the
  timeout. Unreachable webhook → deny (fail closed).
- `auto_approver(True/False)` — canned answers for tests only.

Resolve is **idempotent**: the daemon records each `pending_id`'s outcome, so
a second `POST /v1/resolve` (client after webhook, or a retry) returns the
recorded receipt instead of minting twice. First resolution wins on races.

## CLI reference

- `anchor init [--constitution code-agent] [--dir .]` — scaffold a project.
- `anchor run [--port PORT]` — start the daemon from `./anchor.yaml`
  (falls back to the packaged constitution when the project one is missing).
- `anchor-gateway [--port 8471] [--constitution ...]` — the daemon directly.
- Env: `ANCHOR_GATEWAY_PORT`, `ANCHOR_PROJECT_DIR`, `ANCHOR_VERDICTS_LOG`
  (proofs use the last one to keep the production log clean).

## Prove it

```bash
~/workspace/anchor-v1/.venv/bin/python proof.py          # Phase 1: PROOF: PASS
~/workspace/anchor-v1/.venv/bin/python proof_phase2.py   # Phase 2: PROOF2: PASS
~/workspace/anchor-v1/.venv/bin/python proof_phase3.py   # Phase 3: PROOF3: PASS
```

Phase 2 drives its own daemon on :8472 with an isolated verdicts log: per-shim
allow+deny through the real daemon (13 checks: LangChain, OpenAI Agents
guardrail, CrewAI), CLI ask→approve→allow and ask→timeout→deny, webhook
ask→resolve→allow, `anchor init` + `anchor run` end to end, and a final
`chain_ok` on the proof log. Full model-driven `Runner`/`kickoff` runs need
API keys and are out of scope; the shims are proven at the interception
boundary, which is where governance lives.

Phase 3 drives one daemon per constitution pack (browser, support, data) on
:8473 with isolated logs: per-pack allow+deny+ask through the real daemon
(35 checks incl. ask→resolve→allow round trips), each log `chain_ok`.
The dashboard is proven separately: `GET /dashboard` → 200 `text/html`,
all markers present, verdicts live from `/v1/verdicts`.

Evidence: `verdicts.log` (hash-chained; `GET /v1/verdicts` reports `chain_ok`).

## What's real vs. simplified (no padding)

- **Real:** `AcsGuardian.handle_event` on `TOOL_CALL_PROPOSED` with the
  constitution as `decision_fn`; real COSE one-use capability mint on allow
  (verified + registered, `capability_id` is the receipt); the guardian's
  native ASK flow (`pending_id` → `resolve_pending`) with an idempotent daemon
  receipt layer on top (no double-spend); fail-closed on every error path,
  including daemon-unreachable and hung approvers.
- **Simplified:** the daemon mints to its own holder identity — holder-of-key
  proofs and PEP consumption belong to an execution plane, and this layer
  judges without executing (by design). The verdict log lives in one file;
  multi-daemon / fleet sync is a Phase 4 decision, not a dashboard feature.
- **Conservative edges (fail-closed, documented):** the generalized
  path-substring rule scans *all* params, so e.g. a code string mentioning
  `/etc/passwd` is denied; `$` and backticks are denied in `run_cmd` args but
  allowed in file content (inert data); network policy denies unparseable
  URLs and non-absolute `url_params`; credential-shaped param keys always ask.
  The YAML parser is a stdlib subset (block + flow lists, folded `>` blocks,
  comments) — constitutions needing more wait for full PyYAML.

## Layout

- `gateway/server.py` — stdlib-only daemon (`/v1/decide`, `/v1/resolve`,
  `/v1/pending/<id>`, `/v1/verdicts`, `/v1/health`, `/dashboard`)
- `gateway/dashboard.html` — self-contained verdict dashboard page
- `gateway/constitution.py` — YAML-subset loader + three-tier decision_fn
  (tiers, path-substring rule, `network:` URL policy, `tool_rules:`)
- `constitutions/` — `code-agent`, `browser-agent`, `support-agent`,
  `data-agent` policy packs (versioned)
- `anchor/client.py` — `govern`, `wrap_tool`, `decide_tool_call`,
  `GovernedDeny`, `GovernedResult`, `Decision`
- `anchor/approvals.py` — `cli_approver`, `webhook_approver`, `auto_approver`
- `anchor/integrations/` — `langchain`, `openai_agents`, `crewai` shims
- `anchor/cli.py` — the `anchor` CLI (`init`, `run`)
- `docs/` — `CONSTITUTION_AUTHORING.md`, `SECURITY_MODEL.md`
- `proof.py`, `proof_phase2.py`, `proof_phase3.py`, `proof-project/`
- `PUBLISH_CHECKLIST.md`, `REQUIREMENTS.md`, `CHALLENGE.md` (launch held)
