@aporthq/aport-agent-guardrails 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSE +217 -0
  2. package/README.md +481 -0
  3. package/bin/agent-guardrails +133 -0
  4. package/bin/aport-create-passport.sh +444 -0
  5. package/bin/aport-cursor-hook.sh +90 -0
  6. package/bin/aport-guardrail-api.sh +108 -0
  7. package/bin/aport-guardrail-bash.sh +394 -0
  8. package/bin/aport-guardrail-v2.sh +5 -0
  9. package/bin/aport-guardrail.sh +5 -0
  10. package/bin/aport-resolve-paths.sh +71 -0
  11. package/bin/aport-status.sh +276 -0
  12. package/bin/frameworks/crewai.sh +49 -0
  13. package/bin/frameworks/cursor.sh +95 -0
  14. package/bin/frameworks/langchain.sh +48 -0
  15. package/bin/frameworks/n8n.sh +36 -0
  16. package/bin/frameworks/openclaw.sh +19 -0
  17. package/bin/lib/allowlist.sh +18 -0
  18. package/bin/lib/common.sh +28 -0
  19. package/bin/lib/config.sh +46 -0
  20. package/bin/lib/constants.sh +232 -0
  21. package/bin/lib/detect.sh +65 -0
  22. package/bin/lib/error.sh +269 -0
  23. package/bin/lib/passport.sh +19 -0
  24. package/bin/lib/templates/.gitkeep +1 -0
  25. package/bin/lib/templates/config.yaml +6 -0
  26. package/bin/lib/validation.sh +206 -0
  27. package/bin/openclaw +660 -0
  28. package/docs/ADDING_A_FRAMEWORK.md +87 -0
  29. package/docs/AGENTS.md.example +40 -0
  30. package/docs/CODE_REVIEW.md +192 -0
  31. package/docs/DEPLOYMENT_READINESS.md +81 -0
  32. package/docs/FAQ_SECURITY_SCANNERS.md +373 -0
  33. package/docs/FRAMEWORK_ROADMAP.md +41 -0
  34. package/docs/HOSTED_PASSPORT_SETUP.md +362 -0
  35. package/docs/IMPLEMENTING_YOUR_OWN_EVALUATOR.md +433 -0
  36. package/docs/OPENCLAW_COMPATIBILITY.md +73 -0
  37. package/docs/OPENCLAW_LOCAL_INTEGRATION.md +596 -0
  38. package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +54 -0
  39. package/docs/QUICKSTART.md +470 -0
  40. package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +470 -0
  41. package/docs/README.md +28 -0
  42. package/docs/RELEASE.md +87 -0
  43. package/docs/REPO_LAYOUT.md +47 -0
  44. package/docs/SKILLS_ECOSYSTEM_ANALYSIS_FEB17.md +1260 -0
  45. package/docs/TOOL_POLICY_MAPPING.md +46 -0
  46. package/docs/UPGRADE.md +46 -0
  47. package/docs/VERIFICATION_METHODS.md +97 -0
  48. package/docs/assets/README.md +8 -0
  49. package/docs/assets/porter.svg +54 -0
  50. package/docs/development/ERROR_CODES.md +616 -0
  51. package/docs/frameworks/GITHUB_ISSUE_PROPOSALS.md +1105 -0
  52. package/docs/frameworks/crewai.md +114 -0
  53. package/docs/frameworks/cursor.md +159 -0
  54. package/docs/frameworks/langchain.md +72 -0
  55. package/docs/frameworks/n8n.md +40 -0
  56. package/docs/frameworks/openclaw.md +40 -0
  57. package/docs/launch/ADD_APORT_AWESOME_LISTS_INSTRUCTIONS.md +146 -0
  58. package/docs/launch/ANNOUNCEMENT_GUIDE.md +266 -0
  59. package/docs/launch/AWESOME_REPOS.md +53 -0
  60. package/docs/launch/CURSOR_VSCODE_HOOKS_RESEARCH.md +77 -0
  61. package/docs/launch/DEMO_TERMINAL_OUTPUT.txt +48 -0
  62. package/docs/launch/DRY_AND_PLAN_CHECKLIST.md +47 -0
  63. package/docs/launch/EVIDENCE_README.md +61 -0
  64. package/docs/launch/EVIDENCE_TERMINAL_CAPTURE.txt +10 -0
  65. package/docs/launch/FRAMEWORK_SUPPORT_PLAN.md +1640 -0
  66. package/docs/launch/LAUNCH_READINESS_CHECKLIST.md +237 -0
  67. package/docs/launch/LAUNCH_STRATEGY_SUMMARY.md +464 -0
  68. package/docs/launch/OPENCLAW_FEEDBACK_AND_FIXES.md +85 -0
  69. package/docs/launch/POST_1_VALENTINE_IMPROVED.md +233 -0
  70. package/docs/launch/POST_2_GUARDRAIL_IMPROVED.md +369 -0
  71. package/docs/launch/PRE_LAUNCH_FIXES.md +766 -0
  72. package/docs/launch/QUICK_LAUNCH_CHECKLIST.md +400 -0
  73. package/docs/launch/READINESS_SUMMARY.md +262 -0
  74. package/docs/launch/README.md +68 -0
  75. package/docs/launch/USER_STORIES.md +327 -0
  76. package/docs/launch/scripts/add-aport-awesome-pr.sh +69 -0
  77. package/docs/operations/MONITORING.md +588 -0
  78. package/docs/reviews/2026-02-18-staff-review.md +268 -0
  79. package/extensions/openclaw-aport/README.md +415 -0
  80. package/extensions/openclaw-aport/index.js +625 -0
  81. package/extensions/openclaw-aport/openclaw-aport.js +7 -0
  82. package/extensions/openclaw-aport/openclaw.plugin.json +46 -0
  83. package/extensions/openclaw-aport/package.json +36 -0
  84. package/extensions/openclaw-aport/test.js +307 -0
  85. package/external/aport-policies/README.md +363 -0
  86. package/external/aport-policies/agent.session.create.v1/README.md +345 -0
  87. package/external/aport-policies/agent.session.create.v1/policy.json +162 -0
  88. package/external/aport-policies/agent.tool.register.v1/README.md +361 -0
  89. package/external/aport-policies/agent.tool.register.v1/policy.json +172 -0
  90. package/external/aport-policies/code.release.publish.v1/README.md +51 -0
  91. package/external/aport-policies/code.release.publish.v1/policy.json +121 -0
  92. package/external/aport-policies/code.repository.merge.v1/README.md +287 -0
  93. package/external/aport-policies/code.repository.merge.v1/express.example.js +332 -0
  94. package/external/aport-policies/code.repository.merge.v1/fastapi.example.py +370 -0
  95. package/external/aport-policies/code.repository.merge.v1/policy.json +162 -0
  96. package/external/aport-policies/data.export.create.v1/README.md +226 -0
  97. package/external/aport-policies/data.export.create.v1/express.example.js +172 -0
  98. package/external/aport-policies/data.export.create.v1/fastapi.example.py +165 -0
  99. package/external/aport-policies/data.export.create.v1/policy.json +133 -0
  100. package/external/aport-policies/data.report.ingest.v1/README.md +134 -0
  101. package/external/aport-policies/data.report.ingest.v1/express.example.js +105 -0
  102. package/external/aport-policies/data.report.ingest.v1/minimal-example.js +68 -0
  103. package/external/aport-policies/data.report.ingest.v1/policy.json +174 -0
  104. package/external/aport-policies/finance.crypto.trade.v1/README.md +146 -0
  105. package/external/aport-policies/finance.crypto.trade.v1/express.example.js +109 -0
  106. package/external/aport-policies/finance.crypto.trade.v1/minimal-example.js +65 -0
  107. package/external/aport-policies/finance.crypto.trade.v1/policy.json +176 -0
  108. package/external/aport-policies/finance.payment.charge.v1/README.md +326 -0
  109. package/external/aport-policies/finance.payment.charge.v1/express.example.js +250 -0
  110. package/external/aport-policies/finance.payment.charge.v1/fastapi.example.py +227 -0
  111. package/external/aport-policies/finance.payment.charge.v1/minimal-example.js +64 -0
  112. package/external/aport-policies/finance.payment.charge.v1/policy.json +224 -0
  113. package/external/aport-policies/finance.payment.charge.v1/tests/contexts.jsonl +12 -0
  114. package/external/aport-policies/finance.payment.charge.v1/tests/expected.jsonl +12 -0
  115. package/external/aport-policies/finance.payment.charge.v1/tests/passport.instance.json +42 -0
  116. package/external/aport-policies/finance.payment.charge.v1/tests/passport.template.json +40 -0
  117. package/external/aport-policies/finance.payment.charge.v1/tests/payments-charge-policy.test.js +817 -0
  118. package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py +486 -0
  119. package/external/aport-policies/finance.payment.payout.v1/README.md +78 -0
  120. package/external/aport-policies/finance.payment.payout.v1/policy.json +181 -0
  121. package/external/aport-policies/finance.payment.refund.v1/README.md +275 -0
  122. package/external/aport-policies/finance.payment.refund.v1/express.example.js +167 -0
  123. package/external/aport-policies/finance.payment.refund.v1/fastapi.example.py +136 -0
  124. package/external/aport-policies/finance.payment.refund.v1/minimal-example.js +183 -0
  125. package/external/aport-policies/finance.payment.refund.v1/policy.json +216 -0
  126. package/external/aport-policies/finance.payment.refund.v1/tests/refunds-policy.test.js +924 -0
  127. package/external/aport-policies/finance.payment.refund.v1/tests/test_refunds_policy.py +778 -0
  128. package/external/aport-policies/finance.transaction.execute.v1/README.md +309 -0
  129. package/external/aport-policies/finance.transaction.execute.v1/express.example.js +261 -0
  130. package/external/aport-policies/finance.transaction.execute.v1/fastapi.example.py +231 -0
  131. package/external/aport-policies/finance.transaction.execute.v1/minimal-example.js +78 -0
  132. package/external/aport-policies/finance.transaction.execute.v1/policy.json +189 -0
  133. package/external/aport-policies/finance.transaction.execute.v1/tests/contexts.jsonl +12 -0
  134. package/external/aport-policies/finance.transaction.execute.v1/tests/expected.jsonl +12 -0
  135. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.instance.json +42 -0
  136. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.template.json +42 -0
  137. package/external/aport-policies/finance.transaction.execute.v1/tests/test_transactions_policy.py +214 -0
  138. package/external/aport-policies/finance.transaction.execute.v1/tests/transactions-policy.test.js +306 -0
  139. package/external/aport-policies/governance.data.access.v1/README.md +292 -0
  140. package/external/aport-policies/governance.data.access.v1/express.example.js +321 -0
  141. package/external/aport-policies/governance.data.access.v1/fastapi.example.py +279 -0
  142. package/external/aport-policies/governance.data.access.v1/minimal-example.js +65 -0
  143. package/external/aport-policies/governance.data.access.v1/policy.json +208 -0
  144. package/external/aport-policies/governance.data.access.v1/tests/contexts.jsonl +12 -0
  145. package/external/aport-policies/governance.data.access.v1/tests/data-access-policy.test.js +308 -0
  146. package/external/aport-policies/governance.data.access.v1/tests/expected.jsonl +12 -0
  147. package/external/aport-policies/governance.data.access.v1/tests/passport.instance.json +56 -0
  148. package/external/aport-policies/governance.data.access.v1/tests/passport.template.json +56 -0
  149. package/external/aport-policies/governance.data.access.v1/tests/test_data_access_policy.py +214 -0
  150. package/external/aport-policies/legal.contract.review.v1/README.md +109 -0
  151. package/external/aport-policies/legal.contract.review.v1/policy.json +378 -0
  152. package/external/aport-policies/legal.contract.review.v1/tests/legal-contract-review-policy.test.js +609 -0
  153. package/external/aport-policies/legal.contract.review.v1/tests/passport.template.json +49 -0
  154. package/external/aport-policies/mcp.tool.execute.v1/README.md +301 -0
  155. package/external/aport-policies/mcp.tool.execute.v1/policy.json +141 -0
  156. package/external/aport-policies/messaging.message.send.v1/README.md +230 -0
  157. package/external/aport-policies/messaging.message.send.v1/express.example.js +183 -0
  158. package/external/aport-policies/messaging.message.send.v1/fastapi.example.py +193 -0
  159. package/external/aport-policies/messaging.message.send.v1/policy.json +144 -0
  160. package/external/aport-policies/policy-template.json +107 -0
  161. package/external/aport-policies/system.command.execute.v1/README.md +275 -0
  162. package/external/aport-policies/system.command.execute.v1/policy.json +146 -0
  163. package/external/aport-spec/CONTRIBUTING.md +273 -0
  164. package/external/aport-spec/LICENSE +21 -0
  165. package/external/aport-spec/README.md +168 -0
  166. package/external/aport-spec/conformance/README.md +294 -0
  167. package/external/aport-spec/conformance/cases/data.export.v1/contexts/allow_users.json +6 -0
  168. package/external/aport-spec/conformance/cases/data.export.v1/contexts/deny_pii.json +6 -0
  169. package/external/aport-spec/conformance/cases/data.export.v1/expected/allow_users.decision.json +19 -0
  170. package/external/aport-spec/conformance/cases/data.export.v1/expected/deny_pii.decision.json +19 -0
  171. package/external/aport-spec/conformance/cases/data.export.v1/passports/template.json +29 -0
  172. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/allow_50usd.json +9 -0
  173. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_150usd.json +9 -0
  174. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_currency.json +9 -0
  175. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/allow_50usd.decision.json +19 -0
  176. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_150usd.decision.json +19 -0
  177. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_currency.decision.json +19 -0
  178. package/external/aport-spec/conformance/cases/payments.refunds.v1/passports/template.json +42 -0
  179. package/external/aport-spec/conformance/package.json +44 -0
  180. package/external/aport-spec/conformance/pnpm-lock.yaml +642 -0
  181. package/external/aport-spec/conformance/src/cases.ts +371 -0
  182. package/external/aport-spec/conformance/src/ed25519.ts +167 -0
  183. package/external/aport-spec/conformance/src/jcs.ts +85 -0
  184. package/external/aport-spec/conformance/src/runner.ts +533 -0
  185. package/external/aport-spec/conformance/src/validators.ts +185 -0
  186. package/external/aport-spec/conformance/test-runner.js +315 -0
  187. package/external/aport-spec/conformance/tsconfig.json +21 -0
  188. package/external/aport-spec/error-schema.json +192 -0
  189. package/external/aport-spec/index.json +12 -0
  190. package/external/aport-spec/integrations/clawmoat/README.md +12 -0
  191. package/external/aport-spec/integrations/shield/README.md +245 -0
  192. package/external/aport-spec/integrations/shield/adapters/index.js +116 -0
  193. package/external/aport-spec/integrations/shield/adapters/system-command-execute.js +133 -0
  194. package/external/aport-spec/integrations/shield/test/README.md +58 -0
  195. package/external/aport-spec/integrations/shield/test/shield.md +40 -0
  196. package/external/aport-spec/integrations/shield/test/test-shield-to-verify.js +274 -0
  197. package/external/aport-spec/metrics-schema.json +504 -0
  198. package/external/aport-spec/oap/CHANGELOG.md +54 -0
  199. package/external/aport-spec/oap/VERSION.md +40 -0
  200. package/external/aport-spec/oap/capability-registry.md +229 -0
  201. package/external/aport-spec/oap/conformance.md +257 -0
  202. package/external/aport-spec/oap/decision-schema.json +114 -0
  203. package/external/aport-spec/oap/examples/context.refund.usd.50.json +9 -0
  204. package/external/aport-spec/oap/examples/decision.allow.sample.json +20 -0
  205. package/external/aport-spec/oap/examples/decision.deny.sample.json +23 -0
  206. package/external/aport-spec/oap/examples/passport.instance.v1.json +50 -0
  207. package/external/aport-spec/oap/examples/passport.template.v1.json +71 -0
  208. package/external/aport-spec/oap/oap-spec.md +426 -0
  209. package/external/aport-spec/oap/passport-schema.json +396 -0
  210. package/external/aport-spec/oap/security.md +213 -0
  211. package/external/aport-spec/oap/vc/context-oap-v1.jsonld +137 -0
  212. package/external/aport-spec/oap/vc/examples/oap-decision-vc.json +37 -0
  213. package/external/aport-spec/oap/vc/examples/oap-passport-vc.json +68 -0
  214. package/external/aport-spec/oap/vc/tools/INTEGRATION.md +375 -0
  215. package/external/aport-spec/oap/vc/tools/README.md +278 -0
  216. package/external/aport-spec/oap/vc/tools/examples/decision-to-vc.js +66 -0
  217. package/external/aport-spec/oap/vc/tools/examples/passport-to-vc.js +83 -0
  218. package/external/aport-spec/oap/vc/tools/examples/vc-to-decision.js +77 -0
  219. package/external/aport-spec/oap/vc/tools/examples/vc-to-passport.js +94 -0
  220. package/external/aport-spec/oap/vc/tools/package.json +38 -0
  221. package/external/aport-spec/oap/vc/tools/pnpm-lock.yaml +472 -0
  222. package/external/aport-spec/oap/vc/tools/src/cli.ts +226 -0
  223. package/external/aport-spec/oap/vc/tools/src/crypto-utils.ts +427 -0
  224. package/external/aport-spec/oap/vc/tools/src/index.ts +653 -0
  225. package/external/aport-spec/oap/vc/tools/src/test.ts +148 -0
  226. package/external/aport-spec/oap/vc/tools/src/vp.ts +382 -0
  227. package/external/aport-spec/oap/vc/tools/test-simple.js +214 -0
  228. package/external/aport-spec/oap/vc/tools/tsconfig.json +19 -0
  229. package/external/aport-spec/oap/vc/vc-mapping.md +443 -0
  230. package/external/aport-spec/passport-schema.json +586 -0
  231. package/external/aport-spec/rate-limiting.md +136 -0
  232. package/external/aport-spec/transport-profile.md +325 -0
  233. package/external/aport-spec/webhook-spec.md +314 -0
  234. package/package.json +70 -0
  235. package/skills/aport-agent-guardrail/SKILL.md +314 -0
  236. package/src/evaluator.js +252 -0
  237. package/src/server/index.js +72 -0
