driftshield-sdk 0.2.0__tar.gz

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 (221) hide show
  1. driftshield_sdk-0.2.0/.gitignore +16 -0
  2. driftshield_sdk-0.2.0/PKG-INFO +72 -0
  3. driftshield_sdk-0.2.0/README.md +37 -0
  4. driftshield_sdk-0.2.0/pyproject.toml +110 -0
  5. driftshield_sdk-0.2.0/src/driftshield/__init__.py +31 -0
  6. driftshield_sdk-0.2.0/src/driftshield/api/__init__.py +0 -0
  7. driftshield_sdk-0.2.0/src/driftshield/api/app.py +47 -0
  8. driftshield_sdk-0.2.0/src/driftshield/api/auth.py +13 -0
  9. driftshield_sdk-0.2.0/src/driftshield/api/dependencies.py +24 -0
  10. driftshield_sdk-0.2.0/src/driftshield/api/ingest_workflow.py +102 -0
  11. driftshield_sdk-0.2.0/src/driftshield/api/routes/__init__.py +0 -0
  12. driftshield_sdk-0.2.0/src/driftshield/api/routes/behaviour.py +160 -0
  13. driftshield_sdk-0.2.0/src/driftshield/api/routes/connectors.py +209 -0
  14. driftshield_sdk-0.2.0/src/driftshield/api/routes/health.py +13 -0
  15. driftshield_sdk-0.2.0/src/driftshield/api/routes/ingest.py +39 -0
  16. driftshield_sdk-0.2.0/src/driftshield/api/routes/reports.py +327 -0
  17. driftshield_sdk-0.2.0/src/driftshield/api/routes/sessions.py +686 -0
  18. driftshield_sdk-0.2.0/src/driftshield/api/schemas.py +350 -0
  19. driftshield_sdk-0.2.0/src/driftshield/api/security.py +63 -0
  20. driftshield_sdk-0.2.0/src/driftshield/api/server.py +18 -0
  21. driftshield_sdk-0.2.0/src/driftshield/cli/__init__.py +0 -0
  22. driftshield_sdk-0.2.0/src/driftshield/cli/_batch.py +244 -0
  23. driftshield_sdk-0.2.0/src/driftshield/cli/_submit.py +101 -0
  24. driftshield_sdk-0.2.0/src/driftshield/cli/commands/__init__.py +0 -0
  25. driftshield_sdk-0.2.0/src/driftshield/cli/commands/analyze.py +177 -0
  26. driftshield_sdk-0.2.0/src/driftshield/cli/commands/batch.py +175 -0
  27. driftshield_sdk-0.2.0/src/driftshield/cli/commands/connectors.py +328 -0
  28. driftshield_sdk-0.2.0/src/driftshield/cli/commands/export_validations.py +24 -0
  29. driftshield_sdk-0.2.0/src/driftshield/cli/commands/generate_fixtures.py +27 -0
  30. driftshield_sdk-0.2.0/src/driftshield/cli/commands/ingest.py +386 -0
  31. driftshield_sdk-0.2.0/src/driftshield/cli/commands/inspect.py +167 -0
  32. driftshield_sdk-0.2.0/src/driftshield/cli/commands/list.py +74 -0
  33. driftshield_sdk-0.2.0/src/driftshield/cli/commands/report.py +55 -0
  34. driftshield_sdk-0.2.0/src/driftshield/cli/commands/show_result.py +106 -0
  35. driftshield_sdk-0.2.0/src/driftshield/cli/commands/signatures.py +96 -0
  36. driftshield_sdk-0.2.0/src/driftshield/cli/commands/submit.py +104 -0
  37. driftshield_sdk-0.2.0/src/driftshield/cli/commands/telemetry.py +238 -0
  38. driftshield_sdk-0.2.0/src/driftshield/cli/discovery.py +96 -0
  39. driftshield_sdk-0.2.0/src/driftshield/cli/main.py +64 -0
  40. driftshield_sdk-0.2.0/src/driftshield/cli/output.py +125 -0
  41. driftshield_sdk-0.2.0/src/driftshield/connectors/__init__.py +1 -0
  42. driftshield_sdk-0.2.0/src/driftshield/connectors/registry.py +193 -0
  43. driftshield_sdk-0.2.0/src/driftshield/connectors/watcher.py +272 -0
  44. driftshield_sdk-0.2.0/src/driftshield/core/__init__.py +0 -0
  45. driftshield_sdk-0.2.0/src/driftshield/core/analysis/__init__.py +0 -0
  46. driftshield_sdk-0.2.0/src/driftshield/core/analysis/heuristics.py +434 -0
  47. driftshield_sdk-0.2.0/src/driftshield/core/analysis/inflection.py +356 -0
  48. driftshield_sdk-0.2.0/src/driftshield/core/analysis/risk.py +113 -0
  49. driftshield_sdk-0.2.0/src/driftshield/core/analysis/session.py +119 -0
  50. driftshield_sdk-0.2.0/src/driftshield/core/canonical_analysis.py +1208 -0
  51. driftshield_sdk-0.2.0/src/driftshield/core/deterministic_matching.py +671 -0
  52. driftshield_sdk-0.2.0/src/driftshield/core/graph/__init__.py +0 -0
  53. driftshield_sdk-0.2.0/src/driftshield/core/graph/builder.py +229 -0
  54. driftshield_sdk-0.2.0/src/driftshield/core/graph/models.py +261 -0
  55. driftshield_sdk-0.2.0/src/driftshield/core/integrity.py +177 -0
  56. driftshield_sdk-0.2.0/src/driftshield/core/models.py +467 -0
  57. driftshield_sdk-0.2.0/src/driftshield/core/normalization.py +375 -0
  58. driftshield_sdk-0.2.0/src/driftshield/core/visibility.py +187 -0
  59. driftshield_sdk-0.2.0/src/driftshield/db/__init__.py +0 -0
  60. driftshield_sdk-0.2.0/src/driftshield/db/behaviour_service.py +192 -0
  61. driftshield_sdk-0.2.0/src/driftshield/db/connector_service.py +209 -0
  62. driftshield_sdk-0.2.0/src/driftshield/db/engine.py +19 -0
  63. driftshield_sdk-0.2.0/src/driftshield/db/hosted_schema_sql.py +230 -0
  64. driftshield_sdk-0.2.0/src/driftshield/db/ingest_service.py +66 -0
  65. driftshield_sdk-0.2.0/src/driftshield/db/migrations/README +1 -0
  66. driftshield_sdk-0.2.0/src/driftshield/db/migrations/env.py +81 -0
  67. driftshield_sdk-0.2.0/src/driftshield/db/migrations/script.py.mako +28 -0
  68. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/1c2f9f4b7d21_add_transcript_provenance_and_dedupe.py +45 -0
  69. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/20260511_01_add_hosted_submission_schema.py +28 -0
  70. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/20260512_02_seed_oss_fallback_installation.py +39 -0
  71. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/20260516_03_json_to_jsonb_for_distinct_and_equality.py +64 -0
  72. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/2d4f6b9e8c13_add_connectors_table.py +56 -0
  73. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/7f2d6c4a9b31_add_analyst_validations_table.py +48 -0
  74. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/8f1a2b3c4d5e_add_connector_last_ingested_at.py +107 -0
  75. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/9b3d7e1a4c2f_remove_recurrence_tables_from_oss.py +75 -0
  76. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/b7c1e3d5f6a2_add_forensic_cases_table.py +47 -0
  77. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/c3d9e4f1a2b5_add_behaviour_event_tables.py +89 -0
  78. driftshield_sdk-0.2.0/src/driftshield/db/migrations/versions/e0b85984643e_initial_schema_5_core_tables.py +101 -0
  79. driftshield_sdk-0.2.0/src/driftshield/db/models.py +267 -0
  80. driftshield_sdk-0.2.0/src/driftshield/db/persistence.py +779 -0
  81. driftshield_sdk-0.2.0/src/driftshield/db/validation_service.py +366 -0
  82. driftshield_sdk-0.2.0/src/driftshield/fixtures/__init__.py +6 -0
  83. driftshield_sdk-0.2.0/src/driftshield/fixtures/transcript_generator.py +132 -0
  84. driftshield_sdk-0.2.0/src/driftshield/intake_contract.py +158 -0
  85. driftshield_sdk-0.2.0/src/driftshield/parsers/__init__.py +1 -0
  86. driftshield_sdk-0.2.0/src/driftshield/parsers/claude_code.py +302 -0
  87. driftshield_sdk-0.2.0/src/driftshield/parsers/claude_desktop.py +6 -0
  88. driftshield_sdk-0.2.0/src/driftshield/parsers/codex_cli.py +6 -0
  89. driftshield_sdk-0.2.0/src/driftshield/parsers/codex_desktop.py +6 -0
  90. driftshield_sdk-0.2.0/src/driftshield/parsers/crewai.py +153 -0
  91. driftshield_sdk-0.2.0/src/driftshield/parsers/langchain.py +318 -0
  92. driftshield_sdk-0.2.0/src/driftshield/parsers/local_chat.py +202 -0
  93. driftshield_sdk-0.2.0/src/driftshield/parsers/openclaw.py +231 -0
  94. driftshield_sdk-0.2.0/src/driftshield/parsers/openclaw_trajectory.py +454 -0
  95. driftshield_sdk-0.2.0/src/driftshield/parsers/protocol.py +22 -0
  96. driftshield_sdk-0.2.0/src/driftshield/parsers/registry.py +227 -0
  97. driftshield_sdk-0.2.0/src/driftshield/public.py +700 -0
  98. driftshield_sdk-0.2.0/src/driftshield/recursive_redactor.py +474 -0
  99. driftshield_sdk-0.2.0/src/driftshield/remote_submission.py +215 -0
  100. driftshield_sdk-0.2.0/src/driftshield/remote_upload.py +284 -0
  101. driftshield_sdk-0.2.0/src/driftshield/reports/__init__.py +0 -0
  102. driftshield_sdk-0.2.0/src/driftshield/reports/builder.py +678 -0
  103. driftshield_sdk-0.2.0/src/driftshield/reports/json_export.py +99 -0
  104. driftshield_sdk-0.2.0/src/driftshield/reports/markdown.py +22 -0
  105. driftshield_sdk-0.2.0/src/driftshield/reports/models.py +106 -0
  106. driftshield_sdk-0.2.0/src/driftshield/reports/templates/full.md.j2 +70 -0
  107. driftshield_sdk-0.2.0/src/driftshield/reports/templates/summary.md.j2 +51 -0
  108. driftshield_sdk-0.2.0/src/driftshield/signatures/__init__.py +126 -0
  109. driftshield_sdk-0.2.0/src/driftshield/signatures/community.py +237 -0
  110. driftshield_sdk-0.2.0/src/driftshield/signatures/distribution.py +303 -0
  111. driftshield_sdk-0.2.0/src/driftshield/signatures/packs/community-general.json +213 -0
  112. driftshield_sdk-0.2.0/src/driftshield/telemetry/__init__.py +19 -0
  113. driftshield_sdk-0.2.0/src/driftshield/telemetry/service.py +258 -0
  114. driftshield_sdk-0.2.0/tests/__init__.py +1 -0
  115. driftshield_sdk-0.2.0/tests/api/__init__.py +0 -0
  116. driftshield_sdk-0.2.0/tests/api/test_auth.py +57 -0
  117. driftshield_sdk-0.2.0/tests/api/test_behaviour.py +205 -0
  118. driftshield_sdk-0.2.0/tests/api/test_connectors.py +247 -0
  119. driftshield_sdk-0.2.0/tests/api/test_dogfood_flow_matrix.py +299 -0
  120. driftshield_sdk-0.2.0/tests/api/test_graph.py +122 -0
  121. driftshield_sdk-0.2.0/tests/api/test_health.py +19 -0
  122. driftshield_sdk-0.2.0/tests/api/test_ingest.py +564 -0
  123. driftshield_sdk-0.2.0/tests/api/test_reports.py +568 -0
  124. driftshield_sdk-0.2.0/tests/api/test_schemas.py +82 -0
  125. driftshield_sdk-0.2.0/tests/api/test_sessions.py +651 -0
  126. driftshield_sdk-0.2.0/tests/api/test_validations.py +361 -0
  127. driftshield_sdk-0.2.0/tests/cli/__init__.py +0 -0
  128. driftshield_sdk-0.2.0/tests/cli/test_analyze.py +192 -0
  129. driftshield_sdk-0.2.0/tests/cli/test_batch.py +877 -0
  130. driftshield_sdk-0.2.0/tests/cli/test_connectors.py +226 -0
  131. driftshield_sdk-0.2.0/tests/cli/test_discovery.py +145 -0
  132. driftshield_sdk-0.2.0/tests/cli/test_export_validations.py +64 -0
  133. driftshield_sdk-0.2.0/tests/cli/test_generate_fixtures.py +22 -0
  134. driftshield_sdk-0.2.0/tests/cli/test_ingest.py +335 -0
  135. driftshield_sdk-0.2.0/tests/cli/test_inspect.py +81 -0
  136. driftshield_sdk-0.2.0/tests/cli/test_list.py +35 -0
  137. driftshield_sdk-0.2.0/tests/cli/test_main.py +59 -0
  138. driftshield_sdk-0.2.0/tests/cli/test_output.py +185 -0
  139. driftshield_sdk-0.2.0/tests/cli/test_report_command.py +83 -0
  140. driftshield_sdk-0.2.0/tests/cli/test_show_result.py +364 -0
  141. driftshield_sdk-0.2.0/tests/cli/test_signatures.py +265 -0
  142. driftshield_sdk-0.2.0/tests/cli/test_submit.py +140 -0
  143. driftshield_sdk-0.2.0/tests/cli/test_telemetry.py +1538 -0
  144. driftshield_sdk-0.2.0/tests/core/__init__.py +0 -0
  145. driftshield_sdk-0.2.0/tests/core/analysis/__init__.py +0 -0
  146. driftshield_sdk-0.2.0/tests/core/analysis/test_assumption_mutation.py +77 -0
  147. driftshield_sdk-0.2.0/tests/core/analysis/test_dogfood_golden.py +29 -0
  148. driftshield_sdk-0.2.0/tests/core/analysis/test_explanations.py +171 -0
  149. driftshield_sdk-0.2.0/tests/core/analysis/test_inflection.py +221 -0
  150. driftshield_sdk-0.2.0/tests/core/analysis/test_risk.py +393 -0
  151. driftshield_sdk-0.2.0/tests/core/analysis/test_session_analyzer.py +203 -0
  152. driftshield_sdk-0.2.0/tests/core/graph/__init__.py +0 -0
  153. driftshield_sdk-0.2.0/tests/core/graph/test_builder.py +175 -0
  154. driftshield_sdk-0.2.0/tests/core/graph/test_models.py +241 -0
  155. driftshield_sdk-0.2.0/tests/core/test_canonical_analysis.py +454 -0
  156. driftshield_sdk-0.2.0/tests/core/test_delta_records.py +178 -0
  157. driftshield_sdk-0.2.0/tests/core/test_deterministic_matching.py +415 -0
  158. driftshield_sdk-0.2.0/tests/core/test_models.py +325 -0
  159. driftshield_sdk-0.2.0/tests/core/test_provenance_environment.py +104 -0
  160. driftshield_sdk-0.2.0/tests/core/test_qualification_state.py +108 -0
  161. driftshield_sdk-0.2.0/tests/core/test_visibility.py +172 -0
  162. driftshield_sdk-0.2.0/tests/db/__init__.py +0 -0
  163. driftshield_sdk-0.2.0/tests/db/test_behaviour_service.py +134 -0
  164. driftshield_sdk-0.2.0/tests/db/test_connector_watcher.py +347 -0
  165. driftshield_sdk-0.2.0/tests/db/test_connectors.py +130 -0
  166. driftshield_sdk-0.2.0/tests/db/test_engine.py +28 -0
  167. driftshield_sdk-0.2.0/tests/db/test_hosted_schema_sql.py +38 -0
  168. driftshield_sdk-0.2.0/tests/db/test_migrations.py +89 -0
  169. driftshield_sdk-0.2.0/tests/db/test_models.py +294 -0
  170. driftshield_sdk-0.2.0/tests/db/test_persistence.py +640 -0
  171. driftshield_sdk-0.2.0/tests/db/test_persistence_integration.py +177 -0
  172. driftshield_sdk-0.2.0/tests/db/test_validation_service.py +230 -0
  173. driftshield_sdk-0.2.0/tests/fixtures/__init__.py +0 -0
  174. driftshield_sdk-0.2.0/tests/fixtures/lineage.py +89 -0
  175. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/__init__.py +14 -0
  176. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/claude_code.json +66 -0
  177. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/claude_desktop.json +30 -0
  178. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/codex.json +29 -0
  179. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/crewai.json +31 -0
  180. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/langchain.json +45 -0
  181. driftshield_sdk-0.2.0/tests/fixtures/redactor_corpus/openai_chat.json +26 -0
  182. driftshield_sdk-0.2.0/tests/fixtures/scenarios.py +385 -0
  183. driftshield_sdk-0.2.0/tests/fixtures/test_transcript_generator.py +31 -0
  184. driftshield_sdk-0.2.0/tests/fixtures/transcripts/dogfood/assumption_mutation_session.jsonl +4 -0
  185. driftshield_sdk-0.2.0/tests/fixtures/transcripts/dogfood/clean_session.jsonl +4 -0
  186. driftshield_sdk-0.2.0/tests/fixtures/transcripts/dogfood/constraint_violation_session.jsonl +2 -0
  187. driftshield_sdk-0.2.0/tests/fixtures/transcripts/dogfood/multi_flag_session.jsonl +4 -0
  188. driftshield_sdk-0.2.0/tests/fixtures/transcripts/dogfood/policy_divergence_session.jsonl +2 -0
  189. driftshield_sdk-0.2.0/tests/fixtures/transcripts/golden/dogfood_corpus_expectations.json +93 -0
  190. driftshield_sdk-0.2.0/tests/fixtures/transcripts/golden/redaction_snapshot.json +20 -0
  191. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_claude_code_session.jsonl +59 -0
  192. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_claude_desktop_session.json +33 -0
  193. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_codex_cli_session.jsonl +4 -0
  194. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_codex_desktop_session.json +44 -0
  195. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_crewai_session.json +34 -0
  196. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_langchain_session.json +44 -0
  197. driftshield_sdk-0.2.0/tests/fixtures/transcripts/sample_openclaw_trajectory.json +145 -0
  198. driftshield_sdk-0.2.0/tests/integration/__init__.py +1 -0
  199. driftshield_sdk-0.2.0/tests/integration/test_real_transcript.py +49 -0
  200. driftshield_sdk-0.2.0/tests/parsers/__init__.py +1 -0
  201. driftshield_sdk-0.2.0/tests/parsers/test_claude_code.py +165 -0
  202. driftshield_sdk-0.2.0/tests/parsers/test_crewai.py +157 -0
  203. driftshield_sdk-0.2.0/tests/parsers/test_dogfood_golden.py +34 -0
  204. driftshield_sdk-0.2.0/tests/parsers/test_langchain.py +297 -0
  205. driftshield_sdk-0.2.0/tests/parsers/test_local_sources.py +71 -0
  206. driftshield_sdk-0.2.0/tests/parsers/test_normalized_pipeline.py +83 -0
  207. driftshield_sdk-0.2.0/tests/parsers/test_openclaw.py +62 -0
  208. driftshield_sdk-0.2.0/tests/parsers/test_openclaw_trajectory.py +325 -0
  209. driftshield_sdk-0.2.0/tests/reports/__init__.py +0 -0
  210. driftshield_sdk-0.2.0/tests/reports/test_builder.py +200 -0
  211. driftshield_sdk-0.2.0/tests/reports/test_json_export.py +69 -0
  212. driftshield_sdk-0.2.0/tests/reports/test_markdown.py +78 -0
  213. driftshield_sdk-0.2.0/tests/reports/test_models.py +52 -0
  214. driftshield_sdk-0.2.0/tests/test_community_signature_pack.py +216 -0
  215. driftshield_sdk-0.2.0/tests/test_door.py +586 -0
  216. driftshield_sdk-0.2.0/tests/test_intake_contract.py +423 -0
  217. driftshield_sdk-0.2.0/tests/test_recursive_redactor.py +560 -0
  218. driftshield_sdk-0.2.0/tests/test_remote_submission.py +440 -0
  219. driftshield_sdk-0.2.0/tests/test_remote_upload.py +323 -0
  220. driftshield_sdk-0.2.0/tests/test_scenarios.py +125 -0
  221. driftshield_sdk-0.2.0/tests/test_signatures_public_api.py +166 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .env
