@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.
- package/README.md +8 -3
- package/bin/openclaw +14 -14
- package/docs/PROVIDER.md +6 -6
- package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +42 -408
- package/docs/RELEASE.md +3 -2
- package/docs/frameworks/openclaw.md +123 -39
- package/extensions/openclaw-aport/CHANGELOG.md +8 -2
- package/extensions/openclaw-aport/MIGRATION.md +22 -375
- package/extensions/openclaw-aport/README.md +72 -362
- package/extensions/openclaw-aport/api-client.js +22 -0
- package/extensions/openclaw-aport/audit.js +32 -0
- package/extensions/openclaw-aport/decision.js +21 -0
- package/extensions/openclaw-aport/index.js +169 -591
- 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 +89 -0
- package/package.json +1 -1
- package/extensions/openclaw-aport/index.ts +0 -547
- package/extensions/openclaw-aport/test.js +0 -356
|
@@ -1,59 +1,63 @@
|
|
|
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
|
-
|
|
9
|
+
Use the published setup command. No repo clone is required.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npx @aporthq/aport-agent-guardrails openclaw
|
|
13
|
+
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
If you already have a hosted passport on aport.io, pass the `agent_id`:
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
After setup, start OpenClaw with the generated config:
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
|
-
|
|
31
|
-
./bin/openclaw
|
|
33
|
+
openclaw gateway start --config ~/.openclaw/config.yaml
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
80
|
+
### API mode
|
|
170
81
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
## Tool mapping
|
|
188
93
|
|
|
189
|
-
|
|
94
|
+
The plugin keeps the existing OpenClaw-specific tool mappings. Common examples:
|
|
190
95
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
##
|
|
125
|
+
## Notes
|
|
414
126
|
|
|
415
|
-
-
|
|
416
|
-
-
|
|
417
|
-
-
|
|
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
|
+
}
|