@aporthq/aport-agent-guardrails 1.0.11 → 1.0.12
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 +2 -2
- package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +18 -0
- package/docs/SECURITY_MODEL.md +27 -3
- package/docs/VERIFICATION_METHODS.md +1 -0
- package/docs/frameworks/crewai.md +5 -0
- package/docs/frameworks/langchain.md +5 -0
- package/extensions/openclaw-aport/README.md +1 -1
- package/extensions/openclaw-aport/index.ts +47 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Your agent should **only do what you explicitly allow**. APort runs in the hook
|
|
|
60
60
|
|
|
61
61
|
| Policy | What it guards |
|
|
62
62
|
|--------|----------------|
|
|
63
|
-
| **system.command.execute.v1** | Shell commands — allowlist,
|
|
63
|
+
| **system.command.execute.v1** | Shell commands — allowlist, 50+ blocked patterns (`rm -rf`, `sudo`, `nc`, `find -exec rm`, injection); passport `allowed_paths` override for path rules |
|
|
64
64
|
| **mcp.tool.execute.v1** | MCP tool calls — server allowlist, rate limits |
|
|
65
65
|
| **messaging.message.send.v1** | Message sends — rate caps, capability checks |
|
|
66
66
|
| **agent.session.create.v1** / **agent.tool.register.v1** | Sessions and tool registration |
|
|
@@ -335,7 +335,7 @@ graph TB
|
|
|
335
335
|
**✅ Pre-action authorization (agent misbehavior):**
|
|
336
336
|
- **Prompt injection** - Hook-based enforcement; agent cannot bypass via prompts
|
|
337
337
|
- **Malicious skills** - Third-party OpenClaw skills validated before execution
|
|
338
|
-
- **Unauthorized commands** - Allowlist +
|
|
338
|
+
- **Unauthorized commands** - Allowlist + 50+ blocked patterns (rm -rf, sudo, nc, find -exec rm, etc.)
|
|
339
339
|
- **Data exfiltration** - File access, messaging, web requests controlled by policy
|
|
340
340
|
- **Resource limits** - Rate limits, size caps, transaction amounts enforced
|
|
341
341
|
|
|
@@ -32,6 +32,7 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
|
|
|
32
32
|
- **read** and **write** are **now mapped** to APort policies:
|
|
33
33
|
- **read** → `data.file.read.v1` (enforces path allowlists, blocked patterns for SSH keys/credentials/.env)
|
|
34
34
|
- **write** → `data.file.write.v1` (enforces path allowlists, blocks system directories, optional extension restrictions)
|
|
35
|
+
- **Middleware param spreading:** The LangChain and CrewAI Node middlewares automatically parse tool input JSON and spread parameters (e.g. `file_path`) into the top-level verification context. This is required because the API's policy schema validates `file_path` as a required field. Without this, `read`/`write` tool calls would fail with 400 Bad Request.
|
|
35
36
|
- Configure passport limits for file operations:
|
|
36
37
|
- `limits.data.file.read.allowed_paths` - Array of allowed path prefixes (e.g. `["/tmp/*", "/home/user/projects/*"]`)
|
|
37
38
|
- `limits.data.file.read.blocked_patterns` - Array of patterns to block (e.g. `["**/.ssh/**", "**/.env"]`)
|
|
@@ -41,6 +42,23 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
## Passport-configurable path overrides
|
|
46
|
+
|
|
47
|
+
The `system.command.execute.v1` policy includes hardcoded security patterns that block access to sensitive system directories (`/etc/`, `/sys/`, `/proc/`, etc.), sensitive hidden files, credential files, and more. Passport owners can override **path-sensitivity heuristics** by setting `limits.allowed_paths` or `limits.allowed_directories` in the passport:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"limits": {
|
|
52
|
+
"allowed_paths": ["/root/", "/home/agent/work/"],
|
|
53
|
+
"allowed_commands": ["*"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
When `allowed_paths` is set and the command references one of those paths, overridable rules (like "Access to sensitive system directories" or "Access to secrets and credentials files") are skipped. **Catastrophic protections are never overridable** — fork bombs, `rm -rf /`, reverse shells, `nc`/`netcat`, and `find -exec rm` are always blocked regardless of passport config.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
44
62
|
## Summary table
|
|
45
63
|
|
|
46
64
|
| OpenClaw tool (examples) | APort policy | Passport limits (key) |
|
package/docs/SECURITY_MODEL.md
CHANGED
|
@@ -30,8 +30,9 @@ APort operates as a **pre-action authorization layer** that enforces policies **
|
|
|
30
30
|
|
|
31
31
|
**Unauthorized tool usage:**
|
|
32
32
|
- Agent tries to execute commands not in allowlist
|
|
33
|
-
- Commands match blocked patterns (`rm -rf`, `sudo`, `chmod 777`, etc.)
|
|
33
|
+
- Commands match blocked patterns (`rm -rf`, `sudo`, `chmod 777`, `nc`/`netcat`, `find -exec rm`, etc.)
|
|
34
34
|
- Shell escapes and interpreter bypasses (python -c, base64 encoding)
|
|
35
|
+
- Stderr redirects (`2>/dev/null`) are allowed; dangerous redirects to files are blocked
|
|
35
36
|
- Result: Only allowlisted, non-dangerous commands execute
|
|
36
37
|
|
|
37
38
|
**Resource exhaustion and limit violations:**
|
|
@@ -135,7 +136,7 @@ APort's three-layer security model:
|
|
|
135
136
|
- Use case: Testing custom policies before registering them
|
|
136
137
|
|
|
137
138
|
**Example policies (out of the box):**
|
|
138
|
-
- `system.command.execute.v1` - Shell commands (allowlist,
|
|
139
|
+
- `system.command.execute.v1` - Shell commands (allowlist, 50+ blocked patterns, passport `allowed_paths` override)
|
|
139
140
|
- `data.file.read.v1` / `data.file.write.v1` - File access control
|
|
140
141
|
- `web.fetch.v1` / `web.browser.v1` - Web requests and browser automation
|
|
141
142
|
- `messaging.message.send.v1` - Message rate limits, recipient allowlist
|
|
@@ -292,6 +293,7 @@ APort's three-layer security model:
|
|
|
292
293
|
APort uses secure defaults out of the box:
|
|
293
294
|
|
|
294
295
|
✅ `failClosed: true` - Block tools on errors (security over availability)
|
|
296
|
+
✅ `fail_open_on_api_error: false` - API infrastructure errors (4xx/5xx, network) also fail closed by default
|
|
295
297
|
✅ `allowUnmappedTools: false` - Unmapped tools blocked (deny-by-default)
|
|
296
298
|
✅ API mode recommended for production
|
|
297
299
|
✅ Passport status checked first (suspended/revoked → deny all)
|
|
@@ -335,6 +337,25 @@ APort uses secure defaults out of the box:
|
|
|
335
337
|
|
|
336
338
|
---
|
|
337
339
|
|
|
340
|
+
#### `fail_open_on_api_error: false` vs `fail_open_on_api_error: true`
|
|
341
|
+
|
|
342
|
+
**False (default, recommended):**
|
|
343
|
+
- API infrastructure errors (4xx/5xx, network failures, timeouts) → deny tool execution
|
|
344
|
+
- Same behavior as `failClosed: true` for API errors
|
|
345
|
+
|
|
346
|
+
**True (use with caution):**
|
|
347
|
+
- API infrastructure errors → allow tool execution with `[fail-open]` warning
|
|
348
|
+
- Genuine policy denials (HTTP 200 with `allow: false`) are **never** overridden—always denied
|
|
349
|
+
- Useful when API availability is unreliable but you still want policy enforcement when the API is reachable
|
|
350
|
+
|
|
351
|
+
**Security impact:** MEDIUM - Only API errors become allow; actual policy denials still block.
|
|
352
|
+
|
|
353
|
+
**When to use true:** Production environments where API downtime shouldn't halt agent operations, combined with monitoring for `[fail-open]` decisions.
|
|
354
|
+
|
|
355
|
+
**Config:** Set in `config.yaml` as `fail_open_on_api_error: true` or env var `APORT_FAIL_OPEN_ON_API_ERROR=1`.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
338
359
|
#### `allowUnmappedTools: false` vs `allowUnmappedTools: true`
|
|
339
360
|
|
|
340
361
|
**False (default, recommended):**
|
|
@@ -546,7 +567,10 @@ A: API for production (signed decisions, protected passport, global suspend). Lo
|
|
|
546
567
|
A: In API mode, policies come from APort API over HTTPS. In local mode, policies are embedded in bash scripts (protected by filesystem permissions). Custom policies can be passed in request body for testing.
|
|
547
568
|
|
|
548
569
|
**Q: What if the API is down?**
|
|
549
|
-
A: Default behavior (failClosed: true) denies tool calls. For high-availability scenarios, consider running a self-hosted agent-passport instance or use local mode as fallback.
|
|
570
|
+
A: Default behavior (failClosed: true) denies tool calls. You can set `fail_open_on_api_error: true` to allow on API infrastructure errors while still enforcing genuine policy denials. For high-availability scenarios, consider running a self-hosted agent-passport instance or use local mode as fallback.
|
|
571
|
+
|
|
572
|
+
**Q: Can passport owners override blocked path rules (e.g. allow /root/)?**
|
|
573
|
+
A: Yes. Set `limits.allowed_paths` (or `limits.allowed_directories`) in the passport to override path-sensitivity heuristics like "Access to sensitive system directories." Catastrophic protections (fork bombs, `rm -rf /`, reverse shells, `nc`/`netcat`) can **never** be overridden by passport config.
|
|
550
574
|
|
|
551
575
|
**Q: Can decisions be tampered with?**
|
|
552
576
|
A: Local mode: hash-protected (detects naive tampering). API mode: cryptographically signed (Ed25519, cannot forge).
|
|
@@ -93,5 +93,6 @@ So: **local is robust enough for the core policies** (exec, messaging, repo merg
|
|
|
93
93
|
|
|
94
94
|
- **Local (bash):** Useful for privacy, offline, and the core use cases (exec allowlist/blocklist, messaging recipient, repo/PR limits). For full OAP parity and future policy packs, use **API mode**.
|
|
95
95
|
- **API (default):** Recommended for production and when you want the same behavior as [APort in Goose](https://raw.githubusercontent.com/aporthq/.github/refs/heads/main/profile/APORT_GOOSE_ARCHITECTURE.md) and the full generic evaluator (JSON Schema, assurance, regions, evaluation_rules, signed decisions).
|
|
96
|
+
- **`fail_open_on_api_error`:** In API mode, set this config option to `true` if you want API infrastructure errors (4xx/5xx, network failures) to return allow instead of deny. Genuine policy denials (HTTP 200 with `allow: false`) are **never** overridden. Default: `false` (fail-closed on API errors). Set via config YAML or env var `APORT_FAIL_OPEN_ON_API_ERROR=1`.
|
|
96
97
|
|
|
97
98
|
The installer (`./bin/openclaw`) defaults to **API mode**; choose local only when you need to run without the network.
|
|
@@ -95,10 +95,15 @@ withAPortGuardrail(() => {
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
### How tool parameters are handled
|
|
99
|
+
|
|
100
|
+
The Node middleware automatically spreads object tool inputs into the verification context. For example, `{ tool_input: { file_path: "/tmp/data.txt" } }` will pass `file_path` at the top level of the context, ensuring policies like `data.file.read.v1` receive required fields for validation.
|
|
101
|
+
|
|
98
102
|
## Config
|
|
99
103
|
|
|
100
104
|
- **Config path:** `~/.aport/crewai/config.yaml`, or `.aport/config.yaml` in the project root.
|
|
101
105
|
- **Mode:** `api` (default for production) or `local` (bash evaluator, no network). Same options as [LangChain](langchain.md) and OpenClaw.
|
|
106
|
+
- **`fail_open_on_api_error`**: Set to `true` in config to allow tool execution when the APort API is unreachable (genuine policy denials are never overridden). Default: `false` (fail-closed).
|
|
102
107
|
|
|
103
108
|
## Suspend (kill switch)
|
|
104
109
|
|
|
@@ -58,10 +58,15 @@ const callback = new APortGuardrailCallback(); // optional: { configPath: '...',
|
|
|
58
58
|
// On deny, the callback throws GuardrailViolationError.
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### How tool parameters are handled
|
|
62
|
+
|
|
63
|
+
The Node middleware automatically parses JSON tool input and spreads parameters (e.g. `file_path`, `command`) into the verification context. This ensures policies like `data.file.read.v1` and `data.file.write.v1` receive the required `file_path` field at the top level for proper validation.
|
|
64
|
+
|
|
61
65
|
## Config
|
|
62
66
|
|
|
63
67
|
- **Config:** `~/.aport/langchain/` or `.aport/config.yaml`
|
|
64
68
|
- **Usage:** Add the callback to your agent (see above).
|
|
69
|
+
- **`fail_open_on_api_error`**: Set to `true` in config to allow tool execution when the APort API is unreachable (genuine policy denials are never overridden). Default: `false` (fail-closed).
|
|
65
70
|
|
|
66
71
|
## Suspend (kill switch)
|
|
67
72
|
|
|
@@ -93,7 +93,7 @@ plugins:
|
|
|
93
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
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
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
|
|
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 allowed by default when `allowUnmappedTools` is true. 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
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
9
9
|
import { spawn } from "child_process";
|
|
10
10
|
import { createHash, randomUUID } from "crypto";
|
|
11
|
-
import { readFile, mkdir } from "fs/promises";
|
|
11
|
+
import { readFile, mkdir, appendFile } from "fs/promises";
|
|
12
|
+
import { appendFileSync, mkdirSync, existsSync } from "fs";
|
|
12
13
|
import { join, dirname } from "path";
|
|
13
14
|
import { homedir } from "os";
|
|
14
15
|
|
|
@@ -175,6 +176,21 @@ const plugin = {
|
|
|
175
176
|
passportFile: agentId ? null : passportFile,
|
|
176
177
|
agentId,
|
|
177
178
|
});
|
|
179
|
+
// Audit log for API mode (local mode is logged by the bash script via OPENCLAW_AUDIT_LOG)
|
|
180
|
+
const configDir = dirname(passportFile);
|
|
181
|
+
const auditLogPath = join(configDir, "audit.log");
|
|
182
|
+
const ctxSummary = typeof context.command === "string" ? context.command
|
|
183
|
+
: typeof context.file_path === "string" ? context.file_path
|
|
184
|
+
: typeof context.recipient === "string" ? context.recipient
|
|
185
|
+
: undefined;
|
|
186
|
+
logAuditEntry(auditLogPath, {
|
|
187
|
+
tool: effectiveToolName,
|
|
188
|
+
allow: Boolean(decision.allow),
|
|
189
|
+
policy: effectivePolicyName,
|
|
190
|
+
code: decision.reasons?.[0]?.code,
|
|
191
|
+
agentId: agentId || undefined,
|
|
192
|
+
context: ctxSummary,
|
|
193
|
+
});
|
|
178
194
|
} else {
|
|
179
195
|
decision = await verifyViaScript(scriptToolName, context, {
|
|
180
196
|
guardrailScript,
|
|
@@ -501,3 +517,33 @@ function expandPath(path: string): string {
|
|
|
501
517
|
}
|
|
502
518
|
return path;
|
|
503
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Write one-line audit entry matching bash guardrail format.
|
|
523
|
+
* Deny: sync (blocking). Allow: async (non-blocking). Best-effort: never throws.
|
|
524
|
+
*/
|
|
525
|
+
function logAuditEntry(
|
|
526
|
+
auditLogPath: string,
|
|
527
|
+
entry: { tool: string; allow: boolean; policy: string; code?: string; agentId?: string; context?: string },
|
|
528
|
+
): void {
|
|
529
|
+
try {
|
|
530
|
+
const ts = new Date().toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
531
|
+
const code = entry.code || (entry.allow ? "oap.allowed" : "oap.denied");
|
|
532
|
+
let line = `[${ts}] tool=${entry.tool} allow=${entry.allow} policy=${entry.policy} code=${code}`;
|
|
533
|
+
if (entry.agentId) line += ` agent_id=${entry.agentId}`;
|
|
534
|
+
if (entry.context) {
|
|
535
|
+
const sanitized = entry.context.replace(/[\r\n]+/g, " ").replace(/"/g, '\\"').slice(0, 120);
|
|
536
|
+
line += ` context="${sanitized}"`;
|
|
537
|
+
}
|
|
538
|
+
line += "\n";
|
|
539
|
+
const dir = dirname(auditLogPath);
|
|
540
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
541
|
+
if (!entry.allow) {
|
|
542
|
+
appendFileSync(auditLogPath, line, "utf8");
|
|
543
|
+
} else {
|
|
544
|
+
appendFile(auditLogPath, line, "utf8").catch(() => {});
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
/* best-effort */
|
|
548
|
+
}
|
|
549
|
+
}
|