11
+ .venv/
12
+ venv/
13
+ *.db
14
+ *.sqlite
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: driftshield-sdk
3
+ Version: 0.2.0
4
+ Summary: Open-source AI decision forensics toolkit
5
+ Project-URL: Homepage, https://github.com/tborys/driftshield
6
+ Project-URL: Changelog, https://github.com/tborys/driftshield/blob/main/CHANGELOG.md
7
+ Project-URL: Documentation, https://github.com/tborys/driftshield/blob/main/docs/public-api.md
8
+ Project-URL: Issues, https://github.com/tborys/driftshield/issues
9
+ Author: Tomasz Borys, Devinder Pharar
10
+ License-Expression: AGPL-3.0-or-later
11
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: alembic>=1.13.0
17
+ Requires-Dist: fastapi>=0.109.0
18
+ Requires-Dist: jinja2>=3.1.0
19
+ Requires-Dist: psycopg2-binary>=2.9.9
20
+ Requires-Dist: pydantic-settings>=2.14.2
21
+ Requires-Dist: pydantic>=2.5.0
22
+ Requires-Dist: python-multipart>=0.0.31
23
+ Requires-Dist: rich>=13.0.0
24
+ Requires-Dist: sqlalchemy>=2.0.0
25
+ Requires-Dist: typer>=0.9.0
26
+ Requires-Dist: uvicorn[standard]>=0.27.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
29
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # driftshield
37
+
38
+ Find where an AI agent first went wrong.
39
+
40
+ DriftShield reconstructs a failed agent run as a decision graph, shows where
41
+ reasoning first drifted and matches the failure against known community
42
+ signatures. It reads Claude Code, Codex CLI, Claude Desktop, Codex Desktop,
43
+ OpenClaw, CrewAI and LangChain transcripts.
44
+
45
+ ```bash
46
+ pip install driftshield-sdk
47
+ driftshield analyze session.jsonl
48
+ ```
49
+
50
+ The distribution is called `driftshield-sdk` on PyPI because the name
51
+ `driftshield` was already too close to an unrelated project. The import
52
+ package and the command are still `driftshield`.
53
+
54
+ The library surface is two calls:
55
+
56
+ ```python
57
+ from driftshield import analyse_run, submit
58
+
59
+ run = analyse_run(open("session.jsonl", "rb").read(), source="session.jsonl")
60
+ print(run.qualification_state, run.findings)
61
+
62
+ receipt = submit(run) # community lane: redacted, no key needed
63
+ ```
64
+
65
+ `analyse_run` works offline and touches nothing outside the process. `submit`
66
+ is the only call that sends anything off the machine, and it only ever sends
67
+ `run.redacted_transcript`.
68
+
69
+ Documentation, the self hosted web UI and the full CLI reference live in the
70
+ repository: https://github.com/tborys/driftshield
71
+
72
+ Licensed under AGPL-3.0-or-later.
@@ -0,0 +1,37 @@
1
+ # driftshield
2
+
3
+ Find where an AI agent first went wrong.
4
+
5
+ DriftShield reconstructs a failed agent run as a decision graph, shows where
6
+ reasoning first drifted and matches the failure against known community
7
+ signatures. It reads Claude Code, Codex CLI, Claude Desktop, Codex Desktop,
8
+ OpenClaw, CrewAI and LangChain transcripts.
9
+
10
+ ```bash
11
+ pip install driftshield-sdk
12
+ driftshield analyze session.jsonl
13
+ ```
14
+
15
+ The distribution is called `driftshield-sdk` on PyPI because the name
16
+ `driftshield` was already too close to an unrelated project. The import
17
+ package and the command are still `driftshield`.
18
+
19
+ The library surface is two calls:
20
+
21
+ ```python
22
+ from driftshield import analyse_run, submit
23
+
24
+ run = analyse_run(open("session.jsonl", "rb").read(), source="session.jsonl")
25
+ print(run.qualification_state, run.findings)
26
+
27
+ receipt = submit(run) # community lane: redacted, no key needed
28
+ ```
29
+
30
+ `analyse_run` works offline and touches nothing outside the process. `submit`
31
+ is the only call that sends anything off the machine, and it only ever sends
32
+ `run.redacted_transcript`.
33
+
34
+ Documentation, the self hosted web UI and the full CLI reference live in the
35
+ repository: https://github.com/tborys/driftshield
36
+
37
+ Licensed under AGPL-3.0-or-later.
@@ -0,0 +1,110 @@
1
+ [project]
2
+ name = "driftshield-sdk"
3
+ version = "0.2.0"
4
+ description = "Open-source AI decision forensics toolkit"
5
+ authors = [
6
+ { name = "Tomasz Borys" },
7
+ { name = "Devinder Pharar" },
8
+ ]
9
+ readme = "README.md"
10
+ license = "AGPL-3.0-or-later"
11
+ requires-python = ">=3.12"
12
+ classifiers = [
13
+ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.14",
17
+ ]
18
+ dependencies = [
19
+ "fastapi>=0.109.0",
20
+ "uvicorn[standard]>=0.27.0",
21
+ "sqlalchemy>=2.0.0",
22
+ "alembic>=1.13.0",
23
+ "psycopg2-binary>=2.9.9",
24
+ "pydantic>=2.5.0",
25
+ "pydantic-settings>=2.14.2",
26
+ "python-multipart>=0.0.31",
27
+ "typer>=0.9.0",
28
+ "rich>=13.0.0",
29
+ "jinja2>=3.1.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/tborys/driftshield"
34
+ Changelog = "https://github.com/tborys/driftshield/blob/main/CHANGELOG.md"
35
+ Documentation = "https://github.com/tborys/driftshield/blob/main/docs/public-api.md"
36
+ Issues = "https://github.com/tborys/driftshield/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0.0",
41
+ "pytest-cov>=4.1.0",
42
+ "pytest-asyncio>=0.23.0",
43
+ "httpx>=0.27.0",
44
+ "ruff>=0.1.0",
45
+ "mypy>=1.8.0",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["hatchling"]
50
+ build-backend = "hatchling.build"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/driftshield"]
54
+ include = [
55
+ "src/driftshield/signatures/packs/*.json",
56
+ ]
57
+
58
+ [tool.hatch.build.targets.sdist]
59
+ only-include = [
60
+ "README.md",
61
+ "src/driftshield",
62
+ "tests",
63
+ ]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ asyncio_mode = "auto"
68
+
69
+ [tool.ruff]
70
+ line-length = 100
71
+ target-version = "py312"
72
+
73
+ [project.scripts]
74
+ driftshield = "driftshield.cli.main:app"
75
+ driftshield-api = "driftshield.api.server:main"
76
+
77
+ [tool.mypy]
78
+ python_version = "3.12"
79
+ strict = true
80
+
81
+ [[tool.mypy.overrides]]
82
+ module = [
83
+ "driftshield.api.ingest_workflow",
84
+ "driftshield.api.routes.health",
85
+ "driftshield.api.routes.ingest",
86
+ "driftshield.api.security",
87
+ "driftshield.cli.commands.connectors",
88
+ "driftshield.cli.commands.ingest",
89
+ "driftshield.cli.commands.list",
90
+ "driftshield.cli.commands.report",
91
+ "driftshield.connectors.watcher",
92
+ "driftshield.core.analysis.heuristics",
93
+ "driftshield.core.analysis.risk",
94
+ "driftshield.core.analysis.session",
95
+ "driftshield.core.graph.models",
96
+ "driftshield.core.models",
97
+ "driftshield.db.ingest_service",
98
+ "driftshield.db.migrations.versions.9b3d7e1a4c2f_remove_recurrence_tables_from_oss",
99
+ "driftshield.db.models",
100
+ "driftshield.db.validation_service",
101
+ "driftshield.fixtures.transcript_generator",
102
+ "driftshield.parsers.claude_code",
103
+ "driftshield.parsers.crewai",
104
+ "driftshield.parsers.langchain",
105
+ "driftshield.parsers.local_chat",
106
+ "driftshield.parsers.openclaw",
107
+ "driftshield.signatures.community",
108
+ "tests.fixtures.scenarios",
109
+ ]
110
+ ignore_errors = true
@@ -0,0 +1,31 @@
1
+ """DriftShield - AI Decision Forensics.
2
+
3
+ The public API is exactly two operations, ``analyse_run`` and ``submit``, with
4
+ their result and error types. Everything else in the package is internal.
5
+ """
6
+
7
+ from driftshield.public import (
8
+ AnalysedRun,
9
+ Finding,
10
+ NoParseableEventsError,
11
+ SignatureHit,
12
+ SubmitError,
13
+ SubmitReceipt,
14
+ UnsupportedFormatError,
15
+ analyse_run,
16
+ submit,
17
+ )
18
+
19
+ __version__ = "0.2.0"
20
+
21
+ __all__ = [
22
+ "AnalysedRun",
23
+ "Finding",
24
+ "NoParseableEventsError",
25
+ "SignatureHit",
26
+ "SubmitError",
27
+ "SubmitReceipt",
28
+ "UnsupportedFormatError",
29
+ "analyse_run",
30
+ "submit",
31
+ ]
File without changes
@@ -0,0 +1,47 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.responses import FileResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ from driftshield import __version__
9
+ from driftshield.api.routes.behaviour import router as behaviour_router
10
+ from driftshield.api.routes.connectors import router as connectors_router
11
+ from driftshield.api.routes.health import router as health_router
12
+ from driftshield.api.routes.ingest import router as ingest_router
13
+ from driftshield.api.routes.reports import router as reports_router
14
+ from driftshield.api.routes.sessions import router as sessions_router
15
+ from driftshield.api.security import RequestSizeLimitMiddleware
16
+
17
+
18
+ def create_app() -> FastAPI:
19
+ app = FastAPI(
20
+ title="DriftShield",
21
+ description="AI Decision Forensics API",
22
+ version=__version__,
23
+ )
24
+ app.add_middleware(RequestSizeLimitMiddleware)
25
+ app.include_router(behaviour_router)
26
+ app.include_router(connectors_router)
27
+ app.include_router(health_router)
28
+ app.include_router(ingest_router)
29
+ app.include_router(sessions_router)
30
+ app.include_router(reports_router)
31
+
32
+ # Serve React static files in production
33
+ # When installed as a package, __file__ points to site-packages.
34
+ # Use STATIC_DIR env var (set in Docker) or fall back to relative path for local dev.
35
+ static_dir = Path(os.environ.get("STATIC_DIR", Path(__file__).parent.parent.parent.parent / "static"))
36
+ if static_dir.exists():
37
+ app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
38
+
39
+ @app.get("/{path:path}")
40
+ async def serve_spa(path: str) -> FileResponse:
41
+ """Serve React SPA. All non-API routes fall through to index.html."""
42
+ file_path = static_dir / path
43
+ if file_path.exists() and file_path.is_file():
44
+ return FileResponse(str(file_path))
45
+ return FileResponse(str(static_dir / "index.html"))
46
+
47
+ return app
@@ -0,0 +1,13 @@
1
+ from fastapi import HTTPException, Security
2
+ from fastapi.security import APIKeyHeader
3
+
4
+ from driftshield.api.security import get_expected_api_key
5
+
6
+ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
7
+
8
+
9
+ def require_api_key(api_key: str | None = Security(api_key_header)) -> str:
10
+ expected = get_expected_api_key()
11
+ if not api_key or api_key != expected:
12
+ raise HTTPException(status_code=401, detail="Missing or invalid API key")
13
+ return api_key
@@ -0,0 +1,24 @@
1
+ from collections.abc import Generator
2
+
3
+ from sqlalchemy.orm import Session as DBSession
4
+
5
+ from driftshield.db.engine import get_engine, get_session_factory
6
+
7
+ _engine = None
8
+ _session_factory = None
9
+
10
+
11
+ def get_db() -> Generator[DBSession, None, None]:
12
+ global _engine, _session_factory
13
+ if _engine is None:
14
+ _engine = get_engine()
15
+ _session_factory = get_session_factory(_engine)
16
+ session = _session_factory()
17
+ try:
18
+ yield session
19
+ session.commit()
20
+ except Exception:
21
+ session.rollback()
22
+ raise
23
+ finally:
24
+ session.close()
@@ -0,0 +1,102 @@
1
+ from fastapi import HTTPException, UploadFile
2
+ from sqlalchemy.exc import IntegrityError
3
+ from sqlalchemy.orm import Session as DBSession
4
+
5
+ from driftshield.api.security import get_max_request_bytes
6
+ from driftshield.core.analysis.session import AnalysisResult
7
+ from driftshield.db.behaviour_service import BehaviourEventService
8
+ from driftshield.db.ingest_service import TranscriptIngestService, metrics_payload_from_analysis_result
9
+ from driftshield.db.persistence import IngestOutcome, PersistenceService
10
+ from driftshield.public import analyse_run
11
+ from driftshield.telemetry import TelemetryService
12
+
13
+
14
+ def ingest_transcript_bytes(
15
+ db: DBSession,
16
+ *,
17
+ raw_bytes: bytes,
18
+ format_name: str,
19
+ filename: str | None,
20
+ commit: bool = True,
21
+ ) -> tuple[IngestOutcome, AnalysisResult | None, str]:
22
+ _validate_request_size(raw_bytes)
23
+ try:
24
+ run = analyse_run(
25
+ raw_bytes,
26
+ source=filename,
27
+ format=None if format_name == "auto" else format_name,
28
+ )
29
+ except ValueError as exc:
30
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
31
+ normalised = run.detected_format
32
+
33
+ ingest_service = TranscriptIngestService(db)
34
+ try:
35
+ outcome, analysis_result = ingest_service.ingest_run(run)
36
+ if not outcome.deduplicated and analysis_result is not None and analysis_result.events:
37
+ BehaviourEventService(db).link_new_run_after_pattern_view(session_id=outcome.session_id)
38
+ if commit:
39
+ db.commit()
40
+ if not outcome.deduplicated and analysis_result is not None:
41
+ record_analysis_telemetry(analysis_result)
42
+ except ValueError as exc:
43
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
44
+ except IntegrityError:
45
+ db.rollback()
46
+ outcome = PersistenceService(db).get_ingest_outcome(run.provenance)
47
+ if outcome is None:
48
+ raise
49
+ analysis_result = None
50
+ except HTTPException:
51
+ db.rollback()
52
+ raise
53
+ except Exception as exc:
54
+ db.rollback()
55
+ raise HTTPException(
56
+ status_code=422,
57
+ detail=f"Failed to process transcript: {exc}",
58
+ ) from exc
59
+
60
+ return outcome, analysis_result, normalised
61
+
62
+
63
+ def read_upload_bytes(
64
+ file: UploadFile,
65
+ *,
66
+ chunk_size: int = 1024 * 1024,
67
+ ) -> bytes:
68
+ max_request_bytes = get_max_request_bytes()
69
+ total_bytes = 0
70
+ chunks: list[bytes] = []
71
+
72
+ while True:
73
+ chunk = file.file.read(chunk_size)
74
+ if not chunk:
75
+ break
76
+
77
+ total_bytes += len(chunk)
78
+ if total_bytes > max_request_bytes:
79
+ raise HTTPException(status_code=413, detail=f"Request body exceeds {max_request_bytes} bytes")
80
+ chunks.append(chunk)
81
+
82
+ return b"".join(chunks)
83
+
84
+
85
+ def record_analysis_telemetry(result: AnalysisResult) -> None:
86
+ metrics = metrics_payload_from_analysis_result(result)
87
+ try:
88
+ TelemetryService().record_analysis_event(
89
+ outcome_status=metrics["outcome_status"],
90
+ match_count=metrics["match_count"],
91
+ primary_mechanism_id=metrics["primary_family_id"],
92
+ mixed_mechanism=metrics["mixed_family"],
93
+ not_classifiable_reason=metrics["not_classifiable_reason"],
94
+ )
95
+ except Exception:
96
+ pass
97
+
98
+
99
+ def _validate_request_size(raw_bytes: bytes) -> None:
100
+ max_request_bytes = get_max_request_bytes()
101
+ if len(raw_bytes) > max_request_bytes:
102
+ raise HTTPException(status_code=413, detail=f"Request body exceeds {max_request_bytes} bytes")
@@ -0,0 +1,160 @@
1
+ import uuid
2
+ from typing import Literal, cast
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from sqlalchemy.orm import Session as DBSession
6
+
7
+ from driftshield.api.auth import require_api_key
8
+ from driftshield.api.dependencies import get_db
9
+ from driftshield.api.schemas import (
10
+ BehaviourEventCreateRequest,
11
+ BehaviourEventResponse,
12
+ BehaviourSubjectCreateRequest,
13
+ BehaviourSubjectResponse,
14
+ )
15
+ from driftshield.db.behaviour_service import BehaviourEventService, BehaviourSubjectSnapshot
16
+ from driftshield.db.models import SessionModel
17
+
18
+ router = APIRouter()
19
+
20
+ SubjectType = Literal["trusted_pattern", "report", "linked_run_set"]
21
+ SurfaceType = Literal["api", "ui", "report"]
22
+ EventType = Literal[
23
+ "pattern_viewed",
24
+ "pattern_expanded",
25
+ "pattern_revisited",
26
+ "pattern_linked_runs_viewed",
27
+ "new_run_after_pattern_view",
28
+ ]
29
+
30
+ _ALLOWED_SUBJECT_TYPES = {"trusted_pattern", "report", "linked_run_set"}
31
+ _ALLOWED_SURFACES = {"api", "ui", "report"}
32
+ _ALLOWED_EVENT_TYPES = {
33
+ "pattern_viewed",
34
+ "pattern_expanded",
35
+ "pattern_revisited",
36
+ "pattern_linked_runs_viewed",
37
+ "new_run_after_pattern_view",
38
+ }
39
+
40
+
41
+ @router.post("/api/behaviour/subjects", response_model=BehaviourSubjectResponse, status_code=201)
42
+ def create_behaviour_subject(
43
+ payload: BehaviourSubjectCreateRequest,
44
+ api_key: str = Depends(require_api_key),
45
+ db: DBSession = Depends(get_db),
46
+ ) -> BehaviourSubjectResponse:
47
+ del api_key
48
+ _validate_subject_payload(payload)
49
+ _require_session_exists(db, payload.session_id, detail="Behaviour subject session not found")
50
+
51
+ service = BehaviourEventService(db)
52
+ subject = service.create_subject(
53
+ subject_type=cast(SubjectType, payload.subject_type),
54
+ pattern_reference=payload.pattern_reference,
55
+ trust_band=payload.trust_band,
56
+ surface=cast(SurfaceType, payload.surface),
57
+ session_id=payload.session_id,
58
+ first_exposed_at=payload.first_exposed_at,
59
+ metadata_json=payload.metadata_json,
60
+ )
61
+ snapshot = service.get_subject_snapshot(subject.id)
62
+ assert snapshot is not None
63
+ db.commit()
64
+ return _subject_response(snapshot)
65
+
66
+
67
+ @router.get("/api/behaviour/subjects/{subject_id}", response_model=BehaviourSubjectResponse)
68
+ def get_behaviour_subject(
69
+ subject_id: uuid.UUID,
70
+ api_key: str = Depends(require_api_key),
71
+ db: DBSession = Depends(get_db),
72
+ ) -> BehaviourSubjectResponse:
73
+ del api_key
74
+ snapshot = BehaviourEventService(db).get_subject_snapshot(subject_id)
75
+ if snapshot is None:
76
+ raise HTTPException(status_code=404, detail="Behaviour subject not found")
77
+ return _subject_response(snapshot)
78
+
79
+
80
+ @router.post("/api/behaviour/events", response_model=BehaviourEventResponse, status_code=201)
81
+ def create_behaviour_event(
82
+ payload: BehaviourEventCreateRequest,
83
+ api_key: str = Depends(require_api_key),
84
+ db: DBSession = Depends(get_db),
85
+ ) -> BehaviourEventResponse:
86
+ del api_key
87
+ if payload.event_type not in _ALLOWED_EVENT_TYPES:
88
+ raise HTTPException(status_code=422, detail="Unsupported behaviour event type")
89
+ _require_session_exists(
90
+ db,
91
+ payload.linked_session_id,
92
+ detail="Linked behaviour event session not found",
93
+ )
94
+
95
+ service = BehaviourEventService(db)
96
+ try:
97
+ event = service.record_event(
98
+ subject_id=payload.subject_id,
99
+ event_type=cast(EventType, payload.event_type),
100
+ actor_id=payload.actor_id,
101
+ originating_session_id=payload.originating_session_id,
102
+ linked_session_id=payload.linked_session_id,
103
+ occurred_at=payload.occurred_at,
104
+ metadata_json=payload.metadata_json,
105
+ )
106
+ except LookupError as exc:
107
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
108
+
109
+ db.commit()
110
+ return BehaviourEventResponse(
111
+ id=event.id,
112
+ subject_id=event.subject_id,
113
+ occurred_at=event.occurred_at,
114
+ event_type=event.event_type,
115
+ actor_id=event.actor_id,
116
+ originating_session_id=event.originating_session_id,
117
+ linked_session_id=event.linked_session_id,
118
+ metadata_json=event.metadata_json or {},
119
+ )
120
+
121
+
122
+ def _validate_subject_payload(payload: BehaviourSubjectCreateRequest) -> None:
123
+ if payload.subject_type not in _ALLOWED_SUBJECT_TYPES:
124
+ raise HTTPException(status_code=422, detail="Unsupported behaviour subject type")
125
+ if payload.surface not in _ALLOWED_SURFACES:
126
+ raise HTTPException(status_code=422, detail="Unsupported behaviour surface")
127
+ if payload.subject_type != "trusted_pattern" and payload.trust_band == "trusted":
128
+ raise HTTPException(
129
+ status_code=422,
130
+ detail="Only trusted_pattern subjects can be marked trusted in OSS v1",
131
+ )
132
+
133
+
134
+ def _require_session_exists(
135
+ db: DBSession,
136
+ session_id: uuid.UUID | None,
137
+ *,
138
+ detail: str,
139
+ ) -> None:
140
+ if session_id is None:
141
+ return
142
+ if db.get(SessionModel, session_id) is None:
143
+ raise HTTPException(status_code=404, detail=detail)
144
+
145
+
146
+ def _subject_response(snapshot: BehaviourSubjectSnapshot) -> BehaviourSubjectResponse:
147
+ subject = snapshot.subject
148
+ return BehaviourSubjectResponse(
149
+ id=subject.id,
150
+ session_id=subject.session_id,
151
+ subject_type=subject.subject_type,
152
+ pattern_reference=subject.pattern_reference,
153
+ trust_band=subject.trust_band,
154
+ surface=subject.surface,
155
+ first_exposed_at=subject.first_exposed_at,
156
+ metadata_json=subject.metadata_json or {},
157
+ tracking_status=snapshot.tracking_status,
158
+ follow_up_status=snapshot.follow_up_status,
159
+ event_counts=snapshot.event_counts,
160
+ )