@aporthq/aport-agent-guardrails 1.0.21 → 1.0.22

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.
@@ -1,59 +1,63 @@
1
1
  # APort OpenClaw Plugin
2
2
 
3
- [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen.svg)](package.json) **APort Node SDK:** [npm](https://www.npmjs.com/package/@aporthq/sdk-node)
3
+ Deterministic pre-action authorization for OpenClaw agents.
4
4
 
5
- **Deterministic pre-action authorization for OpenClaw agents.**
5
+ This plugin registers `before_tool_call` and evaluates every tool call against an Open Agent Passport before the tool executes.
6
6
 
7
- > **Part of [aport-agent-guardrails](https://github.com/aporthq/aport-agent-guardrails)**: This is the OpenClaw plugin within a multi-framework repository that provides guardrails for LangChain, CrewAI, n8n, Cursor, and OpenClaw.
7
+ ## Recommended install
8
8
 
9
- This plugin registers `before_tool_call` hooks to enforce APort policies **before every tool execution**. No more relying on prompts or hoping the AI follows instructions - the platform enforces policy.
9
+ Use the published setup command. No repo clone is required.
10
10
 
11
- > **📢 OpenClaw 2026.3+ SDK Update:** This plugin now uses `definePluginEntry` with focused SDK subpath imports (`openclaw/plugin-sdk/plugin-entry`) and is aligned with current OpenClaw plugin docs. If you're upgrading from an older version, see [MIGRATION.md](./MIGRATION.md).
12
-
13
- ---
11
+ ```bash
12
+ npx @aporthq/aport-agent-guardrails openclaw
13
+ ```
14
14
 
15
- ## Features
15
+ If you already have a hosted passport on aport.io, pass the `agent_id`:
16
16
 
17
- ✅ **100% Deterministic** - Platform enforces, AI cannot bypass
18
- **Fail-Closed** - Blocks on error (configurable)
19
- ✅ **Local or API Mode** - Use local script or APort cloud API
20
- ✅ **Zero OpenClaw Changes** - Uses existing plugin API
21
- ✅ **Audit Logging** - Optional after_tool_call hook
17
+ ```bash
18
+ npx @aporthq/aport-agent-guardrails openclaw ap_your_agent_id
19
+ ```
22
20
 
23
- ---
21
+ That command:
24
22
 
25
- ## Installation
23
+ 1. Chooses your OpenClaw config directory
24
+ 2. Creates a passport or wires a hosted `agent_id`
25
+ 3. Installs this plugin with `openclaw plugins install -l ...`
26
+ 4. Writes plugin config into `config.yaml` and `openclaw.json`
27
+ 5. Installs wrappers under `CONFIG_DIR/.skills/` for manual guardrail and status commands
28
+ 6. Runs a setup smoke test
26
29
 
27
- ### Option 1: Via Setup Script (Recommended)
30
+ After setup, start OpenClaw with the generated config:
28
31
 
29
32
  ```bash
30
- # From aport-agent-guardrails repo root — one run secures OpenClaw
31
- ./bin/openclaw
33
+ openclaw gateway start --config ~/.openclaw/config.yaml
32
34
  ```
33
35
 
34
- The setup script will:
35
- 1. Ask for OpenClaw config directory (default `~/.openclaw`)
36
- 2. Create passport (OAP v1.0 spec) there
37
- 3. Prompt to install this OpenClaw plugin (deterministic enforcement)
38
- 4. Generate `config.yaml` with plugin config (passport path, guardrail script path, mode)
39
- 5. Install guardrail wrappers in `CONFIG_DIR/.skills/` (including `aport-guardrail-bash.sh` used by the plugin in local mode)
40
- 6. Optionally install the APort skill, AGENTS.md rule, and run a smoke test
41
- 7. Verify plugin installation
36
+ ## What OpenClaw already gives you
37
+
38
+ OpenClaw already ships sandboxing, tool policy, elevated exec controls, and install-time scanning. Those are real security controls, not marketing copy.
39
+
40
+ ## What APort adds
41
+
42
+ APort complements those controls with external authorization and audit:
42
43
 
43
- **One run is enough.** After that, start OpenClaw with the generated config (e.g. `openclaw gateway start --config ~/.openclaw/config.yaml`); the plugin will enforce policy on every tool call. See [QUICKSTART_OPENCLAW_PLUGIN.md](../../docs/QUICKSTART_OPENCLAW_PLUGIN.md).
44
+ - per-agent passports and capability limits
45
+ - parameter-aware deny decisions, not just static tool allowlists
46
+ - local or hosted kill switch by suspending the passport
47
+ - signed decision receipts and centralized audit in API mode
48
+ - the same authorization model across OpenClaw and other frameworks
44
49
 
45
- ### Option 2: Manual Installation
50
+ If OpenClaw's built-in sandbox and tool policy are enough for your deployment, use them. If you need portable authorization, identity-scoped limits, or fleet-wide kill switch and audit, add APort on top.
51
+
52
+ ## Development install
53
+
54
+ If you are working from a local checkout, install the plugin directly from the extension directory:
46
55
 
47
56
  ```bash
48
- # From your OpenClaw config directory
49
57
  openclaw plugins install -l /path/to/aport-agent-guardrails/extensions/openclaw-aport
50
58
  ```
51
59
 
52
- ---
53
-
54
- ## Configuration
55
-
56
- Add to your OpenClaw `config.yaml`:
60
+ Then configure it in your OpenClaw config:
57
61
 
58
62
  ```yaml
59
63
  plugins:
@@ -62,358 +66,64 @@ plugins:
62
66
  openclaw-aport:
63
67
  enabled: true
64
68
  config:
65
- # Mode: "local" (use guardrail script) or "api" (use APort cloud API)
66
- mode: local
67
-
68
- # Passport file location (in aport/ subdir; legacy: ~/.openclaw/passport.json)
69
+ mode: api
69
70
  passportFile: ~/.openclaw/aport/passport.json
70
-
71
- # For local mode: path to guardrail script
72
- guardrailScript: ~/.openclaw/.skills/aport-guardrail-bash.sh
73
-
74
- # For API mode: APort API endpoint
75
71
  apiUrl: https://api.aport.io
76
- # Optional: set APORT_API_KEY in the environment if your API requires auth
77
-
78
- # Fail-closed: block on error (default: true)
79
72
  failClosed: true
80
-
81
- # Run APort verify on every tool call; never reuse a previous decision (default: true)
82
- alwaysVerifyEachToolCall: true
83
-
84
- # Map exec to system.command.execute.v1 and check passport allowed_commands (default: true).
85
- # Set to false to never block exec (OpenClaw can run any command; no guardrail for exec).
86
- mapExecToPolicy: true
73
+ allowUnmappedTools: true
87
74
  ```
88
75
 
89
- ---
90
-
91
- ## exec, allowed_commands, and unmapped tools
92
-
93
- - **exec** is OpenClaw’s main “run something” tool: it can run the guardrail script (we delegate to the inner tool) or a real shell command (e.g. `mkdir`, `npm install`). By default we map **exec** → **system.command.execute.v1** and check the **command** against your passport’s **limits.system.command.execute.allowed_commands**. If `mkdir` (or another command) is not in that list, the policy denies with `oap.command_not_allowed`.
94
- - **Fix:** Add every command you need to **allowed_commands** in your passport (e.g. `mkdir`, `cp`, `ls`, `cat`, `echo`, `pwd`, `mv`, `touch`, `npx`, `open`). Re-run the passport wizard to get an expanded default list, or edit `~/.openclaw/aport/passport.json` (or `~/.openclaw/passport.json` for legacy) and add to `limits.system.command.execute.allowed_commands`. If the guardrail is ever run via **exec** (e.g. a skill runs `bash ~/.openclaw/.skills/aport-guardrail.sh ...`), include **`bash`** (or the full script path) in **allowed_commands** so that invocation is allowed; the wizard default includes `bash` and `sh`.
95
- - **Optional:** Set **mapExecToPolicy: false** in plugin config so **exec** is not mapped; then exec is treated as an unmapped tool and allowed (no command allowlist). Use only if you rely on other controls; this disables guardrail protection for shell commands.
96
- - **read, write** are now mapped to `data.file.read.v1` and `data.file.write.v1` policies, enforcing path allowlists and blocked patterns. The LangChain/CrewAI middlewares automatically spread tool input parameters (e.g. `file_path`) into the verification context for proper API validation. **edit, browser, cron, etc.** remain unmapped and are allowed by default (`allowUnmappedTools: true`) for backward compatibility. Set `allowUnmappedTools: false` if you want strict blocking for unmapped tools. Tool→policy mapping and passport limits are documented in [TOOL_POLICY_MAPPING.md](../../docs/TOOL_POLICY_MAPPING.md) and [OPENCLAW_TOOLS_AND_POLICIES.md](../../docs/OPENCLAW_TOOLS_AND_POLICIES.md).
97
-
98
- ---
99
-
100
- ## Every tool call = fresh APort check (no caching)
101
-
102
- The plugin **never reuses a previous decision**. Each `before_tool_call` runs a new verify (local script or API). In local mode each call gets a **unique decision file path** (`decisions/<timestamp>-<id>.json`); the plugin only reads the file it passed to that invocation, so there is no cache or reuse.
103
-
104
- - **mkdir** → APort runs → Deny
105
- - **mkdir** again → APort runs again → Allow or Deny based on **current** passport/limits
106
-
107
- **Exec with no command:** If OpenClaw sends an `exec` tool call with an empty or missing command (e.g. a probe or placeholder), the plugin allows it without calling the guardrail so those pre-checks are not blocked. The real `exec` with a command (e.g. `ls`) is still evaluated by the guardrail.
108
-
109
- If you updated your passport (e.g. added a command to `allowed_commands` or changed limits), the next tool call is evaluated against the new state. Set `alwaysVerifyEachToolCall: false` only if you add a future cache and want to opt out of per-call verification.
110
-
111
- ---
112
-
113
- ## Agent instructions (AGENTS.md)
114
-
115
- The **guardrail** always runs for every tool call. The **agent** (LLM) must not assume "same tool → same result as last time." Add this to your OpenClaw project's **AGENTS.md** (or equivalent) so the agent always invokes the tool and lets APort decide each time:
116
-
117
- ```markdown
118
- ## APort guardrails
119
- - **Always invoke the tool** when the user requests an action. Do not skip or assume a tool will be denied because a previous invocation was denied.
120
- - APort is re-evaluated on every tool call; passport or limits may have changed. The plugin does not reuse previous decisions.
121
- ```
122
-
123
- ---
124
-
125
- ## How It Works
126
-
127
- ```mermaid
128
- sequenceDiagram
129
- participant Agent as 🤖 OpenClaw Agent
130
- participant Plugin as 🛡️ APort Plugin
131
- participant Guard as 📋 Guardrail
132
- participant Tool as 🔧 Tool
133
-
134
- Agent->>Plugin: before_tool_call(toolName, params)
135
- Plugin->>Plugin: Map tool → policy
136
- Plugin->>Guard: Verify policy
137
-
138
- alt Policy Allows
139
- Guard-->>Plugin: ✅ Decision: Allow
140
- Plugin-->>Agent: {} (continue)
141
- Agent->>Tool: Execute tool
142
- else Policy Denies
143
- Guard-->>Plugin: ❌ Decision: Deny
144
- Plugin-->>Agent: { block: true, blockReason }
145
- Agent->>Agent: Throw error (tool NOT executed)
146
- end
147
- ```
148
-
149
- ### Tool-to-Policy Mapping
150
-
151
- | OpenClaw Tool | APort Policy |
152
- |---------------|--------------|
153
- | `git.create_pr`, `git.merge`, `git.push` | `code.repository.merge.v1` |
154
- | `exec.run`, `system.command.*`, `bash` | `system.command.execute.v1` |
155
- | `message.send`, `messaging.*` | `messaging.message.send.v1` |
156
- | `mcp.*` | `mcp.tool.execute.v1` |
157
- | `session.create` | `agent.session.create.v1` |
158
- | `tool.register` | `agent.tool.register.v1` |
159
- | `payment.refund` | `finance.payment.refund.v1` |
160
- | `payment.charge` | `finance.payment.charge.v1` |
161
- | `data.export` | `data.export.create.v1` |
162
-
163
- Unmapped tools are **allowed by default** (`allowUnmappedTools: true`) for backward compatibility. Set `allowUnmappedTools: false` for stricter security.
164
-
165
- ---
76
+ Hosted passport mode uses `agentId` instead of `passportFile`.
166
77
 
167
78
  ## Modes
168
79
 
169
- ### Local Mode
80
+ ### API mode
170
81
 
171
- **Best for:** Privacy, offline use, no network dependency
82
+ - Uses `fetch()` directly from the plugin
83
+ - Returns signed decisions from `api.aport.io`
84
+ - Configure `apiKey` in plugin config if your deployment requires it
172
85
 
173
- ```yaml
174
- config:
175
- mode: local
176
- passportFile: ~/.openclaw/aport/passport.json
177
- guardrailScript: ~/.openclaw/.skills/aport-guardrail-bash.sh
178
- ```
179
-
180
- **How it works:**
181
- 1. Plugin calls local bash script
182
- 2. Script evaluates policy using local passport
183
- 3. Returns decision (exit 0 = allow, exit 1 = deny)
86
+ ### Local mode
184
87
 
185
- **No network required** - everything runs locally.
88
+ - Uses the built-in JavaScript evaluator shipped with the plugin
89
+ - No `child_process` spawn is required
90
+ - `guardrailScript` remains as a legacy compatibility field for manual smoke tests and shell tooling, but current plugin versions do not depend on it for local-mode enforcement
186
91
 
187
- ### API Mode
92
+ ## Tool mapping
188
93
 
189
- **Best for:** Advanced features, cloud kill switch, audit logs
94
+ The plugin keeps the existing OpenClaw-specific tool mappings. Common examples:
190
95
 
191
- ```yaml
192
- config:
193
- mode: api
194
- passportFile: ~/.openclaw/aport/passport.json
195
- apiUrl: https://api.aport.io # or your self-hosted API URL
196
- # Set APORT_API_KEY in the environment if your API requires auth
197
- ```
96
+ - `exec`, `exec.run` -> `system.command.execute.v1`
97
+ - `git.create_pr`, `git.merge`, `git.push` -> `code.repository.merge.v1`
98
+ - `message.send` -> `messaging.message.send.v1`
99
+ - `read`, `view`, `glob` -> `data.file.read.v1`
100
+ - `write`, `edit`, `multiedit` -> `data.file.write.v1`
101
+ - `mcp__*` -> `mcp.tool.execute.v1`
198
102
 
199
- **How it works:**
200
- 1. Plugin loads local passport
201
- 2. Sends passport + context to APort API
202
- 3. API evaluates (passport NOT stored, stateless)
203
- 4. Returns signed decision
103
+ `allowUnmappedTools: true` keeps the previous OpenClaw compatibility behavior for custom skills and unmapped tools.
204
104
 
205
- **Network required** - sends passport to API for evaluation.
105
+ ## Exec behavior
206
106
 
207
- ---
107
+ `exec` is OpenClaw's main shell-style tool. By default the plugin maps it to `system.command.execute.v1` and checks the underlying command against `limits["system.command.execute"].allowed_commands` in the passport.
208
108
 
209
- ## Testing
210
-
211
- ### Test the Plugin
212
-
213
- ```bash
214
- # 1. Install plugin (via setup or manually)
215
- openclaw plugins install -l /path/to/extensions/openclaw-aport
216
-
217
- # 2. Configure in config.yaml (see above)
218
-
219
- # 3. Start OpenClaw agent
220
- openclaw agent start
221
-
222
- # 4. Try a command that should be allowed
223
- # (Agent will call plugin before executing)
224
- "Create a file called test.txt"
225
-
226
- # 5. Try a command that should be denied
227
- "Run: rm -rf /"
228
- # Expected: Plugin blocks with reason from passport limits
229
- ```
230
-
231
- ### Check Plugin Logs
232
-
233
- ```bash
234
- # Plugin logs to OpenClaw logs
235
- openclaw logs | grep "APort Guardrails"
236
-
237
- # Should see:
238
- # [APort Guardrails] Loaded: mode=local, passportFile=~/.openclaw/aport/passport.json
239
- # [APort Guardrails] Checking tool: exec.run → policy: system.command.execute.v1
240
- # [APort Guardrails] ALLOW: system.command.execute - mkdir test
241
- ```
242
-
243
- ---
109
+ If the plugin sees a delegated guardrail invocation such as `aport-guardrail-bash.sh <tool> <json>`, it unwraps the inner tool and evaluates that policy instead of treating the wrapper as an ordinary shell command.
244
110
 
245
111
  ## Troubleshooting
246
112
 
247
- ### Plugin not loading
248
-
249
- ```bash
250
- # Check plugin list
251
- openclaw plugins list
252
-
253
- # Should show:
254
- # openclaw-aport (enabled)
255
- ```
256
-
257
- If not listed:
258
- 1. Verify installation: `openclaw plugins install -l /path/to/extensions/openclaw-aport`
259
- 2. Check config.yaml has `plugins.entries.openclaw-aport.enabled: true`
260
- 3. Restart OpenClaw gateway
261
-
262
- ### Tools not being blocked
263
-
264
- Check:
265
- 1. **Plugin enabled?** `openclaw plugins list` should show `openclaw-aport (enabled)`
266
- 2. **Tool mapped?** See "Tool-to-Policy Mapping" above. Unmapped tools are allowed by default (`allowUnmappedTools: true`). Set `allowUnmappedTools: false` if you want strict blocking for unmapped tools.
267
- 3. **Passport allows it?** Check passport limits in `~/.openclaw/aport/passport.json`
268
- 4. **Script working?** Test directly: `~/.openclaw/.skills/aport-guardrail-bash.sh system.command.execute '{"command":"ls"}'`
269
-
270
- ### Error: "Failed to run guardrail script"
271
-
272
- Check:
273
- 1. Script exists: `ls -l ~/.openclaw/.skills/aport-guardrail-bash.sh`
274
- 2. Script executable: `chmod +x ~/.openclaw/.skills/aport-guardrail-bash.sh`
275
- 3. Script works: Run test command above
276
-
277
- ### Error: "API request failed"
278
-
279
- Check:
280
- 1. API URL correct: `echo $APORT_API_URL` or check config.yaml
281
- 2. API running: `curl $APORT_API_URL/health` (if self-hosted)
282
- 3. If your API requires auth: set `APORT_API_KEY` in the environment (do not put it in config)
283
- 4. Network connectivity
284
-
285
- ### Error: "MissingEnvVarError: Missing env var APORT_API_KEY"
286
-
287
- OpenClaw substitutes `${VAR}` in config and requires the variable to exist. **Do not put `apiKey: \${APORT_API_KEY}` in config.** Fix:
288
-
289
- 1. **Remove apiKey from config:** Edit `~/.openclaw/openclaw.json` and delete the `"apiKey": "${APORT_API_KEY}"` line under `plugins.entries.openclaw-aport.config`, or run `make openclaw-setup` again (setup no longer writes apiKey to config).
290
- 2. If your API requires auth, set `APORT_API_KEY` in the environment only; the plugin reads it at runtime.
291
-
292
- ---
293
-
294
- ## Security Considerations
295
-
296
- ### Fail-Closed by Default
297
-
298
- By default, `failClosed: true` means **any error blocks the tool**:
299
- - Script not found → BLOCK
300
- - API unreachable → BLOCK
301
- - Invalid passport → BLOCK
302
-
303
- This is secure-by-default. To fail-open (not recommended):
304
-
305
- ```yaml
306
- config:
307
- failClosed: false # Allow on error (NOT RECOMMENDED)
308
- ```
309
-
310
- ### Plugin Trust
311
-
312
- Plugins run **in-process** with full access to OpenClaw. Only install from trusted sources:
313
- - Official APort plugin (this)
314
- - Your own forks/modifications
315
-
316
- Use `plugins.allow` allowlist in config.yaml:
317
-
318
- ```yaml
319
- plugins:
320
- allow:
321
- - openclaw-aport
322
- - your-other-trusted-plugin
323
- ```
324
-
325
- ### Bypass Prevention
326
-
327
- **With this plugin:** AI **cannot** bypass policy enforcement. The platform calls `before_tool_call` before every tool.
328
-
329
- **Without this plugin (AGENTS.md only):** AI **can** bypass via:
330
- - Prompt injection
331
- - Forgetting to call guardrail
332
- - Deciding action is "safe"
333
-
334
- **Bottom line:** Plugin = deterministic. AGENTS.md = best-effort (not secure).
335
-
336
- ---
337
-
338
- ## Decisions and audit (OAP)
339
-
340
- APort decisions are **structured and auditable**. They follow the [OAP v1.0 decision schema](https://github.com/aporthq/aport-spec/oap/decision-schema) (e.g. `decision_id`, `policy_id`, `allow`, `reasons`, `passport_digest`, `signature`, `kid`). The agent-passport API returns signed decisions and can chain them in an audit trail (KV/D1 + audit actions).
341
-
342
- **Local mode (this plugin):**
343
- - **Decisions** are written to `<config_dir>/decisions/<timestamp>-<id>.json` and **kept** (not deleted). Each file is a full OAP decision (allow or deny). Config dir is derived from `passportFile` (e.g. `~/.openclaw` → `~/.openclaw/decisions/`).
344
- - **Audit log** one-line summary is appended to `<config_dir>/audit.log` by the guardrail script (tool, decision_id, allow, policy, code).
345
- - Local evaluations use **unsigned** decisions (`signature: "ed25519:local-unsigned"`, `kid: "oap:local:dev-key"`). This is the open-source/local promise: structured decisions and audit trail, with optional signing in API or enterprise.
346
-
347
- **API mode:** The APort API can return signed decisions (`ed25519:...`, `kid: oap:registry:...`) and log decisions server-side (e.g. DecisionService, chained audit). Use API mode when you need signed, verifiable decisions and central audit.
348
-
349
- **Tamper-resistant local decisions:** Each decision file includes a **content_hash** (SHA-256 of the canonical decision payload). A **chain** is maintained in `decisions/.chain-state.json`: each decision stores `prev_decision_id` and `prev_content_hash`. If a file is edited or the chain is reordered, the plugin detects it (content_hash mismatch) and logs a warning. Decisions remain valid for allow/deny; the check is for audit integrity.
350
-
351
- **References:** `agent-passport` [spec/oap/decision-schema.json](https://github.com/aporthq/agent-passport/blob/main/spec/oap/decision-schema.json), [examples](https://github.com/aporthq/agent-passport/tree/main/spec/oap/examples), and [functions/api/verify/policy/[pack_id].ts](https://github.com/aporthq/agent-passport/blob/main/functions/api/verify/policy/%5Bpack_id%5D.ts) for how decisions are built and logged.
352
-
353
- ---
354
-
355
- ## Performance and non-blocking behavior
356
-
357
- - **Critical path:** Only policy evaluation and writing the decision file (so the plugin can read allow/deny) block the tool call. Chain state is updated synchronously so the next decision can link; audit log append runs in the background and must not block.
358
- - **Plugin:** Tamper checking (content_hash verification) runs in `setImmediate` after the allow/deny return, so it never delays the tool call.
359
- - **Guardrail script:** Audit log append is done in a background subshell (`( echo ... >> audit.log ) &`). Chain state write is best-effort (failures do not change the script exit code).
360
-
361
- **Tests:** Run `npm test` in this directory. Unit tests cover mapping, integrity verification, and canonicalize; performance tests assert that hot paths stay within latency bounds; integration test runs the guardrail script when the repo is available and checks content_hash and chain.
362
-
363
- ## Development
364
-
365
- ### Running Locally
366
-
367
- ```bash
368
- # From this directory
369
- cd extensions/openclaw-aport
370
-
371
- # Test plugin registration
372
- node index.js
373
-
374
- # Link for local testing
375
- npm link
376
- openclaw plugins install $(pwd)
377
- ```
378
-
379
- ### Debugging
380
-
381
- Add debug logging:
382
-
383
- ```javascript
384
- // In index.js
385
- api.on('before_tool_call', async (event, ctx) => {
386
- console.log('[DEBUG] Tool:', event.toolName);
387
- console.log('[DEBUG] Params:', event.params);
388
- // ...
389
- });
390
- ```
391
-
392
- View logs:
393
- ```bash
394
- openclaw logs --follow | grep -E "(APort|DEBUG)"
395
- ```
396
-
397
- ---
398
-
399
- ## License
400
-
401
- Apache 2.0 - See [LICENSE](../../LICENSE)
113
+ ### Plugin install failed
402
114
 
403
- ---
115
+ Current OpenClaw releases perform install-time security scanning. This plugin is designed to pass that scan, but if installation still fails:
404
116
 
405
- ## Support
117
+ 1. Make sure you are installing the current package version
118
+ 2. Prefer the setup command `npx @aporthq/aport-agent-guardrails openclaw`
119
+ 3. For local development, install from the extension directory with `-l`
406
120
 
407
- - **Documentation:** [aport-agent-guardrails/docs](../../docs/)
408
- - **Issues:** [GitHub Issues](https://github.com/aporthq/aport-agent-guardrails/issues)
409
- - **Discord:** [discord.gg/aport](https://discord.gg/aport)
121
+ ### Existing source-linked config points into an old `npx` cache
410
122
 
411
- ---
123
+ Re-run the setup command. The installer removes stale `plugins.load.paths` and `plugins.installs.openclaw-aport` entries so OpenClaw does not keep pointing at a transient `~/.npm/_npx/...` directory.
412
124
 
413
- ## Roadmap
125
+ ## Notes
414
126
 
415
- - [ ] **Policy analytics** - Track allow/deny rates per policy
416
- - [ ] **Custom mappings** - User-defined tool-to-policy mappings
417
- - [ ] **Performance metrics** - Measure policy evaluation latency
418
- - [ ] **Batch verification** - Verify multiple tools at once
419
- - [ ] **Policy caching** - Cache decisions for repeated actions
127
+ - Current public OpenClaw integration is plugin-based
128
+ - No upstream native guardrail-provider merge is required for this plugin path
129
+ - If OpenClaw later ships a native provider seam, APort can support that as an additional path without replacing the current plugin install flow
@@ -0,0 +1,22 @@
1
+ export async function verifyViaApi({ apiUrl, apiKey, policyName, context, passport, agentId }) {
2
+ const baseUrl = String(apiUrl || "https://api.aport.io").replace(/\/$/, "");
3
+ const headers = { "Content-Type": "application/json" };
4
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
5
+
6
+ const body = agentId
7
+ ? JSON.stringify({ context: { agent_id: agentId, ...context } })
8
+ : JSON.stringify({ passport, context });
9
+
10
+ const response = await fetch(`${baseUrl}/api/verify/policy/${policyName}`, {
11
+ method: "POST",
12
+ headers,
13
+ body,
14
+ });
15
+
16
+ if (!response.ok) {
17
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
18
+ }
19
+
20
+ const data = await response.json();
21
+ return data.decision || data;
22
+ }
@@ -0,0 +1,32 @@
1
+ import { appendFile, appendFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ export function logAuditEntry(auditLogPath, entry) {
5
+ try {
6
+ const ts = new Date().toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
7
+ const code = entry.code || (entry.allow ? "oap.allowed" : "oap.denied");
8
+ let line = `[${ts}] tool=${entry.tool}`;
9
+ if (entry.decisionId) line += ` decision_id=${entry.decisionId}`;
10
+ line += ` allow=${entry.allow} policy=${entry.policy} code=${code}`;
11
+ if (entry.agentId) line += ` agent_id=${entry.agentId}`;
12
+ if (entry.context) {
13
+ const sanitized = String(entry.context)
14
+ .replace(/[\r\n]+/g, " ")
15
+ .replace(/"/g, '\\"')
16
+ .slice(0, 120);
17
+ line += ` context="${sanitized}"`;
18
+ }
19
+ line += "\n";
20
+
21
+ const dir = dirname(auditLogPath);
22
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
23
+
24
+ if (entry.allow) {
25
+ appendFile(auditLogPath, line, "utf8", () => {});
26
+ } else {
27
+ appendFileSync(auditLogPath, line, "utf8");
28
+ }
29
+ } catch {
30
+ // Best-effort only.
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function canonicalize(obj) {
4
+ if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
5
+ if (Array.isArray(obj)) return `[${obj.map(canonicalize).join(",")}]`;
6
+ const keys = Object.keys(obj).sort();
7
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalize(obj[key])}`).join(",")}}`;
8
+ }
9
+
10
+ export function verifyDecisionIntegrity(decision) {
11
+ if (!decision || !decision.content_hash) return true;
12
+ const { content_hash, ...rest } = decision;
13
+ const computed = `sha256:${createHash("sha256").update(canonicalize(rest), "utf8").digest("hex")}`;
14
+ return computed === content_hash;
15
+ }
16
+
17
+ export function formatReasons(decision) {
18
+ const reasons = Array.isArray(decision?.reasons) ? decision.reasons : [];
19
+ const primaryMessage = reasons[0]?.message || decision?.reason || "";
20
+ return { reasons, primaryMessage };
21
+ }