HOW IT WORKS
The LLM calls Gatekeeper. Gatekeeper calls the tool.
Your agent loses direct access to subprocess, fs.writeFile, and
fetch. Instead, it calls a local HTTP endpoint. That endpoint decides whether
the action happens — and writes a receipt either way.
Request flow
- 1. Agent calls Gatekeeper.
POST /v1/toolwith a signed principal, tool name, and arguments. - 2. Policy evaluation. Deny patterns → tool-specific rules → principal/role constraints. Sub-millisecond.
- 3. Decision. One of: allow, approve (pending), or deny.
- 4. If approve: Gatekeeper generates an HMAC-signed, single-use URL. You click it. The request resumes.
- 5. Execute. Gatekeeper runs the tool (not the agent). Secrets stay on the gatekeeper side.
- 6. Audit. Every step — request, decision, approval, execution, errors — appended to the audit log.
Calling Gatekeeper from an agent
Via the TypeScript client. The client is thin — mostly a typed wrapper around the HTTP API so you can swap in any other language client trivially.
import { GatekeeperClient } from "@runestone-labs/gatekeeper-client";
const gk = new GatekeeperClient({
baseUrl: "http://localhost:7337",
principal: { agent: "finance-sync", role: "local_dev" },
});
// The agent doesn't call fetch directly.
const res = await gk.tool("http.request", {
method: "GET",
url: "https://finnhub.io/api/v1/quote?symbol=NVDA",
});
// Decision lands here. Either the response body, or
// { status: "pending_approval", approval_url: "..." }. Policy is YAML, not code
The agent can't modify it at runtime. You can review it in a PR. Diff it in CI. Hash it into every audit event so you always know which policy was active when a decision was made.
# gatekeeper.policy.yaml
version: 1
principals:
- match: { role: "local_dev" }
grants: [shell.exec, files.write, http.request, memory.upsert]
- match: { role: "prod" }
grants: [http.request, memory.upsert] # no shell in prod
tools:
shell.exec:
allowed_cwd_prefixes: ["/workspace/", "/tmp/runestone/"]
deny_patterns: ["rm -rf", "curl | sh", ":(){ :|:& };:"]
decision: approve
http.request:
allowed_hosts: ["*.finnhub.io", "api.coinbase.com"]
deny_private_ips: true
decision: allow
files.write:
allowed_paths: ["/workspace/**"]
deny_extensions: [".env", ".pem", ".key"]
decision: allow Approval: HMAC-signed, single-use, no SaaS
When the decision is approve, Gatekeeper holds the request and returns a URL.
The URL carries an HMAC signature (shared secret, not public-key — this is a local service)
and a short-TTL nonce stored server-side.
- Click once → request resumes.
- Click twice → nonce is consumed, replay rejected.
- Wait an hour → link expires, request stays blocked.
- Forward it to a teammate — as long as they can reach your local Gatekeeper, it works.
{
"status": "pending_approval",
"approval_url":
"http://localhost:7337/approve/abc123
?sig=3e9f...
&exp=1745339291",
"reason":
"shell.exec with 'sudo' flagged for review",
"request_id":
"req_01HW..."
} Audit log: append-only JSONL
Daily rotating file by default. Optional Postgres sink if you want to query it.
Every event is stamped with the policy_hash in effect, so six months later you can prove
which rules were active when a decision was made.
{"ts":"2026-04-22T14:03:11.204Z","event":"request","actor":"agent:finance-sync","role":"local_dev","tool":"http.request","args_digest":"sha256:bb4..."}
{"ts":"2026-04-22T14:03:11.205Z","event":"decision","request_id":"req_01HW","decision":"allow","policy_hash":"sha256:a3f9..."}
{"ts":"2026-04-22T14:03:11.482Z","event":"executed","request_id":"req_01HW","duration_ms":276,"status":200}
{"ts":"2026-04-22T14:04:02.110Z","event":"request","actor":"agent:finance-sync","tool":"shell.exec","args":{"cmd":"sudo apt update"}}
{"ts":"2026-04-22T14:04:02.111Z","event":"decision","request_id":"req_01HX","decision":"approve","reason":"sudo requires approval"}
{"ts":"2026-04-22T14:04:37.009Z","event":"approval_consumed","request_id":"req_01HX","approver":"evan@runestonelabs.io"}
{"ts":"2026-04-22T14:04:40.223Z","event":"executed","request_id":"req_01HX","duration_ms":3200,"status":"ok"} Cost audit — agents spend real money
LLM calls are tool calls too. Gatekeeper prices each one (per-model input/output tokens, cache hits, tool-use premiums) and stamps the cost into the audit log. You see actual dollars per agent, per workflow, per day — no estimation, no sampling.
Set budgets the same way you set policy. They ship dormant — observe real spend via
/usage and /llm-usage for a couple weeks, then pin caps at
~2× p95 with confidence.
- Per-agent, per-workflow, per-tool budgets.
- Rolling windows: daily, weekly, monthly.
- Over-budget decision is first-class:
allow,approve, ordeny. - Cache-hit credit — Anthropic prompt caching shows up as its own line item.
budgets:
agent:finance-sync:
daily_usd: 2.00
decision: approve # over-budget → pause
agent:research_*:
monthly_usd: 50.00
decision: deny # hard stop
workflow:nightly-scrape:
daily_usd: 5.00 Want the full architecture doc?
The repo has a longer architecture write-up, a policy guide, and an approval flow doc. All source, no marketing.