@aporthq/aport-agent-guardrails 1.0.10 → 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.
Files changed (126) hide show
  1. package/README.md +16 -3
  2. package/bin/aport-create-passport.sh +105 -1
  3. package/bin/aport-guardrail-bash.sh +157 -12
  4. package/bin/aport-status.sh +24 -0
  5. package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +31 -5
  6. package/docs/SECURITY_MODEL.md +608 -0
  7. package/docs/TOOL_POLICY_MAPPING.md +2 -0
  8. package/docs/VERIFICATION_METHODS.md +1 -0
  9. package/docs/frameworks/crewai.md +5 -0
  10. package/docs/frameworks/langchain.md +5 -0
  11. package/extensions/openclaw-aport/README.md +1 -1
  12. package/extensions/openclaw-aport/index.ts +105 -77
  13. package/extensions/openclaw-aport/node_modules/@types/node/LICENSE +21 -0
  14. package/extensions/openclaw-aport/node_modules/@types/node/README.md +15 -0
  15. package/extensions/openclaw-aport/node_modules/@types/node/assert/strict.d.ts +8 -0
  16. package/extensions/openclaw-aport/node_modules/@types/node/assert.d.ts +1005 -0
  17. package/extensions/openclaw-aport/node_modules/@types/node/async_hooks.d.ts +586 -0
  18. package/extensions/openclaw-aport/node_modules/@types/node/buffer.buffer.d.ts +457 -0
  19. package/extensions/openclaw-aport/node_modules/@types/node/buffer.d.ts +1901 -0
  20. package/extensions/openclaw-aport/node_modules/@types/node/child_process.d.ts +1453 -0
  21. package/extensions/openclaw-aport/node_modules/@types/node/cluster.d.ts +578 -0
  22. package/extensions/openclaw-aport/node_modules/@types/node/compatibility/disposable.d.ts +14 -0
  23. package/extensions/openclaw-aport/node_modules/@types/node/compatibility/index.d.ts +9 -0
  24. package/extensions/openclaw-aport/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
  25. package/extensions/openclaw-aport/node_modules/@types/node/compatibility/iterators.d.ts +20 -0
  26. package/extensions/openclaw-aport/node_modules/@types/node/console.d.ts +452 -0
  27. package/extensions/openclaw-aport/node_modules/@types/node/constants.d.ts +21 -0
  28. package/extensions/openclaw-aport/node_modules/@types/node/crypto.d.ts +4504 -0
  29. package/extensions/openclaw-aport/node_modules/@types/node/dgram.d.ts +596 -0
  30. package/extensions/openclaw-aport/node_modules/@types/node/diagnostics_channel.d.ts +551 -0
  31. package/extensions/openclaw-aport/node_modules/@types/node/dns/promises.d.ts +477 -0
  32. package/extensions/openclaw-aport/node_modules/@types/node/dns.d.ts +860 -0
  33. package/extensions/openclaw-aport/node_modules/@types/node/domain.d.ts +170 -0
  34. package/extensions/openclaw-aport/node_modules/@types/node/events.d.ts +863 -0
  35. package/extensions/openclaw-aport/node_modules/@types/node/fs/promises.d.ts +1208 -0
  36. package/extensions/openclaw-aport/node_modules/@types/node/fs.d.ts +4332 -0
  37. package/extensions/openclaw-aport/node_modules/@types/node/globals.d.ts +170 -0
  38. package/extensions/openclaw-aport/node_modules/@types/node/globals.typedarray.d.ts +21 -0
  39. package/extensions/openclaw-aport/node_modules/@types/node/http.d.ts +1919 -0
  40. package/extensions/openclaw-aport/node_modules/@types/node/http2.d.ts +2580 -0
  41. package/extensions/openclaw-aport/node_modules/@types/node/https.d.ts +549 -0
  42. package/extensions/openclaw-aport/node_modules/@types/node/index.d.ts +92 -0
  43. package/extensions/openclaw-aport/node_modules/@types/node/inspector.generated.d.ts +2775 -0
  44. package/extensions/openclaw-aport/node_modules/@types/node/module.d.ts +503 -0
  45. package/extensions/openclaw-aport/node_modules/@types/node/net.d.ts +924 -0
  46. package/extensions/openclaw-aport/node_modules/@types/node/os.d.ts +480 -0
  47. package/extensions/openclaw-aport/node_modules/@types/node/package.json +145 -0
  48. package/extensions/openclaw-aport/node_modules/@types/node/path.d.ts +191 -0
  49. package/extensions/openclaw-aport/node_modules/@types/node/perf_hooks.d.ts +860 -0
  50. package/extensions/openclaw-aport/node_modules/@types/node/process.d.ts +1632 -0
  51. package/extensions/openclaw-aport/node_modules/@types/node/punycode.d.ts +117 -0
  52. package/extensions/openclaw-aport/node_modules/@types/node/querystring.d.ts +140 -0
  53. package/extensions/openclaw-aport/node_modules/@types/node/readline/promises.d.ts +154 -0
  54. package/extensions/openclaw-aport/node_modules/@types/node/readline.d.ts +715 -0
  55. package/extensions/openclaw-aport/node_modules/@types/node/repl.d.ts +430 -0
  56. package/extensions/openclaw-aport/node_modules/@types/node/stream/consumers.d.ts +38 -0
  57. package/extensions/openclaw-aport/node_modules/@types/node/stream/promises.d.ts +90 -0
  58. package/extensions/openclaw-aport/node_modules/@types/node/stream/web.d.ts +527 -0
  59. package/extensions/openclaw-aport/node_modules/@types/node/stream.d.ts +1680 -0
  60. package/extensions/openclaw-aport/node_modules/@types/node/string_decoder.d.ts +67 -0
  61. package/extensions/openclaw-aport/node_modules/@types/node/test.d.ts +1208 -0
  62. package/extensions/openclaw-aport/node_modules/@types/node/timers/promises.d.ts +108 -0
  63. package/extensions/openclaw-aport/node_modules/@types/node/timers.d.ts +286 -0
  64. package/extensions/openclaw-aport/node_modules/@types/node/tls.d.ts +1204 -0
  65. package/extensions/openclaw-aport/node_modules/@types/node/trace_events.d.ts +171 -0
  66. package/extensions/openclaw-aport/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +455 -0
  67. package/extensions/openclaw-aport/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +19 -0
  68. package/extensions/openclaw-aport/node_modules/@types/node/ts5.6/index.d.ts +92 -0
  69. package/extensions/openclaw-aport/node_modules/@types/node/tty.d.ts +206 -0
  70. package/extensions/openclaw-aport/node_modules/@types/node/url.d.ts +957 -0
  71. package/extensions/openclaw-aport/node_modules/@types/node/util.d.ts +2083 -0
  72. package/extensions/openclaw-aport/node_modules/@types/node/v8.d.ts +753 -0
  73. package/extensions/openclaw-aport/node_modules/@types/node/vm.d.ts +704 -0
  74. package/extensions/openclaw-aport/node_modules/@types/node/wasi.d.ts +160 -0
  75. package/extensions/openclaw-aport/node_modules/@types/node/web-globals/abortcontroller.d.ts +34 -0
  76. package/extensions/openclaw-aport/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
  77. package/extensions/openclaw-aport/node_modules/@types/node/web-globals/events.d.ts +81 -0
  78. package/extensions/openclaw-aport/node_modules/@types/node/web-globals/fetch.d.ts +38 -0
  79. package/extensions/openclaw-aport/node_modules/@types/node/worker_threads.d.ts +698 -0
  80. package/extensions/openclaw-aport/node_modules/@types/node/zlib.d.ts +517 -0
  81. package/extensions/openclaw-aport/node_modules/undici-types/README.md +6 -0
  82. package/extensions/openclaw-aport/node_modules/undici-types/agent.d.ts +31 -0
  83. package/extensions/openclaw-aport/node_modules/undici-types/api.d.ts +43 -0
  84. package/extensions/openclaw-aport/node_modules/undici-types/balanced-pool.d.ts +18 -0
  85. package/extensions/openclaw-aport/node_modules/undici-types/cache.d.ts +36 -0
  86. package/extensions/openclaw-aport/node_modules/undici-types/client.d.ts +97 -0
  87. package/extensions/openclaw-aport/node_modules/undici-types/connector.d.ts +34 -0
  88. package/extensions/openclaw-aport/node_modules/undici-types/content-type.d.ts +21 -0
  89. package/extensions/openclaw-aport/node_modules/undici-types/cookies.d.ts +28 -0
  90. package/extensions/openclaw-aport/node_modules/undici-types/diagnostics-channel.d.ts +67 -0
  91. package/extensions/openclaw-aport/node_modules/undici-types/dispatcher.d.ts +241 -0
  92. package/extensions/openclaw-aport/node_modules/undici-types/errors.d.ts +128 -0
  93. package/extensions/openclaw-aport/node_modules/undici-types/fetch.d.ts +209 -0
  94. package/extensions/openclaw-aport/node_modules/undici-types/file.d.ts +39 -0
  95. package/extensions/openclaw-aport/node_modules/undici-types/filereader.d.ts +54 -0
  96. package/extensions/openclaw-aport/node_modules/undici-types/formdata.d.ts +108 -0
  97. package/extensions/openclaw-aport/node_modules/undici-types/global-dispatcher.d.ts +9 -0
  98. package/extensions/openclaw-aport/node_modules/undici-types/global-origin.d.ts +7 -0
  99. package/extensions/openclaw-aport/node_modules/undici-types/handlers.d.ts +9 -0
  100. package/extensions/openclaw-aport/node_modules/undici-types/header.d.ts +4 -0
  101. package/extensions/openclaw-aport/node_modules/undici-types/index.d.ts +63 -0
  102. package/extensions/openclaw-aport/node_modules/undici-types/interceptors.d.ts +5 -0
  103. package/extensions/openclaw-aport/node_modules/undici-types/mock-agent.d.ts +50 -0
  104. package/extensions/openclaw-aport/node_modules/undici-types/mock-client.d.ts +25 -0
  105. package/extensions/openclaw-aport/node_modules/undici-types/mock-errors.d.ts +12 -0
  106. package/extensions/openclaw-aport/node_modules/undici-types/mock-interceptor.d.ts +93 -0
  107. package/extensions/openclaw-aport/node_modules/undici-types/mock-pool.d.ts +25 -0
  108. package/extensions/openclaw-aport/node_modules/undici-types/package.json +55 -0
  109. package/extensions/openclaw-aport/node_modules/undici-types/patch.d.ts +71 -0
  110. package/extensions/openclaw-aport/node_modules/undici-types/pool-stats.d.ts +19 -0
  111. package/extensions/openclaw-aport/node_modules/undici-types/pool.d.ts +28 -0
  112. package/extensions/openclaw-aport/node_modules/undici-types/proxy-agent.d.ts +30 -0
  113. package/extensions/openclaw-aport/node_modules/undici-types/readable.d.ts +61 -0
  114. package/extensions/openclaw-aport/node_modules/undici-types/webidl.d.ts +220 -0
  115. package/extensions/openclaw-aport/node_modules/undici-types/websocket.d.ts +131 -0
  116. package/extensions/openclaw-aport/package-lock.json +11225 -0
  117. package/external/aport-policies/README.md +10 -2
  118. package/external/aport-policies/code.release.publish.v1/policy.json +1 -1
  119. package/external/aport-policies/data.file.read.v1/policy.json +126 -0
  120. package/external/aport-policies/data.file.write.v1/policy.json +158 -0
  121. package/external/aport-policies/finance.payment.charge.v1/README.md +10 -1
  122. package/external/aport-policies/web.browser.v1/policy.json +161 -0
  123. package/external/aport-policies/web.fetch.v1/policy.json +149 -0
  124. package/external/aport-spec/integrations/shield/README.md +3 -3
  125. package/package.json +3 -2
  126. package/skills/aport-agent-guardrail/SKILL.md +260 -163
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, 40+ blocked patterns (`rm -rf`, `sudo`, injection) |
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 |
@@ -326,10 +326,23 @@ graph TB
326
326
 