@@ -0,0 +1,332 @@
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 repo policy to PR creation routes
8
+ app.post(
9
+ "/repo/pr",
10
+ requirePolicy("code.repository.merge.v1"),
11
+ async (req, res) => {
12
+ try {
13
+ const { repo, base_branch, head_branch, title, body, files_changed } =
14
+ req.body;
15
+ const passport = req.policyResult.passport;
16
+
17
+ // Check repository allowlist
18
+ const allowedRepos =
19
+ passport.capabilities.find((cap) => cap.id === "repo.pr.create")?.params
20
+ ?.allowed_repos || [];
21
+
22
+ if (allowedRepos.length > 0 && !allowedRepos.includes(repo)) {
23
+ return res.status(403).json({
24
+ error: "Repository not allowed",
25
+ allowed_repos: allowedRepos,
26
+ upgrade_instructions:
27
+ "Add repository to your passport's allowed_repos",
28
+ });
29
+ }
30
+
31
+ // Check base branch allowlist
32
+ const allowedBranches =
33
+ passport.capabilities.find((cap) => cap.id === "repo.pr.create")?.params
34
+ ?.allowed_base_branches || [];
35
+
36
+ if (
37
+ allowedBranches.length > 0 &&
38
+ !allowedBranches.includes(base_branch)
39
+ ) {
40
+ return res.status(403).json({
41
+ error: "Base branch not allowed",
42
+ allowed_branches: allowedBranches,
43
+ });
44
+ }
45
+
46
+ // Check file limits
47
+ const maxFiles =
48
+ passport.capabilities.find((cap) => cap.id === "repo.pr.create")?.params
49
+ ?.max_files_changed || Infinity;
50
+
51
+ if (files_changed && files_changed.length > maxFiles) {
52
+ return res.status(403).json({
53
+ error: "Too many files changed",
54
+ max_files: maxFiles,
55
+ requested: files_changed.length,
56
+ });
57
+ }
58
+
59
+ // Check total lines added
60
+ const totalLinesAdded =
61
+ files_changed?.reduce(
62
+ (sum, file) => sum + (file.lines_added || 0),
63
+ 0
64
+ ) || 0;
65
+ const maxLines =
66
+ passport.capabilities.find((cap) => cap.id === "repo.pr.create")?.params
67
+ ?.max_total_added_lines || Infinity;
68
+
69
+ if (totalLinesAdded > maxLines) {
70
+ return res.status(403).json({
71
+ error: "Too many lines added",
72
+ max_lines: maxLines,
73
+ requested: totalLinesAdded,
74
+ });
75
+ }
76
+
77
+ // Check path allowlist
78
+ const pathAllowlist =
79
+ passport.capabilities.find((cap) => cap.id === "repo.pr.create")?.params
80
+ ?.path_allowlist || [];
81
+
82
+ if (pathAllowlist.length > 0 && files_changed) {
83
+ const disallowedFiles = files_changed.filter(
84
+ (file) =>
85
+ !pathAllowlist.some((pattern) =>
86
+ file.path.match(new RegExp(pattern))
87
+ )
88
+ );
89
+
90
+ if (disallowedFiles.length > 0) {
91
+ return res.status(403).json({
92
+ error: "Files outside allowed paths",
93
+ disallowed_files: disallowedFiles.map((f) => f.path),
94
+ path_allowlist: pathAllowlist,
95
+ });
96
+ }
97
+ }
98
+
99
+ // Check daily PR limit
100
+ const dailyUsage = await getDailyPRUsage(passport.agent_id);
101
+ if (dailyUsage >= passport.limits.max_prs_per_day) {
102
+ return res.status(403).json({
103
+ error: "Daily PR limit exceeded",
104
+ limit: passport.limits.max_prs_per_day,
105
+ current_usage: dailyUsage,
106
+ });
107
+ }
108
+
109
+ // Create PR using your Git service
110
+ const pr_id = await createPullRequest({
111
+ repo,
112
+ base_branch,
113
+ head_branch,
114
+ title,
115
+ body,
116
+ files_changed,
117
+ agent_id: passport.agent_id,
118
+ agent_name: passport.name,
119
+ });
120
+
121
+ // Record usage
122
+ await recordPRUsage(passport.agent_id);
123
+
124
+ // Log the PR creation
125
+ console.log(
126
+ `PR created: ${pr_id} in ${repo} by agent ${passport.agent_id}`
127
+ );
128
+
129
+ res.json({
130
+ success: true,
131
+ pr_id,
132
+ repo,
133
+ base_branch,
134
+ head_branch,
135
+ files_changed: files_changed?.length || 0,
136
+ lines_added: totalLinesAdded,
137
+ status: "created",
138
+ });
139
+ } catch (error) {
140
+ console.error("PR creation error:", error);
141
+ res.status(500).json({ error: "Internal server error" });
142
+ }
143
+ }
144
+ );
145
+
146
+ // Apply repo policy to merge routes
147
+ app.post(
148
+ "/repo/merge",
149
+ requirePolicy("code.repository.merge.v1"),
150
+ async (req, res) => {
151
+ try {
152
+ const { repo, pr_id, merge_method, delete_branch } = req.body;
153
+ const passport = req.policyResult.passport;
154
+
155
+ // Get PR details first
156
+ const prDetails = await getPRDetails(repo, pr_id);
157
+
158
+ // Check repository allowlist
159
+ const allowedRepos =
160
+ passport.capabilities.find((cap) => cap.id === "repo.merge")?.params
161
+ ?.allowed_repos || [];
162
+
163
+ if (allowedRepos.length > 0 && !allowedRepos.includes(repo)) {
164
+ return res.status(403).json({
165
+ error: "Repository not allowed for merging",
166
+ allowed_repos: allowedRepos,
167
+ });
168
+ }
169
+
170
+ // Check base branch allowlist
171
+ const allowedBranches =
172
+ passport.capabilities.find((cap) => cap.id === "repo.merge")?.params
173
+ ?.allowed_base_branches || [];
174
+
175
+ if (
176
+ allowedBranches.length > 0 &&
177
+ !allowedBranches.includes(prDetails.base_branch)
178
+ ) {
179
+ return res.status(403).json({
180
+ error: "Base branch not allowed for merging",
181
+ allowed_branches: allowedBranches,
182
+ });
183
+ }
184
+
185
+ // Check required labels
186
+ const requiredLabels =
187
+ passport.capabilities.find((cap) => cap.id === "repo.merge")?.params
188
+ ?.required_labels || [];
189
+
190
+ if (requiredLabels.length > 0) {
191
+ const missingLabels = requiredLabels.filter(
192
+ (label) => !prDetails.labels.includes(label)
193
+ );
194
+ if (missingLabels.length > 0) {
195
+ return res.status(403).json({
196
+ error: "Required labels missing",
197
+ missing_labels: missingLabels,
198
+ current_labels: prDetails.labels,
199
+ });
200
+ }
201
+ }
202
+
203
+ // Check required reviews
204
+ const requiredReviews =
205
+ passport.capabilities.find((cap) => cap.id === "repo.merge")?.params
206
+ ?.required_reviews || 0;
207
+
208
+ if (prDetails.approvals < requiredReviews) {
209
+ return res.status(403).json({
210
+ error: "Insufficient reviews",
211
+ required: requiredReviews,
212
+ current: prDetails.approvals,
213
+ });
214
+ }
215
+
216
+ // Check PR size limit
217
+ const prSizeKB = prDetails.size_kb;
218
+ if (prSizeKB > passport.limits.max_pr_size_kb) {
219
+ return res.status(403).json({
220
+ error: "PR size exceeds limit",
221
+ size_kb: prSizeKB,
222
+ limit_kb: passport.limits.max_pr_size_kb,
223
+ });
224
+ }
225
+
226
+ // Check daily merge limit
227
+ const dailyUsage = await getDailyMergeUsage(passport.agent_id);
228
+ if (dailyUsage >= passport.limits.max_merges_per_day) {
229
+ return res.status(403).json({
230
+ error: "Daily merge limit exceeded",
231
+ limit: passport.limits.max_merges_per_day,
232
+ current_usage: dailyUsage,
233
+ });
234
+ }
235
+
236
+ // Merge PR using your Git service
237
+ const merge_result = await mergePullRequest({
238
+ repo,
239
+ pr_id,
240
+ merge_method,
241
+ delete_branch,
242
+ agent_id: passport.agent_id,
243
+ agent_name: passport.name,
244
+ });
245
+
246
+ // Record usage
247
+ await recordMergeUsage(passport.agent_id);
248
+
249
+ // Log the merge
250
+ console.log(
251
+ `PR merged: ${pr_id} in ${repo} by agent ${passport.agent_id}`
252
+ );
253
+
254
+ res.json({
255
+ success: true,
256
+ pr_id,
257
+ repo,
258
+ merge_sha: merge_result.sha,
259
+ merge_method,
260
+ status: "merged",
261
+ });
262
+ } catch (error) {
263
+ console.error("PR merge error:", error);
264
+ res.status(500).json({ error: "Internal server error" });
265
+ }
266
+ }
267
+ );
268
+
269
+ // Mock functions (implement with your actual Git service)
270
+ async function createPullRequest({
271
+ repo,
272
+ base_branch,
273
+ head_branch,
274
+ title,
275
+ body,
276
+ files_changed,
277
+ agent_id,
278
+ agent_name,
279
+ }) {
280
+ // Simulate PR creation
281
+ await new Promise((resolve) => setTimeout(resolve, 200));
282
+ return `pr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
283
+ }
284
+
285
+ async function getPRDetails(repo, pr_id) {
286
+ // Mock PR details
287
+ return {
288
+ base_branch: "main",
289
+ labels: ["enhancement", "agent-created"],
290
+ approvals: 2,
291
+ size_kb: 45,
292
+ };
293
+ }
294
+
295
+ async function mergePullRequest({
296
+ repo,
297
+ pr_id,
298
+ merge_method,
299
+ delete_branch,
300
+ agent_id,
301
+ agent_name,
302
+ }) {
303
+ // Simulate PR merge
304
+ await new Promise((resolve) => setTimeout(resolve, 300));
305
+ return {
306
+ sha: `sha_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
307
+ };
308
+ }
309
+
310
+ async function getDailyPRUsage(agent_id) {
311
+ // Get current daily PR usage
312
+ return 0; // Mock value
313
+ }
314
+
315
+ async function getDailyMergeUsage(agent_id) {
316
+ // Get current daily merge usage
317
+ return 0; // Mock value
318
+ }
319
+
320
+ async function recordPRUsage(agent_id) {
321
+ console.log(`Recorded PR creation for agent ${agent_id}`);
322
+ }
323
+
324
+ async function recordMergeUsage(agent_id) {
325
+ console.log(`Recorded PR merge for agent ${agent_id}`);
326
+ }
327
+
328
+ const PORT = process.env.PORT || 3000;
329
+ app.listen(PORT, () => {
330
+ console.log(`Repository service running on port ${PORT}`);
331
+ console.log("Protected by APort code.repository.merge.v1 policy pack");
332
+ });
@@ -0,0 +1,370 @@
1
+ from fastapi import FastAPI, HTTPException, Request
2
+ from pydantic import BaseModel, Field
3
+ from typing import List, Optional, Dict, Any
4
+ from aport.middleware import require_policy
5
+ import asyncio
6
+ import re
7
+
8
+ app = FastAPI(title="Repository Service", version="1.0.0")
9
+
10
+ class FileChange(BaseModel):
11
+ path: str
12
+ lines_added: Optional[int] = 0
13
+ lines_removed: Optional[int] = 0
14
+
15
+ class PRRequest(BaseModel):
16
+ repo: str
17
+ base_branch: str
18
+ head_branch: str
19
+ title: str
20
+ body: Optional[str] = ""
21
+ files_changed: Optional[List[FileChange]] = []
22
+
23
+ class MergeRequest(BaseModel):
24
+ repo: str
25
+ pr_id: str
26
+ merge_method: Optional[str] = "merge"
27
+ delete_branch: Optional[bool] = False
28
+
29
+ @app.post("/repo/pr")
30
+ @require_policy("code.repository.merge.v1")
31
+ async def create_pull_request(request: Request, pr_data: PRRequest):
32
+ try:
33
+ passport = request.state.policy_result.passport
34
+
35
+ # Get PR creation capability
36
+ pr_capability = next(
37
+ (cap for cap in passport.capabilities if cap.id == "repo.pr.create"),
38
+ None
39
+ )
40
+
41
+ # Check repository allowlist
42
+ allowed_repos = (
43
+ pr_capability.params.get("allowed_repos", [])
44
+ if pr_capability and pr_capability.params
45
+ else []
46
+ )
47
+
48
+ if allowed_repos and pr_data.repo not in allowed_repos:
49
+ raise HTTPException(
50
+ status_code=403,
51
+ detail={
52
+ "error": "Repository not allowed",
53
+ "allowed_repos": allowed_repos,
54
+ "upgrade_instructions": "Add repository to your passport's allowed_repos"
55
+ }
56
+ )
57
+
58
+ # Check base branch allowlist
59
+ allowed_branches = (
60
+ pr_capability.params.get("allowed_base_branches", [])
61
+ if pr_capability and pr_capability.params
62
+ else []
63
+ )
64
+
65
+ if allowed_branches and pr_data.base_branch not in allowed_branches:
66
+ raise HTTPException(
67
+ status_code=403,
68
+ detail={
69
+ "error": "Base branch not allowed",
70
+ "allowed_branches": allowed_branches
71
+ }
72
+ )
73
+
74
+ # Check file limits
75
+ max_files = (
76
+ pr_capability.params.get("max_files_changed", float('inf'))
77
+ if pr_capability and pr_capability.params
78
+ else float('inf')
79
+ )
80
+
81
+ if pr_data.files_changed and len(pr_data.files_changed) > max_files:
82
+ raise HTTPException(
83
+ status_code=403,
84
+ detail={
85
+ "error": "Too many files changed",
86
+ "max_files": max_files,
87
+ "requested": len(pr_data.files_changed)
88
+ }
89
+ )
90
+
91
+ # Check total lines added
92
+ total_lines_added = sum(
93
+ file.lines_added or 0 for file in (pr_data.files_changed or [])
94
+ )
95
+ max_lines = (
96
+ pr_capability.params.get("max_total_added_lines", float('inf'))
97
+ if pr_capability and pr_capability.params
98
+ else float('inf')
99
+ )
100
+
101
+ if total_lines_added > max_lines:
102
+ raise HTTPException(
103
+ status_code=403,
104
+ detail={
105
+ "error": "Too many lines added",
106
+ "max_lines": max_lines,
107
+ "requested": total_lines_added
108
+ }
109
+ )
110
+
111
+ # Check path allowlist
112
+ path_allowlist = (
113
+ pr_capability.params.get("path_allowlist", [])
114
+ if pr_capability and pr_capability.params
115
+ else []
116
+ )
117
+
118
+ if path_allowlist and pr_data.files_changed:
119
+ disallowed_files = [
120
+ file for file in pr_data.files_changed
121
+ if not any(re.match(pattern, file.path) for pattern in path_allowlist)
122
+ ]
123
+
124
+ if disallowed_files:
125
+ raise HTTPException(
126
+ status_code=403,
127
+ detail={
128
+ "error": "Files outside allowed paths",
129
+ "disallowed_files": [f.path for f in disallowed_files],
130
+ "path_allowlist": path_allowlist
131
+ }
132
+ )
133
+
134
+ # Check daily PR limit
135
+ daily_usage = await get_daily_pr_usage(passport.agent_id)
136
+ pr_limit = passport.limits.get("max_prs_per_day", float('inf'))
137
+
138
+ if daily_usage >= pr_limit:
139
+ raise HTTPException(
140
+ status_code=403,
141
+ detail={
142
+ "error": "Daily PR limit exceeded",
143
+ "limit": pr_limit,
144
+ "current_usage": daily_usage
145
+ }
146
+ )
147
+
148
+ # Create PR using your Git service
149
+ pr_id = await create_pull_request_service({
150
+ "repo": pr_data.repo,
151
+ "base_branch": pr_data.base_branch,
152
+ "head_branch": pr_data.head_branch,
153
+ "title": pr_data.title,
154
+ "body": pr_data.body,
155
+ "files_changed": pr_data.files_changed,
156
+ "agent_id": passport.agent_id,
157
+ "agent_name": passport.name,
158
+ })
159
+
160
+ # Record usage
161
+ await record_pr_usage(passport.agent_id)
162
+
163
+ # Log the PR creation
164
+ print(f"PR created: {pr_id} in {pr_data.repo} by agent {passport.agent_id}")
165
+
166
+ return {
167
+ "success": True,
168
+ "pr_id": pr_id,
169
+ "repo": pr_data.repo,
170
+ "base_branch": pr_data.base_branch,
171
+ "head_branch": pr_data.head_branch,
172
+ "files_changed": len(pr_data.files_changed or []),
173
+ "lines_added": total_lines_added,
174
+ "status": "created",
175
+ }
176
+
177
+ except HTTPException:
178
+ raise
179
+ except Exception as e:
180
+ print(f"PR creation error: {e}")
181
+ raise HTTPException(status_code=500, detail="Internal server error")
182
+
183
+ @app.post("/repo/merge")
184
+ @require_policy("code.repository.merge.v1")
185
+ async def merge_pull_request(request: Request, merge_data: MergeRequest):
186
+ try:
187
+ passport = request.state.policy_result.passport
188
+
189
+ # Get PR details first
190
+ pr_details = await get_pr_details(merge_data.repo, merge_data.pr_id)
191
+
192
+ # Get merge capability
193
+ merge_capability = next(
194
+ (cap for cap in passport.capabilities if cap.id == "repo.merge"),
195
+ None
196
+ )
197
+
198
+ # Check repository allowlist
199
+ allowed_repos = (
200
+ merge_capability.params.get("allowed_repos", [])
201
+ if merge_capability and merge_capability.params
202
+ else []
203
+ )
204
+
205
+ if allowed_repos and merge_data.repo not in allowed_repos:
206
+ raise HTTPException(
207
+ status_code=403,
208
+ detail={
209
+ "error": "Repository not allowed for merging",
210
+ "allowed_repos": allowed_repos
211
+ }
212
+ )
213
+
214
+ # Check base branch allowlist
215
+ allowed_branches = (
216
+ merge_capability.params.get("allowed_base_branches", [])
217
+ if merge_capability and merge_capability.params
218
+ else []
219
+ )
220
+
221
+ if allowed_branches and pr_details["base_branch"] not in allowed_branches:
222
+ raise HTTPException(
223
+ status_code=403,
224
+ detail={
225
+ "error": "Base branch not allowed for merging",
226
+ "allowed_branches": allowed_branches
227
+ }
228
+ )
229
+
230
+ # Check required labels
231
+ required_labels = (
232
+ merge_capability.params.get("required_labels", [])
233
+ if merge_capability and merge_capability.params
234
+ else []
235
+ )
236
+
237
+ if required_labels:
238
+ missing_labels = [
239
+ label for label in required_labels
240
+ if label not in pr_details["labels"]
241
+ ]
242
+ if missing_labels:
243
+ raise HTTPException(
244
+ status_code=403,
245
+ detail={
246
+ "error": "Required labels missing",
247
+ "missing_labels": missing_labels,
248
+ "current_labels": pr_details["labels"]
249
+ }
250
+ )
251
+
252
+ # Check required reviews
253
+ required_reviews = (
254
+ merge_capability.params.get("required_reviews", 0)
255
+ if merge_capability and merge_capability.params
256
+ else 0
257
+ )
258
+
259
+ if pr_details["approvals"] < required_reviews:
260
+ raise HTTPException(
261
+ status_code=403,
262
+ detail={
263
+ "error": "Insufficient reviews",
264
+ "required": required_reviews,
265
+ "current": pr_details["approvals"]
266
+ }
267
+ )
268
+
269
+ # Check PR size limit
270
+ pr_size_kb = pr_details["size_kb"]
271
+ size_limit = passport.limits.get("max_pr_size_kb", float('inf'))
272
+
273
+ if pr_size_kb > size_limit:
274
+ raise HTTPException(
275
+ status_code=403,
276
+ detail={
277
+ "error": "PR size exceeds limit",
278
+ "size_kb": pr_size_kb,
279
+ "limit_kb": size_limit
280
+ }
281
+ )
282
+
283
+ # Check daily merge limit
284
+ daily_usage = await get_daily_merge_usage(passport.agent_id)
285
+ merge_limit = passport.limits.get("max_merges_per_day", float('inf'))
286
+
287
+ if daily_usage >= merge_limit:
288
+ raise HTTPException(
289
+ status_code=403,
290
+ detail={
291
+ "error": "Daily merge limit exceeded",
292
+ "limit": merge_limit,
293
+ "current_usage": daily_usage
294
+ }
295
+ )
296
+
297
+ # Merge PR using your Git service
298
+ merge_result = await merge_pull_request_service({
299
+ "repo": merge_data.repo,
300
+ "pr_id": merge_data.pr_id,
301
+ "merge_method": merge_data.merge_method,
302
+ "delete_branch": merge_data.delete_branch,
303
+ "agent_id": passport.agent_id,
304
+ "agent_name": passport.name,
305
+ })
306
+
307
+ # Record usage
308
+ await record_merge_usage(passport.agent_id)
309
+
310
+ # Log the merge
311
+ print(f"PR merged: {merge_data.pr_id} in {merge_data.repo} by agent {passport.agent_id}")
312
+
313
+ return {
314
+ "success": True,
315
+ "pr_id": merge_data.pr_id,
316
+ "repo": merge_data.repo,
317
+ "merge_sha": merge_result["sha"],
318
+ "merge_method": merge_data.merge_method,
319
+ "status": "merged",
320
+ }
321
+
322
+ except HTTPException:
323
+ raise
324
+ except Exception as e:
325
+ print(f"PR merge error: {e}")
326
+ raise HTTPException(status_code=500, detail="Internal server error")
327
+
328
+ # Mock functions (implement with your actual Git service)
329
+ async def create_pull_request_service(pr_data: dict) -> str:
330
+ """Mock PR creation function"""
331
+ await asyncio.sleep(0.2) # Simulate API call
332
+ return f"pr_{asyncio.get_event_loop().time()}_{hash(str(pr_data)) % 1000000}"
333
+
334
+ async def get_pr_details(repo: str, pr_id: str) -> Dict[str, Any]:
335
+ """Mock PR details retrieval"""
336
+ return {
337
+ "base_branch": "main",
338
+ "labels": ["enhancement", "agent-created"],
339
+ "approvals": 2,
340
+ "size_kb": 45
341
+ }
342
+
343
+ async def merge_pull_request_service(merge_data: dict) -> Dict[str, str]:
344
+ """Mock PR merge function"""
345
+ await asyncio.sleep(0.3) # Simulate API call
346
+ return {
347
+ "sha": f"sha_{asyncio.get_event_loop().time()}_{hash(str(merge_data)) % 1000000}"
348
+ }
349
+
350
+ async def get_daily_pr_usage(agent_id: str) -> int:
351
+ """Get current daily PR usage"""
352
+ return 0 # Mock value
353
+
354
+ async def get_daily_merge_usage(agent_id: str) -> int:
355
+ """Get current daily merge usage"""
356
+ return 0 # Mock value
357
+
358
+ async def record_pr_usage(agent_id: str) -> None:
359
+ """Record PR creation usage"""
360
+ print(f"Recorded PR creation for agent {agent_id}")
361
+
362
+ async def record_merge_usage(agent_id: str) -> None:
363
+ """Record PR merge usage"""
364
+ print(f"Recorded PR merge for agent {agent_id}")
365
+
366
+ if __name__ == "__main__":
367
+ import uvicorn
368
+ print("Repository service starting...")
369
+ print("Protected by APort code.repository.merge.v1 policy pack")
370
+ uvicorn.run(app, host="0.0.0.0", port=8000)