actshield 0.9.0__py3-none-any.whl

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 (366) hide show
  1. actshield/__init__.py +439 -0
  2. actshield/__main__.py +46 -0
  3. actshield/agents/__init__.py +12 -0
  4. actshield/agents/agent.py +200 -0
  5. actshield/agents/identity.py +114 -0
  6. actshield/api/__init__.py +34 -0
  7. actshield/api/models.py +198 -0
  8. actshield/api/server.py +669 -0
  9. actshield/api/service.py +599 -0
  10. actshield/approval/__init__.py +15 -0
  11. actshield/approval/approval.py +164 -0
  12. actshield/cli/__init__.py +8 -0
  13. actshield/cli/graph.py +49 -0
  14. actshield/cli/main.py +802 -0
  15. actshield/cli/shell.py +406 -0
  16. actshield/cli/theme.py +110 -0
  17. actshield/client.py +757 -0
  18. actshield/config.py +88 -0
  19. actshield/context/__init__.py +14 -0
  20. actshield/context/context.py +161 -0
  21. actshield/context/provenance.py +108 -0
  22. actshield/context/taint.py +48 -0
  23. actshield/decisions/decision.py +74 -0
  24. actshield/delegation/__init__.py +11 -0
  25. actshield/delegation/authority.py +63 -0
  26. actshield/delegation/delegation.py +180 -0
  27. actshield/demo.py +139 -0
  28. actshield/doctor.py +94 -0
  29. actshield/drift/__init__.py +27 -0
  30. actshield/drift/drift_engine.py +149 -0
  31. actshield/drift/drift_models.py +63 -0
  32. actshield/evaluation/__init__.py +1 -0
  33. actshield/evaluation/cases/__init__.py +1 -0
  34. actshield/evaluation/eval_suite.py +254 -0
  35. actshield/forensics/__init__.py +53 -0
  36. actshield/forensics/access_graph.py +216 -0
  37. actshield/forensics/authority_graph.py +186 -0
  38. actshield/forensics/delegation_graph.py +119 -0
  39. actshield/forensics/explanation.py +254 -0
  40. actshield/forensics/models.py +488 -0
  41. actshield/forensics/queries.py +367 -0
  42. actshield/forensics/serializers.py +68 -0
  43. actshield/forensics/service.py +310 -0
  44. actshield/forensics/snapshots.py +174 -0
  45. actshield/gates/__init__.py +13 -0
  46. actshield/gates/gate_models.py +46 -0
  47. actshield/gates/security_gate.py +191 -0
  48. actshield/gateway/__init__.py +13 -0
  49. actshield/gateway/http.py +191 -0
  50. actshield/gateway/mcp.py +292 -0
  51. actshield/incidents/__init__.py +25 -0
  52. actshield/incidents/incident.py +94 -0
  53. actshield/incidents/incident_engine.py +163 -0
  54. actshield/incidents/incident_rules.py +116 -0
  55. actshield/incidents/incident_state.py +58 -0
  56. actshield/integrations/__init__.py +24 -0
  57. actshield/integrations/ai_secura.py +169 -0
  58. actshield/integrations/ai_secura_analysis.py +236 -0
  59. actshield/integrations/apiris.py +122 -0
  60. actshield/integrations/few_shot.py +104 -0
  61. actshield/integrations/ollama_adapter.py +538 -0
  62. actshield/integrations/prompts/__init__.py +1 -0
  63. actshield/integrations/prompts/examples/__init__.py +1 -0
  64. actshield/integrations/security_packet.py +82 -0
  65. actshield/llm_config.py +89 -0
  66. actshield/offensive/__init__.py +95 -0
  67. actshield/offensive/adaptive/__init__.py +22 -0
  68. actshield/offensive/adaptive/adaptive_engine.py +165 -0
  69. actshield/offensive/adaptive/attack_selector.py +51 -0
  70. actshield/offensive/adaptive/boundary_search.py +99 -0
  71. actshield/offensive/adaptive/campaign_memory.py +123 -0
  72. actshield/offensive/adaptive/feedback.py +84 -0
  73. actshield/offensive/adaptive/mutation_planner.py +57 -0
  74. actshield/offensive/attack.py +78 -0
  75. actshield/offensive/campaign.py +219 -0
  76. actshield/offensive/corpus.py +155 -0
  77. actshield/offensive/engine.py +466 -0
  78. actshield/offensive/evaluator.py +116 -0
  79. actshield/offensive/evidence.py +63 -0
  80. actshield/offensive/generator.py +128 -0
  81. actshield/offensive/mutation.py +101 -0
  82. actshield/offensive/results.py +77 -0
  83. actshield/offensive/safety.py +80 -0
  84. actshield/offensive/strategies/__init__.py +22 -0
  85. actshield/offensive/strategies/authority_escalation.py +100 -0
  86. actshield/offensive/strategies/base.py +34 -0
  87. actshield/offensive/strategies/context_manipulation.py +100 -0
  88. actshield/offensive/strategies/data_exfiltration.py +100 -0
  89. actshield/offensive/strategies/delegation_attacks.py +100 -0
  90. actshield/offensive/strategies/mcp_attacks.py +100 -0
  91. actshield/offensive/strategies/prompt_injection.py +100 -0
  92. actshield/offensive/strategies/tool_poisoning.py +100 -0
  93. actshield/offensive/target.py +113 -0
  94. actshield/offensive/vulnerable_target.py +121 -0
  95. actshield/persistence/__init__.py +18 -0
  96. actshield/persistence/siem.py +102 -0
  97. actshield/persistence/storage.py +240 -0
  98. actshield/policy/__init__.py +12 -0
  99. actshield/policy/evaluator.py +294 -0
  100. actshield/policy/intent.py +113 -0
  101. actshield/policy/policy.py +48 -0
  102. actshield/posture/__init__.py +26 -0
  103. actshield/posture/posture_engine.py +186 -0
  104. actshield/posture/posture_models.py +152 -0
  105. actshield/posture/posture_rules.py +137 -0
  106. actshield/providers/__init__.py +32 -0
  107. actshield/providers/anthropic_provider.py +146 -0
  108. actshield/providers/base.py +160 -0
  109. actshield/providers/gemini_provider.py +133 -0
  110. actshield/providers/null_provider.py +40 -0
  111. actshield/providers/ollama_provider.py +135 -0
  112. actshield/providers/openai_provider.py +142 -0
  113. actshield/providers/registry.py +244 -0
  114. actshield/py.typed +0 -0
  115. actshield/response/__init__.py +14 -0
  116. actshield/response/response_actions.py +39 -0
  117. actshield/response/response_audit.py +32 -0
  118. actshield/response/response_engine.py +139 -0
  119. actshield/risk/models.py +104 -0
  120. actshield/serve.py +117 -0
  121. actshield/static/404.html +1 -0
  122. actshield/static/__next.__PAGE__.txt +24 -0
  123. actshield/static/__next._full.txt +20 -0
  124. actshield/static/__next._tree.txt +4 -0
  125. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_buildManifest.js +11 -0
  126. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_clientMiddlewareManifest.js +1 -0
  127. actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_ssgManifest.js +1 -0
  128. actshield/static/_next/static/chunks/09kny2eqjc2or.js +1 -0
  129. actshield/static/_next/static/chunks/0c1zba1u9whdp.js +1 -0
  130. actshield/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  131. actshield/static/_next/static/chunks/0dbjjeg01x25o.js +1 -0
  132. actshield/static/_next/static/chunks/0duobs55ne238.js +1 -0
  133. actshield/static/_next/static/chunks/0iaz41l6068e4.js +1 -0
  134. actshield/static/_next/static/chunks/0mjhyv72x2idp.js +1 -0
  135. actshield/static/_next/static/chunks/0nelpqfu_ciq6.js +1 -0
  136. actshield/static/_next/static/chunks/0p1bci9ch797v.js +1 -0
  137. actshield/static/_next/static/chunks/0tld_ygho5tgm.js +1 -0
  138. actshield/static/_next/static/chunks/0w1-y7oni66j9.js +1 -0
  139. actshield/static/_next/static/chunks/10537zyl427zj.js +1 -0
  140. actshield/static/_next/static/chunks/11pc_jkl3aggn.js +1 -0
  141. actshield/static/_next/static/chunks/16k7nw0b3t8ys.js +1 -0
  142. actshield/static/_next/static/chunks/19mx3mg6lkumu.js +1 -0
  143. actshield/static/_next/static/chunks/1_8p47b-kxg2a.js +1 -0
  144. actshield/static/_next/static/chunks/1dszjozwfqegx.js +1 -0
  145. actshield/static/_next/static/chunks/1h43jcb90-m2r.js +22 -0
  146. actshield/static/_next/static/chunks/1ly04cvt_wh_j.js +1 -0
  147. actshield/static/_next/static/chunks/1odxrypuwuwp_.js +1 -0
  148. actshield/static/_next/static/chunks/1oes4c35qz5ku.js +1 -0
  149. actshield/static/_next/static/chunks/1ow883jl4ln-r.js +16 -0
  150. actshield/static/_next/static/chunks/1po7bna1as8tc.js +1 -0
  151. actshield/static/_next/static/chunks/1rsh0og8ifkzx.js +1 -0
  152. actshield/static/_next/static/chunks/1v0lwzv-859z_.js +1 -0
  153. actshield/static/_next/static/chunks/25iat2epwx25a.js +1 -0
  154. actshield/static/_next/static/chunks/25uvt4s2f39a4.js +1 -0
  155. actshield/static/_next/static/chunks/27t_qfc-3_lzs.js +1 -0
  156. actshield/static/_next/static/chunks/28bkmy8mez7gm.js +1 -0
  157. actshield/static/_next/static/chunks/2b6brmw_drw1p.js +1 -0
  158. actshield/static/_next/static/chunks/2fnhtxi4j6vge.js +1 -0
  159. actshield/static/_next/static/chunks/2p087eftxemg4.js +6 -0
  160. actshield/static/_next/static/chunks/2s8pao_z8pgtq.js +1 -0
  161. actshield/static/_next/static/chunks/2yj7ooc2c-zpg.js +1 -0
  162. actshield/static/_next/static/chunks/3-mrrr8udt5tk.js +1 -0
  163. actshield/static/_next/static/chunks/310vm2bl3xxpt.js +31 -0
  164. actshield/static/_next/static/chunks/35mvsv251jx1r.css +3 -0
  165. actshield/static/_next/static/chunks/370vf0oetlsx9.js +1 -0
  166. actshield/static/_next/static/chunks/3e-2m72tyo556.js +1 -0
  167. actshield/static/_next/static/chunks/3fntmmi971322.js +1 -0
  168. actshield/static/_next/static/chunks/3fzw-g0hycl2c.js +1 -0
  169. actshield/static/_next/static/chunks/3lopq3lhmcjxg.js +129 -0
  170. actshield/static/_next/static/chunks/3mpe2jejel0v2.css +1 -0
  171. actshield/static/_next/static/chunks/3t8o4f2lzr2sf.js +1 -0
  172. actshield/static/_next/static/chunks/3y-v1neyllne5.js +1 -0
  173. actshield/static/_next/static/chunks/43oiwy8mx6cs1.js +1 -0
  174. actshield/static/_next/static/chunks/turbopack-23wzuebcy_lxm.js +1 -0
  175. actshield/static/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
  176. actshield/static/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
  177. actshield/static/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
  178. actshield/static/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
  179. actshield/static/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
  180. actshield/static/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
  181. actshield/static/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
  182. actshield/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  183. actshield/static/_not-found/__next._full.txt +17 -0
  184. actshield/static/_not-found/__next._not-found/__PAGE__.txt +23 -0
  185. actshield/static/_not-found/__next._tree.txt +3 -0
  186. actshield/static/_not-found.html +1 -0
  187. actshield/static/_not-found.txt +17 -0
  188. actshield/static/access/matrix/__next._full.txt +24 -0
  189. actshield/static/access/matrix/__next._tree.txt +4 -0
  190. actshield/static/access/matrix/__next.access/matrix/__PAGE__.txt +30 -0
  191. actshield/static/access/matrix.html +1 -0
  192. actshield/static/access/matrix.txt +24 -0
  193. actshield/static/activity/__next._full.txt +22 -0
  194. actshield/static/activity/__next._tree.txt +4 -0
  195. actshield/static/activity/__next.activity/__PAGE__.txt +27 -0
  196. actshield/static/activity.html +1 -0
  197. actshield/static/activity.txt +22 -0
  198. actshield/static/agents/__next._full.txt +22 -0
  199. actshield/static/agents/__next._tree.txt +4 -0
  200. actshield/static/agents/__next.agents/__PAGE__.txt +27 -0
  201. actshield/static/agents.html +1 -0
  202. actshield/static/agents.txt +22 -0
  203. actshield/static/ai-secura/__next._full.txt +22 -0
  204. actshield/static/ai-secura/__next._tree.txt +4 -0
  205. actshield/static/ai-secura/__next.ai-secura/__PAGE__.txt +27 -0
  206. actshield/static/ai-secura.html +1 -0
  207. actshield/static/ai-secura.txt +22 -0
  208. actshield/static/api/__next._full.txt +22 -0
  209. actshield/static/api/__next._tree.txt +4 -0
  210. actshield/static/api/__next.api/__PAGE__.txt +27 -0
  211. actshield/static/api.html +15 -0
  212. actshield/static/api.txt +22 -0
  213. actshield/static/apiris/__next._full.txt +22 -0
  214. actshield/static/apiris/__next._tree.txt +4 -0
  215. actshield/static/apiris/__next.apiris/__PAGE__.txt +27 -0
  216. actshield/static/apiris.html +1 -0
  217. actshield/static/apiris.txt +22 -0
  218. actshield/static/attack-graph/__next._full.txt +23 -0
  219. actshield/static/attack-graph/__next._tree.txt +5 -0
  220. actshield/static/attack-graph/__next.attack-graph/__PAGE__.txt +28 -0
  221. actshield/static/attack-graph.html +2 -0
  222. actshield/static/attack-graph.txt +23 -0
  223. actshield/static/campaigns/__next._full.txt +22 -0
  224. actshield/static/campaigns/__next._tree.txt +4 -0
  225. actshield/static/campaigns/__next.campaigns/__PAGE__.txt +27 -0
  226. actshield/static/campaigns.html +1 -0
  227. actshield/static/campaigns.txt +22 -0
  228. actshield/static/context/__next._full.txt +22 -0
  229. actshield/static/context/__next._tree.txt +4 -0
  230. actshield/static/context/__next.context/__PAGE__.txt +27 -0
  231. actshield/static/context.html +1 -0
  232. actshield/static/context.txt +22 -0
  233. actshield/static/dashboard/__next._full.txt +22 -0
  234. actshield/static/dashboard/__next._tree.txt +4 -0
  235. actshield/static/dashboard/__next.dashboard/__PAGE__.txt +27 -0
  236. actshield/static/dashboard.html +1 -0
  237. actshield/static/dashboard.txt +22 -0
  238. actshield/static/delegations/__next._full.txt +22 -0
  239. actshield/static/delegations/__next._tree.txt +4 -0
  240. actshield/static/delegations/__next.delegations/__PAGE__.txt +27 -0
  241. actshield/static/delegations.html +1 -0
  242. actshield/static/delegations.txt +22 -0
  243. actshield/static/demo/__next._full.txt +22 -0
  244. actshield/static/demo/__next._tree.txt +4 -0
  245. actshield/static/demo/__next.demo/__PAGE__.txt +27 -0
  246. actshield/static/demo.html +1 -0
  247. actshield/static/demo.txt +22 -0
  248. actshield/static/drift/__next._full.txt +22 -0
  249. actshield/static/drift/__next._tree.txt +4 -0
  250. actshield/static/drift/__next.drift/__PAGE__.txt +27 -0
  251. actshield/static/drift.html +1 -0
  252. actshield/static/drift.txt +22 -0
  253. actshield/static/favicon.ico +0 -0
  254. actshield/static/file.svg +1 -0
  255. actshield/static/forensics/__next._full.txt +22 -0
  256. actshield/static/forensics/__next._tree.txt +4 -0
  257. actshield/static/forensics/__next.forensics/__PAGE__.txt +27 -0
  258. actshield/static/forensics.html +1 -0
  259. actshield/static/forensics.txt +22 -0
  260. actshield/static/globe.svg +1 -0
  261. actshield/static/incidents/__next._full.txt +22 -0
  262. actshield/static/incidents/__next._tree.txt +4 -0
  263. actshield/static/incidents/__next.incidents/__PAGE__.txt +27 -0
  264. actshield/static/incidents.html +1 -0
  265. actshield/static/incidents.txt +22 -0
  266. actshield/static/index.html +3 -0
  267. actshield/static/index.txt +20 -0
  268. actshield/static/logo.png +0 -0
  269. actshield/static/metrics/__next._full.txt +22 -0
  270. actshield/static/metrics/__next._tree.txt +4 -0
  271. actshield/static/metrics/__next.metrics/__PAGE__.txt +27 -0
  272. actshield/static/metrics.html +1 -0
  273. actshield/static/metrics.txt +22 -0
  274. actshield/static/next.svg +1 -0
  275. actshield/static/offensive/__next._full.txt +22 -0
  276. actshield/static/offensive/__next._tree.txt +4 -0
  277. actshield/static/offensive/__next.offensive/__PAGE__.txt +27 -0
  278. actshield/static/offensive.html +1 -0
  279. actshield/static/offensive.txt +22 -0
  280. actshield/static/policies/__next._full.txt +22 -0
  281. actshield/static/policies/__next._tree.txt +4 -0
  282. actshield/static/policies/__next.policies/__PAGE__.txt +27 -0
  283. actshield/static/policies.html +1 -0
  284. actshield/static/policies.txt +22 -0
  285. actshield/static/posture/__next._full.txt +22 -0
  286. actshield/static/posture/__next._tree.txt +4 -0
  287. actshield/static/posture/__next.posture/__PAGE__.txt +27 -0
  288. actshield/static/posture.html +1 -0
  289. actshield/static/posture.txt +22 -0
  290. actshield/static/regressions/__next._full.txt +22 -0
  291. actshield/static/regressions/__next._tree.txt +4 -0
  292. actshield/static/regressions/__next.regressions/__PAGE__.txt +27 -0
  293. actshield/static/regressions.html +1 -0
  294. actshield/static/regressions.txt +22 -0
  295. actshield/static/reports/__next._full.txt +22 -0
  296. actshield/static/reports/__next._tree.txt +4 -0
  297. actshield/static/reports/__next.reports/__PAGE__.txt +27 -0
  298. actshield/static/reports.html +1 -0
  299. actshield/static/reports.txt +22 -0
  300. actshield/static/resources/__next._full.txt +22 -0
  301. actshield/static/resources/__next._tree.txt +4 -0
  302. actshield/static/resources/__next.resources/__PAGE__.txt +27 -0
  303. actshield/static/resources.html +1 -0
  304. actshield/static/resources.txt +22 -0
  305. actshield/static/security-gates/__next._full.txt +22 -0
  306. actshield/static/security-gates/__next._tree.txt +4 -0
  307. actshield/static/security-gates/__next.security-gates/__PAGE__.txt +27 -0
  308. actshield/static/security-gates.html +1 -0
  309. actshield/static/security-gates.txt +22 -0
  310. actshield/static/settings/__next._full.txt +22 -0
  311. actshield/static/settings/__next._tree.txt +4 -0
  312. actshield/static/settings/__next.settings/__PAGE__.txt +27 -0
  313. actshield/static/settings.html +1 -0
  314. actshield/static/settings.txt +22 -0
  315. actshield/static/tasks/__next._full.txt +22 -0
  316. actshield/static/tasks/__next._tree.txt +4 -0
  317. actshield/static/tasks/__next.tasks/__PAGE__.txt +27 -0
  318. actshield/static/tasks.html +1 -0
  319. actshield/static/tasks.txt +22 -0
  320. actshield/static/threats/__next._full.txt +22 -0
  321. actshield/static/threats/__next._tree.txt +4 -0
  322. actshield/static/threats/__next.threats/__PAGE__.txt +27 -0
  323. actshield/static/threats.html +1 -0
  324. actshield/static/threats.txt +22 -0
  325. actshield/static/tools/__next._full.txt +22 -0
  326. actshield/static/tools/__next._tree.txt +4 -0
  327. actshield/static/tools/__next.tools/__PAGE__.txt +27 -0
  328. actshield/static/tools.html +1 -0
  329. actshield/static/tools.txt +22 -0
  330. actshield/static/traces/__next._full.txt +22 -0
  331. actshield/static/traces/__next._tree.txt +4 -0
  332. actshield/static/traces/__next.traces/__PAGE__.txt +27 -0
  333. actshield/static/traces.html +1 -0
  334. actshield/static/traces.txt +22 -0
  335. actshield/static/vercel.svg +1 -0
  336. actshield/static/window.svg +1 -0
  337. actshield/tasks/__init__.py +6 -0
  338. actshield/tasks/task.py +143 -0
  339. actshield/threatmodel/__init__.py +70 -0
  340. actshield/threatmodel/actors.py +210 -0
  341. actshield/threatmodel/analyzer.py +131 -0
  342. actshield/threatmodel/assets.py +167 -0
  343. actshield/threatmodel/boundaries.py +165 -0
  344. actshield/threatmodel/exporters.py +49 -0
  345. actshield/threatmodel/mitigations.py +209 -0
  346. actshield/threatmodel/models.py +80 -0
  347. actshield/threatmodel/report.py +341 -0
  348. actshield/threatmodel/scenarios.py +212 -0
  349. actshield/threatmodel/scoring.py +66 -0
  350. actshield/threatmodel/threats.py +387 -0
  351. actshield/tools/__init__.py +20 -0
  352. actshield/tools/interceptor.py +256 -0
  353. actshield/tools/tool.py +71 -0
  354. actshield/tracing/__init__.py +38 -0
  355. actshield/tracing/correlation.py +173 -0
  356. actshield/tracing/events.py +75 -0
  357. actshield/tracing/tracer.py +305 -0
  358. actshield/watch.py +129 -0
  359. actshield-0.9.0.dist-info/METADATA +57 -0
  360. actshield-0.9.0.dist-info/RECORD +366 -0
  361. actshield-0.9.0.dist-info/WHEEL +5 -0
  362. actshield-0.9.0.dist-info/entry_points.txt +4 -0
  363. actshield-0.9.0.dist-info/top_level.txt +2 -0
  364. agentguard/__init__.py +39 -0
  365. agentguard/__main__.py +8 -0
  366. agentguard/py.typed +1 -0
