@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,183 @@
|
|
|
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 messaging policy to all messaging routes
|
|
8
|
+
app.post(
|
|
9
|
+
"/messages",
|
|
10
|
+
requirePolicy("messaging.message.send.v1"),
|
|
11
|
+
async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const { channel, recipients, content, mentions } = req.body;
|
|
14
|
+
const passport = req.policyResult.passport;
|
|
15
|
+
|
|
16
|
+
// Check channel allowlist
|
|
17
|
+
const allowedChannels =
|
|
18
|
+
passport.capabilities.find((cap) => cap.id === "messaging.send")?.params
|
|
19
|
+
?.channels_allowlist || [];
|
|
20
|
+
|
|
21
|
+
if (allowedChannels.length > 0 && !allowedChannels.includes(channel)) {
|
|
22
|
+
return res.status(403).json({
|
|
23
|
+
error: "Channel not allowed",
|
|
24
|
+
allowed_channels: allowedChannels,
|
|
25
|
+
upgrade_instructions:
|
|
26
|
+
"Add channel to your passport's channels_allowlist",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check mention policy
|
|
31
|
+
const mentionPolicy =
|
|
32
|
+
passport.capabilities.find((cap) => cap.id === "messaging.send")?.params
|
|
33
|
+
?.mention_policy || "limited";
|
|
34
|
+
|
|
35
|
+
if (mentions && mentions.length > 0) {
|
|
36
|
+
if (mentionPolicy === "none") {
|
|
37
|
+
return res.status(403).json({
|
|
38
|
+
error: "Mentions not allowed",
|
|
39
|
+
mention_policy: mentionPolicy,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (mentionPolicy === "limited" && mentions.includes("@everyone")) {
|
|
43
|
+
return res.status(403).json({
|
|
44
|
+
error: "@everyone mentions not allowed with limited mention policy",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Rate limiting check (would integrate with actual rate limiter)
|
|
50
|
+
const rateLimitCheck = await checkMessageRateLimit(passport.agent_id);
|
|
51
|
+
if (!rateLimitCheck.allowed) {
|
|
52
|
+
return res.status(429).json({
|
|
53
|
+
error: "Rate limit exceeded",
|
|
54
|
+
retry_after: rateLimitCheck.retry_after,
|
|
55
|
+
limits: {
|
|
56
|
+
per_minute: passport.limits.msgs_per_min,
|
|
57
|
+
per_day: passport.limits.msgs_per_day,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Send message using your messaging service
|
|
63
|
+
const message_id = await sendMessage({
|
|
64
|
+
channel,
|
|
65
|
+
recipients,
|
|
66
|
+
content,
|
|
67
|
+
mentions,
|
|
68
|
+
agent_id: passport.agent_id,
|
|
69
|
+
agent_name: passport.name,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Record usage for rate limiting
|
|
73
|
+
await recordMessageUsage(passport.agent_id);
|
|
74
|
+
|
|
75
|
+
// Log the message
|
|
76
|
+
console.log(
|
|
77
|
+
`Message sent: ${message_id} to ${channel} by agent ${passport.agent_id}`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
res.json({
|
|
81
|
+
success: true,
|
|
82
|
+
message_id,
|
|
83
|
+
channel,
|
|
84
|
+
recipients: recipients.length,
|
|
85
|
+
status: "sent",
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error("Message sending error:", error);
|
|
89
|
+
res.status(500).json({ error: "Internal server error" });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Broadcast message endpoint
|
|
95
|
+
app.post(
|
|
96
|
+
"/messages/broadcast",
|
|
97
|
+
requirePolicy("messaging.message.send.v1"),
|
|
98
|
+
async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const { channels, content, mentions } = req.body;
|
|
101
|
+
const passport = req.policyResult.passport;
|
|
102
|
+
|
|
103
|
+
// Check if broadcast is within daily limits
|
|
104
|
+
const estimatedMessages = channels.length;
|
|
105
|
+
const dailyUsage = await getDailyMessageUsage(passport.agent_id);
|
|
106
|
+
|
|
107
|
+
if (dailyUsage + estimatedMessages > passport.limits.msgs_per_day) {
|
|
108
|
+
return res.status(403).json({
|
|
109
|
+
error: "Broadcast would exceed daily message limit",
|
|
110
|
+
current_usage: dailyUsage,
|
|
111
|
+
limit: passport.limits.msgs_per_day,
|
|
112
|
+
requested: estimatedMessages,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Process broadcast
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const channel of channels) {
|
|
119
|
+
try {
|
|
120
|
+
const message_id = await sendMessage({
|
|
121
|
+
channel,
|
|
122
|
+
content,
|
|
123
|
+
mentions,
|
|
124
|
+
agent_id: passport.agent_id,
|
|
125
|
+
agent_name: passport.name,
|
|
126
|
+
});
|
|
127
|
+
results.push({ channel, message_id, status: "sent" });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
results.push({ channel, error: error.message, status: "failed" });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Record usage
|
|
134
|
+
await recordMessageUsage(passport.agent_id, channels.length);
|
|
135
|
+
|
|
136
|
+
res.json({
|
|
137
|
+
success: true,
|
|
138
|
+
results,
|
|
139
|
+
total_sent: results.filter((r) => r.status === "sent").length,
|
|
140
|
+
total_failed: results.filter((r) => r.status === "failed").length,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("Broadcast error:", error);
|
|
144
|
+
res.status(500).json({ error: "Internal server error" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Mock functions (implement with your actual messaging service)
|
|
150
|
+
async function sendMessage({
|
|
151
|
+
channel,
|
|
152
|
+
recipients,
|
|
153
|
+
content,
|
|
154
|
+
mentions,
|
|
155
|
+
agent_id,
|
|
156
|
+
agent_name,
|
|
157
|
+
}) {
|
|
158
|
+
// Simulate message sending
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
160
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function checkMessageRateLimit(agent_id) {
|
|
164
|
+
// Implement with Redis or in-memory rate limiter
|
|
165
|
+
// For demo, always allow
|
|
166
|
+
return { allowed: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function recordMessageUsage(agent_id, count = 1) {
|
|
170
|
+
// Record usage in your rate limiting system
|
|
171
|
+
console.log(`Recorded ${count} message(s) for agent ${agent_id}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function getDailyMessageUsage(agent_id) {
|
|
175
|
+
// Get current daily usage from your tracking system
|
|
176
|
+
return 0; // Mock value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const PORT = process.env.PORT || 3000;
|
|
180
|
+
app.listen(PORT, () => {
|
|
181
|
+
console.log(`Messaging service running on port ${PORT}`);
|
|
182
|
+
console.log("Protected by APort messaging.message.send.v1 policy pack");
|
|
183
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from aport.middleware import require_policy
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
app = FastAPI(title="Messaging Service", version="1.0.0")
|
|
8
|
+
|
|
9
|
+
class MessageRequest(BaseModel):
|
|
10
|
+
channel: str
|
|
11
|
+
recipients: List[str]
|
|
12
|
+
content: str
|
|
13
|
+
mentions: Optional[List[str]] = []
|
|
14
|
+
|
|
15
|
+
class BroadcastRequest(BaseModel):
|
|
16
|
+
channels: List[str]
|
|
17
|
+
content: str
|
|
18
|
+
mentions: Optional[List[str]] = []
|
|
19
|
+
|
|
20
|
+
@app.post("/messages")
|
|
21
|
+
@require_policy("messaging.message.send.v1")
|
|
22
|
+
async def send_message(request: Request, message_data: MessageRequest):
|
|
23
|
+
try:
|
|
24
|
+
passport = request.state.policy_result.passport
|
|
25
|
+
|
|
26
|
+
# Check channel allowlist
|
|
27
|
+
messaging_capability = next(
|
|
28
|
+
(cap for cap in passport.capabilities if cap.id == "messaging.send"),
|
|
29
|
+
None
|
|
30
|
+
)
|
|
31
|
+
allowed_channels = (
|
|
32
|
+
messaging_capability.params.get("channels_allowlist", [])
|
|
33
|
+
if messaging_capability and messaging_capability.params
|
|
34
|
+
else []
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if allowed_channels and message_data.channel not in allowed_channels:
|
|
38
|
+
raise HTTPException(
|
|
39
|
+
status_code=403,
|
|
40
|
+
detail={
|
|
41
|
+
"error": "Channel not allowed",
|
|
42
|
+
"allowed_channels": allowed_channels,
|
|
43
|
+
"upgrade_instructions": "Add channel to your passport's channels_allowlist"
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Check mention policy
|
|
48
|
+
mention_policy = (
|
|
49
|
+
messaging_capability.params.get("mention_policy", "limited")
|
|
50
|
+
if messaging_capability and messaging_capability.params
|
|
51
|
+
else "limited"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if message_data.mentions:
|
|
55
|
+
if mention_policy == "none":
|
|
56
|
+
raise HTTPException(
|
|
57
|
+
status_code=403,
|
|
58
|
+
detail={
|
|
59
|
+
"error": "Mentions not allowed",
|
|
60
|
+
"mention_policy": mention_policy
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
if mention_policy == "limited" and "@everyone" in message_data.mentions:
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=403,
|
|
66
|
+
detail={
|
|
67
|
+
"error": "@everyone mentions not allowed with limited mention policy"
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Rate limiting check (would integrate with actual rate limiter)
|
|
72
|
+
rate_limit_check = await check_message_rate_limit(passport.agent_id)
|
|
73
|
+
if not rate_limit_check["allowed"]:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=429,
|
|
76
|
+
detail={
|
|
77
|
+
"error": "Rate limit exceeded",
|
|
78
|
+
"retry_after": rate_limit_check.get("retry_after"),
|
|
79
|
+
"limits": {
|
|
80
|
+
"per_minute": passport.limits.get("msgs_per_min"),
|
|
81
|
+
"per_day": passport.limits.get("msgs_per_day")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Send message using your messaging service
|
|
87
|
+
message_id = await send_message_service({
|
|
88
|
+
"channel": message_data.channel,
|
|
89
|
+
"recipients": message_data.recipients,
|
|
90
|
+
"content": message_data.content,
|
|
91
|
+
"mentions": message_data.mentions,
|
|
92
|
+
"agent_id": passport.agent_id,
|
|
93
|
+
"agent_name": passport.name,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Record usage for rate limiting
|
|
97
|
+
await record_message_usage(passport.agent_id)
|
|
98
|
+
|
|
99
|
+
# Log the message
|
|
100
|
+
print(f"Message sent: {message_id} to {message_data.channel} by agent {passport.agent_id}")
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"message_id": message_id,
|
|
105
|
+
"channel": message_data.channel,
|
|
106
|
+
"recipients": len(message_data.recipients),
|
|
107
|
+
"status": "sent",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
except HTTPException:
|
|
111
|
+
raise
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"Message sending error: {e}")
|
|
114
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
115
|
+
|
|
116
|
+
@app.post("/messages/broadcast")
|
|
117
|
+
@require_policy("messaging.message.send.v1")
|
|
118
|
+
async def broadcast_message(request: Request, broadcast_data: BroadcastRequest):
|
|
119
|
+
try:
|
|
120
|
+
passport = request.state.policy_result.passport
|
|
121
|
+
|
|
122
|
+
# Check if broadcast is within daily limits
|
|
123
|
+
estimated_messages = len(broadcast_data.channels)
|
|
124
|
+
daily_usage = await get_daily_message_usage(passport.agent_id)
|
|
125
|
+
|
|
126
|
+
daily_limit = passport.limits.get("msgs_per_day", float('inf'))
|
|
127
|
+
if daily_usage + estimated_messages > daily_limit:
|
|
128
|
+
raise HTTPException(
|
|
129
|
+
status_code=403,
|
|
130
|
+
detail={
|
|
131
|
+
"error": "Broadcast would exceed daily message limit",
|
|
132
|
+
"current_usage": daily_usage,
|
|
133
|
+
"limit": daily_limit,
|
|
134
|
+
"requested": estimated_messages
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Process broadcast
|
|
139
|
+
results = []
|
|
140
|
+
for channel in broadcast_data.channels:
|
|
141
|
+
try:
|
|
142
|
+
message_id = await send_message_service({
|
|
143
|
+
"channel": channel,
|
|
144
|
+
"content": broadcast_data.content,
|
|
145
|
+
"mentions": broadcast_data.mentions,
|
|
146
|
+
"agent_id": passport.agent_id,
|
|
147
|
+
"agent_name": passport.name,
|
|
148
|
+
})
|
|
149
|
+
results.append({"channel": channel, "message_id": message_id, "status": "sent"})
|
|
150
|
+
except Exception as error:
|
|
151
|
+
results.append({"channel": channel, "error": str(error), "status": "failed"})
|
|
152
|
+
|
|
153
|
+
# Record usage
|
|
154
|
+
await record_message_usage(passport.agent_id, len(broadcast_data.channels))
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"success": True,
|
|
158
|
+
"results": results,
|
|
159
|
+
"total_sent": len([r for r in results if r["status"] == "sent"]),
|
|
160
|
+
"total_failed": len([r for r in results if r["status"] == "failed"]),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
except HTTPException:
|
|
164
|
+
raise
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"Broadcast error: {e}")
|
|
167
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
168
|
+
|
|
169
|
+
# Mock functions (implement with your actual messaging service)
|
|
170
|
+
async def send_message_service(message_data: dict) -> str:
|
|
171
|
+
"""Mock message sending function"""
|
|
172
|
+
await asyncio.sleep(0.1) # Simulate API call
|
|
173
|
+
return f"msg_{asyncio.get_event_loop().time()}_{hash(str(message_data)) % 1000000}"
|
|
174
|
+
|
|
175
|
+
async def check_message_rate_limit(agent_id: str) -> dict:
|
|
176
|
+
"""Mock rate limit checker"""
|
|
177
|
+
# Implement with Redis or in-memory rate limiter
|
|
178
|
+
# For demo, always allow
|
|
179
|
+
return {"allowed": True}
|
|
180
|
+
|
|
181
|
+
async def record_message_usage(agent_id: str, count: int = 1) -> None:
|
|
182
|
+
"""Record message usage in your tracking system"""
|
|
183
|
+
print(f"Recorded {count} message(s) for agent {agent_id}")
|
|
184
|
+
|
|
185
|
+
async def get_daily_message_usage(agent_id: str) -> int:
|
|
186
|
+
"""Get current daily usage from your tracking system"""
|
|
187
|
+
return 0 # Mock value
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
import uvicorn
|
|
191
|
+
print("Messaging service starting...")
|
|
192
|
+
print("Protected by APort messaging.message.send.v1 policy pack")
|
|
193
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "messaging.message.send.v1",
|
|
3
|
+
"name": "Messaging Protection Policy",
|
|
4
|
+
"description": "Pre-action governance for messaging operations. Enforces rate limits, channel restrictions, mention policies, and content validation for PLG on-ramp.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "active",
|
|
7
|
+
"requires_capabilities": ["messaging.send"],
|
|
8
|
+
"min_assurance": "L0",
|
|
9
|
+
"limits_required": [
|
|
10
|
+
"msgs_per_min",
|
|
11
|
+
"msgs_per_day",
|
|
12
|
+
"allowed_recipients",
|
|
13
|
+
"approval_required"
|
|
14
|
+
],
|
|
15
|
+
"required_fields": ["channel_id", "message", "message_type"],
|
|
16
|
+
"optional_fields": ["mentions", "attachments", "thread_id", "reply_to"],
|
|
17
|
+
"enforcement": {
|
|
18
|
+
"channels_allowlist_enforced": true,
|
|
19
|
+
"mention_policy_enforced": true,
|
|
20
|
+
"rate_limits_enforced": true
|
|
21
|
+
},
|
|
22
|
+
"mcp": {
|
|
23
|
+
"require_allowlisted_if_present": true
|
|
24
|
+
},
|
|
25
|
+
"advice": [
|
|
26
|
+
"Implement rate limiting per agent and per channel",
|
|
27
|
+
"Monitor for spam patterns and suspicious activity",
|
|
28
|
+
"Log all message attempts for Verifiable Attestation",
|
|
29
|
+
"Consider implementing channel-specific limits",
|
|
30
|
+
"Use mention policies to prevent @everyone abuse",
|
|
31
|
+
"Subscribe to status webhooks for instant suspend",
|
|
32
|
+
"Implement content filtering for inappropriate messages",
|
|
33
|
+
"Use progressive rate limiting for new agents"
|
|
34
|
+
],
|
|
35
|
+
"required_context": {
|
|
36
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["channel_id", "message", "message_type"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"channel_id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"minLength": 1,
|
|
43
|
+
"description": "Target channel identifier"
|
|
44
|
+
},
|
|
45
|
+
"message": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"minLength": 1,
|
|
48
|
+
"maxLength": 2000,
|
|
49
|
+
"description": "Message content"
|
|
50
|
+
},
|
|
51
|
+
"message_type": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"enum": ["text", "embed", "file", "reaction"],
|
|
54
|
+
"description": "Type of message"
|
|
55
|
+
},
|
|
56
|
+
"mentions": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": { "type": "string" },
|
|
59
|
+
"description": "User or role mentions in the message"
|
|
60
|
+
},
|
|
61
|
+
"attachments": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"items": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"url": { "type": "string" },
|
|
67
|
+
"filename": { "type": "string" },
|
|
68
|
+
"size": { "type": "integer" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"description": "File attachments"
|
|
72
|
+
},
|
|
73
|
+
"thread_id": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Thread identifier for threaded messages"
|
|
76
|
+
},
|
|
77
|
+
"reply_to": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Message ID being replied to"
|
|
80
|
+
},
|
|
81
|
+
"mcp_servers": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": { "type": "string" },
|
|
84
|
+
"description": "MCP servers being used in this request (e.g., [\"https://mcp.slack.com\"])"
|
|
85
|
+
},
|
|
86
|
+
"mcp_tools": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": { "type": "string" },
|
|
89
|
+
"description": "MCP tools being used in this request (e.g., [\"slack.messages.send\"])"
|
|
90
|
+
},
|
|
91
|
+
"mcp_server": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Single MCP server being used (backward compatibility - use mcp_servers array for multiple)"
|
|
94
|
+
},
|
|
95
|
+
"mcp_tool": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Single MCP tool being used (backward compatibility - use mcp_tools array for multiple)"
|
|
98
|
+
},
|
|
99
|
+
"mcp_session": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "MCP session identifier for audit trail (optional)"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"evaluation_rules": [
|
|
106
|
+
{
|
|
107
|
+
"name": "passport_status_active",
|
|
108
|
+
"condition": "passport.status == 'active'",
|
|
109
|
+
"deny_code": "oap.passport_suspended",
|
|
110
|
+
"description": "Passport must be active"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "messaging_capability",
|
|
114
|
+
"condition": "'messaging.send' in passport.capabilities",
|
|
115
|
+
"deny_code": "oap.unknown_capability",
|
|
116
|
+
"description": "Agent must have messaging.send capability"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "rate_limit_minute",
|
|
120
|
+
"condition": "minute_count < limits.msgs_per_min",
|
|
121
|
+
"deny_code": "oap.rate_limit_exceeded",
|
|
122
|
+
"description": "Per-minute rate limit must not be exceeded"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "rate_limit_daily",
|
|
126
|
+
"condition": "daily_count < limits.msgs_per_day",
|
|
127
|
+
"deny_code": "oap.rate_limit_exceeded",
|
|
128
|
+
"description": "Daily rate limit must not be exceeded"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "content_validation",
|
|
132
|
+
"condition": "message.length <= 2000",
|
|
133
|
+
"deny_code": "oap.content_too_long",
|
|
134
|
+
"description": "Message must not exceed length limit"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"cache": {
|
|
138
|
+
"default_ttl_seconds": 60,
|
|
139
|
+
"suspend_invalidate_seconds": 30
|
|
140
|
+
},
|
|
141
|
+
"deprecation": null,
|
|
142
|
+
"created_at": "2025-01-16T00:00:00Z",
|
|
143
|
+
"updated_at": "2025-01-30T00:00:00Z"
|
|
144
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "policy.{operation}.v1",
|
|
3
|
+
"name": "{Operation} Policy",
|
|
4
|
+
"description": "Pre-action governance for {operation} operations. {Brief description of what this policy protects and enforces}.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "active",
|
|
7
|
+
"requires_capabilities": ["{capability.name}"],
|
|
8
|
+
"min_assurance": "L2",
|
|
9
|
+
"limits_required": ["require_assurance_at_least", "idempotency_required"],
|
|
10
|
+
"required_fields": ["field1", "field2"],
|
|
11
|
+
"optional_fields": ["optional_field1", "optional_field2"],
|
|
12
|
+
"enforcement": {
|
|
13
|
+
"assurance_required": "limits.{capability}.require_assurance_at_least",
|
|
14
|
+
"idempotency_required": true
|
|
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
|
+
"Log all {operation} attempts for Verifiable Attestation",
|
|
23
|
+
"Implement appropriate rate limiting",
|
|
24
|
+
"Use idempotency keys to prevent duplicate operations",
|
|
25
|
+
"Provide clear error messages to help agents self-remediate"
|
|
26
|
+
],
|
|
27
|
+
"required_context": {
|
|
28
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["field1", "field2"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"field1": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"minLength": 1,
|
|
35
|
+
"description": "Description of field1"
|
|
36
|
+
},
|
|
37
|
+
"field2": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Description of field2"
|
|
40
|
+
},
|
|
41
|
+
"optional_field1": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Description of optional field1"
|
|
44
|
+
},
|
|
45
|
+
"optional_field2": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 0,
|
|
48
|
+
"description": "Description of optional field2"
|
|
49
|
+
},
|
|
50
|
+
"mcp_servers": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": { "type": "string" },
|
|
53
|
+
"description": "MCP servers being used in this request (e.g., [\"https://mcp.stripe.com\", \"https://mcp.notion.com\"])"
|
|
54
|
+
},
|
|
55
|
+
"mcp_tools": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "type": "string" },
|
|
58
|
+
"description": "MCP tools being used in this request (e.g., [\"stripe.refunds.create\", \"notion.pages.export\"])"
|
|
59
|
+
},
|
|
60
|
+
"mcp_server": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Single MCP server being used (backward compatibility - use mcp_servers array for multiple)"
|
|
63
|
+
},
|
|
64
|
+
"mcp_tool": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Single MCP tool being used (backward compatibility - use mcp_tools array for multiple)"
|
|
67
|
+
},
|
|
68
|
+
"mcp_session": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "MCP session identifier for audit trail (optional)"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"evaluation_rules": [
|
|
75
|
+
{
|
|
76
|
+
"name": "passport_status_active",
|
|
77
|
+
"condition": "passport.status == 'active'",
|
|
78
|
+
"deny_code": "oap.passport_suspended",
|
|
79
|
+
"description": "Passport must be active"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "assurance_minimum",
|
|
83
|
+
"condition": "passport.assurance_level >= limits.{capability}.require_assurance_at_least",
|
|
84
|
+
"deny_code": "oap.assurance_insufficient",
|
|
85
|
+
"description": "Assurance level must meet minimum requirement"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "capability_check",
|
|
89
|
+
"condition": "'{capability.name}' in passport.capabilities",
|
|
90
|
+
"deny_code": "oap.unknown_capability",
|
|
91
|
+
"description": "Agent must have required capability"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "idempotency_check",
|
|
95
|
+
"condition": "idempotency_key not in recent_keys",
|
|
96
|
+
"deny_code": "oap.idempotency_conflict",
|
|
97
|
+
"description": "Idempotency key must be unique"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"cache": {
|
|
101
|
+
"default_ttl_seconds": 60,
|
|
102
|
+
"suspend_invalidate_seconds": 30
|
|
103
|
+
},
|
|
104
|
+
"deprecation": null,
|
|
105
|
+
"created_at": "2025-01-30T00:00:00Z",
|
|
106
|
+
"updated_at": "2025-01-30T00:00:00Z"
|
|
107
|
+
}
|