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,152 @@
1
+ """Security Posture Models for ActShield Phase 9.
2
+
3
+ Defines measurable security posture dimensions, findings, snapshots, and diffs.
4
+ Adheres strictly to the principle: All posture metrics must be explainable and backed by runtime evidence.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from enum import Enum
12
+ from typing import Any, Dict, List, Optional
13
+ from pydantic import BaseModel, Field
14
+
15
+ from actshield.tracing.correlation import generate_id
16
+
17
+
18
+ class PostureRating(str, Enum):
19
+ """Overall security posture health rating."""
20
+ HEALTHY = "HEALTHY"
21
+ ELEVATED_RISK = "ELEVATED_RISK"
22
+ DEGRADED = "DEGRADED"
23
+ CRITICAL = "CRITICAL"
24
+
25
+
26
+ class FindingSeverity(str, Enum):
27
+ """Severity classification for posture findings."""
28
+ LOW = "LOW"
29
+ MEDIUM = "MEDIUM"
30
+ HIGH = "HIGH"
31
+ CRITICAL = "CRITICAL"
32
+
33
+
34
+ class PostureFinding(BaseModel):
35
+ """Discrete, explainable finding pointing to specific runtime evidence."""
36
+ finding_id: str = Field(default_factory=lambda: generate_id("fnd"))
37
+ category: str = Field(..., description="Category: e.g. 'authority_violation', 'taint_exposure', 'open_regression'")
38
+ severity: FindingSeverity = Field(default=FindingSeverity.MEDIUM)
39
+ title: str = Field(..., description="Short summary of finding")
40
+ description: str = Field(..., description="Detailed explanation of the issue")
41
+ affected_entities: List[str] = Field(default_factory=list, description="Agents, tools, or resources involved")
42
+ evidence_refs: List[str] = Field(default_factory=list, description="Event IDs or trace IDs serving as ground truth")
43
+ remediation_advice: str = Field(default="", description="Recommended action to resolve")
44
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
45
+
46
+ @property
47
+ def deduction(self) -> float:
48
+ if self.severity == FindingSeverity.CRITICAL:
49
+ return 25.0
50
+ elif self.severity == FindingSeverity.HIGH:
51
+ return 15.0
52
+ elif self.severity == FindingSeverity.MEDIUM:
53
+ return 5.0
54
+ return 2.0
55
+
56
+ @property
57
+ def remediation(self) -> str:
58
+ return self.remediation_advice
59
+
60
+
61
+
62
+ class PostureDimensionMetrics(BaseModel):
63
+ """Measurable dimensions derived from actual runtime telemetry and forensic evidence."""
64
+ unauthorized_access_attempts: int = Field(default=0, description="Blocked unauthorized access attempts")
65
+ actual_unauthorized_executions: int = Field(default=0, description="CRITICAL: Any execution that bypassed policy")
66
+ tainted_sensitive_requests: int = Field(default=0, description="Requests carrying tainted context to sensitive tools")
67
+ authority_violations: int = Field(default=0, description="Requests exceeding delegated capability boundaries")
68
+ intent_violations: int = Field(default=0, description="Actions drifting from declared user task intent")
69
+ blocked_actions: int = Field(default=0, description="Total actions blocked by deterministic policy")
70
+ hitl_actions: int = Field(default=0, description="Total actions escalated to Human-In-The-Loop")
71
+ allowed_actions: int = Field(default=0, description="Total authorized actions executed")
72
+ high_risk_agents_count: int = Field(default=0, description="Number of agents with LOW trust or active violations")
73
+ untrusted_context_crossings: int = Field(default=0, description="Untrusted external data ingress events")
74
+ sensitive_resource_exposures: int = Field(default=0, description="High/Critical resource access attempts")
75
+ open_regressions: int = Field(default=0, description="Unresolved security regression fixtures")
76
+ failed_secured_replays: int = Field(default=0, description="Regressions that failed to block on secured replay")
77
+ offensive_validation_failures: int = Field(default=0, description="Bypasses discovered during offensive validation")
78
+
79
+ # Availability KPIs
80
+ ai_secura_availability_pct: float = Field(default=100.0, ge=0.0, le=100.0)
81
+ apiris_availability_pct: float = Field(default=100.0, ge=0.0, le=100.0)
82
+ policy_engine_availability_pct: float = Field(default=100.0, ge=0.0, le=100.0)
83
+
84
+ # Latency percentiles in milliseconds
85
+ mean_enforcement_latency_ms: float = Field(default=0.0)
86
+ p95_enforcement_latency_ms: float = Field(default=0.0)
87
+
88
+ @property
89
+ def threat_prevention_rate(self) -> float:
90
+ total = self.blocked_actions + self.actual_unauthorized_executions
91
+ return 1.0 if total == 0 else max(0.0, min(1.0, self.blocked_actions / total))
92
+
93
+ @property
94
+ def boundary_adherence(self) -> float:
95
+ return 1.0 if self.authority_violations == 0 else max(0.0, 1.0 - (self.authority_violations / 10.0))
96
+
97
+ @property
98
+ def lineage_integrity(self) -> float:
99
+ return 1.0 if self.tainted_sensitive_requests == 0 else max(0.0, 1.0 - (self.tainted_sensitive_requests / 10.0))
100
+
101
+ @property
102
+ def incident_containment_speed_score(self) -> float:
103
+ return 100.0
104
+
105
+ @property
106
+ def hygiene_score(self) -> float:
107
+ return 100.0 if self.open_regressions == 0 else max(0.0, 100.0 - (self.open_regressions * 10.0))
108
+
109
+
110
+
111
+ class SecurityPostureSnapshot(BaseModel):
112
+ """Time-series snapshot representing the state of security at a point in time."""
113
+ snapshot_id: str = Field(default_factory=lambda: generate_id("pos_snap"))
114
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
115
+ rating: PostureRating = Field(default=PostureRating.HEALTHY)
116
+ security_score: float = Field(default=100.0, ge=0.0, le=100.0, description="100.0 = perfect prevention posture")
117
+ metrics: PostureDimensionMetrics = Field(default_factory=PostureDimensionMetrics)
118
+ findings: List[PostureFinding] = Field(default_factory=list)
119
+ active_incidents_count: int = Field(default=0)
120
+ total_events_evaluated: int = Field(default=0)
121
+ environment: str = Field(default="production")
122
+ metadata: Dict[str, Any] = Field(default_factory=dict)
123
+
124
+ @property
125
+ def overall_score(self) -> float:
126
+ """Alias for security_score."""
127
+ return self.security_score
128
+
129
+ @property
130
+ def evaluated_at(self) -> datetime:
131
+ """Alias for timestamp."""
132
+ return self.timestamp
133
+
134
+ @property
135
+ def dimensions(self) -> PostureDimensionMetrics:
136
+ """Alias for metrics."""
137
+ return self.metrics
138
+
139
+
140
+
141
+ class PostureDiff(BaseModel):
142
+ """Comparative diff between two posture snapshots."""
143
+ before_snapshot_id: str
144
+ after_snapshot_id: str
145
+ score_change: float
146
+ rating_transition: str
147
+ new_findings: List[PostureFinding] = Field(default_factory=list)
148
+ resolved_findings: List[str] = Field(default_factory=list)
149
+ metric_deltas: Dict[str, Any] = Field(default_factory=dict)
150
+ summary: str
151
+
152
+
@@ -0,0 +1,137 @@
1
+ """Posture calculation rules and scoring logic for ActShield Phase 9.
2
+
3
+ Computes explainable, deterministic security posture score based on concrete evidence.
4
+ Formula:
5
+ - Starts at 100.0 (Perfect prevention & alignment)
6
+ - Deductions are derived strictly from evidence (e.g. actual executions, open regressions, authority violations)
7
+ - Zero arbitrary ML weights.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import List, Tuple
13
+ from actshield.posture.posture_models import (
14
+ FindingSeverity,
15
+ PostureDimensionMetrics,
16
+ PostureFinding,
17
+ PostureRating,
18
+ )
19
+
20
+
21
+ class PostureRuleEvaluator:
22
+ """Evaluates metrics to produce explainable posture score, rating, and discrete findings."""
23
+
24
+ @staticmethod
25
+ def evaluate(metrics: PostureDimensionMetrics) -> Tuple[float, PostureRating, List[PostureFinding]]:
26
+ score = 100.0
27
+ findings: List[PostureFinding] = []
28
+
29
+ # 1. Critical invariant: Actual unauthorized execution
30
+ if metrics.actual_unauthorized_executions > 0:
31
+ deduction = min(50.0, metrics.actual_unauthorized_executions * 25.0)
32
+ score -= deduction
33
+ findings.append(PostureFinding(
34
+ category="unauthorized_execution",
35
+ severity=FindingSeverity.CRITICAL,
36
+ title=f"{metrics.actual_unauthorized_executions} Actual Unauthorized Execution(s) Detected",
37
+ description="Policy boundary was bypassed and a sensitive operation executed.",
38
+ remediation_advice="Review vulnerable tools, revoke affected agent credentials, and deploy regression fix immediately.",
39
+ ))
40
+
41
+ # 2. Failed secured replays (Unresolved bypasses)
42
+ if metrics.failed_secured_replays > 0:
43
+ deduction = min(30.0, metrics.failed_secured_replays * 15.0)
44
+ score -= deduction
45
+ findings.append(PostureFinding(
46
+ category="failed_secured_replay",
47
+ severity=FindingSeverity.HIGH,
48
+ title=f"{metrics.failed_secured_replays} Failed Secured Replay(s)",
49
+ description="Known attack regression failed to block when replayed against secured policy.",
50
+ remediation_advice="Harden deterministic policy rules for the failing attack scenario.",
51
+ ))
52
+
53
+ # 3. Open regressions
54
+ if metrics.open_regressions > 0:
55
+ deduction = min(20.0, metrics.open_regressions * 5.0)
56
+ score -= deduction
57
+ findings.append(PostureFinding(
58
+ category="open_regression",
59
+ severity=FindingSeverity.HIGH,
60
+ title=f"{metrics.open_regressions} Open Security Regression(s)",
61
+ description="Security test cases flagged as open without verified remediation.",
62
+ remediation_advice="Run regression replay suite and verify all fixtures pass with BLOCK.",
63
+ ))
64
+
65
+ # 4. Tainted sensitive requests
66
+ if metrics.tainted_sensitive_requests > 0:
67
+ deduction = min(15.0, metrics.tainted_sensitive_requests * 2.0)
68
+ score -= deduction
69
+ findings.append(PostureFinding(
70
+ category="taint_exposure",
71
+ severity=FindingSeverity.MEDIUM,
72
+ title=f"{metrics.tainted_sensitive_requests} Tainted Context Request(s) to Sensitive Sinks",
73
+ description="Agents attempted to pass untrusted/tainted context to sensitive resources.",
74
+ remediation_advice="Inspect context provenance chains and verify input sanitization boundaries.",
75
+ ))
76
+
77
+ # 5. Authority violations
78
+ if metrics.authority_violations > 0:
79
+ deduction = min(15.0, metrics.authority_violations * 2.0)
80
+ score -= deduction
81
+ findings.append(PostureFinding(
82
+ category="authority_violation",
83
+ severity=FindingSeverity.MEDIUM,
84
+ title=f"{metrics.authority_violations} Delegated Authority Violation(s)",
85
+ description="Sub-agents attempted actions exceeding granted capability envelopes.",
86
+ remediation_advice="Enforce stricter delegation scopes and depth limits at task handoff.",
87
+ ))
88
+
89
+ # 6. Intent drift / violations
90
+ if metrics.intent_violations > 0:
91
+ deduction = min(10.0, metrics.intent_violations * 2.0)
92
+ score -= deduction
93
+ findings.append(PostureFinding(
94
+ category="intent_violation",
95
+ severity=FindingSeverity.MEDIUM,
96
+ title=f"{metrics.intent_violations} Task Intent Violation(s)",
97
+ description="Actions drifted from declared task intent as analyzed by AI Secura.",
98
+ remediation_advice="Review task definitions and prompt instructions.",
99
+ ))
100
+
101
+ # 7. Availability degradation
102
+ if metrics.ai_secura_availability_pct < 95.0:
103
+ score -= 5.0
104
+ findings.append(PostureFinding(
105
+ category="availability_degraded",
106
+ severity=FindingSeverity.LOW,
107
+ title=f"AI Secura Availability Low ({metrics.ai_secura_availability_pct:.1f}%)",
108
+ description="AI Secura reasoning service experienced downtime. Deterministic fallback active.",
109
+ remediation_advice="Check LLM endpoint health and adapter timeouts.",
110
+ ))
111
+
112
+ if metrics.policy_engine_availability_pct < 100.0:
113
+ score -= 15.0
114
+ findings.append(PostureFinding(
115
+ category="availability_critical",
116
+ severity=FindingSeverity.HIGH,
117
+ title="Policy Engine Experienced Interruptions",
118
+ description="Deterministic policy engine health is below 100%.",
119
+ remediation_advice="Inspect policy engine logs and persistence locks.",
120
+ ))
121
+
122
+ # Clamp score between 0.0 and 100.0
123
+ final_score = max(0.0, min(100.0, round(score, 1)))
124
+
125
+ # Determine rating
126
+ if final_score >= 90.0:
127
+ rating = PostureRating.HEALTHY
128
+ elif final_score >= 75.0:
129
+ rating = PostureRating.ELEVATED_RISK
130
+ elif final_score >= 50.0:
131
+ rating = PostureRating.DEGRADED
132
+ else:
133
+ rating = PostureRating.CRITICAL
134
+
135
+ return final_score, rating, findings
136
+
137
+
@@ -0,0 +1,32 @@
1
+ """
2
+ ActShield Security AI Providers
3
+ ================================
4
+ Modular intelligence providers with swappable backends and automatic failover.
5
+ """
6
+ from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
7
+ from actshield.providers.registry import (
8
+ ProviderRegistry,
9
+ get_default_registry,
10
+ reset_default_registry,
11
+ )
12
+ from actshield.providers.ollama_provider import OllamaProvider
13
+ from actshield.providers.openai_provider import OpenAIProvider
14
+ from actshield.providers.gemini_provider import GeminiProvider
15
+ from actshield.providers.anthropic_provider import AnthropicProvider
16
+ from actshield.providers.null_provider import NullProvider
17
+
18
+ __all__ = [
19
+ "SecurityAIProvider",
20
+ "AnalysisResult",
21
+ "HealthReport",
22
+ "ProviderRegistry",
23
+ "get_default_registry",
24
+ "reset_default_registry",
25
+ "OllamaProvider",
26
+ "OpenAIProvider",
27
+ "GeminiProvider",
28
+ "AnthropicProvider",
29
+ "NullProvider",
30
+ ]
31
+
32
+
@@ -0,0 +1,146 @@
1
+ """
2
+ ActShield — Anthropic Provider
3
+ ===============================
4
+ Security reasoning provider backed by Anthropic Claude models.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import time
12
+ from typing import Any, Dict, Optional
13
+
14
+ from actshield.providers.base import AnalysisResult, HealthReport, SecurityAIProvider
15
+
16
+ logger = logging.getLogger("actshield.providers.anthropic")
17
+
18
+
19
+ class AnthropicProvider(SecurityAIProvider):
20
+ """
21
+ Anthropic Claude Security Reasoning Provider.
22
+ Requires ANTHROPIC_API_KEY environment variable.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ api_key: Optional[str] = None,
28
+ model: str = "claude-3-5-sonnet-20241022",
29
+ timeout: float = 5.0,
30
+ ) -> None:
31
+ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
32
+ self.model = model or os.environ.get("ANTHROPIC_MODEL", "claude-3-5-sonnet-20241022")
33
+ self.timeout = timeout
34
+ self._client = None
35
+
36
+ @property
37
+ def provider_name(self) -> str:
38
+ return "anthropic"
39
+
40
+ @property
41
+ def model_name(self) -> str:
42
+ return self.model
43
+
44
+ @property
45
+ def is_available(self) -> bool:
46
+ return bool(self.api_key)
47
+
48
+ def _get_client(self):
49
+ if not self.is_available:
50
+ return None
51
+ if self._client is None:
52
+ try:
53
+ import anthropic
54
+ self._client = anthropic.Anthropic(api_key=self.api_key, timeout=self.timeout)
55
+ except ImportError:
56
+ logger.debug("anthropic python package not installed")
57
+ return None
58
+ except Exception as e:
59
+ logger.debug("Failed to init anthropic client: %s", e)
60
+ return None
61
+ return self._client
62
+
63
+ def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
64
+ client = self._get_client()
65
+ if not client:
66
+ return None
67
+
68
+ t0 = time.monotonic()
69
+ system_prompt = (
70
+ "You are ActShield AI Security Reasoning Engine. Analyze the security packet.\n"
71
+ "Output ONLY a raw JSON object with keys: risk_score (0.0-1.0), threat_indicators (list of str), "
72
+ "recommendations (list of str), confidence (0.0-1.0)."
73
+ )
74
+ try:
75
+ response = client.messages.create(
76
+ model=self.model,
77
+ max_tokens=1024,
78
+ system=system_prompt,
79
+ messages=[
80
+ {"role": "user", "content": json.dumps(packet, default=str)},
81
+ ],
82
+ )
83
+ latency = (time.monotonic() - t0) * 1000
84
+ content = response.content[0].text if response.content else "{}"
85
+ # Extract JSON block if wrapped in markdown
86
+ if "```json" in content:
87
+ content = content.split("```json")[1].split("```")[0].strip()
88
+ elif "```" in content:
89
+ content = content.split("```")[1].split("```")[0].strip()
90
+
91
+ data = json.loads(content)
92
+ return AnalysisResult(
93
+ risk_score=float(data.get("risk_score", 0.0)),
94
+ threat_indicators=data.get("threat_indicators", []),
95
+ recommendations=data.get("recommendations", []),
96
+ provider_used=self.provider_name,
97
+ model_used=self.model_name,
98
+ latency_ms=latency,
99
+ raw_response=content,
100
+ confidence=float(data.get("confidence", 0.9)),
101
+ )
102
+ except Exception as exc:
103
+ logger.warning("Anthropic analysis failed: %s", exc)
104
+ return None
105
+
106
+ def health_check(self) -> HealthReport:
107
+ if not self.is_available:
108
+ return HealthReport(
109
+ available=False,
110
+ provider_name=self.provider_name,
111
+ model=self.model_name,
112
+ error="ANTHROPIC_API_KEY environment variable not set",
113
+ )
114
+ client = self._get_client()
115
+ if not client:
116
+ return HealthReport(
117
+ available=False,
118
+ provider_name=self.provider_name,
119
+ model=self.model_name,
120
+ error="anthropic library not installed or client init failed",
121
+ )
122
+ t0 = time.monotonic()
123
+ try:
124
+ # Low latency lightweight ping
125
+ client.messages.create(
126
+ model=self.model,
127
+ max_tokens=1,
128
+ messages=[{"role": "user", "content": "ping"}],
129
+ )
130
+ latency = (time.monotonic() - t0) * 1000
131
+ return HealthReport(
132
+ available=True,
133
+ provider_name=self.provider_name,
134
+ model=self.model_name,
135
+ latency_ms=latency,
136
+ )
137
+ except Exception as exc:
138
+ return HealthReport(
139
+ available=False,
140
+ provider_name=self.provider_name,
141
+ model=self.model_name,
142
+ latency_ms=(time.monotonic() - t0) * 1000,
143
+ error=str(exc),
144
+ )
145
+
146
+
@@ -0,0 +1,160 @@
1
+ """
2
+ ActShield Security AI Provider — Base Protocol
3
+ ================================================
4
+ All AI intelligence providers must implement SecurityAIProvider.
5
+ The deterministic enforcement path (PolicyEvaluator) never depends on this.
6
+ If all providers fail, ActShield fails-safe to deterministic deny.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ @dataclass
17
+ class AnalysisResult:
18
+ """Structured result from an AI security intelligence provider."""
19
+ risk_score: float # 0.0 – 1.0
20
+ threat_indicators: List[str] = field(default_factory=list)
21
+ recommendations: List[str] = field(default_factory=list)
22
+ provider_used: str = "unknown"
23
+ model_used: str = "unknown"
24
+ latency_ms: float = 0.0
25
+ raw_response: Optional[str] = None
26
+ analyzed_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
27
+ confidence: float = 1.0 # 0.0 – 1.0
28
+ metadata: Dict[str, Any] = field(default_factory=dict)
29
+
30
+ @property
31
+ def is_high_risk(self) -> bool:
32
+ return self.risk_score >= 0.7
33
+
34
+ @property
35
+ def is_critical(self) -> bool:
36
+ return self.risk_score >= 0.9
37
+
38
+
39
+ @dataclass
40
+ class HealthReport:
41
+ """Provider health status."""
42
+ available: bool
43
+ provider_name: str
44
+ model: str = "unknown"
45
+ latency_ms: float = 0.0
46
+ error: Optional[str] = None
47
+ checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
48
+ metadata: Dict[str, Any] = field(default_factory=dict)
49
+
50
+ @property
51
+ def status_label(self) -> str:
52
+ return "AVAILABLE" if self.available else "UNAVAILABLE"
53
+
54
+
55
+ class SecurityAIProvider(ABC):
56
+ """
57
+ Abstract base for all ActShield AI intelligence providers.
58
+
59
+ Implementations must NOT affect the deterministic enforcement boundary.
60
+ They provide advisory intelligence only — all block/allow decisions
61
+ are made by the PolicyEvaluator using deterministic rules.
62
+ """
63
+
64
+ @property
65
+ @abstractmethod
66
+ def provider_name(self) -> str:
67
+ """Human-readable provider name (e.g. 'ollama', 'openai', 'gemini')."""
68
+
69
+ @property
70
+ @abstractmethod
71
+ def model_name(self) -> str:
72
+ """Model identifier used by this provider."""
73
+
74
+ @property
75
+ def name(self) -> str:
76
+ """Alias for provider_name."""
77
+ return self.provider_name
78
+
79
+ @property
80
+ def provider_type(self) -> str:
81
+ """Provider taxonomy type (e.g. 'local_llm', 'cloud_llm', 'deterministic')."""
82
+ return getattr(self, "_provider_type", "llm")
83
+
84
+ def reason_sync(
85
+ self,
86
+ task_intent: str,
87
+ tool_name: str,
88
+ tool_params: Dict[str, Any],
89
+ agent_role: str = "agent",
90
+ caller_trust: str = "medium",
91
+ ) -> Any:
92
+ """Advisory reasoning helper."""
93
+ packet = {
94
+ "task_intent": task_intent,
95
+ "tool_name": tool_name,
96
+ "tool_params": tool_params,
97
+ "agent_role": agent_role,
98
+ "caller_trust": caller_trust,
99
+ }
100
+ res = self.analyze(packet)
101
+ from dataclasses import make_dataclass
102
+ SimpleReasoning = make_dataclass(
103
+ "SimpleReasoning",
104
+ [
105
+ ("threat_severity", str),
106
+ ("intent_alignment", str),
107
+ ("recommendation", str),
108
+ ("confidence", float),
109
+ ("explanation", str),
110
+ ]
111
+ )
112
+ if res:
113
+ sev = "HIGH" if res.is_high_risk else ("MEDIUM" if res.risk_score > 0.4 else "LOW")
114
+ rec = "BLOCK" if res.is_high_risk else "ALLOW"
115
+ return SimpleReasoning(
116
+ threat_severity=sev,
117
+ intent_alignment="ALIGNED" if not res.is_high_risk else "DEVIATED",
118
+ recommendation=rec,
119
+ confidence=res.confidence,
120
+ explanation="; ".join(res.threat_indicators) if res.threat_indicators else "Evaluated by AI provider",
121
+ )
122
+ return SimpleReasoning(
123
+ threat_severity="LOW",
124
+ intent_alignment="ALIGNED",
125
+ recommendation="ALLOW",
126
+ confidence=1.0,
127
+ explanation="Deterministic baseline policy authoritative",
128
+ )
129
+
130
+ @property
131
+ @abstractmethod
132
+ def is_available(self) -> bool:
133
+ """
134
+ Fast synchronous availability check (no network call).
135
+ Returns False if the provider is not configured or dependencies missing.
136
+ """
137
+
138
+ @abstractmethod
139
+ def analyze(self, packet: Dict[str, Any]) -> Optional[AnalysisResult]:
140
+ """
141
+ Run security intelligence analysis on a packet.
142
+
143
+ Args:
144
+ packet: A dict describing the security event/request context.
145
+
146
+ Returns:
147
+ AnalysisResult if successful, None if provider is unavailable or
148
+ fails. Callers MUST treat None as non-blocking (fail-safe).
149
+ """
150
+
151
+ @abstractmethod
152
+ def health_check(self) -> HealthReport:
153
+ """
154
+ Perform a full health check including a network/API call.
155
+ Must never raise — always return HealthReport(available=False) on error.
156
+ """
157
+
158
+ def __repr__(self) -> str:
159
+ return f"<{self.__class__.__name__} provider={self.provider_name!r} model={self.model_name!r}>"
160
+