327
327
  </div>
328
328
 
329
- - **Local-first:** Passport and policy live on your machine (or in repo); no cloud required for basic enforcement.
330
- - **Fail-closed:** Missing or invalid passport → deny.
329
+ - **Local-first:** Passport and policy live on your machine (or in repo); no cloud required for basic enforcement.
330
+ - **Fail-closed:** Missing or invalid passport → deny.
331
331
  - **Opt-in cloud:** Use API mode for global kill switch, signed receipts, and team sync.
332
332
 
333
+ ### What APort Protects
334
+
335
+ **✅ Pre-action authorization (agent misbehavior):**
336
+ - **Prompt injection** - Hook-based enforcement; agent cannot bypass via prompts
337
+ - **Malicious skills** - Third-party OpenClaw skills validated before execution
338
+ - **Unauthorized commands** - Allowlist + 50+ blocked patterns (rm -rf, sudo, nc, find -exec rm, etc.)
339
+ - **Data exfiltration** - File access, messaging, web requests controlled by policy
340
+ - **Resource limits** - Rate limits, size caps, transaction amounts enforced
341
+
342
+ **Application-layer security model:** APort enforces policies at the agent action layer (between agent decision and tool execution). It operates within the OS trust boundary—standard for authorization systems like OAuth, IAM, and policy engines.
343
+
344
+ **For production:** Use API mode (`mode: api` with `agent_id`) for cryptographically signed decisions, protected passports, and global suspend. See [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md) for full threat model, attack scenarios, and best practices.
345
+
333
346
  ---
