@aporthq/aport-agent-guardrails 1.0.21 → 1.0.23
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.
- package/README.md +8 -3
- package/bin/openclaw +82 -31
- package/docs/PROVIDER.md +6 -6
- package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +42 -408
- package/docs/RELEASE.md +3 -2
- package/docs/TOOL_POLICY_MAPPING.md +2 -2
- package/docs/frameworks/openclaw.md +137 -38
- package/extensions/openclaw-aport/CHANGELOG.md +14 -1
- package/extensions/openclaw-aport/MIGRATION.md +22 -375
- package/extensions/openclaw-aport/README.md +88 -350
- package/extensions/openclaw-aport/api-client.js +30 -0
- package/extensions/openclaw-aport/audit.js +32 -0
- package/extensions/openclaw-aport/decision.js +21 -0
- package/extensions/openclaw-aport/index.js +169 -592
- package/extensions/openclaw-aport/local-evaluator.js +303 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +5 -5
- package/extensions/openclaw-aport/package-lock.json +2 -2
- package/extensions/openclaw-aport/package.json +27 -6
- package/extensions/openclaw-aport/tool-mapping.js +276 -0
- package/package.json +1 -1
- package/extensions/openclaw-aport/index.ts +0 -547
- package/extensions/openclaw-aport/test.js +0 -356
|
@@ -1,419 +1,157 @@
|
|
|
1
1
|
# APort OpenClaw Plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic pre-action authorization for OpenClaw agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This plugin registers `before_tool_call` and evaluates every tool call against an Open Agent Passport before the tool executes.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Recommended install
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
---
|
|
14
|
-
|
|
15
|
-
## Features
|
|
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
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
-
|
|
27
|
-
### Option 1: Via Setup Script (Recommended)
|
|
9
|
+
Use the published setup command. No repo clone is required.
|
|
28
10
|
|
|
29
11
|
```bash
|
|
30
|
-
|
|
31
|
-
./bin/openclaw
|
|
12
|
+
npx @aporthq/aport-agent-guardrails openclaw
|
|
32
13
|
```
|
|
33
14
|
|
|
34
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
### Option 2: Manual Installation
|
|
15
|
+
If you already have a hosted passport on aport.io, pass the `agent_id`:
|
|
46
16
|
|
|
47
17
|
```bash
|
|
48
|
-
|
|
49
|
-
openclaw plugins install -l /path/to/aport-agent-guardrails/extensions/openclaw-aport
|
|
18
|
+
npx @aporthq/aport-agent-guardrails openclaw ap_your_agent_id
|
|
50
19
|
```
|
|
51
20
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Configuration
|
|
55
|
-
|
|
56
|
-
Add to your OpenClaw `config.yaml`:
|
|
57
|
-
|
|
58
|
-
```yaml
|
|
59
|
-
plugins:
|
|
60
|
-
enabled: true
|
|
61
|
-
entries:
|
|
62
|
-
openclaw-aport:
|
|
63
|
-
enabled: true
|
|
64
|
-
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
|
-
passportFile: ~/.openclaw/aport/passport.json
|
|
70
|
-
|
|
71
|
-
# For local mode: path to guardrail script
|
|
72
|
-
guardrailScript: ~/.openclaw/.skills/aport-guardrail-bash.sh
|
|
21
|
+
That command:
|
|
73
22
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
77
29
|
|
|
78
|
-
|
|
79
|
-
failClosed: true
|
|
30
|
+
After setup, start OpenClaw with the generated config:
|
|
80
31
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
32
|
+
```bash
|
|
33
|
+
openclaw gateway start --config ~/.openclaw/config.yaml
|
|
87
34
|
```
|
|
88
35
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
## exec, allowed_commands, and unmapped tools
|
|
36
|
+
## If you installed with `openclaw plugins install`
|
|
92
37
|
|
|
93
|
-
|
|
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).
|
|
38
|
+
If you installed the plugin directly with:
|
|
97
39
|
|
|
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
|
|
40
|
+
```bash
|
|
41
|
+
openclaw plugins install @aporthq/openclaw-aport
|
|
147
42
|
```
|
|
148
43
|
|
|
149
|
-
|
|
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.
|
|
44
|
+
that installs only the plugin bundle. It does not create a passport, choose API vs local mode, or write plugin config.
|
|
164
45
|
|
|
165
|
-
|
|
46
|
+
Run the full APort setup immediately after install:
|
|
166
47
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
### Local Mode
|
|
170
|
-
|
|
171
|
-
**Best for:** Privacy, offline use, no network dependency
|
|
172
|
-
|
|
173
|
-
```yaml
|
|
174
|
-
config:
|
|
175
|
-
mode: local
|
|
176
|
-
passportFile: ~/.openclaw/aport/passport.json
|
|
177
|
-
guardrailScript: ~/.openclaw/.skills/aport-guardrail-bash.sh
|
|
48
|
+
```bash
|
|
49
|
+
npx @aporthq/aport-agent-guardrails openclaw
|
|
178
50
|
```
|
|
179
51
|
|
|
180
|
-
|
|
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)
|
|
184
|
-
|
|
185
|
-
**No network required** - everything runs locally.
|
|
52
|
+
If you already have a hosted passport, use:
|
|
186
53
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
**Best for:** Advanced features, cloud kill switch, audit logs
|
|
190
|
-
|
|
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
|
|
54
|
+
```bash
|
|
55
|
+
npx @aporthq/aport-agent-guardrails openclaw ap_your_agent_id
|
|
197
56
|
```
|
|
198
57
|
|
|
199
|
-
|
|
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
|
|
58
|
+
## What OpenClaw already gives you
|
|
204
59
|
|
|
205
|
-
|
|
60
|
+
OpenClaw already ships sandboxing, tool policy, elevated exec controls, and install-time scanning. Those are real security controls, not marketing copy.
|
|
206
61
|
|
|
207
|
-
|
|
62
|
+
## What APort adds
|
|
208
63
|
|
|
209
|
-
|
|
64
|
+
APort complements those controls with external authorization and audit:
|
|
210
65
|
|
|
211
|
-
|
|
66
|
+
- per-agent passports and capability limits
|
|
67
|
+
- parameter-aware deny decisions, not just static tool allowlists
|
|
68
|
+
- local or hosted kill switch by suspending the passport
|
|
69
|
+
- signed decision receipts and centralized audit in API mode
|
|
70
|
+
- the same authorization model across OpenClaw and other frameworks
|
|
212
71
|
|
|
213
|
-
|
|
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)
|
|
72
|
+
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.
|
|
218
73
|
|
|
219
|
-
|
|
220
|
-
openclaw agent start
|
|
74
|
+
## Development install
|
|
221
75
|
|
|
222
|
-
|
|
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
|
|
76
|
+
If you are working from a local checkout, install the plugin directly from the extension directory:
|
|
232
77
|
|
|
233
78
|
```bash
|
|
234
|
-
|
|
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
|
|
79
|
+
openclaw plugins install -l /path/to/aport-agent-guardrails/extensions/openclaw-aport
|
|
241
80
|
```
|
|
242
81
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
## Troubleshooting
|
|
246
|
-
|
|
247
|
-
### Plugin not loading
|
|
82
|
+
That command installs only the OpenClaw plugin bundle. It does not create a passport, choose API vs local mode, or write plugin config. For a full working setup, use:
|
|
248
83
|
|
|
249
84
|
```bash
|
|
250
|
-
|
|
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)
|
|
85
|
+
npx @aporthq/aport-agent-guardrails openclaw
|
|
308
86
|
```
|
|
309
87
|
|
|
310
|
-
|
|
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:
|
|
88
|
+
Then configure it in your OpenClaw config:
|
|
317
89
|
|
|
318
90
|
```yaml
|
|
319
91
|
plugins:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
-
|
|
92
|
+
enabled: true
|
|
93
|
+
entries:
|
|
94
|
+
openclaw-aport:
|
|
95
|
+
enabled: true
|
|
96
|
+
config:
|
|
97
|
+
mode: api
|
|
98
|
+
passportFile: ~/.openclaw/aport/passport.json
|
|
99
|
+
apiUrl: https://api.aport.io
|
|
100
|
+
failClosed: true
|
|
101
|
+
allowUnmappedTools: true
|
|
323
102
|
```
|
|
324
103
|
|
|
325
|
-
|
|
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).
|
|
104
|
+
Hosted passport mode uses `agentId` instead of `passportFile`.
|
|
341
105
|
|
|
342
|
-
|
|
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.
|
|
106
|
+
## Modes
|
|
348
107
|
|
|
349
|
-
|
|
108
|
+
### API mode
|
|
350
109
|
|
|
351
|
-
|
|
110
|
+
- Uses `fetch()` directly from the plugin
|
|
111
|
+
- Returns signed decisions from `api.aport.io`
|
|
112
|
+
- Configure `apiKey` in plugin config if your deployment requires it
|
|
352
113
|
|
|
353
|
-
|
|
114
|
+
### Local mode
|
|
354
115
|
|
|
355
|
-
|
|
116
|
+
- Uses the built-in JavaScript evaluator shipped with the plugin
|
|
117
|
+
- No `child_process` spawn is required
|
|
118
|
+
- `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
|
|
356
119
|
|
|
357
|
-
|
|
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).
|
|
120
|
+
## Tool mapping
|
|
360
121
|
|
|
361
|
-
|
|
122
|
+
The plugin keeps the existing OpenClaw-specific tool mappings. Common examples:
|
|
362
123
|
|
|
363
|
-
|
|
124
|
+
- `exec`, `exec.run` -> `system.command.execute.v1`
|
|
125
|
+
- `git.create_pr`, `git.merge`, `git.push` -> `code.repository.merge.v1`
|
|
126
|
+
- `message` with send-family actions like `send`, `reply`, `broadcast`, `sendAttachment`, `upload-file`, or `react` -> `messaging.message.send.v1`
|
|
127
|
+
- `read`, `view`, `glob` -> `data.file.read.v1`
|
|
128
|
+
- `write`, `edit`, `multiedit` -> `data.file.write.v1`
|
|
129
|
+
- bundle MCP tools exposed as `serverName__toolName` -> `mcp.tool.execute.v1`
|
|
364
130
|
|
|
365
|
-
|
|
131
|
+
`allowUnmappedTools: true` keeps the previous OpenClaw compatibility behavior for custom skills and unmapped tools.
|
|
366
132
|
|
|
367
|
-
|
|
368
|
-
# From this directory
|
|
369
|
-
cd extensions/openclaw-aport
|
|
133
|
+
## Exec behavior
|
|
370
134
|
|
|
371
|
-
|
|
372
|
-
node index.js
|
|
135
|
+
`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.
|
|
373
136
|
|
|
374
|
-
|
|
375
|
-
npm link
|
|
376
|
-
openclaw plugins install $(pwd)
|
|
377
|
-
```
|
|
137
|
+
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.
|
|
378
138
|
|
|
379
|
-
|
|
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
|
|
139
|
+
## Troubleshooting
|
|
400
140
|
|
|
401
|
-
|
|
141
|
+
### Plugin install failed
|
|
402
142
|
|
|
403
|
-
|
|
143
|
+
Current OpenClaw releases perform install-time security scanning. This plugin is designed to pass that scan, but if installation still fails:
|
|
404
144
|
|
|
405
|
-
|
|
145
|
+
1. Make sure you are installing the current package version
|
|
146
|
+
2. Prefer the setup command `npx @aporthq/aport-agent-guardrails openclaw`
|
|
147
|
+
3. For local development, install from the extension directory with `-l`
|
|
406
148
|
|
|
407
|
-
-
|
|
408
|
-
- **Issues:** [GitHub Issues](https://github.com/aporthq/aport-agent-guardrails/issues)
|
|
409
|
-
- **Discord:** [discord.gg/aport](https://discord.gg/aport)
|
|
149
|
+
### Existing source-linked config points into an old `npx` cache
|
|
410
150
|
|
|
411
|
-
|
|
151
|
+
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
152
|
|
|
413
|
-
##
|
|
153
|
+
## Notes
|
|
414
154
|
|
|
415
|
-
-
|
|
416
|
-
-
|
|
417
|
-
-
|
|
418
|
-
- [ ] **Batch verification** - Verify multiple tools at once
|
|
419
|
-
- [ ] **Policy caching** - Cache decisions for repeated actions
|
|
155
|
+
- Current public OpenClaw integration is plugin-based
|
|
156
|
+
- No upstream native guardrail-provider merge is required for this plugin path
|
|
157
|
+
- 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,30 @@
|
|
|
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
|
+
let details = "";
|
|
18
|
+
try {
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
if (text) details = text;
|
|
21
|
+
} catch {
|
|
22
|
+
details = "";
|
|
23
|
+
}
|
|
24
|
+
const suffix = details ? ` - ${details}` : "";
|
|
25
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}${suffix}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
return data.decision || data;
|
|
30
|
+
}
|
|
@@ -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
|
+
}
|