@agentwares/agentguard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/cli.js +4179 -0
- package/dist/cli.js.map +1 -0
- package/dist/fixtures/crm-server.js +1216 -0
- package/dist/fixtures/crm-server.js.map +1 -0
- package/dist/fixtures/demo-agent.js +120 -0
- package/dist/fixtures/demo-agent.js.map +1 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +4595 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +11 -0
- package/package.json +70 -0
- package/server.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# agentguard
|
|
2
|
+
|
|
3
|
+
**60 seconds to a safe first run.** Your agent already has an MCP config. Put agentguard in front of it, run the agent once in dry-run, and read what it _would_ have done:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @agentwares/agentguard init # reads .mcp.json / Cursor / VS Code config, writes agentguard.yaml (dry-run), routes every server through the proxy
|
|
7
|
+
# restart your MCP client, run your agent as usual — writes are faked, nothing executes upstream
|
|
8
|
+
npx @agentwares/agentguard report # "would have deleted 12 records, sent 5 emails, spent $140 — halted a loop at call 31"
|
|
9
|
+
npx @agentwares/agentguard diff # the record-by-record mutation diff
|
|
10
|
+
# set `mode: enforce` in agentguard.yaml when it looks right
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
# agentguard report — run `run_20260902_a1b2`
|
|
15
|
+
|
|
16
|
+
61 tool calls between 10:02:11 and 10:02:19 across crm.
|
|
17
|
+
|
|
18
|
+
## What this run would have done (dry-run, nothing was executed)
|
|
19
|
+
|
|
20
|
+
It would have **deleted 1 record**, updated 1, created 1, sent 1 message, **spent $12.00**.
|
|
21
|
+
|
|
22
|
+
## Where agentguard stepped in
|
|
23
|
+
|
|
24
|
+
| # | code | tool | why |
|
|
25
|
+
|----|-----------------|--------------------|--------------------------------------------------------------|
|
|
26
|
+
| 10 | `LOOP_DETECTED` | crm_update_contact | called 3 times with the same arguments in the last 30 calls |
|
|
27
|
+
| 61 | `CAP_EXCEEDED` | crm_create_contact | writes cap for this run is 50; used 50, this call would make it 51 |
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
agentguard is an MCP policy proxy for agents that touch production. It sits between the agent and its MCP servers, sees every tool call, and enforces one YAML file:
|
|
31
|
+
|
|
32
|
+
- **Hard spend limits** — per-run and per-day `spend_usd` across every provider, from tool arguments (`stripe_create_charge.amount`), tool results (`cost_usd`), and — with the SDK's guarded `fetch` — LLM token usage from OpenAI, Anthropic and Gemini responses. The call that would exceed the cap gets `CAP_EXCEEDED` with the remaining budget.
|
|
33
|
+
- **Destructive-action gating with approvals** — `approval.tools: [crm_delete_*]` makes the agent get `APPROVAL_REQUIRED` + an id; a human runs `agentguard approve <id>` (or clicks the button in Slack) and the agent's identical retry goes through once.
|
|
34
|
+
- **Kill switch** — `agentguard kill` (a file), `AGENTGUARD_KILL=1` (env), or `POST /kill` (HTTP): every run halts instantly with `KILLED` until `agentguard resume`.
|
|
35
|
+
- **Per-agent scoped credentials** — the proxy holds the upstream tokens; each agent gets an `agk_…` key with its own allowlist, denylist and caps. Only the key's hash lives in the policy.
|
|
36
|
+
- **Dry-run writes with mutation diffs** — classified writes return a plausible success shaped by the tool's output schema so the agent keeps going; `agentguard diff` shows what would have changed.
|
|
37
|
+
- **Semantic loop breaker** — the same `(tool, normalized args)` 3× in the last 30 calls, or an A→B→A→B cycle, returns `LOOP_DETECTED`. Timestamps, ids, whitespace and key order are ignored.
|
|
38
|
+
- **Blast-radius caps** — `tool_calls`, `writes`, `deletes`, `emails`, `spend_usd` and custom counters, per run and per day.
|
|
39
|
+
- **Hash-chained audit log** — every call is a JSONL line with `prev_hash` and `hash`; `agentguard verify` proves nothing was edited, removed or reordered.
|
|
40
|
+
|
|
41
|
+
No LLM calls. No phone-home. No account. MIT.
|
|
42
|
+
|
|
43
|
+
Two install paths, one policy engine: the **MCP proxy** (`npx @agentwares/agentguard`, stdio + Streamable HTTP, multiple upstreams) and the **SDK/middleware** ([`@agentwares/agentguard-sdk`](../../packages/agentguard-sdk)) for OpenAI Agents SDK, LangChain or plain-function tools that never go through MCP.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx @agentwares/agentguard init # in the directory with your .mcp.json / .cursor/mcp.json / .vscode/mcp.json
|
|
49
|
+
npx @agentwares/agentguard init --client ~/Library/Application\ Support/Claude/claude_desktop_config.json # user-level configs only with --client
|
|
50
|
+
npx @agentwares/agentguard init --undo # restore the backup
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`init` writes `agentguard.yaml` next to your config, backs the config up (`*.agentguard-backup`), and replaces its servers with one entry:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"agentguard": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "agentguard", "proxy", "--config", "/abs/path/agentguard.yaml"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Tools keep their names (prefixed `<upstream>__` only on collision). Your MCP client sees one server; agentguard connects to all of them and holds their credentials.
|
|
67
|
+
|
|
68
|
+
Spawned with no arguments at all — what an install from the MCP registry does — `agentguard` serves the same stdio proxy and reads `AGENTGUARD_CONFIG` or `./agentguard.yaml`. In a terminal it prints the help instead.
|
|
69
|
+
|
|
70
|
+
Prefer HTTP (several agents, scoped keys, Slack approve buttons)? `agentguard proxy --http --port 8788` and point clients at `http://127.0.0.1:8788/mcp` with an `X-Run-Id` header per run and `Authorization: Bearer agk_…` per agent.
|
|
71
|
+
|
|
72
|
+
## Policy
|
|
73
|
+
|
|
74
|
+
`agentguard init` generates this file with every knob explained inline. The short form:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
version: 1
|
|
78
|
+
mode: dry-run # dry-run | enforce
|
|
79
|
+
upstreams:
|
|
80
|
+
- name: crm
|
|
81
|
+
url: https://mcp.example.com/mcp
|
|
82
|
+
auth: ${CRM_TOKEN} # the agent never sees this
|
|
83
|
+
- name: files
|
|
84
|
+
command: npx
|
|
85
|
+
args: [-y, "@modelcontextprotocol/server-filesystem", "."]
|
|
86
|
+
classify: # patterns win over annotations win over verb heuristics
|
|
87
|
+
write: [crm_update_*, crm_delete_*, email_send]
|
|
88
|
+
spend: [stripe_*, x402_*]
|
|
89
|
+
unknown: write # unclassifiable tools count as writes (or: read | block)
|
|
90
|
+
caps:
|
|
91
|
+
per_run: { writes: 50, deletes: 10, emails: 5, spend_usd: 25, tool_calls: 400 }
|
|
92
|
+
per_day: { spend_usd: 200 }
|
|
93
|
+
spend:
|
|
94
|
+
tools:
|
|
95
|
+
stripe_create_charge: { amount_arg: amount, divisor: 100, currency_arg: currency }
|
|
96
|
+
loop: { window: 30, max_repeats: 3, max_cycle_len: 4, max_read_repeats: 10 }
|
|
97
|
+
dry_run: { tools: [crm_delete_*], synthesize: true } # always fake these, even in enforce
|
|
98
|
+
approval:
|
|
99
|
+
tools: [crm_delete_*, db_drop_*]
|
|
100
|
+
wait_s: 0 # >0 holds the call open waiting for the decision
|
|
101
|
+
notify: { slack: ${SLACK_WEBHOOK_URL} }
|
|
102
|
+
kill: { file: .agentguard/KILL, env: AGENTGUARD_KILL }
|
|
103
|
+
agents: # agentguard key create deployer --allow 'crm_get_*' --writes 5
|
|
104
|
+
- name: deployer
|
|
105
|
+
key_hash: sha256:…
|
|
106
|
+
allow: [crm_get_*, crm_update_contact]
|
|
107
|
+
caps: { per_run: { writes: 5 } }
|
|
108
|
+
alerts: { slack: ${SLACK_WEBHOOK_URL}, on: [LOOP_DETECTED, CAP_EXCEEDED, KILLED, APPROVAL_REQUIRED] }
|
|
109
|
+
audit: { path: .agentguard/audit.jsonl, redact: true }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Classification order: `classify.*` patterns → MCP `annotations.readOnlyHint` / `destructiveHint` → verb heuristics (`get/list/search…` read, `create/update/delete/send/execute…` write, `pay/charge/refund…` + `stripe_*`/`x402_*` spend). `agentguard tools` prints every tool with its class and why.
|
|
113
|
+
|
|
114
|
+
## What the agent sees
|
|
115
|
+
|
|
116
|
+
Every block is an in-band tool result with `isError: true` and a JSON body the model can act on:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"code": "CAP_EXCEEDED",
|
|
121
|
+
"cause": "writes cap for this run is 50; used 50, this call would make it 51",
|
|
122
|
+
"fix": "stop and report to the user what is done and what remains; a human can raise caps.per_run in agentguard.yaml or start a new run",
|
|
123
|
+
"retryable": false,
|
|
124
|
+
"details": {
|
|
125
|
+
"scope": "per_run",
|
|
126
|
+
"counter": "writes",
|
|
127
|
+
"limit": 50,
|
|
128
|
+
"used": 50,
|
|
129
|
+
"remaining": { "writes": { "per_run": 0 } }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Codes: `KILLED`, `APPROVAL_REQUIRED` (retryable once approved), `APPROVAL_DENIED`, `LOOP_DETECTED`, `CAP_EXCEEDED`, `TOOL_DENIED`, `UNKNOWN_TOOL`, `UPSTREAM_ERROR`. Successful and faked results carry `_meta.agentguard = { class, verb, mode, outcome, dryRun, seq, run_id }`.
|
|
135
|
+
|
|
136
|
+
Run identity: `X-Run-Id` header (HTTP) → `_meta.runId` on the call → session → one id per proxy process. Per-run caps and the loop window are per run; per-day caps are per policy (and per agent).
|
|
137
|
+
|
|
138
|
+
## Commands
|
|
139
|
+
|
|
140
|
+
| Command | What it does |
|
|
141
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `agentguard init [--client path] [--all] [--no-probe] [--mode enforce] [--undo]` | generate the policy, rewrite the client config (project-level by default) |
|
|
143
|
+
| `agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode m]` | run the proxy (stdio default) |
|
|
144
|
+
| `agentguard report [--run id \| --all] [--json]` | what this run did / would have destroyed / spent; where it was halted; chain status |
|
|
145
|
+
| `agentguard diff [--run id]` | mutation diff of faked writes |
|
|
146
|
+
| `agentguard verify [audit.jsonl]` | recompute the hash chain; exit 1 on the first break |
|
|
147
|
+
| `agentguard status [--run id]` | counters vs caps, kill state, pending approvals, running HTTP proxy |
|
|
148
|
+
| `agentguard tools [--json]` | every exposed tool with class, verb, upstream and the reason |
|
|
149
|
+
| `agentguard kill [reason]` / `agentguard resume` | halt everything now / clear it |
|
|
150
|
+
| `agentguard approvals [--all]` / `approve <id>` / `deny <id> [--note …]` | the approval queue |
|
|
151
|
+
| `agentguard key create <agent> [--allow p]… [--deny p] [--writes n] [--spend n] [--mode m]` / `key list` / `key revoke <agent>` | scoped credentials |
|
|
152
|
+
| `agentguard permission-diff [--base ref] [--head ref] [--fail-on-widen]` | which config changes widen agent permissions (also a [GitHub Action](../../assets/permission-diff-action)) |
|
|
153
|
+
|
|
154
|
+
HTTP control endpoints (token in `.agentguard/http.json`): `GET /health`, `GET /status?run=`, `POST /kill`, `POST /resume`, `GET|POST /approve/:id`, `/deny/:id`, `GET /approvals`.
|
|
155
|
+
|
|
156
|
+
## Try it with the fixtures
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
git clone https://github.com/agentwares/agentwares && cd agentwares && pnpm install && pnpm turbo build --filter=agentguard
|
|
160
|
+
cd apps/agentguard-cli
|
|
161
|
+
cat > agentguard.yaml <<'YAML'
|
|
162
|
+
mode: dry-run
|
|
163
|
+
upstreams:
|
|
164
|
+
- name: crm
|
|
165
|
+
command: node
|
|
166
|
+
args: [dist/fixtures/crm-server.js]
|
|
167
|
+
caps: { per_run: { writes: 50 } }
|
|
168
|
+
YAML
|
|
169
|
+
node dist/fixtures/demo-agent.js --config agentguard.yaml # a scripted agent: reads, writes, a deliberate loop, a 60-write burst
|
|
170
|
+
node dist/cli.js report && node dist/cli.js diff && node dist/cli.js verify
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Conformance and tests
|
|
174
|
+
|
|
175
|
+
`pnpm test` runs the CLI suite (24 tests; 64 more in `agentguard-core`, 10 in the SDK): the engine over InMemoryTransport, the spawned stdio proxy, the Streamable HTTP proxy with `X-Run-Id`, scoped keys and control endpoints, `init` against real configs, and a recorded-fixture replay (`fixtures/recorded/crm-session.json`; re-record with `RECORD_FIXTURES=1`). `pnpm conformance` runs the official `@modelcontextprotocol/conformance` server suite against the proxy with a sample server behind it (tools, resources, prompts, completions, logging, progress, sampling and elicitation are relayed).
|
|
176
|
+
|
|
177
|
+
## Limits (honest)
|
|
178
|
+
|
|
179
|
+
- The proxy sees MCP tool calls. Token spend on the model API is only visible through the SDK's guarded `fetch` (or `spend.tools` rules for MCP tools that call models).
|
|
180
|
+
- Dry-run synthesizes results from the tool's `outputSchema`; agents that depend on real ids from a create → update chain will see plausible but fake ids. `dry_run.tools` lets you fake only the dangerous tools in enforce mode.
|
|
181
|
+
- Per-day counters are a JSON file under a directory lock; fine for a workstation or one box, not a fleet. The hosted tier (coming) is the shared-state version.
|
|
182
|
+
- Slack "Approve" buttons are links to the local HTTP proxy; they work for people who can reach it. Without HTTP mode the message carries the `agentguard approve <id>` command.
|
|
183
|
+
|
|
184
|
+
## Related
|
|
185
|
+
|
|
186
|
+
- [`@agentwares/agentguard-sdk`](../../packages/agentguard-sdk) — the same engine for OpenAI Agents SDK / LangChain / plain functions, plus the guarded `fetch` for LLM spend.
|
|
187
|
+
- [`@agentwares/agentguard-core`](../../packages/agentguard-core) — the Web-standard policy engine (bring your own stores).
|
|
188
|
+
- [permission-diff GitHub Action](../../assets/permission-diff-action) — comments on PRs that widen `agentguard.yaml`, `.claude/settings.json` or `mcp.json`.
|