334
347
 
335
348
  ## 🌐 When to use API vs local
@@ -119,11 +119,19 @@ if [ -n "$NON_INTERACTIVE" ]; then
119
119
  exec_cap=y
120
120
  msg_cap=y
121
121
  data_cap=n
122
+ file_read_cap=n
123
+ file_write_cap=n
124
+ web_fetch_cap=n
125
+ web_browser_cap=n
122
126
  max_pr_size=500
123
127
  max_prs_per_day=10
124
128
  max_msgs_per_day=100
125
129
  allowed_repos_input="*"
126
130
  exec_allow_scope="*"
131
+ file_read_paths="*"
132
+ file_write_paths="*"
133
+ web_fetch_domains="*"
134
+ web_browser_domains="*"
127
135
  should_expire=n
128
136
  never_expires="true"
129
137
  expires_at=""
@@ -187,7 +195,7 @@ else
187
195
  # Choose capabilities
188
196
  echo " 🔐 Capabilities"
189
197
  echo " ───────────────"
190
- echo " Choose what your agent can do (y/n). Defaults: PRs, exec, and messaging = yes (matches README/docs); data export = no."
198
+ echo " Choose what your agent can do (y/n). Defaults: PRs, exec, and messaging = yes (matches README/docs); others = no."
191
199
  echo ""
192
200
  read -p " • Create and merge pull requests? [Y/n]: " pr_cap
193
201
  pr_cap=${pr_cap:-y}
@@ -198,6 +206,18 @@ else
198
206
  read -p " • Send messages (email, SMS, etc.)? [Y/n]: " msg_cap
199
207
  msg_cap=${msg_cap:-y}
200
208
 
209
+ read -p " • Read files from disk? [y/N]: " file_read_cap
210
+ file_read_cap=${file_read_cap:-n}
211
+
212
+ read -p " • Write/edit files on disk? [y/N]: " file_write_cap
213
+ file_write_cap=${file_write_cap:-n}
214
+
215
+ read -p " • Fetch data from web (HTTP requests)? [y/N]: " web_fetch_cap
216
+ web_fetch_cap=${web_fetch_cap:-n}
217
+
218
+ read -p " • Automate web browser? [y/N]: " web_browser_cap
219
+ web_browser_cap=${web_browser_cap:-n}
220
+
201
221
  read -p " • Export data (database, files, etc.)? [y/N]: " data_cap
202
222
  data_cap=${data_cap:-n}
203
223
 
@@ -230,6 +250,30 @@ else
230
250
  max_msgs_per_day=${max_msgs_per_day:-100}
