@aporthq/aport-agent-guardrails 1.0.8

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 (237) hide show
  1. package/LICENSE +217 -0
  2. package/README.md +481 -0
  3. package/bin/agent-guardrails +133 -0
  4. package/bin/aport-create-passport.sh +444 -0
  5. package/bin/aport-cursor-hook.sh +90 -0
  6. package/bin/aport-guardrail-api.sh +108 -0
  7. package/bin/aport-guardrail-bash.sh +394 -0
  8. package/bin/aport-guardrail-v2.sh +5 -0
  9. package/bin/aport-guardrail.sh +5 -0
  10. package/bin/aport-resolve-paths.sh +71 -0
  11. package/bin/aport-status.sh +276 -0
  12. package/bin/frameworks/crewai.sh +49 -0
  13. package/bin/frameworks/cursor.sh +95 -0
  14. package/bin/frameworks/langchain.sh +48 -0
  15. package/bin/frameworks/n8n.sh +36 -0
  16. package/bin/frameworks/openclaw.sh +19 -0
  17. package/bin/lib/allowlist.sh +18 -0
  18. package/bin/lib/common.sh +28 -0
  19. package/bin/lib/config.sh +46 -0
  20. package/bin/lib/constants.sh +232 -0
  21. package/bin/lib/detect.sh +65 -0
  22. package/bin/lib/error.sh +269 -0
  23. package/bin/lib/passport.sh +19 -0
  24. package/bin/lib/templates/.gitkeep +1 -0
  25. package/bin/lib/templates/config.yaml +6 -0
  26. package/bin/lib/validation.sh +206 -0
  27. package/bin/openclaw +660 -0
  28. package/docs/ADDING_A_FRAMEWORK.md +87 -0
  29. package/docs/AGENTS.md.example +40 -0
  30. package/docs/CODE_REVIEW.md +192 -0
  31. package/docs/DEPLOYMENT_READINESS.md +81 -0
  32. package/docs/FAQ_SECURITY_SCANNERS.md +373 -0
  33. package/docs/FRAMEWORK_ROADMAP.md +41 -0
  34. package/docs/HOSTED_PASSPORT_SETUP.md +362 -0
  35. package/docs/IMPLEMENTING_YOUR_OWN_EVALUATOR.md +433 -0
  36. package/docs/OPENCLAW_COMPATIBILITY.md +73 -0
  37. package/docs/OPENCLAW_LOCAL_INTEGRATION.md +596 -0
  38. package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +54 -0
  39. package/docs/QUICKSTART.md +470 -0
  40. package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +470 -0
  41. package/docs/README.md +28 -0
  42. package/docs/RELEASE.md +87 -0
  43. package/docs/REPO_LAYOUT.md +47 -0
  44. package/docs/SKILLS_ECOSYSTEM_ANALYSIS_FEB17.md +1260 -0
  45. package/docs/TOOL_POLICY_MAPPING.md +46 -0
  46. package/docs/UPGRADE.md +46 -0
  47. package/docs/VERIFICATION_METHODS.md +97 -0
  48. package/docs/assets/README.md +8 -0
  49. package/docs/assets/porter.svg +54 -0
  50. package/docs/development/ERROR_CODES.md +616 -0
  51. package/docs/frameworks/GITHUB_ISSUE_PROPOSALS.md +1105 -0
  52. package/docs/frameworks/crewai.md +114 -0
  53. package/docs/frameworks/cursor.md +159 -0
  54. package/docs/frameworks/langchain.md +72 -0
  55. package/docs/frameworks/n8n.md +40 -0
  56. package/docs/frameworks/openclaw.md +40 -0
  57. package/docs/launch/ADD_APORT_AWESOME_LISTS_INSTRUCTIONS.md +146 -0
  58. package/docs/launch/ANNOUNCEMENT_GUIDE.md +266 -0
  59. package/docs/launch/AWESOME_REPOS.md +53 -0
  60. package/docs/launch/CURSOR_VSCODE_HOOKS_RESEARCH.md +77 -0
  61. package/docs/launch/DEMO_TERMINAL_OUTPUT.txt +48 -0
  62. package/docs/launch/DRY_AND_PLAN_CHECKLIST.md +47 -0
  63. package/docs/launch/EVIDENCE_README.md +61 -0
  64. package/docs/launch/EVIDENCE_TERMINAL_CAPTURE.txt +10 -0
  65. package/docs/launch/FRAMEWORK_SUPPORT_PLAN.md +1640 -0
  66. package/docs/launch/LAUNCH_READINESS_CHECKLIST.md +237 -0
  67. package/docs/launch/LAUNCH_STRATEGY_SUMMARY.md +464 -0
  68. package/docs/launch/OPENCLAW_FEEDBACK_AND_FIXES.md +85 -0
  69. package/docs/launch/POST_1_VALENTINE_IMPROVED.md +233 -0
  70. package/docs/launch/POST_2_GUARDRAIL_IMPROVED.md +369 -0
  71. package/docs/launch/PRE_LAUNCH_FIXES.md +766 -0
  72. package/docs/launch/QUICK_LAUNCH_CHECKLIST.md +400 -0
  73. package/docs/launch/READINESS_SUMMARY.md +262 -0
  74. package/docs/launch/README.md +68 -0
  75. package/docs/launch/USER_STORIES.md +327 -0
  76. package/docs/launch/scripts/add-aport-awesome-pr.sh +69 -0
  77. package/docs/operations/MONITORING.md +588 -0
  78. package/docs/reviews/2026-02-18-staff-review.md +268 -0
  79. package/extensions/openclaw-aport/README.md +415 -0
  80. package/extensions/openclaw-aport/index.js +625 -0
  81. package/extensions/openclaw-aport/openclaw-aport.js +7 -0
  82. package/extensions/openclaw-aport/openclaw.plugin.json +46 -0
  83. package/extensions/openclaw-aport/package.json +36 -0
  84. package/extensions/openclaw-aport/test.js +307 -0
  85. package/external/aport-policies/README.md +363 -0
  86. package/external/aport-policies/agent.session.create.v1/README.md +345 -0
  87. package/external/aport-policies/agent.session.create.v1/policy.json +162 -0
  88. package/external/aport-policies/agent.tool.register.v1/README.md +361 -0
  89. package/external/aport-policies/agent.tool.register.v1/policy.json +172 -0
  90. package/external/aport-policies/code.release.publish.v1/README.md +51 -0
  91. package/external/aport-policies/code.release.publish.v1/policy.json +121 -0
  92. package/external/aport-policies/code.repository.merge.v1/README.md +287 -0
  93. package/external/aport-policies/code.repository.merge.v1/express.example.js +332 -0
  94. package/external/aport-policies/code.repository.merge.v1/fastapi.example.py +370 -0
  95. package/external/aport-policies/code.repository.merge.v1/policy.json +162 -0
  96. package/external/aport-policies/data.export.create.v1/README.md +226 -0
  97. package/external/aport-policies/data.export.create.v1/express.example.js +172 -0
  98. package/external/aport-policies/data.export.create.v1/fastapi.example.py +165 -0
  99. package/external/aport-policies/data.export.create.v1/policy.json +133 -0
  100. package/external/aport-policies/data.report.ingest.v1/README.md +134 -0
  101. package/external/aport-policies/data.report.ingest.v1/express.example.js +105 -0
  102. package/external/aport-policies/data.report.ingest.v1/minimal-example.js +68 -0
  103. package/external/aport-policies/data.report.ingest.v1/policy.json +174 -0
  104. package/external/aport-policies/finance.crypto.trade.v1/README.md +146 -0
  105. package/external/aport-policies/finance.crypto.trade.v1/express.example.js +109 -0
  106. package/external/aport-policies/finance.crypto.trade.v1/minimal-example.js +65 -0
  107. package/external/aport-policies/finance.crypto.trade.v1/policy.json +176 -0
  108. package/external/aport-policies/finance.payment.charge.v1/README.md +326 -0
  109. package/external/aport-policies/finance.payment.charge.v1/express.example.js +250 -0
  110. package/external/aport-policies/finance.payment.charge.v1/fastapi.example.py +227 -0
  111. package/external/aport-policies/finance.payment.charge.v1/minimal-example.js +64 -0
  112. package/external/aport-policies/finance.payment.charge.v1/policy.json +224 -0
  113. package/external/aport-policies/finance.payment.charge.v1/tests/contexts.jsonl +12 -0
  114. package/external/aport-policies/finance.payment.charge.v1/tests/expected.jsonl +12 -0
  115. package/external/aport-policies/finance.payment.charge.v1/tests/passport.instance.json +42 -0
  116. package/external/aport-policies/finance.payment.charge.v1/tests/passport.template.json +40 -0
  117. package/external/aport-policies/finance.payment.charge.v1/tests/payments-charge-policy.test.js +817 -0
  118. package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py +486 -0
  119. package/external/aport-policies/finance.payment.payout.v1/README.md +78 -0
  120. package/external/aport-policies/finance.payment.payout.v1/policy.json +181 -0
  121. package/external/aport-policies/finance.payment.refund.v1/README.md +275 -0
  122. package/external/aport-policies/finance.payment.refund.v1/express.example.js +167 -0
  123. package/external/aport-policies/finance.payment.refund.v1/fastapi.example.py +136 -0
  124. package/external/aport-policies/finance.payment.refund.v1/minimal-example.js +183 -0
  125. package/external/aport-policies/finance.payment.refund.v1/policy.json +216 -0
  126. package/external/aport-policies/finance.payment.refund.v1/tests/refunds-policy.test.js +924 -0
  127. package/external/aport-policies/finance.payment.refund.v1/tests/test_refunds_policy.py +778 -0
  128. package/external/aport-policies/finance.transaction.execute.v1/README.md +309 -0
  129. package/external/aport-policies/finance.transaction.execute.v1/express.example.js +261 -0
  130. package/external/aport-policies/finance.transaction.execute.v1/fastapi.example.py +231 -0
  131. package/external/aport-policies/finance.transaction.execute.v1/minimal-example.js +78 -0
  132. package/external/aport-policies/finance.transaction.execute.v1/policy.json +189 -0
  133. package/external/aport-policies/finance.transaction.execute.v1/tests/contexts.jsonl +12 -0
  134. package/external/aport-policies/finance.transaction.execute.v1/tests/expected.jsonl +12 -0
  135. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.instance.json +42 -0
  136. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.template.json +42 -0
  137. package/external/aport-policies/finance.transaction.execute.v1/tests/test_transactions_policy.py +214 -0
  138. package/external/aport-policies/finance.transaction.execute.v1/tests/transactions-policy.test.js +306 -0
  139. package/external/aport-policies/governance.data.access.v1/README.md +292 -0
  140. package/external/aport-policies/governance.data.access.v1/express.example.js +321 -0
  141. package/external/aport-policies/governance.data.access.v1/fastapi.example.py +279 -0
  142. package/external/aport-policies/governance.data.access.v1/minimal-example.js +65 -0
  143. package/external/aport-policies/governance.data.access.v1/policy.json +208 -0
  144. package/external/aport-policies/governance.data.access.v1/tests/contexts.jsonl +12 -0
  145. package/external/aport-policies/governance.data.access.v1/tests/data-access-policy.test.js +308 -0
  146. package/external/aport-policies/governance.data.access.v1/tests/expected.jsonl +12 -0
  147. package/external/aport-policies/governance.data.access.v1/tests/passport.instance.json +56 -0
  148. package/external/aport-policies/governance.data.access.v1/tests/passport.template.json +56 -0
  149. package/external/aport-policies/governance.data.access.v1/tests/test_data_access_policy.py +214 -0
  150. package/external/aport-policies/legal.contract.review.v1/README.md +109 -0
  151. package/external/aport-policies/legal.contract.review.v1/policy.json +378 -0
  152. package/external/aport-policies/legal.contract.review.v1/tests/legal-contract-review-policy.test.js +609 -0
  153. package/external/aport-policies/legal.contract.review.v1/tests/passport.template.json +49 -0
  154. package/external/aport-policies/mcp.tool.execute.v1/README.md +301 -0
  155. package/external/aport-policies/mcp.tool.execute.v1/policy.json +141 -0
  156. package/external/aport-policies/messaging.message.send.v1/README.md +230 -0
  157. package/external/aport-policies/messaging.message.send.v1/express.example.js +183 -0
  158. package/external/aport-policies/messaging.message.send.v1/fastapi.example.py +193 -0
  159. package/external/aport-policies/messaging.message.send.v1/policy.json +144 -0
  160. package/external/aport-policies/policy-template.json +107 -0
  161. package/external/aport-policies/system.command.execute.v1/README.md +275 -0
  162. package/external/aport-policies/system.command.execute.v1/policy.json +146 -0
  163. package/external/aport-spec/CONTRIBUTING.md +273 -0
  164. package/external/aport-spec/LICENSE +21 -0
  165. package/external/aport-spec/README.md +168 -0
  166. package/external/aport-spec/conformance/README.md +294 -0
  167. package/external/aport-spec/conformance/cases/data.export.v1/contexts/allow_users.json +6 -0
  168. package/external/aport-spec/conformance/cases/data.export.v1/contexts/deny_pii.json +6 -0
  169. package/external/aport-spec/conformance/cases/data.export.v1/expected/allow_users.decision.json +19 -0
  170. package/external/aport-spec/conformance/cases/data.export.v1/expected/deny_pii.decision.json +19 -0
  171. package/external/aport-spec/conformance/cases/data.export.v1/passports/template.json +29 -0
  172. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/allow_50usd.json +9 -0
  173. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_150usd.json +9 -0
  174. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_currency.json +9 -0
  175. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/allow_50usd.decision.json +19 -0
  176. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_150usd.decision.json +19 -0
  177. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_currency.decision.json +19 -0
  178. package/external/aport-spec/conformance/cases/payments.refunds.v1/passports/template.json +42 -0
  179. package/external/aport-spec/conformance/package.json +44 -0
  180. package/external/aport-spec/conformance/pnpm-lock.yaml +642 -0
  181. package/external/aport-spec/conformance/src/cases.ts +371 -0
  182. package/external/aport-spec/conformance/src/ed25519.ts +167 -0
  183. package/external/aport-spec/conformance/src/jcs.ts +85 -0
  184. package/external/aport-spec/conformance/src/runner.ts +533 -0
  185. package/external/aport-spec/conformance/src/validators.ts +185 -0
  186. package/external/aport-spec/conformance/test-runner.js +315 -0
  187. package/external/aport-spec/conformance/tsconfig.json +21 -0
  188. package/external/aport-spec/error-schema.json +192 -0
  189. package/external/aport-spec/index.json +12 -0
  190. package/external/aport-spec/integrations/clawmoat/README.md +12 -0
  191. package/external/aport-spec/integrations/shield/README.md +245 -0
  192. package/external/aport-spec/integrations/shield/adapters/index.js +116 -0
  193. package/external/aport-spec/integrations/shield/adapters/system-command-execute.js +133 -0
  194. package/external/aport-spec/integrations/shield/test/README.md +58 -0
  195. package/external/aport-spec/integrations/shield/test/shield.md +40 -0
  196. package/external/aport-spec/integrations/shield/test/test-shield-to-verify.js +274 -0
  197. package/external/aport-spec/metrics-schema.json +504 -0
  198. package/external/aport-spec/oap/CHANGELOG.md +54 -0
  199. package/external/aport-spec/oap/VERSION.md +40 -0
  200. package/external/aport-spec/oap/capability-registry.md +229 -0
  201. package/external/aport-spec/oap/conformance.md +257 -0
  202. package/external/aport-spec/oap/decision-schema.json +114 -0
  203. package/external/aport-spec/oap/examples/context.refund.usd.50.json +9 -0
  204. package/external/aport-spec/oap/examples/decision.allow.sample.json +20 -0
  205. package/external/aport-spec/oap/examples/decision.deny.sample.json +23 -0
  206. package/external/aport-spec/oap/examples/passport.instance.v1.json +50 -0
  207. package/external/aport-spec/oap/examples/passport.template.v1.json +71 -0
  208. package/external/aport-spec/oap/oap-spec.md +426 -0
  209. package/external/aport-spec/oap/passport-schema.json +396 -0
  210. package/external/aport-spec/oap/security.md +213 -0
  211. package/external/aport-spec/oap/vc/context-oap-v1.jsonld +137 -0
  212. package/external/aport-spec/oap/vc/examples/oap-decision-vc.json +37 -0
  213. package/external/aport-spec/oap/vc/examples/oap-passport-vc.json +68 -0
  214. package/external/aport-spec/oap/vc/tools/INTEGRATION.md +375 -0
  215. package/external/aport-spec/oap/vc/tools/README.md +278 -0
  216. package/external/aport-spec/oap/vc/tools/examples/decision-to-vc.js +66 -0
  217. package/external/aport-spec/oap/vc/tools/examples/passport-to-vc.js +83 -0
  218. package/external/aport-spec/oap/vc/tools/examples/vc-to-decision.js +77 -0
  219. package/external/aport-spec/oap/vc/tools/examples/vc-to-passport.js +94 -0
  220. package/external/aport-spec/oap/vc/tools/package.json +38 -0
  221. package/external/aport-spec/oap/vc/tools/pnpm-lock.yaml +472 -0
  222. package/external/aport-spec/oap/vc/tools/src/cli.ts +226 -0
  223. package/external/aport-spec/oap/vc/tools/src/crypto-utils.ts +427 -0
  224. package/external/aport-spec/oap/vc/tools/src/index.ts +653 -0
  225. package/external/aport-spec/oap/vc/tools/src/test.ts +148 -0
  226. package/external/aport-spec/oap/vc/tools/src/vp.ts +382 -0
  227. package/external/aport-spec/oap/vc/tools/test-simple.js +214 -0
  228. package/external/aport-spec/oap/vc/tools/tsconfig.json +19 -0
  229. package/external/aport-spec/oap/vc/vc-mapping.md +443 -0
  230. package/external/aport-spec/passport-schema.json +586 -0
  231. package/external/aport-spec/rate-limiting.md +136 -0
  232. package/external/aport-spec/transport-profile.md +325 -0
  233. package/external/aport-spec/webhook-spec.md +314 -0
  234. package/package.json +70 -0
  235. package/skills/aport-agent-guardrail/SKILL.md +314 -0
  236. package/src/evaluator.js +252 -0
  237. package/src/server/index.js +72 -0
