@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,165 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException, Request, Response
|
|
2
|
+
from fastapi.responses import StreamingResponse
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from aport.middleware import require_policy
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
app = FastAPI(title="Data Export Service", version="1.0.0")
|
|
10
|
+
|
|
11
|
+
class ExportRequest(BaseModel):
|
|
12
|
+
format: str
|
|
13
|
+
include_pii: bool = False
|
|
14
|
+
filters: dict
|
|
15
|
+
|
|
16
|
+
class ExportStatus(BaseModel):
|
|
17
|
+
export_id: str
|
|
18
|
+
status: str
|
|
19
|
+
created_at: str
|
|
20
|
+
estimated_rows: int
|
|
21
|
+
actual_rows: Optional[int] = None
|
|
22
|
+
format: str
|
|
23
|
+
include_pii: bool
|
|
24
|
+
|
|
25
|
+
@app.post("/exports")
|
|
26
|
+
@require_policy("data.export.create.v1")
|
|
27
|
+
async def create_export(request: Request, export_data: ExportRequest):
|
|
28
|
+
try:
|
|
29
|
+
passport = request.state.policy_result.passport
|
|
30
|
+
|
|
31
|
+
# Check PII permission
|
|
32
|
+
if export_data.include_pii and not passport.limits.allow_pii:
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=403,
|
|
35
|
+
detail={
|
|
36
|
+
"error": "PII export not allowed",
|
|
37
|
+
"agent_id": passport.agent_id,
|
|
38
|
+
"upgrade_instructions": "Request PII export capability from your administrator"
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Estimate row count (in real app, query your database)
|
|
43
|
+
estimated_rows = await estimate_export_rows(export_data.filters)
|
|
44
|
+
|
|
45
|
+
# Check row limit
|
|
46
|
+
if estimated_rows > passport.limits.max_export_rows:
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=403,
|
|
49
|
+
detail={
|
|
50
|
+
"error": "Export exceeds row limit",
|
|
51
|
+
"requested": estimated_rows,
|
|
52
|
+
"limit": passport.limits.max_export_rows,
|
|
53
|
+
"upgrade_instructions": "Request smaller export or upgrade limits"
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Process export
|
|
58
|
+
export_id = await create_export_job({
|
|
59
|
+
"format": export_data.format,
|
|
60
|
+
"include_pii": export_data.include_pii,
|
|
61
|
+
"filters": export_data.filters,
|
|
62
|
+
"agent_id": passport.agent_id,
|
|
63
|
+
"agent_name": passport.name,
|
|
64
|
+
"estimated_rows": estimated_rows
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Log the export request
|
|
68
|
+
print(f"Export created: {export_id} ({estimated_rows} rows) by agent {passport.agent_id}")
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"success": True,
|
|
72
|
+
"export_id": export_id,
|
|
73
|
+
"format": export_data.format,
|
|
74
|
+
"estimated_rows": estimated_rows,
|
|
75
|
+
"status": "processing"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Export creation error: {e}")
|
|
80
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
81
|
+
|
|
82
|
+
@app.get("/exports/{export_id}")
|
|
83
|
+
@require_policy("data.export.create.v1")
|
|
84
|
+
async def get_export_status(request: Request, export_id: str):
|
|
85
|
+
try:
|
|
86
|
+
passport = request.state.policy_result.passport
|
|
87
|
+
|
|
88
|
+
export_info = await get_export_status(export_id, passport.agent_id)
|
|
89
|
+
|
|
90
|
+
if not export_info:
|
|
91
|
+
raise HTTPException(status_code=404, detail="Export not found")
|
|
92
|
+
|
|
93
|
+
return export_info
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Export status error: {e}")
|
|
97
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
98
|
+
|
|
99
|
+
@app.get("/exports/{export_id}/download")
|
|
100
|
+
@require_policy("data.export.create.v1")
|
|
101
|
+
async def download_export(request: Request, export_id: str):
|
|
102
|
+
try:
|
|
103
|
+
passport = request.state.policy_result.passport
|
|
104
|
+
|
|
105
|
+
export_file = await get_export_file(export_id, passport.agent_id)
|
|
106
|
+
|
|
107
|
+
if not export_file:
|
|
108
|
+
raise HTTPException(status_code=404, detail="Export file not found")
|
|
109
|
+
|
|
110
|
+
if export_file["status"] != "completed":
|
|
111
|
+
raise HTTPException(status_code=400, detail="Export not ready for download")
|
|
112
|
+
|
|
113
|
+
# Create streaming response
|
|
114
|
+
def generate_file():
|
|
115
|
+
yield export_file["data"]
|
|
116
|
+
|
|
117
|
+
return StreamingResponse(
|
|
118
|
+
generate_file(),
|
|
119
|
+
media_type=export_file["content_type"],
|
|
120
|
+
headers={"Content-Disposition": f"attachment; filename={export_file['filename']}"}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Export download error: {e}")
|
|
125
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
126
|
+
|
|
127
|
+
# Mock functions
|
|
128
|
+
async def estimate_export_rows(filters: dict) -> int:
|
|
129
|
+
"""Simulate database query to estimate rows"""
|
|
130
|
+
await asyncio.sleep(0.05)
|
|
131
|
+
return (hash(str(filters)) % 10000) + 1000
|
|
132
|
+
|
|
133
|
+
async def create_export_job(export_data: dict) -> str:
|
|
134
|
+
"""Simulate export creation"""
|
|
135
|
+
await asyncio.sleep(0.1)
|
|
136
|
+
return f"exp_{asyncio.get_event_loop().time()}_{hash(str(export_data)) % 1000000}"
|
|
137
|
+
|
|
138
|
+
async def get_export_status(export_id: str, agent_id: str) -> Optional[ExportStatus]:
|
|
139
|
+
"""Simulate export status lookup"""
|
|
140
|
+
await asyncio.sleep(0.05)
|
|
141
|
+
return ExportStatus(
|
|
142
|
+
export_id=export_id,
|
|
143
|
+
status="completed",
|
|
144
|
+
created_at="2024-01-16T00:00:00Z",
|
|
145
|
+
estimated_rows=5000,
|
|
146
|
+
actual_rows=4876,
|
|
147
|
+
format="csv",
|
|
148
|
+
include_pii=False
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def get_export_file(export_id: str, agent_id: str) -> Optional[dict]:
|
|
152
|
+
"""Simulate file retrieval"""
|
|
153
|
+
await asyncio.sleep(0.05)
|
|
154
|
+
return {
|
|
155
|
+
"data": "name,email,created_at\nJohn Doe,john@example.com,2024-01-01",
|
|
156
|
+
"content_type": "text/csv",
|
|
157
|
+
"filename": f"export_{export_id}.csv",
|
|
158
|
+
"status": "completed"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
import uvicorn
|
|
163
|
+
print("Data export service starting...")
|
|
164
|
+
print("Protected by APort data.export.create.v1 policy pack")
|
|
165
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "data.export.create.v1",
|
|
3
|
+
"name": "Data Export Protection Policy",
|
|
4
|
+
"description": "Pre-action governance for data export operations. Enforces row limits, PII handling requirements, and export capability validation.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "active",
|
|
7
|
+
"requires_capabilities": ["data.export"],
|
|
8
|
+
"min_assurance": "L1",
|
|
9
|
+
"limits_required": ["max_export_rows", "allow_pii"],
|
|
10
|
+
"required_fields": ["export_type", "format", "filters"],
|
|
11
|
+
"optional_fields": ["include_pii", "date_range", "columns"],
|
|
12
|
+
"enforcement": {
|
|
13
|
+
"rows_lte": "limits.data.export.max_export_rows",
|
|
14
|
+
"pii_allowed": "limits.data.export.allow_pii"
|
|
15
|
+
},
|
|
16
|
+
"mcp": {
|
|
17
|
+
"require_allowlisted_if_present": true
|
|
18
|
+
},
|
|
19
|
+
"advice": [
|
|
20
|
+
"Cache /verify with ETag; 60s TTL",
|
|
21
|
+
"Subscribe to status webhooks for instant suspend",
|
|
22
|
+
"Implement data retention policies",
|
|
23
|
+
"Consider Verifiable Attestation for sensitive exports",
|
|
24
|
+
"Log all export attempts for audit compliance",
|
|
25
|
+
"Implement progressive disclosure for large datasets",
|
|
26
|
+
"Use secure delivery methods for sensitive data"
|
|
27
|
+
],
|
|
28
|
+
"required_context": {
|
|
29
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["export_type", "format", "filters"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"export_type": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["users", "orders", "transactions", "analytics"],
|
|
36
|
+
"description": "Type of data to export"
|
|
37
|
+
},
|
|
38
|
+
"format": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["csv", "json", "xlsx", "parquet"],
|
|
41
|
+
"description": "Export format"
|
|
42
|
+
},
|
|
43
|
+
"filters": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "Filter criteria for the export",
|
|
46
|
+
"properties": {
|
|
47
|
+
"date_from": { "type": "string", "format": "date" },
|
|
48
|
+
"date_to": { "type": "string", "format": "date" },
|
|
49
|
+
"status": { "type": "string" },
|
|
50
|
+
"category": { "type": "string" }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"include_pii": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"description": "Whether to include personally identifiable information"
|
|
56
|
+
},
|
|
57
|
+
"date_range": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Date range for the export",
|
|
60
|
+
"properties": {
|
|
61
|
+
"start": { "type": "string", "format": "date" },
|
|
62
|
+
"end": { "type": "string", "format": "date" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"columns": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": { "type": "string" },
|
|
68
|
+
"description": "Specific columns to include"
|
|
69
|
+
},
|
|
70
|
+
"mcp_servers": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": { "type": "string" },
|
|
73
|
+
"description": "MCP servers being used in this request (e.g., [\"https://mcp.notion.com\"])"
|
|
74
|
+
},
|
|
75
|
+
"mcp_tools": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": { "type": "string" },
|
|
78
|
+
"description": "MCP tools being used in this request (e.g., [\"notion.pages.export\"])"
|
|
79
|
+
},
|
|
80
|
+
"mcp_server": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Single MCP server being used (backward compatibility - use mcp_servers array for multiple)"
|
|
83
|
+
},
|
|
84
|
+
"mcp_tool": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "Single MCP tool being used (backward compatibility - use mcp_tools array for multiple)"
|
|
87
|
+
},
|
|
88
|
+
"mcp_session": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "MCP session identifier for audit trail (optional)"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"evaluation_rules": [
|
|
95
|
+
{
|
|
96
|
+
"name": "passport_status_active",
|
|
97
|
+
"condition": "passport.status == 'active'",
|
|
98
|
+
"deny_code": "oap.passport_suspended",
|
|
99
|
+
"description": "Passport must be active"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "export_capability",
|
|
103
|
+
"condition": "'data.export' in passport.capabilities",
|
|
104
|
+
"deny_code": "oap.unknown_capability",
|
|
105
|
+
"description": "Agent must have data.export capability"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "row_limit_check",
|
|
109
|
+
"condition": "estimated_rows <= limits.max_export_rows",
|
|
110
|
+
"deny_code": "oap.limit_exceeded",
|
|
111
|
+
"description": "Export size must not exceed row limit"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "pii_handling",
|
|
115
|
+
"condition": "NOT (include_pii AND NOT limits.allow_pii)",
|
|
116
|
+
"deny_code": "oap.pii_not_allowed",
|
|
117
|
+
"description": "PII export not allowed for this agent"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "format_support",
|
|
121
|
+
"condition": "format in ['csv', 'json', 'xlsx', 'parquet']",
|
|
122
|
+
"deny_code": "oap.format_unsupported",
|
|
123
|
+
"description": "Export format must be supported"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"cache": {
|
|
127
|
+
"default_ttl_seconds": 60,
|
|
128
|
+
"suspend_invalidate_seconds": 30
|
|
129
|
+
},
|
|
130
|
+
"deprecation": null,
|
|
131
|
+
"created_at": "2025-01-16T00:00:00Z",
|
|
132
|
+
"updated_at": "2025-01-30T00:00:00Z"
|
|
133
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Report Data Ingestion Policy Pack v1
|
|
2
|
+
|
|
3
|
+
Protect your data ingestion endpoints with APort's standardized policy pack. This pack ensures only verified agents with proper capabilities, assurance levels, and data quality controls can ingest report data with appropriate validation and monitoring.
|
|
4
|
+
|
|
5
|
+
## What This Pack Protects
|
|
6
|
+
|
|
7
|
+
- **Route**: `/data/report/ingest/*` (POST)
|
|
8
|
+
- **Risk**: Data quality issues, stale data, unauthorized sources, compliance violations
|
|
9
|
+
- **Impact**: Poor reporting quality, regulatory violations, audit findings, data integrity issues
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
| Requirement | Value | Description |
|
|
14
|
+
|-------------|-------|-------------|
|
|
15
|
+
| **Capability** | `data.report.ingest` | Agent must have data ingestion capability |
|
|
16
|
+
| **Assurance** | `L2` or higher | Standard verification minimum |
|
|
17
|
+
| **Limits** | `approved_sources.{report_type}[]` | Approved data sources per report type |
|
|
18
|
+
| **Limits** | `max_data_age_seconds.{report_type}` | Maximum data age per report type |
|
|
19
|
+
| **Limits** | `max_data_size_mb.{report_type}` | Maximum data size per report type |
|
|
20
|
+
| **Limits** | `max_ingest_frequency_per_hour.{report_type}` | Maximum ingestion frequency |
|
|
21
|
+
| **Limits** | `data_quality_threshold.{report_type}` | Minimum data quality score |
|
|
22
|
+
| **Limits** | `required_validation_checks.{report_type}[]` | Required validation checks |
|
|
23
|
+
| **Regions** | Must match | Agent must be authorized in caller's region |
|
|
24
|
+
| **Idempotency** | Required | Prevents duplicate ingestion |
|
|
25
|
+
|
|
26
|
+
## Implementation
|
|
27
|
+
|
|
28
|
+
### Express.js
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
const { requirePolicy } = require('@aporthq/middleware-express');
|
|
32
|
+
|
|
33
|
+
// Option 1: Explicit agent ID (preferred)
|
|
34
|
+
app.post('/data/report/ingest',
|
|
35
|
+
requirePolicy('data.report.ingest.v1', 'ap_a2d10232c6534523812423eec8a1425c45678'),
|
|
36
|
+
async (req, res) => {
|
|
37
|
+
// Your data ingestion logic here
|
|
38
|
+
// req.policyResult contains the verified passport
|
|
39
|
+
const { report_type, data_source_id, data_timestamp } = req.body;
|
|
40
|
+
const passport = req.policyResult.passport;
|
|
41
|
+
|
|
42
|
+
// Process data ingestion...
|
|
43
|
+
res.json({ success: true, ingest_id: generateId() });
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Client Request Example:**
|
|
49
|
+
```javascript
|
|
50
|
+
fetch('/data/report/ingest', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'X-Agent-Passport-Id': 'ap_a2d10232c6534523812423eec8a1425c45678'
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
report_type: "ESG",
|
|
58
|
+
data_source_id: "api.climate-data.com",
|
|
59
|
+
data_timestamp: "2025-01-30T10:00:00Z",
|
|
60
|
+
metric_type: "carbon_emissions",
|
|
61
|
+
data_size_mb: 5.2,
|
|
62
|
+
validation_checks: ["schema_validation", "range_check"],
|
|
63
|
+
data_quality_score: 0.95,
|
|
64
|
+
idempotency_key: "ingest-esg-123"
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Best Practices
|
|
70
|
+
|
|
71
|
+
1. **Cache Verification**: Cache `/verify` responses with ETag for 300 seconds
|
|
72
|
+
2. **Webhook Integration**: Subscribe to `status.changed` webhooks for instant suspension
|
|
73
|
+
3. **Verifiable Attestation**: Log all data ingestion attempts for compliance
|
|
74
|
+
4. **Data Quality Scoring**: Implement data quality scoring before ingestion
|
|
75
|
+
5. **Data Lineage Tracking**: Use data lineage tracking for audit compliance
|
|
76
|
+
6. **Progressive Validation**: Implement progressive data validation checks
|
|
77
|
+
7. **Anomaly Monitoring**: Monitor for data anomalies and unusual patterns
|
|
78
|
+
8. **Data Encryption**: Use data encryption for sensitive report data
|
|
79
|
+
9. **Data Retention**: Implement data retention policies based on report type
|
|
80
|
+
10. **Source Reputation**: Maintain data source reputation scoring
|
|
81
|
+
11. **Idempotency**: Use idempotency keys to prevent duplicate ingestion
|
|
82
|
+
12. **Freshness Monitoring**: Implement data freshness monitoring and alerts
|
|
83
|
+
|
|
84
|
+
## Support
|
|
85
|
+
|
|
86
|
+
- [Documentation](https://aport.io/docs/policies/data.report.ingest.v1)
|
|
87
|
+
- [Community](https://github.com/aporthq/community)
|
|
88
|
+
- [Support](https://aport.io/support)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
**Last Updated**: 2025-01-30 00:00:00 UTC
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Required Context
|
|
95
|
+
|
|
96
|
+
This policy requires the following context (JSON Schema):
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
101
|
+
"type": "object",
|
|
102
|
+
"required": [
|
|
103
|
+
"report_type",
|
|
104
|
+
"data_source_id",
|
|
105
|
+
"data_timestamp"
|
|
106
|
+
],
|
|
107
|
+
"properties": {
|
|
108
|
+
"report_type": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "The type of report being generated (e.g., 'ESG', 'QuarterlyFinancials')."
|
|
111
|
+
},
|
|
112
|
+
"data_source_id": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "A unique identifier for the source of the data being ingested (e.g., 'api.climate-data.com', 'internal-hr-db')."
|
|
115
|
+
},
|
|
116
|
+
"data_timestamp": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"format": "date-time",
|
|
119
|
+
"description": "The ISO 8601 timestamp of when the data was generated."
|
|
120
|
+
},
|
|
121
|
+
"metric_type": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "The specific metric this data point relates to (e.g., 'carbon_emissions', 'employee_diversity')."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You can also fetch this live via the discovery endpoint:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -s "https://aport.io/api/policies/data.report.ingest.v1?format=schema"
|
|
133
|
+
```
|
|
134
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const { requirePolicy } = require("@aporthq/middleware-express");
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
app.use(express.json());
|
|
6
|
+
|
|
7
|
+
// Apply data.report.ingest policy to all data ingestion routes
|
|
8
|
+
app.post(
|
|
9
|
+
"/data/report/ingest",
|
|
10
|
+
requirePolicy("data.report.ingest.v1"),
|
|
11
|
+
async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const {
|
|
14
|
+
report_type,
|
|
15
|
+
data_source_id,
|
|
16
|
+
data_timestamp,
|
|
17
|
+
metric_type,
|
|
18
|
+
data_size_mb,
|
|
19
|
+
validation_checks,
|
|
20
|
+
data_quality_score,
|
|
21
|
+
ingest_reason,
|
|
22
|
+
idempotency_key,
|
|
23
|
+
} = req.body;
|
|
24
|
+
|
|
25
|
+
const passport = req.policyResult.passport;
|
|
26
|
+
|
|
27
|
+
// Additional business logic validation
|
|
28
|
+
if (!report_type || !data_source_id || !data_timestamp) {
|
|
29
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Process data ingestion using your data system
|
|
33
|
+
const ingest_id = await processDataIngestion({
|
|
34
|
+
report_type,
|
|
35
|
+
data_source_id,
|
|
36
|
+
data_timestamp,
|
|
37
|
+
metric_type,
|
|
38
|
+
data_size_mb,
|
|
39
|
+
validation_checks,
|
|
40
|
+
data_quality_score,
|
|
41
|
+
ingest_reason,
|
|
42
|
+
idempotency_key,
|
|
43
|
+
agent_id: passport.passport_id,
|
|
44
|
+
agent_name: passport.metadata?.template_name || "Unknown Agent",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Log the ingestion
|
|
48
|
+
console.log(
|
|
49
|
+
`Data ingestion processed: ${ingest_id} for ${report_type} report by agent ${passport.passport_id}`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
res.json({
|
|
53
|
+
success: true,
|
|
54
|
+
ingest_id,
|
|
55
|
+
report_type,
|
|
56
|
+
data_source_id,
|
|
57
|
+
data_timestamp,
|
|
58
|
+
status: "processed",
|
|
59
|
+
decision_id: req.policyResult.decision_id,
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Data ingestion processing error:", error);
|
|
63
|
+
res.status(500).json({ error: "Internal server error" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Mock data ingestion processing function
|
|
69
|
+
async function processDataIngestion({
|
|
70
|
+
report_type,
|
|
71
|
+
data_source_id,
|
|
72
|
+
data_timestamp,
|
|
73
|
+
metric_type,
|
|
74
|
+
data_size_mb,
|
|
75
|
+
validation_checks,
|
|
76
|
+
data_quality_score,
|
|
77
|
+
ingest_reason,
|
|
78
|
+
idempotency_key,
|
|
79
|
+
agent_id,
|
|
80
|
+
}) {
|
|
81
|
+
// Simulate data system call
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
83
|
+
|
|
84
|
+
// Log ingestion details for audit
|
|
85
|
+
console.log(`Processing data ingestion:`, {
|
|
86
|
+
report_type,
|
|
87
|
+
data_source_id,
|
|
88
|
+
data_timestamp,
|
|
89
|
+
metric_type,
|
|
90
|
+
data_size_mb,
|
|
91
|
+
validation_checks,
|
|
92
|
+
data_quality_score,
|
|
93
|
+
ingest_reason,
|
|
94
|
+
idempotency_key,
|
|
95
|
+
agent_id,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return `ingest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const PORT = process.env.PORT || 3000;
|
|
102
|
+
app.listen(PORT, () => {
|
|
103
|
+
console.log(`Data ingestion service running on port ${PORT}`);
|
|
104
|
+
console.log("Protected by APort data.report.ingest.v1 policy pack");
|
|
105
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Example: data.report.ingest.v1 Policy
|
|
3
|
+
*
|
|
4
|
+
* This is a quick-start example showing the basic usage of the data.report.ingest.v1 policy.
|
|
5
|
+
* For production use, see the full express.example.js file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const express = require("express");
|
|
9
|
+
const { requirePolicy } = require("@aporthq/middleware-express");
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
app.use(express.json());
|
|
13
|
+
|
|
14
|
+
// Minimal data ingestion endpoint with policy protection
|
|
15
|
+
app.post(
|
|
16
|
+
"/data/ingest",
|
|
17
|
+
requirePolicy("data.report.ingest.v1"),
|
|
18
|
+
async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const { report_type, data_source_id, data_timestamp } = req.body;
|
|
21
|
+
const passport = req.policyResult.passport;
|
|
22
|
+
|
|
23
|
+
// Process the data ingestion (your business logic here)
|
|
24
|
+
const ingest_id = `ingest_${Date.now()}`;
|
|
25
|
+
|
|
26
|
+
console.log(
|
|
27
|
+
`Data ingestion processed: ${ingest_id} for ${report_type} report`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
res.json({
|
|
31
|
+
success: true,
|
|
32
|
+
ingest_id,
|
|
33
|
+
report_type,
|
|
34
|
+
data_source_id,
|
|
35
|
+
data_timestamp,
|
|
36
|
+
decision_id: req.policyResult.decision_id,
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Data ingestion error:", error);
|
|
40
|
+
res.status(500).json({ error: "Internal server error" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Example client request
|
|
46
|
+
const exampleRequest = {
|
|
47
|
+
report_type: "ESG",
|
|
48
|
+
data_source_id: "api.climate-data.com",
|
|
49
|
+
data_timestamp: "2025-01-30T10:00:00Z",
|
|
50
|
+
metric_type: "carbon_emissions",
|
|
51
|
+
data_size_mb: 5.2,
|
|
52
|
+
validation_checks: ["schema_validation", "range_check"],
|
|
53
|
+
data_quality_score: 0.95,
|
|
54
|
+
idempotency_key: "ingest-esg-123",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
console.log("Example request:", JSON.stringify(exampleRequest, null, 2));
|
|
58
|
+
|
|
59
|
+
const PORT = process.env.PORT || 3000;
|
|
60
|
+
app.listen(PORT, () => {
|
|
61
|
+
console.log(`Minimal data ingestion service running on port ${PORT}`);
|
|
62
|
+
console.log("Protected by APort data.report.ingest.v1 policy");
|
|
63
|
+
console.log(
|
|
64
|
+
`Try: curl -X POST http://localhost:${PORT}/data/ingest -H "Content-Type: application/json" -d '${JSON.stringify(
|
|
65
|
+
exampleRequest
|
|
66
|
+
)}'`
|
|
67
|
+
);
|
|
68
|
+
});
|