@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,486 @@
1
+ """
2
+ Comprehensive tests for finance.payment.charge.v1 policy
3
+ Tests all enforcement rules, edge cases, and OAP compliance
4
+ """
5
+
6
+ import pytest
7
+ import asyncio
8
+ from unittest.mock import AsyncMock, patch
9
+ from typing import Dict, Any
10
+
11
+ # Mock the policy verification endpoint
12
+ class MockResponse:
13
+ def __init__(self, json_data, status_code=200):
14
+ self.json_data = json_data
15
+ self.status_code = status_code
16
+ self.ok = status_code < 400
17
+
18
+ async def json(self):
19
+ return self.json_data
20
+
21
+ def mock_policy_verification(response_data):
22
+ """Mock successful policy verification"""
23
+ async def mock_fetch(*args, **kwargs):
24
+ return MockResponse(response_data)
25
+ return mock_fetch
26
+
27
+ def mock_policy_verification_error(status=500):
28
+ """Mock policy verification error"""
29
+ async def mock_fetch(*args, **kwargs):
30
+ return MockResponse({}, status)
31
+ return mock_fetch
32
+
33
+ class TestPaymentsChargeV1Policy:
34
+ """Test suite for finance.payment.charge.v1 policy"""
35
+
36
+ @pytest.fixture
37
+ def valid_context(self):
38
+ return {
39
+ "amount": 1299,
40
+ "currency": "USD",
41
+ "merchant_id": "merch_abc",
42
+ "region": "US",
43
+ "shipping_country": "US",
44
+ "items": [{"sku": "SKU-1", "qty": 1, "category": "electronics"}],
45
+ "idempotency_key": "charge-ord-1001",
46
+ }
47
+
48
+ @pytest.fixture
49
+ def valid_passport(self):
50
+ return {
51
+ "passport_id": "550e8400-e29b-41d4-a716-446655440001",
52
+ "kind": "instance",
53
+ "spec_version": "oap/1.0",
54
+ "owner_id": "org_demo_co",
55
+ "owner_type": "org",
56
+ "assurance_level": "L2",
57
+ "status": "active",
58
+ "capabilities": [
59
+ {
60
+ "id": "payments.charge",
61
+ "params": {"max_amount": 20000, "currency": "USD"}
62
+ }
63
+ ],
64
+ "limits": {
65
+ "payments.charge": {
66
+ "currency_limits": {
67
+ "USD": {"max_per_tx": 20000, "daily_cap": 100000},
68
+ "EUR": {"max_per_tx": 18000, "daily_cap": 90000}
69
+ },
70
+ "allowed_countries": ["US", "CA", "DE", "FR"],
71
+ "blocked_categories": ["weapons", "illicit"],
72
+ "allowed_merchant_ids": ["merch_abc", "merch_xyz"],
73
+ "max_items_per_tx": 5,
74
+ "require_assurance_at_least": "L2",
75
+ "idempotency_required": True
76
+ }
77
+ },
78
+ "regions": ["US", "CA", "EU"],
79
+ "created_at": "2025-01-30T00:00:00Z",
80
+ "updated_at": "2025-01-30T00:00:00Z",
81
+ "version": "1.0.0"
82
+ }
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_oap_decision_structure(self, valid_context, valid_passport):
86
+ """Test that policy returns OAP-compliant decision structure"""
87
+ expected_decision = {
88
+ "decision_id": "550e8400-e29b-41d4-a716-446655440002",
89
+ "policy_id": "finance.payment.charge.v1",
90
+ "agent_id": "550e8400-e29b-41d4-a716-446655440001",
91
+ "owner_id": "org_demo_co",
92
+ "assurance_level": "L2",
93
+ "allow": True,
94
+ "reasons": [
95
+ {
96
+ "code": "oap.allowed",
97
+ "message": "Transaction within limits and policy requirements"
98
+ }
99
+ ],
100
+ "created_at": "2025-01-30T10:30:00Z",
101
+ "expires_in": 3600,
102
+ "passport_digest": "sha256:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef",
103
+ "signature": "ed25519:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef==",
104
+ "kid": "oap:registry:key-2025-01"
105
+ }
106
+
107
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
108
+ # This would be the actual API call in a real test
109
+ response = await mock_policy_verification(expected_decision)()
110
+ decision = await response.json()
111
+
112
+ assert decision["allow"] is True
113
+ assert "decision_id" in decision
114
+ assert "policy_id" in decision
115
+ assert "agent_id" in decision
116
+ assert "owner_id" in decision
117
+ assert "assurance_level" in decision
118
+ assert "reasons" in decision
119
+ assert "created_at" in decision
120
+ assert "expires_in" in decision
121
+ assert "passport_digest" in decision
122
+ assert "signature" in decision
123
+ assert "kid" in decision
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_required_context_validation(self, valid_context, valid_passport):
127
+ """Test validation of required context fields"""
128
+ # Test valid context
129
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
130
+ response = await mock_policy_verification({"allow": True})()
131
+ result = await response.json()
132
+ assert result["allow"] is True
133
+
134
+ # Test missing required fields
135
+ invalid_context = {
136
+ "amount": 1299,
137
+ "currency": "USD",
138
+ # Missing merchant_id, region, items, idempotency_key
139
+ }
140
+
141
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(400)):
142
+ response = await mock_policy_verification_error(400)()
143
+ assert not response.ok
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_currency_validation(self, valid_passport):
147
+ """Test currency support validation"""
148
+ # Test supported currency
149
+ valid_context = {
150
+ "amount": 1000,
151
+ "currency": "EUR",
152
+ "merchant_id": "merch_abc",
153
+ "region": "EU",
154
+ "items": [{"sku": "SKU-1", "qty": 1}],
155
+ "idempotency_key": "charge-ord-1002"
156
+ }
157
+
158
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
159
+ response = await mock_policy_verification({"allow": True})()
160
+ result = await response.json()
161
+ assert result["allow"] is True
162
+
163
+ # Test unsupported currency
164
+ invalid_context = {
165
+ "amount": 1000,
166
+ "currency": "GBP", # Not supported
167
+ "merchant_id": "merch_abc",
168
+ "region": "US",
169
+ "items": [{"sku": "SKU-1", "qty": 1}],
170
+ "idempotency_key": "charge-ord-1003"
171
+ }
172
+
173
+ expected_decision = {
174
+ "allow": False,
175
+ "reasons": [{"code": "oap.currency_unsupported", "message": "Currency GBP is not supported"}]
176
+ }
177
+
178
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
179
+ response = await mock_policy_verification(expected_decision)()
180
+ result = await response.json()
181
+ assert result["allow"] is False
182
+ assert result["reasons"][0]["code"] == "oap.currency_unsupported"
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_amount_limits(self, valid_passport):
186
+ """Test amount limit validation"""
187
+ # Test amount within limit
188
+ valid_context = {
189
+ "amount": 15000, # Within 20000 limit
190
+ "currency": "USD",
191
+ "merchant_id": "merch_abc",
192
+ "region": "US",
193
+ "items": [{"sku": "SKU-1", "qty": 1}],
194
+ "idempotency_key": "charge-ord-1004"
195
+ }
196
+
197
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
198
+ response = await mock_policy_verification({"allow": True})()
199
+ result = await response.json()
200
+ assert result["allow"] is True
201
+
202
+ # Test amount exceeding limit
203
+ invalid_context = {
204
+ "amount": 25000, # Exceeds 20000 limit
205
+ "currency": "USD",
206
+ "merchant_id": "merch_abc",
207
+ "region": "US",
208
+ "items": [{"sku": "SKU-1", "qty": 1}],
209
+ "idempotency_key": "charge-ord-1005"
210
+ }
211
+
212
+ expected_decision = {
213
+ "allow": False,
214
+ "reasons": [{"code": "oap.limit_exceeded", "message": "Amount exceeds per-transaction limit"}]
215
+ }
216
+
217
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
218
+ response = await mock_policy_verification(expected_decision)()
219
+ result = await response.json()
220
+ assert result["allow"] is False
221
+ assert result["reasons"][0]["code"] == "oap.limit_exceeded"
222
+
223
+ @pytest.mark.asyncio
224
+ async def test_item_count_limits(self, valid_passport):
225
+ """Test item count limit validation"""
226
+ # Test items within limit
227
+ valid_context = {
228
+ "amount": 5000,
229
+ "currency": "USD",
230
+ "merchant_id": "merch_abc",
231
+ "region": "US",
232
+ "items": [
233
+ {"sku": "SKU-1", "qty": 1},
234
+ {"sku": "SKU-2", "qty": 1},
235
+ {"sku": "SKU-3", "qty": 1}
236
+ ], # 3 items, within 5 limit
237
+ "idempotency_key": "charge-ord-1006"
238
+ }
239
+
240
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
241
+ response = await mock_policy_verification({"allow": True})()
242
+ result = await response.json()
243
+ assert result["allow"] is True
244
+
245
+ # Test items exceeding limit
246
+ invalid_context = {
247
+ "amount": 5000,
248
+ "currency": "USD",
249
+ "merchant_id": "merch_abc",
250
+ "region": "US",
251
+ "items": [
252
+ {"sku": "A", "qty": 1},
253
+ {"sku": "B", "qty": 1},
254
+ {"sku": "C", "qty": 1},
255
+ {"sku": "D", "qty": 1},
256
+ {"sku": "E", "qty": 1},
257
+ {"sku": "F", "qty": 1}
258
+ ], # 6 items, exceeds 5 limit
259
+ "idempotency_key": "charge-ord-1007"
260
+ }
261
+
262
+ expected_decision = {
263
+ "allow": False,
264
+ "reasons": [{"code": "oap.limit_exceeded", "message": "Item count exceeds maximum allowed"}]
265
+ }
266
+
267
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
268
+ response = await mock_policy_verification(expected_decision)()
269
+ result = await response.json()
270
+ assert result["allow"] is False
271
+ assert result["reasons"][0]["code"] == "oap.limit_exceeded"
272
+
273
+ @pytest.mark.asyncio
274
+ async def test_merchant_validation(self, valid_passport):
275
+ """Test merchant allowlist validation"""
276
+ # Test allowed merchant
277
+ valid_context = {
278
+ "amount": 5000,
279
+ "currency": "USD",
280
+ "merchant_id": "merch_abc", # In allowlist
281
+ "region": "US",
282
+ "items": [{"sku": "SKU-1", "qty": 1}],
283
+ "idempotency_key": "charge-ord-1008"
284
+ }
285
+
286
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
287
+ response = await mock_policy_verification({"allow": True})()
288
+ result = await response.json()
289
+ assert result["allow"] is True
290
+
291
+ # Test forbidden merchant
292
+ invalid_context = {
293
+ "amount": 5000,
294
+ "currency": "USD",
295
+ "merchant_id": "merch_bad", # Not in allowlist
296
+ "region": "US",
297
+ "items": [{"sku": "SKU-1", "qty": 1}],
298
+ "idempotency_key": "charge-ord-1009"
299
+ }
300
+
301
+ expected_decision = {
302
+ "allow": False,
303
+ "reasons": [{"code": "oap.merchant_forbidden", "message": "Merchant not in allowlist"}]
304
+ }
305
+
306
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
307
+ response = await mock_policy_verification(expected_decision)()
308
+ result = await response.json()
309
+ assert result["allow"] is False
310
+ assert result["reasons"][0]["code"] == "oap.merchant_forbidden"
311
+
312
+ @pytest.mark.asyncio
313
+ async def test_country_validation(self, valid_passport):
314
+ """Test country allowlist validation"""
315
+ # Test allowed country
316
+ valid_context = {
317
+ "amount": 5000,
318
+ "currency": "USD",
319
+ "merchant_id": "merch_abc",
320
+ "region": "US",
321
+ "shipping_country": "US", # In allowlist
322
+ "items": [{"sku": "SKU-1", "qty": 1}],
323
+ "idempotency_key": "charge-ord-1010"
324
+ }
325
+
326
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
327
+ response = await mock_policy_verification({"allow": True})()
328
+ result = await response.json()
329
+ assert result["allow"] is True
330
+
331
+ # Test blocked country
332
+ invalid_context = {
333
+ "amount": 5000,
334
+ "currency": "USD",
335
+ "merchant_id": "merch_abc",
336
+ "region": "US",
337
+ "shipping_country": "BR", # Not in allowlist
338
+ "items": [{"sku": "SKU-1", "qty": 1}],
339
+ "idempotency_key": "charge-ord-1011"
340
+ }
341
+
342
+ expected_decision = {
343
+ "allow": False,
344
+ "reasons": [{"code": "oap.region_blocked", "message": "Shipping country not allowed"}]
345
+ }
346
+
347
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
348
+ response = await mock_policy_verification(expected_decision)()
349
+ result = await response.json()
350
+ assert result["allow"] is False
351
+ assert result["reasons"][0]["code"] == "oap.region_blocked"
352
+
353
+ @pytest.mark.asyncio
354
+ async def test_category_blocking(self, valid_passport):
355
+ """Test category blocklist validation"""
356
+ # Test allowed category
357
+ valid_context = {
358
+ "amount": 5000,
359
+ "currency": "USD",
360
+ "merchant_id": "merch_abc",
361
+ "region": "US",
362
+ "items": [{"sku": "SKU-1", "qty": 1, "category": "electronics"}], # Not blocked
363
+ "idempotency_key": "charge-ord-1012"
364
+ }
365
+
366
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
367
+ response = await mock_policy_verification({"allow": True})()
368
+ result = await response.json()
369
+ assert result["allow"] is True
370
+
371
+ # Test blocked category
372
+ invalid_context = {
373
+ "amount": 5000,
374
+ "currency": "USD",
375
+ "merchant_id": "merch_abc",
376
+ "region": "US",
377
+ "items": [{"sku": "SKU-1", "qty": 1, "category": "weapons"}], # Blocked category
378
+ "idempotency_key": "charge-ord-1013"
379
+ }
380
+
381
+ expected_decision = {
382
+ "allow": False,
383
+ "reasons": [{"code": "oap.category_blocked", "message": "Item category is blocked"}]
384
+ }
385
+
386
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
387
+ response = await mock_policy_verification(expected_decision)()
388
+ result = await response.json()
389
+ assert result["allow"] is False
390
+ assert result["reasons"][0]["code"] == "oap.category_blocked"
391
+
392
+ @pytest.mark.asyncio
393
+ async def test_idempotency_validation(self, valid_passport):
394
+ """Test idempotency key validation"""
395
+ # Test unique idempotency key
396
+ valid_context = {
397
+ "amount": 5000,
398
+ "currency": "USD",
399
+ "merchant_id": "merch_abc",
400
+ "region": "US",
401
+ "items": [{"sku": "SKU-1", "qty": 1}],
402
+ "idempotency_key": "charge-ord-unique-123"
403
+ }
404
+
405
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
406
+ response = await mock_policy_verification({"allow": True})()
407
+ result = await response.json()
408
+ assert result["allow"] is True
409
+
410
+ # Test duplicate idempotency key
411
+ invalid_context = {
412
+ "amount": 5000,
413
+ "currency": "USD",
414
+ "merchant_id": "merch_abc",
415
+ "region": "US",
416
+ "items": [{"sku": "SKU-1", "qty": 1}],
417
+ "idempotency_key": "charge-ord-1001" # Already used
418
+ }
419
+
420
+ expected_decision = {
421
+ "allow": False,
422
+ "reasons": [{"code": "oap.idempotency_conflict", "message": "Idempotency key already used"}]
423
+ }
424
+
425
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
426
+ response = await mock_policy_verification(expected_decision)()
427
+ result = await response.json()
428
+ assert result["allow"] is False
429
+ assert result["reasons"][0]["code"] == "oap.idempotency_conflict"
430
+
431
+ @pytest.mark.asyncio
432
+ async def test_assurance_level_validation(self, valid_passport):
433
+ """Test assurance level validation"""
434
+ # Test sufficient assurance level
435
+ valid_context = {
436
+ "amount": 5000,
437
+ "currency": "USD",
438
+ "merchant_id": "merch_abc",
439
+ "region": "US",
440
+ "items": [{"sku": "SKU-1", "qty": 1}],
441
+ "idempotency_key": "charge-ord-1014"
442
+ }
443
+
444
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
445
+ response = await mock_policy_verification({"allow": True})()
446
+ result = await response.json()
447
+ assert result["allow"] is True
448
+
449
+ # Test insufficient assurance level
450
+ invalid_context = {
451
+ "amount": 5000,
452
+ "currency": "USD",
453
+ "merchant_id": "merch_abc",
454
+ "region": "US",
455
+ "items": [{"sku": "SKU-1", "qty": 1}],
456
+ "idempotency_key": "charge-ord-1015"
457
+ }
458
+
459
+ expected_decision = {
460
+ "allow": False,
461
+ "reasons": [{"code": "oap.assurance_insufficient", "message": "Assurance level too low"}]
462
+ }
463
+
464
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
465
+ response = await mock_policy_verification(expected_decision)()
466
+ result = await response.json()
467
+ assert result["allow"] is False
468
+ assert result["reasons"][0]["code"] == "oap.assurance_insufficient"
469
+
470
+ @pytest.mark.asyncio
471
+ async def test_error_handling(self):
472
+ """Test error handling for policy verification"""
473
+ # Test policy verification error
474
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(500)):
475
+ response = await mock_policy_verification_error(500)()
476
+ assert not response.ok
477
+ assert response.status_code == 500
478
+
479
+ # Test malformed request
480
+ with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(400)):
481
+ response = await mock_policy_verification_error(400)()
482
+ assert not response.ok
483
+ assert response.status_code == 400
484
+
485
+ if __name__ == "__main__":
486
+ pytest.main([__file__])
@@ -0,0 +1,78 @@
1
+
2
+ ## Required Context
3
+
4
+ This policy requires the following context (JSON Schema):
5
+
6
+ ```json
7
+ {
8
+ "$schema": "http://json-schema.org/draft-07/schema#",
9
+ "type": "object",
10
+ "required": [
11
+ "amount",
12
+ "currency",
13
+ "destination_type",
14
+ "destination_id",
15
+ "payout_method",
16
+ "idempotency_key"
17
+ ],
18
+ "properties": {
19
+ "amount": {
20
+ "type": "integer",
21
+ "minimum": 1,
22
+ "description": "Payout amount in minor units (e.g., cents)"
23
+ },
24
+ "currency": {
25
+ "type": "string",
26
+ "pattern": "^[A-Z]{3}$",
27
+ "description": "ISO 4217 currency code"
28
+ },
29
+ "destination_type": {
30
+ "type": "string",
31
+ "enum": [
32
+ "bank_account",
33
+ "digital_wallet",
34
+ "crypto_address"
35
+ ],
36
+ "description": "Type of destination account"
37
+ },
38
+ "destination_id": {
39
+ "type": "string",
40
+ "description": "Destination account identifier"
41
+ },
42
+ "payout_method": {
43
+ "type": "string",
44
+ "enum": [
45
+ "wire_transfer",
46
+ "ach",
47
+ "crypto_transfer",
48
+ "digital_wallet"
49
+ ],
50
+ "description": "Method of payout"
51
+ },
52
+ "idempotency_key": {
53
+ "type": "string",
54
+ "minLength": 8,
55
+ "description": "Idempotency key for duplicate prevention"
56
+ },
57
+ "description": {
58
+ "type": "string",
59
+ "description": "Optional payout description"
60
+ },
61
+ "compliance_notes": {
62
+ "type": "string",
63
+ "description": "Compliance verification notes"
64
+ },
65
+ "approval_required": {
66
+ "type": "boolean",
67
+ "description": "Whether payout requires manual approval"
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ You can also fetch this live via the discovery endpoint:
74
+
75
+ ```bash
76
+ curl -s "https://aport.io/api/policies/finance.payment.payout.v1?format=schema"
77
+ ```
78
+