231
251
  fi
232
252
 
253
+ if [ "$file_read_cap" = "y" ] || [ "$file_read_cap" = "Y" ]; then
254
+ echo " File read paths: default is allow any (*); sensitive patterns (.env, id_rsa, etc.) still blocked."
255
+ read -p " Allowed paths (comma-separated, * for all) [*]: " file_read_paths
256
+ file_read_paths=${file_read_paths:-"*"}
257
+ fi
258
+
259
+ if [ "$file_write_cap" = "y" ] || [ "$file_write_cap" = "Y" ]; then
260
+ echo " File write paths: default is allow any (*); recommend restricting to project directories."
261
+ read -p " Allowed paths (comma-separated, * for all) [*]: " file_write_paths
262
+ file_write_paths=${file_write_paths:-"*"}
263
+ fi
264
+
265
+ if [ "$web_fetch_cap" = "y" ] || [ "$web_fetch_cap" = "Y" ]; then
266
+ echo " Web fetch domains: default is allow any (*); private IPs (127.0.0.1, etc.) are blocked."
267
+ read -p " Allowed domains (comma-separated, * for all) [*]: " web_fetch_domains
268
+ web_fetch_domains=${web_fetch_domains:-"*"}
269
+ fi
270
+
271
+ if [ "$web_browser_cap" = "y" ] || [ "$web_browser_cap" = "Y" ]; then
272
+ echo " Browser automation domains: default is allow any (*)."
273
+ read -p " Allowed domains (comma-separated, * for all) [*]: " web_browser_domains
274
+ web_browser_domains=${web_browser_domains:-"*"}
275
+ fi
276
+
233
277
  if [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ]; then
234
278
  read -p " Max export rows [10000]: " max_export_rows
235
279
  max_export_rows=${max_export_rows:-10000}
@@ -292,6 +336,18 @@ fi
292
336
  if [ "$msg_cap" = "y" ] || [ "$msg_cap" = "Y" ]; then
293
337
  capabilities_json="$capabilities_json{\"id\": \"messaging.send\"},"
294
338
  fi
339
+ if [ "$file_read_cap" = "y" ] || [ "$file_read_cap" = "Y" ]; then
340
+ capabilities_json="$capabilities_json{\"id\": \"data.file.read\"},"
341
+ fi
342
+ if [ "$file_write_cap" = "y" ] || [ "$file_write_cap" = "Y" ]; then
343
+ capabilities_json="$capabilities_json{\"id\": \"data.file.write\"},"
344
+ fi
345
+ if [ "$web_fetch_cap" = "y" ] || [ "$web_fetch_cap" = "Y" ]; then
346
+ capabilities_json="$capabilities_json{\"id\": \"web.fetch\"},"
347
+ fi
348
+ if [ "$web_browser_cap" = "y" ] || [ "$web_browser_cap" = "Y" ]; then
349
+ capabilities_json="$capabilities_json{\"id\": \"web.browser\"},"
350
+ fi
295
351
  if [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ]; then
296
352
  capabilities_json="$capabilities_json{\"id\": \"data.export\"},"
297
353
  fi
@@ -330,6 +386,54 @@ if [ "$msg_cap" = "y" ] || [ "$msg_cap" = "Y" ]; then
330
386
  limits_json="$limits_json\"msgs_per_min\": 5, \"msgs_per_day\": $max_msgs_per_day, \"allowed_recipients\": [\"*\"], \"approval_required\": false,"
331
387
  fi
332
388
 
389
+ if [ "$file_read_cap" = "y" ] || [ "$file_read_cap" = "Y" ]; then
390
+ # Parse allowed paths
391
+ IFS=',' read -ra PATHS <<< "$file_read_paths"
392
+ allowed_paths_json="["
393
+ for path in "${PATHS[@]}"; do
394
+ path=$(echo "$path" | xargs) # trim whitespace
395
+ allowed_paths_json="$allowed_paths_json\"$path\","
396
+ done
397
+ allowed_paths_json="${allowed_paths_json%,}]"
398
+ limits_json="$limits_json\"data.file.read\": {\"allowed_paths\": $allowed_paths_json, \"max_file_size_mb\": 100},"
399
+ fi
400
+
401
+ if [ "$file_write_cap" = "y" ] || [ "$file_write_cap" = "Y" ]; then
402
+ # Parse allowed paths
403
+ IFS=',' read -ra PATHS <<< "$file_write_paths"
404
+ allowed_paths_json="["
405
+ for path in "${PATHS[@]}"; do
406
+ path=$(echo "$path" | xargs) # trim whitespace
407
+ allowed_paths_json="$allowed_paths_json\"$path\","
408
+ done
409
+ allowed_paths_json="${allowed_paths_json%,}]"
410
+ limits_json="$limits_json\"data.file.write\": {\"allowed_paths\": $allowed_paths_json, \"max_file_size_mb\": 50},"
411
+ fi
412
+
413
+ if [ "$web_fetch_cap" = "y" ] || [ "$web_fetch_cap" = "Y" ]; then
414
+ # Parse allowed domains
415
+ IFS=',' read -ra DOMAINS <<< "$web_fetch_domains"
416
+ allowed_domains_json="["
417
+ for domain in "${DOMAINS[@]}"; do
418
+ domain=$(echo "$domain" | xargs) # trim whitespace
419
+ allowed_domains_json="$allowed_domains_json\"$domain\","
420
+ done
421
+ allowed_domains_json="${allowed_domains_json%,}]"
422
+ limits_json="$limits_json\"web.fetch\": {\"allowed_domains\": $allowed_domains_json, \"blocked_domains\": [], \"max_requests_per_min\": 60},"
423
+ fi
424
+
425
+ if [ "$web_browser_cap" = "y" ] || [ "$web_browser_cap" = "Y" ]; then
426
+ # Parse allowed domains
427
+ IFS=',' read -ra DOMAINS <<< "$web_browser_domains"
428
+ allowed_domains_json="["
429
+ for domain in "${DOMAINS[@]}"; do
430
+ domain=$(echo "$domain" | xargs) # trim whitespace
431
+ allowed_domains_json="$allowed_domains_json\"$domain\","
432
+ done
433
+ allowed_domains_json="${allowed_domains_json%,}]"
434
+ limits_json="$limits_json\"web.browser\": {\"allowed_domains\": $allowed_domains_json, \"max_screenshots_per_hour\": 100},"
435
+ fi
436
+
333
437
  if [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ]; then