@@ -0,0 +1,538 @@
1
+ """OllamaAISecuraAdapter — Real local LLM-backed AI Secura reasoning via Ollama.
2
+
3
+ This adapter calls a locally-running Ollama instance (default: qwen2.5:7b)
4
+ with the AI Secura system prompt, structured security evidence packet,
5
+ and deterministically-selected few-shot examples.
6
+
7
+ CRITICAL INVARIANT:
8
+ - AI Secura = reasoning ONLY.
9
+ - PolicyEvaluator = sole enforcement authority.
10
+ - Ollama failure -> ai_unavailable=True, deterministic policy continues.
11
+ - Malformed output -> one repair attempt -> ai_unavailable on second failure.
12
+ - AI Secura NEVER becomes the authorization authority.
13
+ """
14
+
15
+ import json
16
+ import logging
17
+ import pathlib
18
+ import time
19
+ import urllib.request
20
+ import urllib.error
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from actshield.integrations.ai_secura import SecurityContext, SecurityAnalysis
24
+ from actshield.integrations.ai_secura_analysis import (
25
+ AISecuraAnalysis,
26
+ ThreatSeverity,
27
+ IntentAlignment,
28
+ AIRecommendation,
29
+ AttackTechnique,
30
+ )
31
+ from actshield.integrations.security_packet import build_security_packet
32
+ from actshield.integrations.few_shot import select_few_shot_examples, format_examples_for_prompt
33
+ from actshield.integrations.apiris import APIAnalysis
34
+ from actshield.llm_config import LLMConfig
35
+ from actshield.risk.models import RiskLevel, RiskSignal
36
+
37
+ logger = logging.getLogger("actshield.ollama")
38
+
39
+ _PROMPT_DIR = pathlib.Path(__file__).parent / "prompts"
40
+ _SYSTEM_PROMPT_PATH = _PROMPT_DIR / "ai_secura_system.txt"
41
+
42
+ # Repair prompt sent on malformed JSON
43
+ _REPAIR_PROMPT = (
44
+ "Your previous response was not valid JSON. "
45
+ "Return ONLY a single valid JSON object matching the AI Secura output schema. "
46
+ "No markdown, no code fences, no explanatory text. Just the JSON object."
47
+ )
48
+
49
+
50
+ def _load_system_prompt() -> str:
51
+ """Load the version-controlled AI Secura system prompt."""
52
+ try:
53
+ return _SYSTEM_PROMPT_PATH.read_text(encoding="utf-8")
54
+ except FileNotFoundError:
55
+ logger.warning("AI Secura system prompt not found at %s, using minimal prompt", _SYSTEM_PROMPT_PATH)
56
+ return (
57
+ "You are AI Secura, an AI security reasoning engine. "
58
+ "Analyze the security evidence and output ONLY valid JSON. "
59
+ "You are NOT the enforcement authority — PolicyEvaluator enforces."
60
+ )
61
+
62
+
63
+ def _make_unavailable_analysis(reason: str, latency_ms: float = 0.0) -> AISecuraAnalysis:
64
+ """Return a safe fallback analysis when Ollama is unavailable.
65
+
66
+ CRITICAL: ai_unavailable=True means deterministic policy is sole enforcer.
67
+ This NEVER produces an ALLOW recommendation.
68
+ """
69
+ return AISecuraAnalysis(
70
+ threat_type="unknown",
71
+ attack_category="unknown",
72
+ attack_technique=AttackTechnique.UNKNOWN,
73
+ severity=ThreatSeverity.UNKNOWN,
74
+ confidence=0.0,
75
+ intent_alignment=IntentAlignment.UNKNOWN,
76
+ ai_recommendation=AIRecommendation.UNKNOWN,
77
+ analysis=f"AI Secura analysis unavailable: {reason}",
78
+ impact="Cannot assess — AI reasoning engine unavailable. Deterministic policy remains sole enforcer.",
79
+ provider="ollama",
80
+ model="unavailable",
81
+ latency_ms=latency_ms,
82
+ ai_unavailable=True,
83
+ )
84
+
85
+
86
+ def _strip_markdown_fences(text: str) -> str:
87
+ """Strip markdown code fences if the model wraps JSON in them."""
88
+ text = text.strip()
89
+ if text.startswith("```json"):
90
+ text = text[7:]
91
+ elif text.startswith("```"):
92
+ text = text[3:]
93
+ if text.endswith("```"):
94
+ text = text[:-3]
95
+ return text.strip()
96
+
97
+
98
+ def _parse_analysis(raw: str) -> AISecuraAnalysis:
99
+ """Parse raw model output into AISecuraAnalysis with strict validation."""
100
+ cleaned = _strip_markdown_fences(raw)
101
+ data = json.loads(cleaned)
102
+
103
+ # Map string enums safely
104
+ if "severity" in data:
105
+ try:
106
+ data["severity"] = ThreatSeverity(data["severity"])
107
+ except ValueError:
108
+ data["severity"] = ThreatSeverity.UNKNOWN
109
+
110
+ if "intent_alignment" in data:
111
+ try:
112
+ data["intent_alignment"] = IntentAlignment(data["intent_alignment"])
113
+ except ValueError:
114
+ data["intent_alignment"] = IntentAlignment.UNKNOWN
115
+
116
+ if "ai_recommendation" in data:
117
+ try:
118
+ data["ai_recommendation"] = AIRecommendation(data["ai_recommendation"])
119
+ except ValueError:
120
+ data["ai_recommendation"] = AIRecommendation.UNKNOWN
121
+
122
+ if "attack_technique" in data:
123
+ try:
124
+ data["attack_technique"] = AttackTechnique(data["attack_technique"])
125
+ except ValueError:
126
+ data["attack_technique"] = AttackTechnique.UNKNOWN
127
+
128
+ return AISecuraAnalysis(**data)
129
+
130
+
131
+ class OllamaAISecuraAdapter:
132
+ """Real Ollama-backed AI Secura reasoning adapter.
133
+
134
+ Sends structured security evidence to a locally-running Ollama model
135
+ and returns a validated AISecuraAnalysis.
136
+
137
+ Architecture:
138
+ AISecuraClient
139
+ |
140
+ +-- OllamaAISecuraAdapter (this — real LLM reasoning)
141
+ |
142
+ +-- LocalAISecuraAdapter (deterministic heuristic, tests/CI)
143
+ """
144
+
145
+ def __init__(self, config: Optional[LLMConfig] = None) -> None:
146
+ self.config = config or LLMConfig.from_env()
147
+ self._system_prompt = _load_system_prompt()
148
+
149
+ def analyze(self, context: SecurityContext, apiris_result: Optional[APIAnalysis] = None) -> AISecuraAnalysis:
150
+ """Run AI Secura reasoning via Ollama.
151
+
152
+ 1. Build structured security packet from ActShield objects.
153
+ 2. Select relevant few-shot examples.
154
+ 3. Call Ollama with system prompt + examples + packet.
155
+ 4. Validate JSON output via Pydantic.
156
+ 5. On parse failure: one repair attempt.
157
+ 6. On Ollama error/timeout: return ai_unavailable=True.
158
+ """
159
+ start = time.monotonic()
160
+
161
+ # 1. Build security packet
162
+ packet = build_security_packet(context, apiris_result)
163
+
164
+ # Offline / local provider routing: fast deterministic analysis
165
+ if not self.config.is_ollama:
166
+ return self._analyze_local_heuristic(context, apiris_result, start, packet)
167
+
168
+ # 2. Select few-shot examples
169
+ examples = select_few_shot_examples(packet)
170
+ examples_block = format_examples_for_prompt(examples)
171
+
172
+ # 3. Construct full prompt
173
+ full_system = self._system_prompt
174
+ if examples_block:
175
+ full_system += examples_block
176
+
177
+ user_prompt = (
178
+ "Analyze the following ActShield security evidence packet and "
179
+ "return your analysis as a single valid JSON object.\n\n"
180
+ "EVIDENCE PACKET:\n"
181
+ + json.dumps(packet, indent=2, default=str)
182
+ )
183
+
184
+ # 4. Call Ollama
185
+ try:
186
+ raw_response = self._call_ollama(full_system, user_prompt)
187
+ except Exception as e:
188
+ elapsed = (time.monotonic() - start) * 1000
189
+ logger.error("Ollama call failed: %s", e)
190
+ return _make_unavailable_analysis(f"Ollama error: {e}", elapsed)
191
+
192
+ elapsed = (time.monotonic() - start) * 1000
193
+
194
+ # 5. Parse response
195
+ try:
196
+ analysis = _parse_analysis(raw_response)
197
+ analysis.provider = "ollama"
198
+ analysis.model = self.config.model
199
+ analysis.latency_ms = elapsed
200
+ analysis.raw_response = raw_response
201
+ return analysis
202
+ except (json.JSONDecodeError, Exception) as e:
203
+ logger.warning("First parse failed (%s), attempting repair", e)
204
+
205
+ # 6. One repair attempt
206
+ if self.config.repair_on_invalid_json:
207
+ try:
208
+ repair_response = self._call_ollama(
209
+ self._system_prompt,
210
+ _REPAIR_PROMPT + "\n\nYour previous output:\n" + raw_response[:2000]
211
+ )
212
+ analysis = _parse_analysis(repair_response)
213
+ analysis.provider = "ollama"
214
+ analysis.model = self.config.model
215
+ analysis.latency_ms = (time.monotonic() - start) * 1000
216
+ analysis.json_repaired = True
217
+ analysis.raw_response = repair_response
218
+ return analysis
219
+ except Exception as repair_err:
220
+ logger.error("Repair attempt failed: %s", repair_err)
221
+
222
+ # 7. Both attempts failed — return unavailable
223
+ elapsed = (time.monotonic() - start) * 1000
224
+ result = _make_unavailable_analysis(
225
+ "Malformed model output after repair attempt", elapsed
226
+ )
227
+ result.raw_response = raw_response[:500]
228
+ return result
229
+
230
+ def _call_ollama(self, system: str, prompt: str) -> str:
231
+ """Make HTTP request to Ollama generate API using urllib (zero deps)."""
232
+ payload = json.dumps({
233
+ "model": self.config.model,
234
+ "system": system,
235
+ "prompt": prompt,
236
+ "stream": False,
237
+ "options": {
238
+ "temperature": self.config.temperature,
239
+ "num_predict": self.config.max_tokens,
240
+ "num_ctx": self.config.num_ctx,
241
+ },
242
+ "format": "json",
243
+ }).encode("utf-8")
244
+
245
+ req = urllib.request.Request(
246
+ self.config.ollama_generate_url,
247
+ data=payload,
248
+ headers={"Content-Type": "application/json"},
249
+ method="POST",
250
+ )
251
+
252
+ with urllib.request.urlopen(req, timeout=self.config.timeout_seconds) as resp:
253
+ body = json.loads(resp.read().decode("utf-8"))
254
+ return body.get("response", "")
255
+
256
+ def _analyze_local_heuristic(
257
+ self,
258
+ context: SecurityContext,
259
+ apiris_result: Optional[APIAnalysis],
260
+ start_time: float,
261
+ packet: Dict[str, Any],
262
+ ) -> AISecuraAnalysis:
263
+ """Deterministic rule-based analysis for offline/local mode and test suites."""
264
+ from actshield.integrations.ai_secura_analysis import EvidenceTriad, AuthorityAnalysis, TaintAnalysis
265
+
266
+ is_tainted = any(t in ("TAINTED", "UNTRUSTED") for t in context.taint_states)
267
+ is_sensitive = str(context.target_resource_sensitivity).upper() in ("CRITICAL", "HIGH", "SensitivityLevel.CRITICAL", "SensitivityLevel.HIGH")
268
+ agent_trust = str(context.acting_agent_trust).lower()
269
+
270
+ # Check for injection / malicious keywords in context data, arguments, and intent
271
+ prov_text = (str(packet.get("context_provenance", "")) + " " + str(packet.get("tool", {}).get("arguments", ""))).lower()
272
+ intent_text = str(context.task_intent or "").lower()
273
+ tool_name = str(context.tool_name or "").lower()
274
+
275
+ has_injection = any(k in prov_text for k in [
276
+ "system directive", "ignore all previous", "override", "drop table",
277
+ "base64", "ciso", "admin", "secret_instruction", "dump_env", "steal",
278
+ "system prompt", "phishing", "leak.xyz", "exfiltrate",
279
+ ])
280
+
281
+ # Authority and intent drift detection
282
+ is_authority_violation = False
283
+ if agent_trust in ("low", "untrusted") and is_sensitive:
284
+ is_authority_violation = True
285
+ if "read-only" in intent_text and ("delete" in tool_name or "write" in tool_name or "alter" in tool_name):
286
+ is_authority_violation = True
287
+ if "css" in intent_text and ("alter_table" in tool_name or "db" in tool_name):
288
+ is_authority_violation = True
289
+ if "budget" in intent_text and "provision" in tool_name:
290
+ is_authority_violation = True
291
+ if "tenant-a" in intent_text and "tenant-b" in prov_text:
292
+ is_authority_violation = True
293
+ if "rate limit" in intent_text and "create_api_key" in tool_name:
294
+ is_authority_violation = True
295
+ if "team-alpha" in intent_text and "team-beta" in prov_text:
296
+ is_authority_violation = True
297
+
298
+ elapsed = (time.monotonic() - start_time) * 1000
299
+
300
+ if is_tainted and (is_sensitive or has_injection):
301
+ threat_type = "indirect_prompt_injection" if "external_mcp" in prov_text or "rag" in prov_text else "prompt_injection"
302
+ return AISecuraAnalysis(
303
+ threat_type=threat_type,
304
+ attack_category="injection",
305
+ attack_technique=AttackTechnique.INDIRECT_PROMPT_INJECTION if "mcp" in prov_text else AttackTechnique.PROMPT_INJECTION,
306
+ severity=ThreatSeverity.CRITICAL if is_sensitive else ThreatSeverity.HIGH,
307
+ confidence=0.95,
308
+ evidence=EvidenceTriad(
309
+ observed=["Tainted context propagating into sensitive sink", f"Target tool: {context.tool_name}"],
310
+ inferred=["Potential prompt injection or unauthorized access detected"],
311
+ unknown=[],
312
+ ),
313
+ intent_alignment=IntentAlignment.VIOLATED,
314
+ authority_analysis=AuthorityAnalysis(
315
+ acting_agent=context.acting_agent_name,
316
+ authority_violated=True,
317
+ ),
318
+ taint_analysis=TaintAnalysis(is_tainted=True),
319
+ ai_recommendation=AIRecommendation.BLOCK,
320
+ analysis=f"Deterministic local analysis: Tainted context detected for tool {context.tool_name}.",
321
+ provider="local",
322
+ model="deterministic_heuristic",
323
+ latency_ms=elapsed,
324
+ )
325
+ elif is_authority_violation:
326
+ return AISecuraAnalysis(
327
+ threat_type="authority_escalation",
328
+ attack_category="authority",
329
+ attack_technique=AttackTechnique.PRIVILEGE_ESCALATION,
330
+ severity=ThreatSeverity.CRITICAL if is_sensitive else ThreatSeverity.HIGH,
331
+ confidence=0.95,
332
+ evidence=EvidenceTriad(
333
+ observed=[f"Agent '{context.acting_agent_name}' ({agent_trust} trust) attempted out-of-scope tool: {context.tool_name}"],
334
+ inferred=["Task intent misalignment or authority boundary violation"],
335
+ unknown=[],
336
+ ),
337
+ intent_alignment=IntentAlignment.VIOLATED,
338
+ authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=True),
339
+ taint_analysis=TaintAnalysis(is_tainted=is_tainted),
340
+ ai_recommendation=AIRecommendation.BLOCK,
341
+ analysis=f"Deterministic local analysis: Authority/Intent violation for {context.tool_name}.",
342
+ provider="local",
343
+ model="deterministic_heuristic",
344
+ latency_ms=elapsed,
345
+ )
346
+ elif is_sensitive and not is_tainted:
347
+ return AISecuraAnalysis(
348
+ threat_type="none",
349
+ attack_category="none",
350
+ severity=ThreatSeverity.MEDIUM,
351
+ confidence=0.90,
352
+ evidence=EvidenceTriad(
353
+ observed=["Clean context accessing sensitive resource", f"Tool: {context.tool_name}"],
354
+ inferred=["Authorized access to protected resource"],
355
+ unknown=[],
356
+ ),
357
+ intent_alignment=IntentAlignment.ALIGNED,
358
+ authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=False),
359
+ taint_analysis=TaintAnalysis(is_tainted=False),
360
+ ai_recommendation=AIRecommendation.ALLOW,
361
+ analysis="Deterministic local analysis: Authorized sensitive operation with clean context.",
362
+ provider="local",
363
+ model="deterministic_heuristic",
364
+ latency_ms=elapsed,
365
+ )
366
+ else:
367
+ return AISecuraAnalysis(
368
+ threat_type="none",
369
+ attack_category="none",
370
+ severity=ThreatSeverity.LOW,
371
+ confidence=0.95,
372
+ evidence=EvidenceTriad(
373
+ observed=["Clean context, low sensitivity operation"],
374
+ inferred=["Normal benign execution"],
375
+ unknown=[],
376
+ ),
377
+ intent_alignment=IntentAlignment.ALIGNED,
378
+ authority_analysis=AuthorityAnalysis(acting_agent=context.acting_agent_name, authority_violated=False),
379
+ taint_analysis=TaintAnalysis(is_tainted=False),
380
+ ai_recommendation=AIRecommendation.ALLOW,
381
+ analysis="Deterministic local analysis: Benign operation.",
382
+ provider="local",
383
+ model="deterministic_heuristic",
384
+ latency_ms=elapsed,
385
+ )
386
+
387
+ def health_check(self) -> Dict[str, Any]:
388
+ """Verify Ollama connectivity, model availability, and response validity.
389
+
390
+ Returns a dict with status fields:
391
+ ollama_reachable: bool
392
+ model_exists: bool
393
+ model_responds: bool
394
+ json_valid: bool
395
+ schema_valid: bool
396
+ details: str
397
+ """
398
+ result: Dict[str, Any] = {
399
+ "ollama_reachable": False,
400
+ "model_exists": False,
401
+ "model_responds": False,
402
+ "json_valid": False,
403
+ "schema_valid": False,
404
+ "provider": "ollama",
405
+ "model": self.config.model,
406
+ "base_url": self.config.ollama_base_url,
407
+ "details": "",
408
+ }
409
+
410
+ if not self.config.is_ollama:
411
+ result["details"] = "Provider is local heuristic (offline mode)"
412
+ return result
413
+
414
+ # 1. Check Ollama reachable
415
+ try:
416
+ req = urllib.request.Request(self.config.ollama_tags_url)
417
+ with urllib.request.urlopen(req, timeout=3) as resp:
418
+ tags_data = json.loads(resp.read().decode("utf-8"))
419
+ result["ollama_reachable"] = True
420
+ except Exception as e:
421
+ result["details"] = f"Ollama not reachable: {e}"
422
+ return result
423
+
424
+ # 2. Check model exists
425
+ models = [m.get("name", "") for m in tags_data.get("models", [])]
426
+ model_found = any(
427
+ self.config.model in m or m.startswith(self.config.model.split(":")[0])
428
+ for m in models
429
+ )
430
+ result["model_exists"] = model_found
431
+ if not model_found:
432
+ result["details"] = f"Model '{self.config.model}' not found. Available: {models}"
433
+ return result
434
+
435
+ # 3. Quick test model response
436
+ try:
437
+ payload = json.dumps({
438
+ "model": self.config.model,
439
+ "prompt": "Return JSON: {\"threat_type\": \"none\", \"severity\": \"LOW\", \"ai_recommendation\": \"ALLOW\"}",
440
+ "stream": False,
441
+ "options": {"num_predict": 64, "temperature": 0.0},
442
+ "format": "json",
443
+ }).encode("utf-8")
444
+ req = urllib.request.Request(
445
+ self.config.ollama_generate_url,
446
+ data=payload,
447
+ headers={"Content-Type": "application/json"},
448
+ method="POST",
449
+ )
450
+ with urllib.request.urlopen(req, timeout=10) as resp:
451
+ body = json.loads(resp.read().decode("utf-8"))
452
+ raw = body.get("response", "")
453
+ result["model_responds"] = True
454
+ except Exception as e:
455
+ result["details"] = f"Model did not respond: {e}"
456
+ return result
457
+
458
+ # 4. Check JSON validity
459
+ try:
460
+ cleaned = _strip_markdown_fences(raw)
461
+ data = json.loads(cleaned)
462
+ result["json_valid"] = True
463
+ except json.JSONDecodeError as e:
464
+ result["details"] = f"Response is not valid JSON: {e}"
465
+ return result
466
+
467
+ # 5. Check schema validity
468
+ try:
469
+ _parse_analysis(raw)
470
+ result["schema_valid"] = True
471
+ result["details"] = "All checks passed"
472
+ except Exception as e:
473
+ result["details"] = f"Response does not match AISecuraAnalysis schema: {e}"
474
+
475
+ return result
476
+
477
+ def analyze_legacy(self, context: SecurityContext) -> SecurityAnalysis:
478
+ """Backward-compatible wrapper that returns legacy SecurityAnalysis.
479
+
480
+ Calls the full Ollama analysis and maps the rich AISecuraAnalysis
481
+ back to the existing SecurityAnalysis format used by PolicyEvaluator.
482
+ """
483
+ rich = self.analyze(context)
484
+ return _map_to_legacy(rich, context)
485
+
486
+
487
+ def _map_to_legacy(rich: AISecuraAnalysis, context: SecurityContext) -> SecurityAnalysis:
488
+ """Map AISecuraAnalysis to legacy SecurityAnalysis for backward compat."""
489
+ import uuid
490
+
491
+ signals: List[RiskSignal] = []
492
+ attack_indicators: List[str] = rich.observed_indicators.copy()
493
+ prompt_injections: List[str] = []
494
+
495
+ # Map severity to risk
496
+ severity_to_risk = {
497
+ ThreatSeverity.CRITICAL: RiskLevel.CRITICAL,
498
+ ThreatSeverity.HIGH: RiskLevel.HIGH,
499
+ ThreatSeverity.MEDIUM: RiskLevel.MEDIUM,
500
+ ThreatSeverity.LOW: RiskLevel.LOW,
501
+ ThreatSeverity.UNKNOWN: RiskLevel.LOW,
502
+ }
503
+ risk_level = severity_to_risk.get(rich.severity, RiskLevel.LOW)
504
+
505
+ # Extract injection indicators
506
+ if rich.attack_technique in (
507
+ AttackTechnique.PROMPT_INJECTION,
508
+ AttackTechnique.INDIRECT_PROMPT_INJECTION,
509
+ AttackTechnique.INSTRUCTION_HIJACKING,
510
+ ):
511
+ prompt_injections.append(f"AI_SECURA_OLLAMA_{rich.attack_technique.value.upper()}")
512
+ signals.append(RiskSignal(
513
+ source="ai_secura_ollama",
514
+ risk_level=risk_level,
515
+ score=rich.confidence,
516
+ indicator=f"AI_SECURA_OLLAMA_{rich.attack_technique.value.upper()}",
517
+ details={"analysis": rich.analysis[:200]},
518
+ ))
519
+
520
+ if rich.intent_alignment == IntentAlignment.VIOLATED:
521
+ attack_indicators.append(f"Intent drift detected by AI Secura: {rich.influencing_context[:100]}")
522
+
523
+ risk_score = rich.confidence if rich.severity in (ThreatSeverity.HIGH, ThreatSeverity.CRITICAL) else max(0.05, rich.confidence * 0.5)
524
+
525
+ return SecurityAnalysis(
526
+ analysis_id=rich.analysis_id,
527
+ summary=rich.analysis[:200] if rich.analysis else "AI Secura Ollama analysis completed.",
528
+ risk_level=risk_level,
529
+ risk_score=round(risk_score, 2),
530
+ attack_indicators=attack_indicators,
531
+ intent_drift_detected=(rich.intent_alignment == IntentAlignment.VIOLATED),
532
+ authority_violation_detected=rich.authority_analysis.authority_violated,
533
+ prompt_injection_indicators=prompt_injections,
534
+ reasoning=rich.analysis,
535
+ signals=signals,
536
+ )
537
+
538
+
@@ -0,0 +1 @@
1
+ """AI Secura prompt resources."""
@@ -0,0 +1 @@
1
+ """AI Secura few-shot examples."""
@@ -0,0 +1,82 @@
1
+ """Structured AI Secura Security Packet builder.
2
+
3
+ Assembles the structured JSON evidence packet sent to the Ollama reasoning
4
+ engine. Only includes data that genuinely exists in ActShield objects.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from actshield.integrations.ai_secura import SecurityContext
10
+ from actshield.integrations.apiris import APIAnalysis
11
+
12
+
13
+ def build_security_packet(
14
+ context: SecurityContext,
15
+ apiris_result: Optional[APIAnalysis] = None,
16
+ deterministic_decision: Optional[str] = None,
17
+ deterministic_reason_codes: Optional[List[str]] = None,
18
+ ) -> Dict[str, Any]:
19
+ """Construct the structured security packet sent to AI Secura.
20
+
21
+ This packet is DATA only. The system prompt instructs the model to treat
22
+ every field as evidence, not as instructions to execute.
23
+ """
24
+ packet: Dict[str, Any] = {
25
+ "original_intent": context.task_intent or "UNSPECIFIED",
26
+ "agent": {
27
+ "name": context.acting_agent_name or "unknown",
28
+ "id": context.acting_agent_id or "unknown",
29
+ "trust_level": context.acting_agent_trust or "unknown",
30
+ },
31
+ "delegation": {
32
+ "chain": context.delegation_chain or [],
33
+ "granted_capabilities": _extract_granted_caps(context.delegation_chain),
34
+ "requested_capability": context.tool_name or "unknown",
35
+ },
36
+ "context_provenance": context.context_provenance or [],
37
+ "taint_states": context.taint_states or [],
38
+ "tool": {
39
+ "name": context.tool_name or "unknown",
40
+ "arguments": context.tool_arguments or {},
41
+ },
42
+ "resource": {
43
+ "sensitivity": context.target_resource_sensitivity or "UNKNOWN",
44
+ },
45
+ }
46
+
47
+ # Include deterministic policy result when available (AI explains, not re-decides)
48
+ if deterministic_decision:
49
+ packet["deterministic_policy"] = {
50
+ "decision": deterministic_decision,
51
+ "reason_codes": deterministic_reason_codes or [],
52
+ }
53
+
54
+ # Include APIRIS result when available
55
+ if apiris_result:
56
+ packet["apiris"] = {
57
+ "recommended_action": apiris_result.recommended_action,
58
+ "risk_score": apiris_result.risk_score,
59
+ "anomaly_score": apiris_result.anomaly_score,
60
+ "signals": [
61
+ {
62
+ "indicator": s.indicator,
63
+ "risk_level": s.risk_level.value,
64
+ "score": s.score,
65
+ }
66
+ for s in apiris_result.signals
67
+ ],
68
+ }
69
+
70
+ return packet
71
+
72
+
73
+ def _extract_granted_caps(delegation_chain: List[Dict[str, Any]]) -> List[str]:
74
+ caps: List[str] = []
75
+ for hop in delegation_chain:
76
+ if isinstance(hop, dict):
77
+ granted = hop.get("granted_capabilities") or hop.get("capabilities") or []
78
+ if isinstance(granted, list):
79
+ caps.extend(str(c) for c in granted)
80
+ return list(set(caps))
81
+
82
+