@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.
- package/LICENSE +217 -0
- package/README.md +481 -0
- package/bin/agent-guardrails +133 -0
- package/bin/aport-create-passport.sh +444 -0
- package/bin/aport-cursor-hook.sh +90 -0
- package/bin/aport-guardrail-api.sh +108 -0
- package/bin/aport-guardrail-bash.sh +394 -0
- package/bin/aport-guardrail-v2.sh +5 -0
- package/bin/aport-guardrail.sh +5 -0
- package/bin/aport-resolve-paths.sh +71 -0
- package/bin/aport-status.sh +276 -0
- package/bin/frameworks/crewai.sh +49 -0
- package/bin/frameworks/cursor.sh +95 -0
- package/bin/frameworks/langchain.sh +48 -0
- package/bin/frameworks/n8n.sh +36 -0
- package/bin/frameworks/openclaw.sh +19 -0
- package/bin/lib/allowlist.sh +18 -0
- package/bin/lib/common.sh +28 -0
- package/bin/lib/config.sh +46 -0
- package/bin/lib/constants.sh +232 -0
- package/bin/lib/detect.sh +65 -0
- package/bin/lib/error.sh +269 -0
- package/bin/lib/passport.sh +19 -0
- package/bin/lib/templates/.gitkeep +1 -0
- package/bin/lib/templates/config.yaml +6 -0
- package/bin/lib/validation.sh +206 -0
- package/bin/openclaw +660 -0
- package/docs/ADDING_A_FRAMEWORK.md +87 -0
- package/docs/AGENTS.md.example +40 -0
- package/docs/CODE_REVIEW.md +192 -0
- package/docs/DEPLOYMENT_READINESS.md +81 -0
- package/docs/FAQ_SECURITY_SCANNERS.md +373 -0
- package/docs/FRAMEWORK_ROADMAP.md +41 -0
- package/docs/HOSTED_PASSPORT_SETUP.md +362 -0
- package/docs/IMPLEMENTING_YOUR_OWN_EVALUATOR.md +433 -0
- package/docs/OPENCLAW_COMPATIBILITY.md +73 -0
- package/docs/OPENCLAW_LOCAL_INTEGRATION.md +596 -0
- package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +54 -0
- package/docs/QUICKSTART.md +470 -0
- package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +470 -0
- package/docs/README.md +28 -0
- package/docs/RELEASE.md +87 -0
- package/docs/REPO_LAYOUT.md +47 -0
- package/docs/SKILLS_ECOSYSTEM_ANALYSIS_FEB17.md +1260 -0
- package/docs/TOOL_POLICY_MAPPING.md +46 -0
- package/docs/UPGRADE.md +46 -0
- package/docs/VERIFICATION_METHODS.md +97 -0
- package/docs/assets/README.md +8 -0
- package/docs/assets/porter.svg +54 -0
- package/docs/development/ERROR_CODES.md +616 -0
- package/docs/frameworks/GITHUB_ISSUE_PROPOSALS.md +1105 -0
- package/docs/frameworks/crewai.md +114 -0
- package/docs/frameworks/cursor.md +159 -0
- package/docs/frameworks/langchain.md +72 -0
- package/docs/frameworks/n8n.md +40 -0
- package/docs/frameworks/openclaw.md +40 -0
- package/docs/launch/ADD_APORT_AWESOME_LISTS_INSTRUCTIONS.md +146 -0
- package/docs/launch/ANNOUNCEMENT_GUIDE.md +266 -0
- package/docs/launch/AWESOME_REPOS.md +53 -0
- package/docs/launch/CURSOR_VSCODE_HOOKS_RESEARCH.md +77 -0
- package/docs/launch/DEMO_TERMINAL_OUTPUT.txt +48 -0
- package/docs/launch/DRY_AND_PLAN_CHECKLIST.md +47 -0
- package/docs/launch/EVIDENCE_README.md +61 -0
- package/docs/launch/EVIDENCE_TERMINAL_CAPTURE.txt +10 -0
- package/docs/launch/FRAMEWORK_SUPPORT_PLAN.md +1640 -0
- package/docs/launch/LAUNCH_READINESS_CHECKLIST.md +237 -0
- package/docs/launch/LAUNCH_STRATEGY_SUMMARY.md +464 -0
- package/docs/launch/OPENCLAW_FEEDBACK_AND_FIXES.md +85 -0
- package/docs/launch/POST_1_VALENTINE_IMPROVED.md +233 -0
- package/docs/launch/POST_2_GUARDRAIL_IMPROVED.md +369 -0
- package/docs/launch/PRE_LAUNCH_FIXES.md +766 -0
- package/docs/launch/QUICK_LAUNCH_CHECKLIST.md +400 -0
- package/docs/launch/READINESS_SUMMARY.md +262 -0
- package/docs/launch/README.md +68 -0
- package/docs/launch/USER_STORIES.md +327 -0
- package/docs/launch/scripts/add-aport-awesome-pr.sh +69 -0
- package/docs/operations/MONITORING.md +588 -0
- package/docs/reviews/2026-02-18-staff-review.md +268 -0
- package/extensions/openclaw-aport/README.md +415 -0
- package/extensions/openclaw-aport/index.js +625 -0
- package/extensions/openclaw-aport/openclaw-aport.js +7 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +46 -0
- package/extensions/openclaw-aport/package.json +36 -0
- package/extensions/openclaw-aport/test.js +307 -0
- package/external/aport-policies/README.md +363 -0
- package/external/aport-policies/agent.session.create.v1/README.md +345 -0
- package/external/aport-policies/agent.session.create.v1/policy.json +162 -0
- package/external/aport-policies/agent.tool.register.v1/README.md +361 -0
- package/external/aport-policies/agent.tool.register.v1/policy.json +172 -0
- package/external/aport-policies/code.release.publish.v1/README.md +51 -0
- package/external/aport-policies/code.release.publish.v1/policy.json +121 -0
- package/external/aport-policies/code.repository.merge.v1/README.md +287 -0
- package/external/aport-policies/code.repository.merge.v1/express.example.js +332 -0
- package/external/aport-policies/code.repository.merge.v1/fastapi.example.py +370 -0
- package/external/aport-policies/code.repository.merge.v1/policy.json +162 -0
- package/external/aport-policies/data.export.create.v1/README.md +226 -0
- package/external/aport-policies/data.export.create.v1/express.example.js +172 -0
- package/external/aport-policies/data.export.create.v1/fastapi.example.py +165 -0
- package/external/aport-policies/data.export.create.v1/policy.json +133 -0
- package/external/aport-policies/data.report.ingest.v1/README.md +134 -0
- package/external/aport-policies/data.report.ingest.v1/express.example.js +105 -0
- package/external/aport-policies/data.report.ingest.v1/minimal-example.js +68 -0
- package/external/aport-policies/data.report.ingest.v1/policy.json +174 -0
- package/external/aport-policies/finance.crypto.trade.v1/README.md +146 -0
- package/external/aport-policies/finance.crypto.trade.v1/express.example.js +109 -0
- package/external/aport-policies/finance.crypto.trade.v1/minimal-example.js +65 -0
- package/external/aport-policies/finance.crypto.trade.v1/policy.json +176 -0
- package/external/aport-policies/finance.payment.charge.v1/README.md +326 -0
- package/external/aport-policies/finance.payment.charge.v1/express.example.js +250 -0
- package/external/aport-policies/finance.payment.charge.v1/fastapi.example.py +227 -0
- package/external/aport-policies/finance.payment.charge.v1/minimal-example.js +64 -0
- package/external/aport-policies/finance.payment.charge.v1/policy.json +224 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/passport.instance.json +42 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/passport.template.json +40 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/payments-charge-policy.test.js +817 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py +486 -0
- package/external/aport-policies/finance.payment.payout.v1/README.md +78 -0
- package/external/aport-policies/finance.payment.payout.v1/policy.json +181 -0
- package/external/aport-policies/finance.payment.refund.v1/README.md +275 -0
- package/external/aport-policies/finance.payment.refund.v1/express.example.js +167 -0
- package/external/aport-policies/finance.payment.refund.v1/fastapi.example.py +136 -0
- package/external/aport-policies/finance.payment.refund.v1/minimal-example.js +183 -0
- package/external/aport-policies/finance.payment.refund.v1/policy.json +216 -0
- package/external/aport-policies/finance.payment.refund.v1/tests/refunds-policy.test.js +924 -0
- package/external/aport-policies/finance.payment.refund.v1/tests/test_refunds_policy.py +778 -0
- package/external/aport-policies/finance.transaction.execute.v1/README.md +309 -0
- package/external/aport-policies/finance.transaction.execute.v1/express.example.js +261 -0
- package/external/aport-policies/finance.transaction.execute.v1/fastapi.example.py +231 -0
- package/external/aport-policies/finance.transaction.execute.v1/minimal-example.js +78 -0
- package/external/aport-policies/finance.transaction.execute.v1/policy.json +189 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/passport.instance.json +42 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/passport.template.json +42 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/test_transactions_policy.py +214 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/transactions-policy.test.js +306 -0
- package/external/aport-policies/governance.data.access.v1/README.md +292 -0
- package/external/aport-policies/governance.data.access.v1/express.example.js +321 -0
- package/external/aport-policies/governance.data.access.v1/fastapi.example.py +279 -0
- package/external/aport-policies/governance.data.access.v1/minimal-example.js +65 -0
- package/external/aport-policies/governance.data.access.v1/policy.json +208 -0
- package/external/aport-policies/governance.data.access.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/governance.data.access.v1/tests/data-access-policy.test.js +308 -0
- package/external/aport-policies/governance.data.access.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/governance.data.access.v1/tests/passport.instance.json +56 -0
- package/external/aport-policies/governance.data.access.v1/tests/passport.template.json +56 -0
- package/external/aport-policies/governance.data.access.v1/tests/test_data_access_policy.py +214 -0
- package/external/aport-policies/legal.contract.review.v1/README.md +109 -0
- package/external/aport-policies/legal.contract.review.v1/policy.json +378 -0
- package/external/aport-policies/legal.contract.review.v1/tests/legal-contract-review-policy.test.js +609 -0
- package/external/aport-policies/legal.contract.review.v1/tests/passport.template.json +49 -0
- package/external/aport-policies/mcp.tool.execute.v1/README.md +301 -0
- package/external/aport-policies/mcp.tool.execute.v1/policy.json +141 -0
- package/external/aport-policies/messaging.message.send.v1/README.md +230 -0
- package/external/aport-policies/messaging.message.send.v1/express.example.js +183 -0
- package/external/aport-policies/messaging.message.send.v1/fastapi.example.py +193 -0
- package/external/aport-policies/messaging.message.send.v1/policy.json +144 -0
- package/external/aport-policies/policy-template.json +107 -0
- package/external/aport-policies/system.command.execute.v1/README.md +275 -0
- package/external/aport-policies/system.command.execute.v1/policy.json +146 -0
- package/external/aport-spec/CONTRIBUTING.md +273 -0
- package/external/aport-spec/LICENSE +21 -0
- package/external/aport-spec/README.md +168 -0
- package/external/aport-spec/conformance/README.md +294 -0
- package/external/aport-spec/conformance/cases/data.export.v1/contexts/allow_users.json +6 -0
- package/external/aport-spec/conformance/cases/data.export.v1/contexts/deny_pii.json +6 -0
- package/external/aport-spec/conformance/cases/data.export.v1/expected/allow_users.decision.json +19 -0
- package/external/aport-spec/conformance/cases/data.export.v1/expected/deny_pii.decision.json +19 -0
- package/external/aport-spec/conformance/cases/data.export.v1/passports/template.json +29 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/allow_50usd.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_150usd.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_currency.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/allow_50usd.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_150usd.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_currency.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/passports/template.json +42 -0
- package/external/aport-spec/conformance/package.json +44 -0
- package/external/aport-spec/conformance/pnpm-lock.yaml +642 -0
- package/external/aport-spec/conformance/src/cases.ts +371 -0
- package/external/aport-spec/conformance/src/ed25519.ts +167 -0
- package/external/aport-spec/conformance/src/jcs.ts +85 -0
- package/external/aport-spec/conformance/src/runner.ts +533 -0
- package/external/aport-spec/conformance/src/validators.ts +185 -0
- package/external/aport-spec/conformance/test-runner.js +315 -0
- package/external/aport-spec/conformance/tsconfig.json +21 -0
- package/external/aport-spec/error-schema.json +192 -0
- package/external/aport-spec/index.json +12 -0
- package/external/aport-spec/integrations/clawmoat/README.md +12 -0
- package/external/aport-spec/integrations/shield/README.md +245 -0
- package/external/aport-spec/integrations/shield/adapters/index.js +116 -0
- package/external/aport-spec/integrations/shield/adapters/system-command-execute.js +133 -0
- package/external/aport-spec/integrations/shield/test/README.md +58 -0
- package/external/aport-spec/integrations/shield/test/shield.md +40 -0
- package/external/aport-spec/integrations/shield/test/test-shield-to-verify.js +274 -0
- package/external/aport-spec/metrics-schema.json +504 -0
- package/external/aport-spec/oap/CHANGELOG.md +54 -0
- package/external/aport-spec/oap/VERSION.md +40 -0
- package/external/aport-spec/oap/capability-registry.md +229 -0
- package/external/aport-spec/oap/conformance.md +257 -0
- package/external/aport-spec/oap/decision-schema.json +114 -0
- package/external/aport-spec/oap/examples/context.refund.usd.50.json +9 -0
- package/external/aport-spec/oap/examples/decision.allow.sample.json +20 -0
- package/external/aport-spec/oap/examples/decision.deny.sample.json +23 -0
- package/external/aport-spec/oap/examples/passport.instance.v1.json +50 -0
- package/external/aport-spec/oap/examples/passport.template.v1.json +71 -0
- package/external/aport-spec/oap/oap-spec.md +426 -0
- package/external/aport-spec/oap/passport-schema.json +396 -0
- package/external/aport-spec/oap/security.md +213 -0
- package/external/aport-spec/oap/vc/context-oap-v1.jsonld +137 -0
- package/external/aport-spec/oap/vc/examples/oap-decision-vc.json +37 -0
- package/external/aport-spec/oap/vc/examples/oap-passport-vc.json +68 -0
- package/external/aport-spec/oap/vc/tools/INTEGRATION.md +375 -0
- package/external/aport-spec/oap/vc/tools/README.md +278 -0
- package/external/aport-spec/oap/vc/tools/examples/decision-to-vc.js +66 -0
- package/external/aport-spec/oap/vc/tools/examples/passport-to-vc.js +83 -0
- package/external/aport-spec/oap/vc/tools/examples/vc-to-decision.js +77 -0
- package/external/aport-spec/oap/vc/tools/examples/vc-to-passport.js +94 -0
- package/external/aport-spec/oap/vc/tools/package.json +38 -0
- package/external/aport-spec/oap/vc/tools/pnpm-lock.yaml +472 -0
- package/external/aport-spec/oap/vc/tools/src/cli.ts +226 -0
- package/external/aport-spec/oap/vc/tools/src/crypto-utils.ts +427 -0
- package/external/aport-spec/oap/vc/tools/src/index.ts +653 -0
- package/external/aport-spec/oap/vc/tools/src/test.ts +148 -0
- package/external/aport-spec/oap/vc/tools/src/vp.ts +382 -0
- package/external/aport-spec/oap/vc/tools/test-simple.js +214 -0
- package/external/aport-spec/oap/vc/tools/tsconfig.json +19 -0
- package/external/aport-spec/oap/vc/vc-mapping.md +443 -0
- package/external/aport-spec/passport-schema.json +586 -0
- package/external/aport-spec/rate-limiting.md +136 -0
- package/external/aport-spec/transport-profile.md +325 -0
- package/external/aport-spec/webhook-spec.md +314 -0
- package/package.json +70 -0
- package/skills/aport-agent-guardrail/SKILL.md +314 -0
- package/src/evaluator.js +252 -0
- package/src/server/index.js +72 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Implementing Your Own Local Evaluator
|
|
2
|
+
|
|
3
|
+
If you want to implement your own local policy evaluator instead of using the APort cloud API, you have everything you need in this repository!
|
|
4
|
+
|
|
5
|
+
## What You Have
|
|
6
|
+
|
|
7
|
+
### 1. Open Agent Passport (OAP) Specification
|
|
8
|
+
|
|
9
|
+
The complete OAP v1.0 specification is available at:
|
|
10
|
+
- **GitHub:** https://github.com/aporthq/aport-spec
|
|
11
|
+
- **Local:** `external/aport-spec/` (git submodule)
|
|
12
|
+
|
|
13
|
+
**Key documents:**
|
|
14
|
+
- `oap/oap-spec.md` - Complete specification
|
|
15
|
+
- `oap/passport-schema.json` - JSON schema for passports
|
|
16
|
+
- `oap/decision-schema.json` - JSON schema for decisions
|
|
17
|
+
- `oap/policy-schema.json` - JSON schema for policy packs
|
|
18
|
+
|
|
19
|
+
### 2. Policy Pack Definitions
|
|
20
|
+
|
|
21
|
+
All policy pack JSON files are available at:
|
|
22
|
+
- **GitHub:** https://github.com/aporthq/aport-policies
|
|
23
|
+
- **Local:** `external/aport-policies/` (git submodule)
|
|
24
|
+
|
|
25
|
+
Each policy pack includes:
|
|
26
|
+
- `policy.json` - Complete policy definition with evaluation rules
|
|
27
|
+
- `examples/` - Example contexts showing expected input format
|
|
28
|
+
- `tests/` - Test cases showing allow/deny scenarios
|
|
29
|
+
|
|
30
|
+
**Example policy pack structure:**
|
|
31
|
+
```
|
|
32
|
+
external/aport-policies/
|
|
33
|
+
system.command.execute.v1/
|
|
34
|
+
policy.json # Policy definition
|
|
35
|
+
examples/
|
|
36
|
+
allow-command.json # Example allowed context
|
|
37
|
+
deny-command.json # Example denied context
|
|
38
|
+
tests/
|
|
39
|
+
system-command.test.js # Test suite
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Evaluation Rules Format
|
|
43
|
+
|
|
44
|
+
Each policy pack contains `evaluation_rules` that define the policy logic:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"id": "system.command.execute.v1",
|
|
49
|
+
"evaluation_rules_version": "1.0",
|
|
50
|
+
"evaluation_rules": [
|
|
51
|
+
{
|
|
52
|
+
"name": "passport_status_active",
|
|
53
|
+
"type": "expression",
|
|
54
|
+
"condition": "passport.status == 'active'",
|
|
55
|
+
"deny_code": "oap.passport_suspended",
|
|
56
|
+
"description": "Passport must be active"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "command_allowed",
|
|
60
|
+
"type": "expression",
|
|
61
|
+
"condition": "context.command IN limits.allowed_commands",
|
|
62
|
+
"deny_code": "oap.command_not_allowed",
|
|
63
|
+
"description": "Command must be in allowed list"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "blocked_pattern_check",
|
|
67
|
+
"type": "custom_validator",
|
|
68
|
+
"validator": "validateBlockedPatterns",
|
|
69
|
+
"deny_code": "oap.blocked_pattern",
|
|
70
|
+
"description": "Command must not contain blocked patterns"
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Required Context Schema
|
|
77
|
+
|
|
78
|
+
Each policy pack defines the expected context format in `required_context`:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"required_context": {
|
|
83
|
+
"command": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "The command to execute",
|
|
86
|
+
"required": true
|
|
87
|
+
},
|
|
88
|
+
"args": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"description": "Command arguments",
|
|
91
|
+
"required": false
|
|
92
|
+
},
|
|
93
|
+
"cwd": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Working directory",
|
|
96
|
+
"required": false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Implementation Guide
|
|
103
|
+
|
|
104
|
+
### Option 1: Expression-Based Evaluator (Simple)
|
|
105
|
+
|
|
106
|
+
If you only need to support `type: "expression"` rules, you can implement a simple expression evaluator.
|
|
107
|
+
|
|
108
|
+
**Supported operators:**
|
|
109
|
+
- Comparison: `==`, `!=`, `>`, `<`, `>=`, `<=`
|
|
110
|
+
- Logical: `AND`, `OR`, `NOT`
|
|
111
|
+
- Membership: `IN`, `NOT IN`
|
|
112
|
+
- String: `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`
|
|
113
|
+
|
|
114
|
+
**Example implementation (pseudocode):**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
def evaluate_expression(rule, passport, context, limits):
|
|
118
|
+
"""
|
|
119
|
+
Evaluate an expression rule against passport + context
|
|
120
|
+
|
|
121
|
+
Returns: (allowed: bool, deny_code: str, message: str)
|
|
122
|
+
"""
|
|
123
|
+
condition = rule["condition"]
|
|
124
|
+
|
|
125
|
+
# Replace variables with values
|
|
126
|
+
condition = condition.replace("passport.status", f'"{passport["status"]}"')
|
|
127
|
+
condition = condition.replace("context.command", f'"{context["command"]}"')
|
|
128
|
+
condition = condition.replace("limits.allowed_commands", str(limits["allowed_commands"]))
|
|
129
|
+
|
|
130
|
+
# Handle IN operator
|
|
131
|
+
if " IN " in condition:
|
|
132
|
+
# e.g., "ls" IN ["ls", "pwd", "git"]
|
|
133
|
+
value, list_str = condition.split(" IN ")
|
|
134
|
+
value = value.strip().strip('"')
|
|
135
|
+
list_items = eval(list_str) # Parse list (use proper parser in production!)
|
|
136
|
+
result = value in list_items
|
|
137
|
+
else:
|
|
138
|
+
# Simple comparison
|
|
139
|
+
result = eval(condition) # Use proper expression parser in production!
|
|
140
|
+
|
|
141
|
+
if result:
|
|
142
|
+
return (True, None, None)
|
|
143
|
+
else:
|
|
144
|
+
return (False, rule["deny_code"], rule["description"])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**⚠️ Security Warning:** Don't use `eval()` in production! Use a proper expression parser that restricts operations.
|
|
148
|
+
|
|
149
|
+
### Option 2: Full Evaluator (Advanced)
|
|
150
|
+
|
|
151
|
+
For a complete implementation with custom validators, you'll need:
|
|
152
|
+
|
|
153
|
+
1. **Expression evaluator** - Handle all expression types safely
|
|
154
|
+
2. **Custom validator registry** - Map validator names to functions
|
|
155
|
+
3. **Standard checks** - Passport status, capabilities, assurance levels
|
|
156
|
+
4. **Decision builder** - Create OAP v1.0 compliant decision objects
|
|
157
|
+
|
|
158
|
+
**Example implementation structure:**
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
class PolicyEvaluator:
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.custom_validators = {
|
|
164
|
+
"validateBlockedPatterns": self.validate_blocked_patterns,
|
|
165
|
+
"validateRateLimit": self.validate_rate_limit,
|
|
166
|
+
# ... more validators
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def evaluate_policy(self, policy_pack, passport, context):
|
|
170
|
+
"""
|
|
171
|
+
Evaluate a policy pack against passport + context
|
|
172
|
+
|
|
173
|
+
Returns: Decision (OAP v1.0 compliant)
|
|
174
|
+
"""
|
|
175
|
+
# 1. Standard checks
|
|
176
|
+
if passport["status"] != "active":
|
|
177
|
+
return self.deny("oap.passport_suspended", "Passport is suspended")
|
|
178
|
+
|
|
179
|
+
# 2. Capability check
|
|
180
|
+
required_caps = policy_pack.get("requires_capabilities", [])
|
|
181
|
+
passport_caps = [c["id"] for c in passport.get("capabilities", [])]
|
|
182
|
+
for cap in required_caps:
|
|
183
|
+
if cap not in passport_caps:
|
|
184
|
+
return self.deny("oap.unknown_capability", f"Missing capability: {cap}")
|
|
185
|
+
|
|
186
|
+
# 3. Assurance level check
|
|
187
|
+
min_assurance = policy_pack.get("min_assurance", "L0")
|
|
188
|
+
if not self.check_assurance(passport["assurance_level"], min_assurance):
|
|
189
|
+
return self.deny("oap.assurance_insufficient", "Insufficient assurance level")
|
|
190
|
+
|
|
191
|
+
# 4. Evaluation rules
|
|
192
|
+
limits = passport.get("limits", {})
|
|
193
|
+
for rule in policy_pack.get("evaluation_rules", []):
|
|
194
|
+
if rule["type"] == "expression":
|
|
195
|
+
result = self.evaluate_expression(rule, passport, context, limits)
|
|
196
|
+
elif rule["type"] == "custom_validator":
|
|
197
|
+
validator_fn = self.custom_validators[rule["validator"]]
|
|
198
|
+
result = validator_fn(passport, context, limits)
|
|
199
|
+
else:
|
|
200
|
+
result = (False, "oap.policy_error", f"Unknown rule type: {rule['type']}")
|
|
201
|
+
|
|
202
|
+
if not result[0]: # If denied
|
|
203
|
+
return self.deny(result[1], result[2])
|
|
204
|
+
|
|
205
|
+
# 5. All checks passed
|
|
206
|
+
return self.allow()
|
|
207
|
+
|
|
208
|
+
def deny(self, code, message):
|
|
209
|
+
return {
|
|
210
|
+
"decision_id": f"local-{uuid.uuid4()}",
|
|
211
|
+
"policy_id": "system.command.execute.v1",
|
|
212
|
+
"passport_id": passport["passport_id"],
|
|
213
|
+
"owner_id": passport["owner_id"],
|
|
214
|
+
"assurance_level": passport["assurance_level"],
|
|
215
|
+
"allow": False,
|
|
216
|
+
"reasons": [{"code": code, "message": message}],
|
|
217
|
+
"issued_at": datetime.utcnow().isoformat() + "Z",
|
|
218
|
+
"expires_at": (datetime.utcnow() + timedelta(hours=1)).isoformat() + "Z",
|
|
219
|
+
"passport_digest": self.compute_digest(passport),
|
|
220
|
+
"signature": "ed25519:local-unsigned",
|
|
221
|
+
"kid": "oap:local:dev-key"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def allow(self):
|
|
225
|
+
# Similar to deny() but with allow=True
|
|
226
|
+
pass
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Option 3: Fork the APort Evaluator (Easiest)
|
|
230
|
+
|
|
231
|
+
The APort cloud API uses a generic evaluator that's designed to work with any policy pack. If you want the exact same implementation, you can:
|
|
232
|
+
|
|
233
|
+
1. **Contact us** - We may open-source the core evaluator in the future
|
|
234
|
+
2. **Request access** - We can provide the evaluator source code for enterprise customers
|
|
235
|
+
3. **Use the API** - The easiest option (see `src/evaluator.js` in this repo)
|
|
236
|
+
|
|
237
|
+
## Testing Your Implementation
|
|
238
|
+
|
|
239
|
+
### Test Data
|
|
240
|
+
|
|
241
|
+
Use the test cases from the policy pack directories:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Get test passport
|
|
245
|
+
cat external/aport-policies/system.command.execute.v1/tests/fixtures/passport.json
|
|
246
|
+
|
|
247
|
+
# Get test contexts (allow case)
|
|
248
|
+
cat external/aport-policies/system.command.execute.v1/examples/allow-command.json
|
|
249
|
+
|
|
250
|
+
# Get test contexts (deny case)
|
|
251
|
+
cat external/aport-policies/system.command.execute.v1/examples/deny-command.json
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Test Cases
|
|
255
|
+
|
|
256
|
+
Each policy pack includes a test suite showing expected behavior:
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
// Example from system.command.execute.v1/tests/system-command.test.js
|
|
260
|
+
describe("system.command.execute.v1", () => {
|
|
261
|
+
it("should allow commands in allowed list", async () => {
|
|
262
|
+
const decision = await evaluatePolicy(policyPack, passport, {
|
|
263
|
+
command: "ls",
|
|
264
|
+
args: ["-la"]
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(decision.allow).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should deny commands not in allowed list", async () => {
|
|
271
|
+
const decision = await evaluatePolicy(policyPack, passport, {
|
|
272
|
+
command: "rm",
|
|
273
|
+
args: ["-rf", "/"]
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(decision.allow).toBe(false);
|
|
277
|
+
expect(decision.reasons[0].code).toBe("oap.command_not_allowed");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should deny blocked patterns", async () => {
|
|
281
|
+
const decision = await evaluatePolicy(policyPack, passport, {
|
|
282
|
+
command: "ls ; rm -rf /" // Command injection
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(decision.allow).toBe(false);
|
|
286
|
+
expect(decision.reasons[0].code).toBe("oap.blocked_pattern");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Compliance Validation
|
|
292
|
+
|
|
293
|
+
Your implementation should pass the OAP v1.0 compliance tests:
|
|
294
|
+
|
|
295
|
+
1. **Passport validation** - Check status, spec_version, required fields
|
|
296
|
+
2. **Capability validation** - Check passport has required capabilities
|
|
297
|
+
3. **Assurance validation** - Check passport meets minimum assurance level
|
|
298
|
+
4. **Context validation** - Check context has required fields
|
|
299
|
+
5. **Decision format** - Check decision matches OAP v1.0 schema
|
|
300
|
+
6. **Signature** - Check decision is properly signed (or marked unsigned)
|
|
301
|
+
|
|
302
|
+
## Example Implementations
|
|
303
|
+
|
|
304
|
+
### Bash (Simple)
|
|
305
|
+
|
|
306
|
+
See `bin/aport-guardrail.sh` in this repo for a basic bash implementation. **Note:** This only handles simple checks and is not production-ready.
|
|
307
|
+
|
|
308
|
+
### Node.js (API Client)
|
|
309
|
+
|
|
310
|
+
See `src/evaluator.js` in this repo for a Node.js client that calls the APort cloud API.
|
|
311
|
+
|
|
312
|
+
### Python (Full Implementation)
|
|
313
|
+
|
|
314
|
+
Coming soon! We're working on a reference implementation in Python.
|
|
315
|
+
|
|
316
|
+
### Go (Full Implementation)
|
|
317
|
+
|
|
318
|
+
Coming soon! We're working on a reference implementation in Go.
|
|
319
|
+
|
|
320
|
+
## Security Considerations
|
|
321
|
+
|
|
322
|
+
### Expression Evaluation
|
|
323
|
+
|
|
324
|
+
⚠️ **Never use `eval()` or `exec()` directly** - This allows arbitrary code execution
|
|
325
|
+
|
|
326
|
+
✅ **Use a restricted expression parser** - Only allow specific operators and functions
|
|
327
|
+
|
|
328
|
+
Example of a safe expression parser (Python):
|
|
329
|
+
```python
|
|
330
|
+
import ast
|
|
331
|
+
import operator
|
|
332
|
+
|
|
333
|
+
# Whitelist of allowed operations
|
|
334
|
+
ALLOWED_OPS = {
|
|
335
|
+
ast.Eq: operator.eq,
|
|
336
|
+
ast.NotEq: operator.ne,
|
|
337
|
+
ast.Lt: operator.lt,
|
|
338
|
+
ast.LtE: operator.le,
|
|
339
|
+
ast.Gt: operator.gt,
|
|
340
|
+
ast.GtE: operator.ge,
|
|
341
|
+
ast.And: operator.and_,
|
|
342
|
+
ast.Or: operator.or_,
|
|
343
|
+
ast.Not: operator.not_,
|
|
344
|
+
ast.In: lambda x, y: x in y,
|
|
345
|
+
ast.NotIn: lambda x, y: x not in y,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
def safe_eval(expr, context):
|
|
349
|
+
"""
|
|
350
|
+
Safely evaluate an expression with restricted operations
|
|
351
|
+
"""
|
|
352
|
+
tree = ast.parse(expr, mode='eval')
|
|
353
|
+
|
|
354
|
+
def eval_node(node):
|
|
355
|
+
if isinstance(node, ast.Expression):
|
|
356
|
+
return eval_node(node.body)
|
|
357
|
+
elif isinstance(node, ast.Compare):
|
|
358
|
+
left = eval_node(node.left)
|
|
359
|
+
result = left
|
|
360
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
361
|
+
if type(op) not in ALLOWED_OPS:
|
|
362
|
+
raise ValueError(f"Operation {type(op).__name__} not allowed")
|
|
363
|
+
right = eval_node(comparator)
|
|
364
|
+
result = ALLOWED_OPS[type(op)](result, right)
|
|
365
|
+
return result
|
|
366
|
+
elif isinstance(node, ast.Name):
|
|
367
|
+
# Look up variable in context
|
|
368
|
+
return context.get(node.id)
|
|
369
|
+
elif isinstance(node, ast.Constant):
|
|
370
|
+
return node.value
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(f"Node type {type(node).__name__} not allowed")
|
|
373
|
+
|
|
374
|
+
return eval_node(tree)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Custom Validators
|
|
378
|
+
|
|
379
|
+
Custom validators should:
|
|
380
|
+
- ✅ Be pure functions (no side effects)
|
|
381
|
+
- ✅ Have timeouts (don't block forever)
|
|
382
|
+
- ✅ Validate all inputs (don't trust context data)
|
|
383
|
+
- ✅ Return consistent formats (allow/deny, code, message)
|
|
384
|
+
- ⚠️ Be careful with DB lookups (rate limits, caching)
|
|
385
|
+
|
|
386
|
+
### Decision Signing
|
|
387
|
+
|
|
388
|
+
For production use, decisions should be Ed25519 signed:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
import nacl.signing
|
|
392
|
+
import nacl.encoding
|
|
393
|
+
import json
|
|
394
|
+
|
|
395
|
+
# Generate signing key (do this once, store securely)
|
|
396
|
+
signing_key = nacl.signing.SigningKey.generate()
|
|
397
|
+
verify_key = signing_key.verify_key
|
|
398
|
+
|
|
399
|
+
# Sign decision
|
|
400
|
+
def sign_decision(decision, signing_key):
|
|
401
|
+
# Create canonical JSON (sorted keys)
|
|
402
|
+
canonical = json.dumps(decision, sort_keys=True, separators=(',', ':'))
|
|
403
|
+
|
|
404
|
+
# Sign with Ed25519
|
|
405
|
+
signature = signing_key.sign(canonical.encode('utf-8'))
|
|
406
|
+
|
|
407
|
+
# Add signature to decision
|
|
408
|
+
decision["signature"] = f"ed25519:{signature.signature.hex()}"
|
|
409
|
+
decision["kid"] = "oap:local:your-key-id"
|
|
410
|
+
|
|
411
|
+
return decision
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Contributing
|
|
415
|
+
|
|
416
|
+
If you implement your own evaluator, we'd love to hear about it!
|
|
417
|
+
|
|
418
|
+
- Share your implementation: https://github.com/aporthq/aport-agent-guardrails/discussions
|
|
419
|
+
- Submit a PR: https://github.com/aporthq/aport-agent-guardrails/pulls
|
|
420
|
+
- Join Discord: https://discord.gg/aport
|
|
421
|
+
|
|
422
|
+
## Resources
|
|
423
|
+
|
|
424
|
+
- **OAP Spec:** https://github.com/aporthq/aport-spec
|
|
425
|
+
- **Policy Packs:** https://github.com/aporthq/aport-policies
|
|
426
|
+
- **API Reference:** https://api.aport.io/docs
|
|
427
|
+
- **Discord:** https://discord.gg/aport
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
All OAP specifications and policy pack definitions are Apache 2.0 licensed. You're free to implement your own evaluator using these specifications.
|
|
432
|
+
|
|
433
|
+
The APort cloud API is proprietary software. The reference evaluator implementation (if/when open sourced) will be Apache 2.0 licensed.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# OpenClaw Compatibility
|
|
2
|
+
|
|
3
|
+
**Last reviewed:** February 2026 (OpenClaw CHANGELOG 2026.2.15 / 2026.2.14, [docs.openclaw.ai](https://docs.openclaw.ai), [architecture](https://docs.openclaw.ai/concepts/architecture))
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Latest OpenClaw release (CHANGELOG)
|
|
8
|
+
|
|
9
|
+
- **Releases:** [github.com/openclaw/openclaw/blob/main/CHANGELOG.md](https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md)
|
|
10
|
+
- **Docs:** [docs.openclaw.ai](https://docs.openclaw.ai)
|
|
11
|
+
|
|
12
|
+
### Relevant to APort
|
|
13
|
+
|
|
14
|
+
| Area | Notes |
|
|
15
|
+
|------|--------|
|
|
16
|
+
| **Paths** | Config/state: `~/.openclaw`. Override: `OPENCLAW_HOME` / `OPENCLAW_STATE_DIR`. We default to `~/.openclaw` and respect `OPENCLAW_HOME` in `bin/openclaw`. |
|
|
17
|
+
| **Skills** | Managed skills: `~/.openclaw/skills/`. We install `skills/aport-guardrail/SKILL.md` there. OpenClaw watches `SKILL.md` when refreshing skills. |
|
|
18
|
+
| **Workspace** | `~/.openclaw/workspace` (AGENTS.md, TOOLS.md, SOUL.md, workspace/skills). Setup **auto-installs** the APort rule into `workspace/AGENTS.md` (create or append). No manual merge. |
|
|
19
|
+
| **Breaking (2026.2.13)** | Legacy `.moltbot` auto-detection removed; everything is `~/.openclaw`. No impact (we never used .moltbot). |
|
|
20
|
+
| **Messaging** | `openclaw message send` and cron use `target` (not just `to`/`channelId`). Our tool name `messaging.message.send` and context JSON are independent; no change needed. |
|
|
21
|
+
| **Hooks** | `before_tool_call` / `after_tool_call` exist. **Deterministic** enforcement requires hooks (or core integration); see below. |
|
|
22
|
+
|
|
23
|
+
### Enforcement model: AGENTS.md is best-effort, not deterministic
|
|
24
|
+
|
|
25
|
+
**Current approach (AGENTS.md + skill):** The APort rule is written into `workspace/AGENTS.md` so the agent is *instructed* to call the guardrail script before effectful actions. That is **best-effort**: the LLM may skip it, forget it, or be prompted to bypass it. It is **not** a guarantee that every tool run is checked.
|
|
26
|
+
|
|
27
|
+
**Purpose of APort:** Pre-action **authorization** should be enforced by the **platform** (OpenClaw calling the guardrail before executing a tool), not by the model following a prompt. Same outcome every time = deterministic enforcement.
|
|
28
|
+
|
|
29
|
+
**Intended production approach:** Use a **hook** (`before_tool_call`) or core integration so OpenClaw invokes the guardrail **before** every tool execution, regardless of what the agent “decides.” The OpenClaw plugin in this repo provides that hook. Until the plugin is installed, the AGENTS.md rule is a **stopgap** to get policy and audit in place; it does not replace deterministic enforcement.
|
|
30
|
+
|
|
31
|
+
### Running with a project-specific OpenClaw home
|
|
32
|
+
|
|
33
|
+
If you set `OPENCLAW_HOME` to a project dir (e.g. `.../valentine-openclaw`), OpenClaw uses that dir as `~/.openclaw` for that process: config, auth, workspace, and skills all live under that dir. The agent will use the APort passport and `.skills` from that project.
|
|
34
|
+
|
|
35
|
+
**Auth / model provider:** That home has its own auth store (e.g. `$OPENCLAW_HOME/.openclaw/agents/main/agent/auth-profiles.json`). It does **not** use your default `~/.openclaw` credentials. So if you only have OpenAI (or WhatsApp, Brave, ElevenLabs, etc.) set up in the default install, you must either:
|
|
36
|
+
|
|
37
|
+
1. **Configure auth for the project home**
|
|
38
|
+
Run the wizard with that home set, then add your provider (e.g. OpenAI):
|
|
39
|
+
```bash
|
|
40
|
+
OPENCLAW_HOME=/path/to/valentine-openclaw openclaw configure
|
|
41
|
+
# Add OpenAI (or your provider) when prompted; or
|
|
42
|
+
OPENCLAW_HOME=/path/to/valentine-openclaw openclaw models auth add
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **Copy auth from your default install**
|
|
46
|
+
Copy the agent auth (and optionally credentials) from `~/.openclaw` into `$OPENCLAW_HOME/.openclaw` so the project home sees the same providers (e.g. OpenAI, WhatsApp). Only do this on a machine you control and keep the project dir private.
|
|
47
|
+
|
|
48
|
+
After that, run the agent with that home and a session target:
|
|
49
|
+
```bash
|
|
50
|
+
OPENCLAW_HOME=/path/to/valentine-openclaw openclaw agent --local --session-id my-test --message "Run node --version and report. Use APort guardrail before any command per AGENTS.md."
|
|
51
|
+
```
|
|
52
|
+
APort decisions will be written to `$OPENCLAW_HOME/decision.json` and `$OPENCLAW_HOME/audit.log`.
|
|
53
|
+
|
|
54
|
+
### What we should add (optional)
|
|
55
|
+
|
|
56
|
+
- **Document OPENCLAW_HOME:** In README or QUICKSTART, mention that if users set `OPENCLAW_HOME` (e.g. to a project-specific dir), our script uses it as the default config dir.
|
|
57
|
+
- **OpenClaw version note:** In README or this doc, state we align with OpenClaw 2026.2.x layout (`~/.openclaw`, `~/.openclaw/skills`, `~/.openclaw/workspace`).
|
|
58
|
+
|
|
59
|
+
### Blockers and risks
|
|
60
|
+
|
|
61
|
+
- **No blockers.** Our integration is script-based (passport + guardrail script + AGENTS.md + optional plugin). The plugin uses OpenClaw's `before_tool_call` hook for deterministic enforcement.
|
|
62
|
+
- **Low risk:** If OpenClaw changes the managed-skill path (e.g. from `~/.openclaw/skills` to another dir), we would update `bin/openclaw` install path; CHANGELOG shows no such change.
|
|
63
|
+
- **Low risk:** Our tool names (`system.command.execute`, `messaging.message.send`, etc.) are our own convention for policy mapping, not OpenClaw tool IDs; renames of OpenClaw’s internal tool names do not block us.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 2. References
|
|
68
|
+
|
|
69
|
+
- OpenClaw CHANGELOG: https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md
|
|
70
|
+
- OpenClaw docs: https://docs.openclaw.ai
|
|
71
|
+
- Gateway architecture: https://docs.openclaw.ai/concepts/architecture
|
|
72
|
+
- Integrations: https://openclaw.ai/integrations
|
|
73
|
+
- OpenClaw plugin: [extensions/openclaw-aport/README.md](../extensions/openclaw-aport/README.md)
|