{"_id":"@aarvion/openclaw-guard","_rev":"2-fc917e8f145d21f570944cd04c79baf2","name":"@aarvion/openclaw-guard","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aarvion/openclaw-guard","version":"0.1.0","_id":"@aarvion/openclaw-guard@0.1.0","maintainers":[{"name":"arshpreet-aarvion","email":"arshpreet@aarvion.ai"}],"dist":{"shasum":"89e649b20179fbcd03c96611f1c4372fac22b7c0","tarball":"https://registry.npmjs.org/@aarvion/openclaw-guard/-/openclaw-guard-0.1.0.tgz","fileCount":7,"integrity":"sha512-qkGu1CJP5Qjir8xKAcA7xjRYYxelu53oRh66mohFqVDvkKCba2f2U7fPB4pG6q9E1actfGXtBmLonFKQKblJYA==","signatures":[{"sig":"MEQCIEl++verKTgjYLBr530wrOltKn7pARlIg72F72EGzBPLAiAToRhs4T7aKOQ+NQs6CEEdS8hJflD6zAsFqLTEb+14aA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":35874},"main":"dist/index.js","type":"module","gitHead":"2e195a4f562449b9330bfba6639a55570bb6eb83","scripts":{"test":"node --test --experimental-strip-types src/*.test.ts","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"arshpreet-aarvion","email":"arshpreet@aarvion.ai"},"openclaw":{"compat":{"pluginApi":">=2026.6.11"},"install":{"npmSpec":"@aarvion/openclaw-guard","localPath":".","defaultChoice":"npm"},"extensions":["./dist/index.js"]},"_npmVersion":"11.11.0","description":"Aarvion guard PEP for OpenClaw: governs agent tool calls via a local policy decision point. Installs into a stock OpenClaw with `openclaw plugin install` - no OpenClaw source changes, no rebuild.","directories":{},"_nodeVersion":"22.21.1","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/openclaw-guard_0.1.0_1783963143453_0.24725941714609911","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Bundled into @aarvionai/guard (v0.2.24+) — no separate install needed. onboard installs the plugin automatically."}},"time":{"created":"2026-07-13T17:19:03.357Z","modified":"2026-07-17T09:32:05.403Z","0.1.0":"2026-07-13T17:19:03.598Z"},"description":"Aarvion guard PEP for OpenClaw: governs agent tool calls via a local policy decision point. Installs into a stock OpenClaw with `openclaw plugin install` - no OpenClaw source changes, no rebuild.","maintainers":[{"name":"arshpreet-aarvion","email":"arshpreet@aarvion.ai"}],"readme":"# @aarvion/openclaw-guard\n\n**Governance for your AI agent — install a plugin, don't fork your agent runtime.**\n\nAarvion Guard gives an [OpenClaw](https://openclaw.ai) agent a policy brain for its\n**actions**. Before the agent runs *any* tool — a shell command, a file write, a\ntweet, a Gmail send, a message it sends as you, a web request, an external MCP\ntool — the guard classifies what the call actually *does* and decides:\n**allow**, **deny**, or **ask** (pause for a human). Decisions are made by a\nlocal, signed OPA policy engine plus a set of consumer **policy packs**, and\nwritten to a tamper-evident hash-chained audit tagged with which agent and\nsession made the call.\n\nYou install it into a **stock OpenClaw** like any other plugin and enable it in\nconfig. It changes **zero lines** of OpenClaw source and needs no rebuild — it\nregisters a `trustedToolPolicy` (OpenClaw's own tool-veto seam) and depends on\nnothing but Node built-ins.\n\n```\n   OpenClaw agent          this plugin (PEP)              aarvion-guard (PDP)\n  ┌──────────────┐  tool   ┌───────────────┐  /v1/govern  ┌──────────────────┐\n  │ wants to run │ ──────► │ trustedTool   │ ───────────► │ normalize → OPA + │\n  │ a tool call  │         │ Policy.eval() │   UDS + auth  │ packs + hash-chain│\n  └──────────────┘ ◄────── └───────────────┘ ◄─────────── └──────────────────┘\n       runs / blocked /      allow · deny · ask              signed policy,\n       waits-for-approval                                    audited decision\n```\n\n## What it governs\n\nThe policy fires for **every** tool the agent calls. Guard-side, each raw call is\nrun through a **semantic normalizer** that turns `{tool, args}` into a typed\naction — `{surface, verb, targets, host, flags, findings}` — so policy is written\nagainst *intent* (send vs read, delete vs list, external vs internal recipient),\nnot brittle substring matches on a command line. That means you govern the whole\naction surface, and you govern it by what it means:\n\n| Surface | Tools it recognises | Example policy |\n|---|---|---|\n| Shell / exec | `exec`, `bash` | block `rm -rf /`, fork bombs, disk wipes |\n| **Twitter / X** | `bird` (post/reply/dm/follow/like) | keep the \"read-only\" agent actually read-only |\n| **Google** | `gog` (Gmail send, Drive delete, Docs share, Calendar) | ask before send; deny Drive `--force` delete; deny anyone-with-link sharing |\n| **GitHub** | `git`, `gh` | block force-push, repo/branch delete, `.github/workflows` edits |\n| Comms / act-as-you | `message`, `sessions_send` → Telegram/Discord/WhatsApp/Reddit | recipient allowlist, quiet hours |\n| Web egress / API | `web_fetch`, `curl` | ask on non-allowlisted hosts; deny destructive verbs (DELETE/PUT/PATCH) |\n| Infra | `docker`, `systemctl`, `launchctl` | block destructive ops |\n| Secret / PII exfil | *(any surface)* | block a `ghp_…`/`sk-ant-…`/`AKIA…` token or PII in any outbound body |\n| External tools | any MCP `<server>__<tool>` | governed the same way, per-tool policy |\n\nUnknown binaries fall through to a `unknown` surface — a pack can choose to *ask*\non those rather than wave them through.\n\n`OPENCLAW_GUARD_TOOLS` picks the set the plugin forwards: `actions` (default —\neverything except read-only tools like `read`/`grep`/`ls`/`web_search`), `all`,\nor `exec`.\n\n## Policy packs\n\nYou don't hand-write rego to use this. The guard ships **seven consumer packs**,\neach a named per-surface guardrail with a mode of `off | observe | ask | enforce`\nand optional per-agent overrides:\n\n- **social-guard** (`bird`) — read-only enforce, or ask-before post/reply/DM/follow.\n- **google-guard** (`gog`) — ask before Gmail send; deny Drive delete / `--force`; deny anyone-with-link sharing.\n- **comms-guard** — recipient allowlist, quiet hours (Telegram / Discord / WhatsApp / Reddit).\n- **dlp-guard** — secret markers + PII in any outbound body → deny or ask.\n- **api-guard** (`web_fetch`/`curl`) — host allowlist; deny destructive HTTP verbs (DELETE/PUT/PATCH).\n- **github-guard** (`git`/`gh`) — force-push, repo/branch delete, workflow/secret edits.\n- **infra-guard** (`docker`/`systemctl`/`launchctl`) — destructive ops deny.\n\nEvery pack ships in **observe** mode. First run is a **learn** posture: the guard\nrecords what each agent actually does and what enforcement *would* have done,\nblocking nothing. When you're ready, one click (**Protect me now** in the console)\npromotes a proposal — read-only agents locked, sensitive verbs → ask, DLP always\nenforce — into real enforcement. The packs are edited from the local console; the\nsame pack schema also compiles to full-strength signed rego for the cloud bundle\non beta.aarvion.ai.\n\n## Three verdicts\n\n- **allow** → the call proceeds.\n- **deny** → OpenClaw refuses the call; the process / action never runs.\n- **ask** → a human must approve before the call runs. This is where the plugin\n  now does more than the old native timeout (see below).\n\n## The `ask` path — approve from your phone\n\nWhen the guard returns **ask**, it has already opened a **pending approval**,\nDM'd the owner over **Telegram** (Approve / Deny buttons), and surfaced the same\nitem in the console **Approvals inbox**. The plugin then **polls the guard**\n(`GET /v1/approvals/{id}` over the same socket) until the owner resolves it:\n\n- owner taps **Approve** (Telegram) or approves in the console → the call runs;\n- owner taps **Deny**, or the budget elapses with no answer → the call is blocked.\n\nSo approval happens **out-of-band**, over a channel the governed agent doesn't\ncontrol — not by holding the tool call open on a native timer, and not routed\nthrough the agent being governed. A no-answer **times out to a deny**\n(fail-safe), so a risky action is never quietly allowed. If it's ever pointed at\nan older guard that doesn't return a `decision_id`, the plugin falls back to\nOpenClaw's native `requireApproval` prompt, which also times out to deny.\n\n## Install (no OpenClaw changes)\n\n```sh\n# build the package\nnpm install && npm run build\n\n# install into your stock OpenClaw (published, or from a local path)\nopenclaw plugins install @aarvion/openclaw-guard\n# during development, link a local checkout:\nopenclaw plugins install ./clients/openclaw-plugin --link\n\n# explicitly enable it (required to register a governing policy — you consciously\n# trust the governor):\nopenclaw plugins enable aarvion-guard\n```\n\nPoint the plugin at the guard via the OpenClaw process environment (e.g. its\n`service-env`):\n\n```sh\nOPENCLAW_GUARD_ENABLED=1\nOPENCLAW_GUARD_SOCKET=/Users/you/.aarvion/govern.sock\nOPENCLAW_GUARD_TOKEN=<the guard.json govern.socket.token>\nOPENCLAW_GUARD_FAIL_MODE=closed   # deny when the guard is unreachable (recommended)\nOPENCLAW_GUARD_TOOLS=actions      # actions (default) | all | exec\n\n# ask-verdict polling (optional; sensible defaults shown):\nOPENCLAW_GUARD_APPROVAL_TIMEOUT_MS=90000   # total window to wait for a human\nOPENCLAW_GUARD_APPROVAL_POLL_MS=2000       # re-poll cadence while pending\nOPENCLAW_GUARD_TIMEOUT_MS=2000             # per-request socket timeout\n```\n\nOn the guard side, enable the PDP in `~/.aarvion/guard.json`:\n\n```json\n{\n  \"govern\": {\n    \"socket\": { \"path\": \"/Users/you/.aarvion/govern.sock\", \"token\": \"<random-secret>\", \"peer_uid\": 501 },\n    \"fail_mode\": { \"exec\": \"closed\", \"tool\": \"closed\", \"egress\": \"closed\", \"send\": \"closed\", \"mcp\": \"closed\" }\n  }\n}\n```\n\n`peer_uid` is the uid the OpenClaw process runs as (`id -u`). Disabled (no\n`OPENCLAW_GUARD_ENABLED`, or a missing socket/token) the plugin is a no-op —\nbehavior is unchanged.\n\nTo route `ask` approvals to Telegram, give the guard a bot token + chat id (via\n`~/.aarvion`, owner-supplied from @BotFather). Until then the console\n**Approvals inbox** is the working approval path.\n\nOpen the local console — Packs board, Learning panel, Approvals inbox — with:\n\n```sh\naarvion-guard dashboard   # opens 127.0.0.1:8790, token-gated, pre-authed\n```\n\nA multi-surface example policy (dangerous-exec, GitHub, egress, secret-exfil, and\nan `ask` path) is in [`examples/govern.rego`](./examples/govern.rego).\n\n## Security precondition (read this)\n\nThe guard's guarantees hold only when the **guard and the agent run as different\nuids**. On a single-user host they share a uid, so this governs the *decision\npath* but is **not** a tamper-proof boundary: a same-uid agent could unset the\nenv, kill the guard, or edit its config. For real enforcement, run the guard as a\nseparate service account (Linux) or under a system extension. This is a\nprecondition of the design, not a limitation of the plugin — don't read it as\n\"unbypassable\".\n\nOne more honest scope line: the guard governs tool **actions**, not the model's\nstreamed response. The runtime hook sits at OpenClaw's `before_tool_call`\nchokepoint; the model's own output stream is out of scope.\n\n## Design\n\nSelf-contained: the package declares its own minimal slice of the OpenClaw host\nAPI, so it builds and loads with **zero `openclaw` imports** — only Node\nbuilt-ins. `e2e.ts` is a standalone proof against a running guard. The semantic\nnormalizer, decision hash-chain, policy packs, and OPA engine all live guard-side\n(so the plugin can't lie its way past semantic policy, modulo the same-uid\ncaveat), and are the same components the network egress proxy uses — both\ngovernance paths evaluate one signed policy and write one audit trail. This is\ndesigned and tested (unit + integration); a separate live proof on the mini adds\nreal-agent evidence.\n","readmeFilename":"README.md"}