334
438
  limits_json="$limits_json\"data.export\": {\"max_rows\": $max_export_rows, \"allow_pii\": $allow_pii_bool, \"allowed_collections\": [\"*\"]},"
335
439
  fi
@@ -145,8 +145,9 @@ write_decision() {
145
145
  issued_at: $issued_at,
146
146
  expires_at: $expires_at,
147
147
  passport_digest: $passport_digest,
148
- signature: "ed25519:local-unsigned",
148
+ signature: "local-unsigned",
149
149
  kid: "oap:local:dev-key",
150
+ verification_mode: "local",
150
151
  prev_decision_id: (if $prev_decision_id == "" then null else $prev_decision_id end),
151
152
  prev_content_hash: (if $prev_content_hash == "" then null else $prev_content_hash end)
152
153
  }')
@@ -163,13 +164,17 @@ write_decision() {
163
164
  # Update chain state for next decision (best-effort; do not block or fail the script)
164
165
  echo "{\"last_decision_id\":\"$decision_id\",\"last_content_hash\":\"$content_hash\"}" > "$chain_state" 2> /dev/null || true
165
166
 
166
- # Audit trail is non-core: append in background so it never blocks the tool call.
167
- # Include capability context (command, recipient, repo/branch) when set.
168
167
  audit_context=""
169
168
  if [ -n "${CONTEXT_SUMMARY:-}" ]; then
170
169
  audit_context=" context=\"${CONTEXT_SUMMARY}\""
171
170
  fi
172
- (echo "[$(date -u +%Y-%m-%d\ %H:%M:%S)] tool=$TOOL_NAME decision_id=$decision_id allow=$allow policy=$policy_id code=$deny_code${audit_context}" >> "$AUDIT_LOG") 2> /dev/null &
171
+ audit_entry="[$(date -u +%Y-%m-%d\ %H:%M:%S)] tool=$TOOL_NAME decision_id=$decision_id allow=$allow policy=$policy_id code=$deny_code${audit_context}"
172
+
173
+ if [ "$allow" = "false" ]; then
174
+ echo "$audit_entry" >> "$AUDIT_LOG" 2> /dev/null || true
175
+ else
176
+ (echo "$audit_entry" >> "$AUDIT_LOG") 2> /dev/null &
177
+ fi
173
178
 
174
179
  if [ "$allow" = "true" ]; then
175
180
  exit 0
@@ -211,20 +216,49 @@ fi
211
216
  # Map tool to policy pack ID
212
217
  POLICY_ID=""
213
218
  case "$TOOL_NAME" in
214
- git.create_pr | git.merge | git.push)
219
+ git.create_pr | git.merge | git.push | git.*)
215
220
  POLICY_ID="code.repository.merge.v1"
216
221
  ;;
217
- exec.run | exec.* | system.*)
222
+ exec | exec.run | exec.* | system.* | bash | shell | command)
218
223
  POLICY_ID="system.command.execute.v1"
219
224
  ;;
220
- message.send | message.* | messaging.*)
225
+ gateway | gateway.* | process | process.*)
226
+ # High risk operations - treat as command execution
227
+ POLICY_ID="system.command.execute.v1"
228
+ ;;
229
+ message.send | message.* | messaging.* | sms | whatsapp | slack | email)
221
230
  POLICY_ID="messaging.message.send.v1"
222
231
  ;;
223
- payment.* | finance.*)
232
+ read | file.read | file.read.* | data.file.read | data.file.read.*)
233
+ POLICY_ID="data.file.read.v1"
234
+ ;;
235
+ write | edit | file.write | file.write.* | file.edit | file.edit.* | data.file.write | data.file.write.*)
236
+ POLICY_ID="data.file.write.v1"
237
+ ;;
238
+ web_fetch | webfetch | web.fetch | web.fetch.* | web_search | websearch | web.search | web.search.*)
239
+ POLICY_ID="web.fetch.v1"
240
+ ;;
241
+ browser | browser.* | web.browser | web.browser.*)
242
+ POLICY_ID="web.browser.v1"
243
+ ;;
244
+ mcp.*)
245
+ POLICY_ID="mcp.tool.execute.v1"
246
+ ;;
247
+ agent.session.* | session.create | session.* | sessions.* | sessions_spawn | sessions_send)
248
+ POLICY_ID="agent.session.create.v1"
249
+ ;;
250
+ cron | cron.*)
251
+ # Scheduled tasks - treat as session management
252
+ POLICY_ID="agent.session.create.v1"
253
+ ;;
254
+ agent.tool.* | tool.register)
255
+ POLICY_ID="agent.tool.register.v1"
256
+ ;;
257
+ payment.refund | refund | payment.charge | charge | payment.* | finance.*)
224
258
  POLICY_ID="finance.payment.refund.v1"