@@ -0,0 +1,394 @@
1
+ #!/bin/bash
2
+ # APort built-in local policy evaluator (bash, no API)
3
+ # Evaluates OAP v1.0 policies locally without any network call
4
+ # Usage: aport-guardrail-bash.sh <tool_name> '<context_json>'
5
+
6
+ set -e
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+ # Resolve paths: config_dir/aport/ (new) or config_dir (legacy)
10
+ # shellcheck source=bin/aport-resolve-paths.sh
11
+ . "${SCRIPT_DIR}/bin/aport-resolve-paths.sh"
12
+
13
+ # Source validation library for input sanitization
14
+ # shellcheck source=bin/lib/validation.sh
15
+ . "${SCRIPT_DIR}/bin/lib/validation.sh"
16
+
17
+ # Get script directory to find submodules (external/ per GIT_SUBMODULES_EXPLAINED.md)
18
+ POLICIES_DIR="$SCRIPT_DIR/external/aport-policies"
19
+ LOCAL_POLICIES_DIR="$SCRIPT_DIR/local-overrides/policies"
20
+
21
+ TOOL_NAME="$1"
22
+ # Default empty object via variable to avoid bash parsing ${2:-{}} as ${2:-{ + literal }
23
+ DEFAULT_CONTEXT='{}'
24
+ CONTEXT_JSON="${2:-$DEFAULT_CONTEXT}"
25
+
26
+ # SECURITY: Validate tool name to prevent injection attacks
27
+ if ! validate_tool_name "$TOOL_NAME"; then
28
+ echo '{"allow":false,"reasons":[{"code":"oap.invalid_tool_name","message":"Tool name contains invalid characters or format"}]}' > "$DECISION_FILE" 2> /dev/null || true
29
+ exit 1
30
+ fi
31
+
32
+ # SECURITY: Validate JSON context size to prevent DoS
33
+ if ! validate_json_size "$CONTEXT_JSON"; then
34
+ echo '{"allow":false,"reasons":[{"code":"oap.context_too_large","message":"Context JSON exceeds maximum size"}]}' > "$DECISION_FILE" 2> /dev/null || true
35
+ exit 1
36
+ fi
37
+
38
+ # DEBUG: Print received arguments (sanitized to prevent leaking sensitive data)
39
+ if [ -n "$DEBUG_APORT" ]; then
40
+ echo "DEBUG: TOOL_NAME=$TOOL_NAME" >&2
41
+ # SECURITY: Sanitize context JSON to prevent logging sensitive data
42
+ SANITIZED_CONTEXT=$(sanitize_log_value "$CONTEXT_JSON" "context")
43
+ echo "DEBUG: CONTEXT_JSON=$SANITIZED_CONTEXT" >&2
44
+ echo "DEBUG: CONTEXT length=${#CONTEXT_JSON}" >&2
45
+ fi
46
+
47
+ # Ensure APort data dir exists (for decision.json, audit.log)
48
+ mkdir -p "$(dirname "$AUDIT_LOG")"
49
+
50
+ # Function to load policy from upstream or local-overrides
51
+ load_policy() {
52
+ local policy_base="$1"
53
+ local policy_file=""
54
+
55
+ # Try official policy from submodule first (with .v1, .v2, etc)
56
+ for version_dir in "$POLICIES_DIR/${policy_base}".v*/; do
57
+ if [ -f "${version_dir}policy.json" ]; then
58
+ policy_file="${version_dir}policy.json"
59
+ break
60
+ fi
61
+ done
62
+
63
+ # Fallback to local overrides
64
+ if [ -z "$policy_file" ] || [ ! -f "$policy_file" ]; then
65
+ for local_file in "$LOCAL_POLICIES_DIR/${policy_base}".v*.json; do
66
+ if [ -f "$local_file" ]; then
67
+ policy_file="$local_file"
68
+ break
69
+ fi
70
+ done
71
+ fi
72
+
73
+ if [ -n "$policy_file" ] && [ -f "$policy_file" ]; then
74
+ cat "$policy_file"
75
+ else
76
+ echo "{}"
77
+ fi
78
+ }
79
+
80
+ # Function to compute JCS-canonicalized SHA-256 digest
81
+ compute_passport_digest() {
82
+ local passport_file="$1"
83
+ echo "sha256:$(jq --sort-keys -c . "$passport_file" | shasum -a 256 | awk '{print $1}')"
84
+ }
85
+
86
+ # Function to build OAP v1.0 compliant decision and exit.
87
+ # Adds content_hash (tamper-resistant) and optional chain (prev_decision_id, prev_content_hash).
88
+ # If a decision file is edited or the chain is reordered, content_hash verification fails.
89
+ write_decision() {
90
+ local allow="$1"
91
+ local policy_id="${2:-unknown}"
92
+ local deny_code="${3:-oap.policy_error}"
93
+ local deny_message="${4:-Policy evaluation failed}"
94
+
95
+ local decision_id=$(uuidgen 2> /dev/null || echo "local-$(date +%s)")
96
+ local passport_id=$(jq -r '.passport_id // "unknown"' "$PASSPORT_FILE")
97
+ local owner_id=$(jq -r '.owner_id // "unknown"' "$PASSPORT_FILE")
98
+ local assurance_level=$(jq -r '.assurance_level // "L0"' "$PASSPORT_FILE")
99
+ local passport_digest=$(compute_passport_digest "$PASSPORT_FILE")
100
+ local issued_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
101
+ local expires_at=$(date -u -v+1H +%Y-%m-%dT%H:%M:%SZ 2> /dev/null || date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)
102
+
103
+ # Build reasons array per OAP v1.0 spec
104
+ local reasons
105
+ if [ "$allow" = "true" ]; then
106
+ reasons='[{"code": "oap.allowed", "message": "All policy checks passed"}]'
107
+ else
108
+ reasons="[{\"code\": \"$deny_code\", \"message\": \"$deny_message\"}]"
109
+ fi
110
+
111
+ # Chain state: last decision id and hash for tamper-resistant chain
112
+ local decisions_dir
113
+ decisions_dir="$(dirname "$DECISION_FILE")"
114
+ local chain_state="$decisions_dir/.chain-state.json"
115
+ local prev_decision_id=""
116
+ local prev_content_hash=""
117
+ if [ -f "$chain_state" ]; then
118
+ prev_decision_id=$(jq -r '.last_decision_id // ""' "$chain_state" 2> /dev/null || true)
119
+ prev_content_hash=$(jq -r '.last_content_hash // ""' "$chain_state" 2> /dev/null || true)
120
+ fi
121
+
122
+ # Build base decision JSON (no content_hash yet)
123
+ local base_json
124
+ base_json=$(jq -n -c --sort-keys \
125
+ --arg decision_id "$decision_id" \
126
+ --arg policy_id "$policy_id" \
127
+ --arg passport_id "$passport_id" \
128
+ --arg owner_id "$owner_id" \
129
+ --arg assurance_level "$assurance_level" \
130
+ --argjson allow "$allow" \
131
+ --argjson reasons "$reasons" \
132
+ --arg issued_at "$issued_at" \
133
+ --arg expires_at "$expires_at" \
134
+ --arg passport_digest "$passport_digest" \
135
+ --arg prev_decision_id "$prev_decision_id" \
136
+ --arg prev_content_hash "$prev_content_hash" \
137
+ '{
138
+ decision_id: $decision_id,
139
+ policy_id: $policy_id,
140
+ passport_id: $passport_id,
141
+ owner_id: $owner_id,
142
+ assurance_level: $assurance_level,
143
+ allow: $allow,
144
+ reasons: $reasons,
145
+ issued_at: $issued_at,
146
+ expires_at: $expires_at,
147
+ passport_digest: $passport_digest,
148
+ signature: "ed25519:local-unsigned",
149
+ kid: "oap:local:dev-key",
150
+ prev_decision_id: (if $prev_decision_id == "" then null else $prev_decision_id end),
151
+ prev_content_hash: (if $prev_content_hash == "" then null else $prev_content_hash end)
152
+ }')
153
+
154
+ # Content hash over canonical form (without content_hash field) — tamper-resistant
155
+ local content_hash
156
+ content_hash="sha256:$(printf '%s' "$base_json" | shasum -a 256 | awk '{print $1}')"
157
+
158
+ # Add content_hash and write final decision (critical path — plugin reads this)
159
+ local final_json
160
+ final_json=$(echo "$base_json" | jq -c --arg h "$content_hash" '. + {content_hash: $h}')
161
+ echo "$final_json" > "$DECISION_FILE"
162
+
163
+ # Update chain state for next decision (best-effort; do not block or fail the script)
164
+ echo "{\"last_decision_id\":\"$decision_id\",\"last_content_hash\":\"$content_hash\"}" > "$chain_state" 2> /dev/null || true
165
+
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
+ audit_context=""
169
+ if [ -n "${CONTEXT_SUMMARY:-}" ]; then
170
+ audit_context=" context=\"${CONTEXT_SUMMARY}\""
171
+ 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 &
173
+
174
+ if [ "$allow" = "true" ]; then
175
+ exit 0
176
+ else
177
+ exit 1
178
+ fi
179
+ }
180
+
181
+ # Check if jq is available
182
+ if ! command -v jq &> /dev/null; then
183
+ echo "Error: jq is required but not installed. Install with: brew install jq" >&2
184
+ write_decision false "unknown" "oap.missing_dependency" "jq not found"
185
+ fi
186
+
187
+ # Load passport (source of truth; status checked first below)
188
+ if [ ! -f "$PASSPORT_FILE" ]; then
189
+ write_decision false "unknown" "oap.passport_not_found" "Passport file not found at $PASSPORT_FILE. Create one with aport-create-passport.sh"
190
+ fi
191
+
192
+ PASSPORT=$(cat "$PASSPORT_FILE")
193
+
194
+ # Validate passport JSON
195
+ if ! echo "$PASSPORT" | jq . > /dev/null 2>&1; then
196
+ write_decision false "unknown" "oap.passport_invalid" "Passport file contains invalid JSON"
197
+ fi
198
+
199
+ # Check passport status first (kill switch = suspended/revoked; passport is source of truth per OAP spec)
200
+ STATUS=$(echo "$PASSPORT" | jq -r '.status // "unknown"')
201
+ if [ "$STATUS" != "active" ]; then
202
+ write_decision false "unknown" "oap.passport_suspended" "Passport status is '$STATUS', not 'active'. Agent suspended."
203
+ fi
204
+
205
+ # Check spec version
206
+ SPEC_VERSION=$(echo "$PASSPORT" | jq -r '.spec_version // "unknown"')
207
+ if [ "$SPEC_VERSION" != "oap/1.0" ]; then
208
+ write_decision false "unknown" "oap.passport_version_mismatch" "Passport spec version is '$SPEC_VERSION', expected 'oap/1.0'"
209
+ fi
210
+
211
+ # Map tool to policy pack ID
212
+ POLICY_ID=""
213
+ case "$TOOL_NAME" in
214
+ git.create_pr | git.merge | git.push)
215
+ POLICY_ID="code.repository.merge.v1"
216
+ ;;
217
+ exec.run | exec.* | system.*)
218
+ POLICY_ID="system.command.execute.v1"
219
+ ;;
220
+ message.send | message.* | messaging.*)
221
+ POLICY_ID="messaging.message.send.v1"
222
+ ;;
223
+ payment.* | finance.*)
224
+ POLICY_ID="finance.payment.refund.v1"
225
+ ;;
226
+ database.write | database.insert | database.update | database.delete | data.export)
227
+ POLICY_ID="data.export.v1"
228
+ ;;
229
+ *)
230
+ # Unknown tool - deny by default for security
231
+ write_decision false "unknown" "oap.unknown_capability" "Tool '$TOOL_NAME' is not mapped to a policy pack"
232
+ ;;
233
+ esac
234
+
235
+ # Capability-specific context summary for audit log (command, recipient, repo/branch, etc.)
236
+ CONTEXT_SUMMARY=""
237
+ if [ -n "$CONTEXT_JSON" ] && [ "$CONTEXT_JSON" != "{}" ]; then
238
+ if [[ "$POLICY_ID" == "system.command.execute"* ]]; then
239
+ CONTEXT_SUMMARY=$(echo "$CONTEXT_JSON" | jq -r '.command // .cmd // .args[0] // ""' 2> /dev/null || true)
240
+ elif [[ "$POLICY_ID" == "messaging.message.send"* ]]; then
241
+ CONTEXT_SUMMARY=$(echo "$CONTEXT_JSON" | jq -r '.recipient // .to // ""' 2> /dev/null || true)
242
+ elif [[ "$POLICY_ID" == "code.repository.merge"* ]]; then
243
+ REPO=$(echo "$CONTEXT_JSON" | jq -r '.repo // .repository // ""' 2> /dev/null || true)
244
+ BRANCH=$(echo "$CONTEXT_JSON" | jq -r '.branch // ""' 2> /dev/null || true)
245
+ [ -n "$REPO" ] && CONTEXT_SUMMARY="$REPO"
246
+ [ -n "$BRANCH" ] && CONTEXT_SUMMARY="${CONTEXT_SUMMARY:+$CONTEXT_SUMMARY }$BRANCH"
247
+ fi
248
+ # Sanitize for one-line audit: no newlines, truncate, escape double quotes
249
+ if [ -n "$CONTEXT_SUMMARY" ]; then
250
+ CONTEXT_SUMMARY=$(printf '%s' "$CONTEXT_SUMMARY" | tr '\n' ' ' | head -c 120 | sed 's/"/\\"/g')
251
+ fi
252
+ fi
253
+
254
+ # Load policy definition
255
+ POLICY_DEF=$(load_policy "$(echo "$POLICY_ID" | sed 's/\.v[0-9]*$//')")
256
+
257
+ # Check if all required capabilities exist in passport
258
+ REQUIRED_CAPS=$(echo "$POLICY_DEF" | jq -r '.requires_capabilities[]? // empty')
259
+ PASSPORT_CAPS=$(echo "$PASSPORT" | jq -r '.capabilities[]?.id // empty')
260
+
261
+ # If policy has required capabilities, check them all
262
+ # (Alias: policy "messaging.send" is satisfied by passport "messaging.message.send")
263
+ if [ -n "$REQUIRED_CAPS" ]; then
264
+ for req_cap in $REQUIRED_CAPS; do
265
+ HAS_CAP=false
266
+ for passport_cap in $PASSPORT_CAPS; do
267
+ if [ "$passport_cap" = "$req_cap" ]; then
268
+ HAS_CAP=true
269
+ break
270
+ fi
271
+ if [ "$req_cap" = "messaging.send" ] && [ "$passport_cap" = "messaging.message.send" ]; then
272
+ HAS_CAP=true
273
+ break
274
+ fi
275
+ done
276
+ if [ "$HAS_CAP" = false ]; then
277
+ write_decision false "$POLICY_ID" "oap.unknown_capability" "Passport does not have required capability '$req_cap' for policy '$POLICY_ID'"
278
+ fi
279
+ done
280
+ fi
281
+
282
+ # Get policy limits from passport
283
+ POLICY_BASE=$(echo "$POLICY_ID" | sed 's/\.v[0-9]*$//')
284
+ # Messaging: API/verifier use flat keys at limits top level; accept nested limits["messaging.message.send"] or flat
285
+ if [[ "$POLICY_ID" == "messaging.message.send"* ]]; then
286
+ LIMITS=$(echo "$PASSPORT" | jq '.limits | if .["messaging.message.send"] then .["messaging.message.send"] else {msgs_per_min, msgs_per_day, allowed_recipients, approval_required} end')
287
+ else
288
+ LIMITS=$(echo "$PASSPORT" | jq ".limits.\"$POLICY_BASE\" // {}")
289
+ fi
290
+
291
+ # Evaluate policy-specific limits
292
+ if [[ "$POLICY_ID" == "code.repository.merge"* ]]; then
293
+ FILES_CHANGED=$(echo "$CONTEXT_JSON" | jq -r '.files_changed // .files // 0')
294
+ MAX_FILES=$(echo "$LIMITS" | jq -r '.max_pr_size_kb // 500')
295
+
296
+ if [ "$FILES_CHANGED" -gt "$MAX_FILES" ]; then
297
+ write_decision false "$POLICY_ID" "oap.limit_exceeded" "PR size $FILES_CHANGED exceeds limit of $MAX_FILES files"
298
+ fi
299
+
300
+ # Check allowed repos (use while-read to avoid glob expansion when pattern is *)
301
+ REPO=$(echo "$CONTEXT_JSON" | jq -r '.repo // .repository // ""')
302
+ if [ -n "$REPO" ]; then
303
+ REPO_ALLOWED=false
304
+ while IFS= read -r pattern; do
305
+ [ -z "$pattern" ] && continue
306
+ if [[ "$REPO" == "$pattern" ]] || [[ "$REPO" == */$pattern ]] || [[ "$pattern" == "*" ]]; then
307
+ REPO_ALLOWED=true
308
+ break
309
+ fi
310
+ done < <(echo "$LIMITS" | jq -r '.allowed_repos[]? // empty')
311
+ HAS_ALLOWED_REPOS=$(echo "$LIMITS" | jq -r '.allowed_repos | length')
312
+ if [ "$REPO_ALLOWED" = false ] && [ "$HAS_ALLOWED_REPOS" -gt 0 ] 2> /dev/null; then
313
+ write_decision false "$POLICY_ID" "oap.repo_not_allowed" "Repository '$REPO' is not in allowed list"
314
+ fi
315
+ fi
316
+
317
+ # Check allowed branches (use while-read to avoid glob expansion when pattern is *)
318
+ BRANCH=$(echo "$CONTEXT_JSON" | jq -r '.branch // ""')
319
+ if [ -n "$BRANCH" ]; then
320
+ BRANCH_ALLOWED=false
321
+ while IFS= read -r pattern; do
322
+ [ -z "$pattern" ] && continue
323
+ if [[ "$BRANCH" == "$pattern" ]] || [[ "$pattern" == "*" ]]; then
324
+ BRANCH_ALLOWED=true
325
+ break
326
+ fi
327
+ done < <(echo "$LIMITS" | jq -r '.allowed_base_branches[]? // empty')
328
+ HAS_ALLOWED_BRANCHES=$(echo "$LIMITS" | jq -r '.allowed_base_branches | length')
329
+ if [ "$BRANCH_ALLOWED" = false ] && [ "$HAS_ALLOWED_BRANCHES" -gt 0 ] 2> /dev/null; then
330
+ write_decision false "$POLICY_ID" "oap.branch_not_allowed" "Branch '$BRANCH' is not in allowed list"
331
+ fi
332
+ fi
333
+ fi
334
+
335
+ if [[ "$POLICY_ID" == "system.command.execute"* ]]; then
336
+ COMMAND=$(echo "$CONTEXT_JSON" | jq -r '.command // .cmd // ""')
337
+ if [ -z "$COMMAND" ]; then
338
+ # Try to extract from args
339
+ COMMAND=$(echo "$CONTEXT_JSON" | jq -r '.args[0] // ""')
340
+ fi
341
+
342
+ if [ -n "$COMMAND" ]; then
343
+ # SECURITY: Validate command doesn't contain injection characters
344
+ if ! validate_command_string "$COMMAND"; then
345
+ write_decision false "$POLICY_ID" "oap.command_injection_detected" "Command contains potentially dangerous characters"
346
+ fi
347
+
348
+ # Check allowed commands using safe prefix matching
349
+ COMMAND_ALLOWED=false
350
+ while IFS= read -r allowed_cmd; do
351
+ [ -z "$allowed_cmd" ] && continue
352
+ # Use safe_prefix_match instead of bash glob patterns
353
+ if safe_prefix_match "$COMMAND" "$allowed_cmd"; then
354
+ COMMAND_ALLOWED=true
355
+ break
356
+ fi
357
+ done < <(echo "$LIMITS" | jq -r '.allowed_commands[]? // empty')
358
+ HAS_ALLOWED=$(echo "$LIMITS" | jq -r '.allowed_commands | length')
359
+ if [ "$COMMAND_ALLOWED" = false ] && [ "${HAS_ALLOWED:-0}" -gt 0 ] 2> /dev/null; then
360
+ write_decision false "$POLICY_ID" "oap.command_not_allowed" "Command '$COMMAND' is not in allowed list"
361
+ fi
362
+
363
+ # Check blocked patterns using safe pattern matching
364
+ while IFS= read -r pattern; do
365
+ [ -z "$pattern" ] && continue
366
+ # Use safe_pattern_match instead of bash glob patterns
367
+ if safe_pattern_match "$COMMAND" "$pattern"; then
368
+ write_decision false "$POLICY_ID" "oap.blocked_pattern" "Command contains blocked pattern: $pattern"
369
+ break
370
+ fi
371
+ done < <(echo "$LIMITS" | jq -r '.blocked_patterns[]? // empty')
372
+ fi
373
+ fi
374
+
375
+ if [[ "$POLICY_ID" == "messaging.message.send"* ]]; then
376
+ RECIPIENT=$(echo "$CONTEXT_JSON" | jq -r '.recipient // .to // ""')
377
+ if [ -n "$RECIPIENT" ]; then
378
+ RECIPIENT_ALLOWED=false
379
+ while IFS= read -r allowed; do
380
+ [ -z "$allowed" ] && continue
381
+ if [ "$RECIPIENT" = "$allowed" ] || [ "$allowed" = "*" ]; then
382
+ RECIPIENT_ALLOWED=true
383
+ break
384
+ fi
385
+ done < <(echo "$LIMITS" | jq -r '.allowed_recipients[]? // empty')
386
+ HAS_ALLOWED=$(echo "$LIMITS" | jq -r '.allowed_recipients | length')
387
+ if [ "$RECIPIENT_ALLOWED" = false ] && [ "${HAS_ALLOWED:-0}" -gt 0 ] 2> /dev/null; then
388
+ write_decision false "$POLICY_ID" "oap.recipient_not_allowed" "Recipient '$RECIPIENT' is not in allowed list"
389
+ fi
390
+ fi
391
+ fi
392
+
393
+ # All checks passed - allow
394
+ write_decision true "$POLICY_ID" "oap.allowed" "All policy checks passed"
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Backward-compat wrapper: runs API evaluator (agent_id or passport).
3
+ # Prefer: bin/aport-guardrail-api.sh
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec "$SCRIPT_DIR/aport-guardrail-api.sh" "$@"
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Backward-compat wrapper: runs built-in bash evaluator (no API).
3
+ # Prefer: bin/aport-guardrail-bash.sh
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec "$SCRIPT_DIR/aport-guardrail-bash.sh" "$@"
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ # Resolve APort data paths: prefer config_dir/aport/, fallback to config_dir (legacy).
3
+ # MUST be sourced by any script that reads/writes passport, decision, or audit:
4
+ # - aport-guardrail-bash.sh
5
+ # - aport-guardrail-api.sh
6
+ # - aport-status.sh
7
+ # (aport-create-passport.sh uses --output; wrappers set env so children get resolved paths.)
8
+ # Sets: OPENCLAW_PASSPORT_FILE, OPENCLAW_DECISION_FILE, OPENCLAW_AUDIT_LOG
9
+ # and PASSPORT_FILE, DECISION_FILE, AUDIT_LOG. Passport status (active|suspended|revoked) is source of truth for suspend; no separate file.
10
+ # Caller must ensure APORT_DATA_DIR exists before writing (e.g. mkdir -p "$(dirname "$AUDIT_LOG")").
11
+
12
+ resolve_aport_paths() {
13
+ local config_dir
14
+ local passport_path
15
+ local data_dir
16
+
17
+ # 1) Explicit path set and file exists → use it (plugin or wrapper)
18
+ if [ -n "${OPENCLAW_PASSPORT_FILE:-}" ] && [ -f "$OPENCLAW_PASSPORT_FILE" ]; then
19
+ data_dir="$(dirname "$OPENCLAW_PASSPORT_FILE")"
20
+ passport_path="$OPENCLAW_PASSPORT_FILE"
21
+ # 2) Explicit path set but file missing → legacy: try parent dir (e.g. .../openclaw/passport.json)
22
+ elif [ -n "${OPENCLAW_PASSPORT_FILE:-}" ]; then
23
+ config_dir="$(cd "$(dirname "$OPENCLAW_PASSPORT_FILE")/.." 2> /dev/null && pwd)"
24
+ if [ -f "${config_dir}/passport.json" ]; then
25
+ passport_path="${config_dir}/passport.json"
26
+ data_dir="$config_dir"
27
+ else
28
+ passport_path="$OPENCLAW_PASSPORT_FILE"
29
+ data_dir="$(dirname "$OPENCLAW_PASSPORT_FILE")"
30
+ fi
31
+ # 3) No env → probe framework-specific default paths (where each framework stores data), then OpenClaw legacy
32
+ else
33
+ config_dir=""
34
+ for candidate in "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
35
+ if [ -f "${candidate}/aport/passport.json" ]; then
36
+ config_dir="$candidate"
37
+ break
38
+ fi
39
+ done
40
+ if [ -z "$config_dir" ]; then
41
+ config_dir="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
42
+ fi
43
+ config_dir="${config_dir/#\~/$HOME}"
44
+ if [ -f "${config_dir}/aport/passport.json" ]; then
45
+ passport_path="${config_dir}/aport/passport.json"
46
+ data_dir="${config_dir}/aport"
47
+ elif [ -f "${config_dir}/passport.json" ]; then
48
+ passport_path="${config_dir}/passport.json"
49
+ data_dir="$config_dir"
50
+ else
51
+ passport_path="${config_dir}/aport/passport.json"
52
+ data_dir="${config_dir}/aport"
53
+ fi
54
+ fi
55
+
56
+ export OPENCLAW_PASSPORT_FILE="$passport_path"
57
+ # Preserve explicitly set decision path (e.g. tests set OPENCLAW_DECISION_FILE); otherwise use data_dir
58
+ if [ -n "${OPENCLAW_DECISION_FILE:-}" ]; then
59
+ export OPENCLAW_DECISION_FILE="$OPENCLAW_DECISION_FILE"
60
+ else
61
+ export OPENCLAW_DECISION_FILE="${data_dir}/decision.json"
62
+ fi
63
+ export OPENCLAW_AUDIT_LOG="${data_dir}/audit.log"
64
+
65
+ PASSPORT_FILE="$OPENCLAW_PASSPORT_FILE"
66
+ DECISION_FILE="$OPENCLAW_DECISION_FILE"
67
+ AUDIT_LOG="$OPENCLAW_AUDIT_LOG"
68
+ }
69
+
70
+ # When sourced, resolve immediately so callers just use the vars
71
+ resolve_aport_paths