Documentation.
How to call the policy engine offline or over HTTP, the YAML policy schema, the OpenClaw and MCP adapters, authentication, telemetry, the kill switch, and what is not proven yet. Every sample below is real code from @the-bot-club/agentguard@0.15.0 and agentguard-tech==0.15.0; nothing here is aspirational.
Quickstart
Two paths to a decision: load a policy locally and evaluate it in-process with no network call and no API key, or send the proposed action to the hosted API. All three examples below return the same decision shape — result, matchedRuleId, riskScore, reason.
TypeScript — local policy, offline
PolicyEngine, exported from @the-bot-club/agentguard, loads a YAML policy document, validates it against the schema below, and evaluates actions in-process.
policies/agent-baseline.yaml
id: agent-baseline
name: Agent baseline policy
version: 1.0.0
default: block
rules:
- id: allow-reads
action: allow
priority: 10
when:
- tool:
in: [read_file, search]
- id: approve-email
action: require_approval
priority: 20
when:
- tool:
in: [send_email]
approvers: [ops-lead]
- id: block-shell
action: block
priority: 5
when:
- tool:
in: [shell_exec]import { randomUUID } from "node:crypto";
import { PolicyEngine } from "@the-bot-club/agentguard";
const engine = new PolicyEngine();
engine.loadFromFile("policies/agent-baseline.yaml");
const decision = engine.evaluate(
{
id: randomUUID(),
agentId: "my-agent",
tool: "read_file",
params: { path: "/data/report.csv" },
inputDataLabels: [],
timestamp: new Date().toISOString(),
},
{ agentId: "my-agent", sessionId: "session-1", policyVersion: "1.0.0" },
"agent-baseline",
);
// decision.result === "allow"loadFromYaml(yamlString) takes the same content as a string instead of a file path. An invalid document throws before any rule runs — see the policy reference below.
TypeScript — hosted API
AgentGuard sends the same proposal to the hosted policy API over HTTPS.
import { AgentGuard } from "@the-bot-club/agentguard";
const guard = new AgentGuard({ apiKey: process.env.AGENTGUARD_API_KEY });
const decision = await guard.evaluate({
tool: "send_email",
params: { to: "finance@example.com" },
});
// decision: { result, matchedRuleId?, riskScore, reason, durationMs }Python — hosted API
The Python client makes the same call.
from agentguard import AgentGuard
guard = AgentGuard(api_key="ag_live_...")
decision = guard.evaluate(tool="send_email", params={"to": "finance@example.com"})
# decision is the API's JSON response, unchanged: result, matchedRuleId,
# riskScore, reason, durationMsBoth HTTP clients call the same endpoint — POST /api/v1/evaluate — and return its response unmodified; see Authentication for the header both clients send.
Policy YAML reference
A policy document is validated against PolicyDocumentSchema before it compiles. Top-level fields:
id,name— strings.version— semver, e.g.1.0.0.default—allow|block|monitor, applied when no rule matches.rules— up to 500 rules, evaluated as below.targets,budgets— optional agent scoping and spend/usage limits.
Each rule:
id,priority(1–1000, lower runs first, default 100).action—allow|block|monitor|require_approval.when— one or more conditions; a rule matches only if all of them do.severity,tags,riskBoost— feed the risk score.rateLimit,approvers,timeoutSec,on_timeout,slackChannel— used by rate-limited andrequire_approvalrules.
when condition forms:
- tool: { in: [read_file, search] }
- tool: { not_in: [shell_exec] }
- tool: { matches: ["db:*"] }
- params: { amount: { gt: 1000 } }
- params: { to: { domain_not_in: ["example.com"] } }
- AND: [ { tool: { in: [send_email] } }, { params: { urgent: { eq: true } } } ]params, context, and dataClass conditions take a field-to-constraint map (eq, gt, gte, lt, lte, in, contains, regex, exists, and more). AND, OR, and NOT nest any of the above; a timeWindow condition restricts a rule to a day/hour range.
Monitor rules never stop evaluation — every matching monitor rule adds to riskScore and none of them can be the final decision. Among rules that do terminate evaluation (allow, block, require_approval), the first match by ascending priority wins; if two matches share a priority, the more restrictive one wins (block > require_approval > allow). When nothing matches, the policy’s default applies — block is fail-closed, allow is fail-open, monitor flags the unknown tool without stopping it.
require_approval is a valid rule action and evaluate() returns it, but V1 ships no approval dispatcher: a require_approval result blocks the call — the OpenClaw and MCP adapters below stop dispatch on it — and there is no path yet for a human to approve it back open.
OpenClaw
The TypeScript package registers a structural before_tool_call hook, at priority 100, that sends the observed tool name and parameters to POST /api/v1/evaluate and can return a block result. Configure it in openclaw.json:
{
"plugins": {
"entries": {
"agentguard": {
"enabled": true,
"config": {
"apiKey": "${AGENTGUARD_API_KEY}",
"agentId": "my-agent",
"strict": true
}
}
},
"installs": {
"agentguard": {
"source": "npm",
"spec": "@the-bot-club/agentguard@0.15.0"
}
}
}
}strict must be true. strict: false (Python: strict=False) is a hard startup error — both the plugin and openclaw_guard() throw immediately, and there is no published fail-open branch. In strict mode, a block decision and an unreachable API both return { block: true, blockReason }; anything else lets the call through unchanged.
Python:
from agentguard.integrations.openclaw import openclaw_guard
guard = openclaw_guard(api_key="ag_live_...", agent_id="my-agent", strict=True)
decision = guard.intercept_sync("web_search", {"query": "recent AI papers"})
if not decision.allowed:
raise PermissionError(decision.reason)This is compatibility telemetry, not a firewall. The hook observes a tool call and asks the policy API for a decision; it does not hold the tool’s raw capability under a separate identity. It can be turned off in openclaw.json, and a caller with any other route to the same tool bypasses it entirely. See Limitations and Trust and limitations.
MCP adapters
The TypeScript package also ships an MCP HTTP proxy and an in-process server wrapper; AgentGuard.evaluateMcp() (TypeScript) and evaluate_mcp() (Python) call the same policy path directly for one tools/call request:
const result = await guard.evaluateMcp("write_file", {
arguments: { path: "/etc/passwd", content: "..." },
});
if (result.blocked) {
return result.mcpErrorResponse; // hand back to the MCP client
}
// otherwise forward the call to the upstream MCP tool serverresult = guard.evaluate_mcp(
"write_file",
arguments={"path": "/etc/passwd", "content": "..."},
)
if result["blocked"]:
return result["mcpErrorResponse"]Both the HTTP proxy and the in-process wrapper evaluate through POST /api/v1/evaluate and carry the same limits as the OpenClaw hook: strict: false is a hard startup error, and neither adapter holds the wrapped tool’s capability under a separate identity — either can be disabled, and a caller with a direct route to the upstream MCP server bypasses both.
Authentication
Every hosted-API call — evaluate, evaluateMcp, the OpenClaw hook, killSwitch, and the rest of the client methods — authenticates with an X-API-Key header:
X-API-Key: ag_live_xxxxxxxxxxxxxxxxxxxxGet a key:
POST https://api.agentguard.tech/api/v1/signup
Content-Type: application/json
{"name": "My Agent"}Returns 201 with an apiKey starting ag_live_. It is shown once — store it yourself; email is optional. A missing or invalid key returns 401 with { error, message, acceptedAuth, docs }. The evaluate endpoints additionally accept a narrower, agent-scoped ag_agent_ key.
Telemetry
Telemetry is opt-in and off by default in both SDKs. Pass telemetry: true (TypeScript) or telemetry=True (Python) to the client constructor to send one fire-and-forget ping on the first evaluate() call:
{
"sdk_version": "0.15.0",
"language": "node",
"node_version": "v20.11.0",
"os_platform": "linux"
}The Python SDK sends the same four keys — including node_version for its own runtime string, e.g. python/3.11.2. No tool names, parameters, or policy content are ever sent. Set AGENTGUARD_NO_TELEMETRY=1 to force telemetry off regardless of the telemetry option.
Kill switch
Toggle your tenant’s kill switch with the same API key used for evaluate:
POST /api/v1/killswitch
X-API-Key: ag_live_...
Content-Type: application/json
{"active": true}or from either client: guard.killSwitch(true) / guard.kill_switch(True). While it is active, every POST /api/v1/evaluate call for that tenant returns a block decision with killSwitchActive: true:
{
"result": "block",
"matchedRuleId": "TENANT_KILL_SWITCH",
"riskScore": 1000,
"reason": "Tenant kill switch is ACTIVE — all your agent actions are blocked.",
"durationMs": 0,
"killSwitchActive": true
}GET /api/v1/killswitch reports the current global and tenant state.
Source
Both packages are licensed under the Business Source License 1.1 — not MIT, not Apache. Read the licence terms shipped in the package before relying on either commercially.
The npm tarball ships compiled JavaScript, .d.ts type declarations, and source maps — no TypeScript source. The PyPI sdist ships the Python source directly; Python has no separate compiled-artifact step.
The core repository is private while the project is pre-GA. Request access at hani@thebot.club.
Support
Questions, bug reports, or a technical evaluation: hani@thebot.club, or book 30 minutes directly.
Limitations
Every adapter on this page — the OpenClaw hook, both MCP adapters, both HTTP clients — is compatibility telemetry: each can observe a proposed action and return a policy decision, but none holds the tool’s raw capability under a separate identity, and each can be disabled or bypassed by a caller with another route to the same tool. Executor-owned firewall proof is still pending. Read Trust and limitations, the named-action proof, and assurance status before relying on any of this for something you cannot independently verify.