225
259
  ;;
226
- database.write | database.insert | database.update | database.delete | data.export)
227
- POLICY_ID="data.export.v1"
260
+ database.write | database.insert | database.update | database.delete | data.export | data.export.*)
261
+ POLICY_ID="data.export.create.v1"
228
262
  ;;
229
263
  *)
230
264
  # Unknown tool - deny by default for security
@@ -232,7 +266,7 @@ case "$TOOL_NAME" in
232
266
  ;;
233
267
  esac
234
268
 
235
- # Capability-specific context summary for audit log (command, recipient, repo/branch, etc.)
269
+ # Capability-specific context summary for audit log (command, recipient, repo/branch, file_path, etc.)
236
270
  CONTEXT_SUMMARY=""
237
271
  if [ -n "$CONTEXT_JSON" ] && [ "$CONTEXT_JSON" != "{}" ]; then
238
272
  if [[ "$POLICY_ID" == "system.command.execute"* ]]; then
@@ -244,6 +278,17 @@ if [ -n "$CONTEXT_JSON" ] && [ "$CONTEXT_JSON" != "{}" ]; then
244
278
  BRANCH=$(echo "$CONTEXT_JSON" | jq -r '.branch // ""' 2> /dev/null || true)
245
279
  [ -n "$REPO" ] && CONTEXT_SUMMARY="$REPO"
246
280
  [ -n "$BRANCH" ] && CONTEXT_SUMMARY="${CONTEXT_SUMMARY:+$CONTEXT_SUMMARY }$BRANCH"
281
+ elif [[ "$POLICY_ID" == "data.file.read.v1" ]] || [[ "$POLICY_ID" == "data.file.write.v1" ]]; then
282
+ CONTEXT_SUMMARY=$(echo "$CONTEXT_JSON" | jq -r '.file_path // .path // ""' 2> /dev/null || true)
283
+ elif [[ "$POLICY_ID" == "web.fetch.v1" ]]; then
284
+ CONTEXT_SUMMARY=$(echo "$CONTEXT_JSON" | jq -r '.url // ""' 2> /dev/null || true)
285
+ elif [[ "$POLICY_ID" == "web.browser.v1" ]]; then
286
+ ACTION=$(echo "$CONTEXT_JSON" | jq -r '.action // ""' 2> /dev/null || true)
287
+ URL=$(echo "$CONTEXT_JSON" | jq -r '.url // ""' 2> /dev/null || true)
288
+ [ -n "$ACTION" ] && CONTEXT_SUMMARY="$ACTION"
289
+ [ -n "$URL" ] && CONTEXT_SUMMARY="${CONTEXT_SUMMARY:+$CONTEXT_SUMMARY }$URL"
290
+ elif [[ "$POLICY_ID" == "agent.session.create.v1" ]]; then
291
+ CONTEXT_SUMMARY=$(echo "$CONTEXT_JSON" | jq -r '.session_id // .agent_id // ""' 2> /dev/null || true)
247
292
  fi
248
293
  # Sanitize for one-line audit: no newlines, truncate, escape double quotes
249
294
  if [ -n "$CONTEXT_SUMMARY" ]; then
@@ -360,7 +405,26 @@ if [[ "$POLICY_ID" == "system.command.execute"* ]]; then
360
405
  write_decision false "$POLICY_ID" "oap.command_not_allowed" "Command '$COMMAND' is not in allowed list"
361
406
  fi
362
407
 
363
- # Check blocked patterns using safe pattern matching
408
+ # Check built-in security patterns (surgical - dangerous operations only)
409
+ # These are always enforced to prevent catastrophic damage
410
+ if [[ "$COMMAND" =~ rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f[^[:space:]]*[[:space:]]+/[[:space:]]*$ ]] \
411
+ || [[ "$COMMAND" =~ rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f[^[:space:]]+/\* ]]; then
412
+ write_decision false "$POLICY_ID" "oap.dangerous_operation" "Destructive file operation: rm -rf / or rm -rf /*"
413
+ fi
414
+ if [[ "$COMMAND" =~ dd[[:space:]]+if=/dev/ ]]; then
415
+ write_decision false "$POLICY_ID" "oap.dangerous_operation" "Dangerous disk operation: dd if=/dev/"
416
+ fi
417
+ if [[ "$COMMAND" =~ mkfs\. ]]; then
418
+ write_decision false "$POLICY_ID" "oap.dangerous_operation" "Filesystem creation: mkfs"
419
+ fi
420
+ if [[ "$COMMAND" =~ (curl|wget)[[:space:]][^\|]*\|[[:space:]]*(bash|sh|zsh|python|node) ]]; then
421
+ write_decision false "$POLICY_ID" "oap.dangerous_operation" "Download-and-execute pattern detected"
422
+ fi
423
+ if [[ "$COMMAND" =~ :\(\)[[:space:]]*\{[[:space:]]*:[[:space:]]*\|[[:space:]]*:[[:space:]]*\&[[:space:]]*\} ]] || [[ "$COMMAND" =~ fork\(\) ]]; then
424
+ write_decision false "$POLICY_ID" "oap.dangerous_operation" "Fork bomb detected"
425
+ fi
426
+
427
+ # Check user-defined blocked patterns using safe pattern matching
364
428
  while IFS= read -r pattern; do
