agent-ambit
v0.3.0
Published
Lightweight permission layer for AI agents — declare what an agent may do in one file, enforce it anywhere
Maintainers
Readme
ambit
Lightweight permission layer for AI agents. Declare what an agent may do in one readable file; enforce it in any agentic system.
Without ambit, a hallucinating model can call delete_database, send_email, or any other tool in its registry — the only limit is what was registered. With ambit, a plain YAML file is the limit.
Architecture · Full usage guide · Runnable demos
Install
npm install agent-ambitUsage
In-process enforcement (any tool registry)
import { wrap } from "agent-ambit";
const guard = wrap(myTools, { policy: "policy.yaml" });
// Blocked calls throw PermissionDeniedError with instructions for the fix.
// Allowed calls pass through to the original function unchanged.
const result = await guard.call("read_file", { path: "/tmp/data.csv" });MCP server
import { wrapServer } from "agent-ambit/mcp";
const server = new McpServer({ name: "filesystem", version: "1.0.0" });
server.tool("read_file", async () => { /* ... */ });
server.tool("write_file", async () => { /* ... */ });
server.tool("delete_file", async () => { /* ... */ });
wrapServer(server, { policy: "policy.yaml" }); // one line
await server.connect(transport);One policy across differently-named agents
Different agents name equivalent tools differently — Claude Code's Bash
vs. a generic run_shell, WebFetch vs. fetch_url. aliases maps a
caller's literal tool name to your policy's canonical name before the
decision is made, so one policy.yaml works across all of them:
const guard = wrap(myTools, {
policy: "policy.yaml", // written in canonical names: run_shell, fetch_url, ...
aliases: { Bash: "run_shell", WebFetch: "fetch_url" },
});Everything the caller sees — error messages, audit callbacks, MCP
responses — still reports the literal name they used; aliasing only feeds
the policy decision. See USAGE.md
and examples/claude-code-hook/ for a full
worked example gating Claude Code itself.
Policy file
# policy.yaml
tier: operator # readonly | operator | admin
# operator allows all tools by default.
# List only the tools you want to restrict.
deny:
- delete_file
- filesystem:write_file # scoped to the "filesystem" MCP server only
- run_shell # applies to all servers (no prefix)| tier | default |
|------|---------|
| readonly | deny all — list tools you want to permit in allow |
| operator | allow all, except tools that read as destructive by name (delete_file, dropTable, purge_cache, ...) — those are denied unless listed in allow |
| admin | allow all, including destructive-sounding tools — the tier with no built-in carve-out |
Deny always wins. A tool in both allow and deny is denied.
The destructive-tool default is a name heuristic, not a semantic guarantee — see THREAT_MODEL.md. For anything with real stakes, list destructive tools in deny explicitly rather than relying on it.
What a denial looks like
PermissionDeniedError: Blocked: delete_file is not permitted under tier "operator".
Add it to allow[] or use a higher tier.The error names the fix. It carries a .decision object with the full allow/deny reasoning for programmatic handling.
Inspect a policy
npx ambit explain policy.yamloperator tier — tools allowed by default
❌ delete_file denied (all servers)
❌ filesystem:write_file denied
❌ run_shell denied (all servers)npx ambit explain policy.yaml --jsonAudit and override hooks
const guard = wrap(myTools, {
policy: "policy.yaml",
// fires on every call — allowed or denied — for audit logging
onCall: (decision) => logger.log(decision),
// return true to override a denial (e.g., after human approval)
onDenied: (decision) => approvalQueue.ask(decision),
});What this does and doesn't protect against
Defends against: a hallucinating or prompt-injected model calling tools it shouldn't. The check happens server-side, before the tool function runs, regardless of which client or model drives the agent.
Does not defend against:
- A fully compromised process — ambit is not a sandbox. If Node itself is compromised, the check can be bypassed.
- Parameter-level misuse of an authorized tool — ambit controls which tools are called, not what arguments they receive.
bashinallow[]meansbashcan run; ambit does not inspect whether the argument islsorrm -rf /. - A malicious server you chose to wrap — ambit enforces your policy against your registry. If the registry itself is malicious, wrapping it does not help.
See THREAT_MODEL.md for the full boundary statement.
Unprefixed deny is broad. deny: [write_file] denies write_file on every MCP server, not just one. To scope a denial to one server, use filesystem:write_file. This is intentional — unprefixed deny is the safe default.
Contributing
See CONTRIBUTING.md. Found a security issue? See SECURITY.md instead of opening a public issue.
Changelog
See CHANGELOG.md.
License
MIT