365
429
  [ -z "$pattern" ] && continue
366
430
  # Use safe_pattern_match instead of bash glob patterns
@@ -390,5 +454,86 @@ if [[ "$POLICY_ID" == "messaging.message.send"* ]]; then
390
454
  fi
391
455
  fi
392
456
 
457
+ # File read policy evaluation
458
+ if [[ "$POLICY_ID" == "data.file.read.v1" ]]; then
459
+ FILE_PATH=$(echo "$CONTEXT_JSON" | jq -r '.file_path // .path // ""')
460
+ if [ -n "$FILE_PATH" ]; then
461
+ # Check allowed paths
462
+ PATH_ALLOWED=false
463
+ while IFS= read -r allowed_path; do
464
+ [ -z "$allowed_path" ] && continue
465
+ # Use safe prefix matching
466
+ if [[ "$FILE_PATH" == "$allowed_path"* ]] || [ "$allowed_path" = "*" ]; then
467
+ PATH_ALLOWED=true
468
+ break
469
+ fi
470
+ done < <(echo "$LIMITS" | jq -r '.allowed_paths[]? // empty')
471
+
472
+ HAS_ALLOWED=$(echo "$LIMITS" | jq -r '.allowed_paths | length' 2> /dev/null || echo "0")
473
+ if [ "$PATH_ALLOWED" = false ] && [ "${HAS_ALLOWED:-0}" -gt 0 ] 2> /dev/null; then
474
+ write_decision false "$POLICY_ID" "oap.path_not_allowed" "File path '$FILE_PATH' is not in allowed list"
475
+ fi
476
+
477
+ # Check blocked patterns (SSH keys, credentials, .env files)
478
+ while IFS= read -r pattern; do
479
+ [ -z "$pattern" ] && continue
480
+ # Simple glob-style matching for blocked patterns
481
+ if [[ "$FILE_PATH" == *"$pattern"* ]] || [[ "$FILE_PATH" == $pattern ]]; then
482
+ write_decision false "$POLICY_ID" "oap.blocked_pattern" "File path matches blocked pattern: $pattern"
483
+ fi
484
+ done < <(echo "$LIMITS" | jq -r '.blocked_patterns[]? // empty')
485
+ fi
486
+ fi
487
+
488
+ # File write policy evaluation
489
+ if [[ "$POLICY_ID" == "data.file.write.v1" ]]; then
490
+ FILE_PATH=$(echo "$CONTEXT_JSON" | jq -r '.file_path // .path // ""')
491
+ if [ -n "$FILE_PATH" ]; then
492
+ # Check allowed paths
493
+ PATH_ALLOWED=false
494
+ while IFS= read -r allowed_path; do
495
+ [ -z "$allowed_path" ] && continue
496
+ # Use safe prefix matching
497
+ if [[ "$FILE_PATH" == "$allowed_path"* ]] || [ "$allowed_path" = "*" ]; then
498
+ PATH_ALLOWED=true
499
+ break
500
+ fi
501
+ done < <(echo "$LIMITS" | jq -r '.allowed_paths[]? // empty')
502
+
503
+ HAS_ALLOWED=$(echo "$LIMITS" | jq -r '.allowed_paths | length' 2> /dev/null || echo "0")
504
+ if [ "$PATH_ALLOWED" = false ] && [ "${HAS_ALLOWED:-0}" -gt 0 ] 2> /dev/null; then
505
+ write_decision false "$POLICY_ID" "oap.path_not_allowed" "File path '$FILE_PATH' is not in allowed list"
506
+ fi
507
+
508
+ # Check blocked paths (system directories)
509
+ while IFS= read -r blocked_path; do
510
+ [ -z "$blocked_path" ] && continue
511
+ # Prefix match for blocked system dirs
512
+ if [[ "$FILE_PATH" == "$blocked_path"* ]]; then
513
+ write_decision false "$POLICY_ID" "oap.path_blocked" "Writing to system directory is not allowed: $blocked_path"
514
+ fi
515
+ done < <(echo "$LIMITS" | jq -r '.blocked_paths[]? // empty')
516
+
517
+ # Check allowed extensions (if configured)
518
+ if [ -n "$(echo "$LIMITS" | jq -r '.allowed_extensions | length' 2> /dev/null)" ]; then
519
+ FILE_EXT=$(echo "$FILE_PATH" | grep -o '\.[^.]*$' | tr '[:upper:]' '[:lower:]')
520
+ if [ -n "$FILE_EXT" ]; then
521
+ EXT_ALLOWED=false
522
+ while IFS= read -r allowed_ext; do
523
+ [ -z "$allowed_ext" ] && continue
524
+ if [ "$FILE_EXT" = "$allowed_ext" ]; then
525
+ EXT_ALLOWED=true
526
+ break
527
+ fi
528
+ done < <(echo "$LIMITS" | jq -r '.allowed_extensions[]? // empty')
529
+
530
+ if [ "$EXT_ALLOWED" = false ]; then
531
+ write_decision false "$POLICY_ID" "oap.extension_not_allowed" "File extension $FILE_EXT is not allowed"
532
+ fi
533
+ fi
534
+ fi
535
+ fi
536
+ fi
537
+
393
538
  # All checks passed - allow
394
539
  write_decision true "$POLICY_ID" "oap.allowed" "All policy checks passed"
@@ -129,6 +129,18 @@ jq -r '.limits | keys[]? // empty' $PASSPORT_FILE | while read policy; do
129
129
  msgs_per_day=$(jq -r ".limits.\"$policy\".msgs_per_day // \"unlimited\"" $PASSPORT_FILE)
130
130
  echo " - Messages/day: $msgs_per_day"
131
131
  ;;
132
+ "data.file.read")
133
+ allowed_paths=$(jq ".limits.\"$policy\".allowed_paths | length" $PASSPORT_FILE 2> /dev/null || echo "0")
134
+ blocked_patterns=$(jq ".limits.\"$policy\".blocked_patterns | length" $PASSPORT_FILE 2> /dev/null || echo "0")
135
+ echo " - Allowed paths: $allowed_paths"
136
+ echo " - Blocked patterns: $blocked_patterns"
137
+ ;;
138
+ "data.file.write")
139
+ allowed_paths=$(jq ".limits.\"$policy\".allowed_paths | length" $PASSPORT_FILE 2> /dev/null || echo "0")
140
+ blocked_paths=$(jq ".limits.\"$policy\".blocked_paths | length" $PASSPORT_FILE 2> /dev/null || echo "0")
141
+ echo " - Allowed paths: $allowed_paths"
142
+ echo " - Blocked paths: $blocked_paths"
143
+ ;;
132
144
  "data.export")
133
145
  max_rows=$(jq -r ".limits.\"$policy\".max_rows // \"unlimited\"" $PASSPORT_FILE)
134
146
  allow_pii=$(jq -r ".limits.\"$policy\".allow_pii // false" $PASSPORT_FILE)
@@ -140,6 +152,18 @@ done
140
152
 
141
153
  echo
142
154
 
155
+ # Protection coverage
156
+ echo "🛡️ APort Protection Coverage"
157
+ echo " System commands: ✅ Protected (system.command.execute.v1)"
158
+ echo " File reads: ✅ Protected (data.file.read.v1)"
159
+ echo " File writes: ✅ Protected (data.file.write.v1)"
160
+ echo " Messaging: ✅ Protected (messaging.message.send.v1)"
161
+ echo " Git operations: ✅ Protected (code.repository.merge.v1)"
162
+ echo " MCP tools: ✅ Protected (mcp.tool.execute.v1)"
163
+ echo
164
+
165
+ echo
166
+
143
167
  # Latest decision (OAP v1.0 format)
144
168
  if [ -f "$DECISION_FILE" ]; then
145
169
  echo "🔍 Latest Decision"
@@ -27,11 +27,35 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
27
27
 
28
28
  ---
29
29
 
30
- ## read, write, and other unmapped tools
30
+ ## read, write, and file operations (NOW PROTECTED)
31
+
32
+ - **read** and **write** are **now mapped** to APort policies:
33
+ - **read** → `data.file.read.v1` (enforces path allowlists, blocked patterns for SSH keys/credentials/.env)
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.
36
+ - Configure passport limits for file operations:
37
+ - `limits.data.file.read.allowed_paths` - Array of allowed path prefixes (e.g. `["/tmp/*", "/home/user/projects/*"]`)
38
+ - `limits.data.file.read.blocked_patterns` - Array of patterns to block (e.g. `["**/.ssh/**", "**/.env"]`)
39
+ - `limits.data.file.write.allowed_paths` - Array of allowed write paths
40
+ - `limits.data.file.write.blocked_paths` - Array of system directories to block (e.g. `["/etc/**", "/bin/**"]`)
41
+ - Other tools like **edit**, **apply_patch**, **browser**, **cron**, **gateway**, **sessions_***, **nodes**, **image**, **web_search**, **web_fetch** remain **unmapped** and **allowed** by default (when `allowUnmappedTools: true`).
31
42
 
32
- - **read**, **write**, **edit**, **apply_patch**, **browser**, **cron**, **gateway**, **sessions_***, **nodes**, **image**, **web_search**, **web_fetch** are **not** mapped to any APort policy in this plugin. They are **unmapped** and **allowed** by default (when `allowUnmappedTools: true`). So **read is enabled** by default: the plugin sees no policy for `read`, returns allow, and OpenClaw runs the read tool.
33
- - A failure like **read failed: ENOENT: no such file or directory** is the **tool** failing (e.g. wrong path or missing file, such as `config.json` when the app uses `config.yaml`), not the guardrail blocking. The guardrail already allowed the call; the error happens when the tool executes.
34
- - To enforce policy on file access you would add a new policy (e.g. **file.read.v1**) and map **read** to it in the plugin, with passport limits (e.g. **allowed_paths**). That is not implemented today.
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.
35
59
 
36
60
  ---
37
61
 
@@ -42,8 +66,10 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
42
66
  | exec | system.command.execute.v1 | limits.system.command.execute |
43
67
  | message, messaging.* | messaging.message.send.v1 | limits.messaging |
44
68
  | git.* | code.repository.merge.v1 | limits.code.repository.merge |
69
+ | read | data.file.read.v1 | limits.data.file.read |
70
+ | write | data.file.write.v1 | limits.data.file.write |
45
71
  | mcp.* | mcp.tool.execute.v1 | (API / MCP limits) |
46
- | read, write, edit, etc. | *(none)* | *(unmapped, allowed by default)* |
72
+ | edit, browser, etc. | *(none)* | *(unmapped, allowed by default)* |
47
73
 
48
74
  ---
49
75