theus-cli-linux-arm64 1.2.2 → 1.3.0

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 (758) hide show
  1. package/RELEASE.json +6 -6
  2. package/bin/theus +0 -0
  3. package/package.json +1 -1
  4. package/vendor/higiene-motor/PARCHES.md +244 -0
  5. package/vendor/higiene-motor/audit_dir.py +166 -0
  6. package/vendor/higiene-motor/audit_lib.py +333 -0
  7. package/vendor/higiene-motor/audit_website.py +635 -0
  8. package/vendor/higiene-motor/av_meta.py +498 -0
  9. package/vendor/higiene-motor/check_staged.py +71 -0
  10. package/vendor/higiene-motor/clean_ctrlregen.py +212 -0
  11. package/vendor/higiene-motor/clean_file.py +237 -0
  12. package/vendor/higiene-motor/clean_image.py +218 -0
  13. package/vendor/higiene-motor/clean_staged.py +143 -0
  14. package/vendor/higiene-motor/clean_text.py +108 -0
  15. package/vendor/higiene-motor/common.py +486 -0
  16. package/vendor/higiene-motor/container_meta.py +2522 -0
  17. package/vendor/higiene-motor/detect_gumbel.py +308 -0
  18. package/vendor/higiene-motor/detect_text_watermark.py +613 -0
  19. package/vendor/higiene-motor/format_dispatch.py +157 -0
  20. package/vendor/higiene-motor/hook_written_file.py +253 -0
  21. package/vendor/higiene-motor/image_meta.py +2171 -0
  22. package/vendor/higiene-motor/inspect_file.py +138 -0
  23. package/vendor/higiene-motor/inspect_image.py +72 -0
  24. package/vendor/higiene-motor/inspect_text.py +86 -0
  25. package/vendor/higiene-motor/markdiffusion_harness.py +533 -0
  26. package/vendor/higiene-motor/rewrite_text.py +737 -0
  27. package/vendor/higiene-motor/score_stylometry.py +460 -0
  28. package/vendor/higiene-motor/score_synthid.py +165 -0
  29. package/vendor/higiene-motor/server.py +1059 -0
  30. package/vendor/higiene-motor/synthid_score_server.py +176 -0
  31. package/vendor/higiene-motor/text_detectors.py +433 -0
  32. package/vendor/higiene-motor/text_unicode.py +730 -0
  33. package/vendor/theus-arsenal/.arsenal-nombres-ola2.json +0 -6
  34. package/vendor/theus-arsenal/.arsenal-nombres-ola3.json +0 -72
  35. package/vendor/theus-arsenal/.mailmap +0 -2
  36. package/vendor/theus-arsenal/.theus-plugin/marketplace.json +0 -25
  37. package/vendor/theus-arsenal/.theus-plugin/plugin.json +0 -20
  38. package/vendor/theus-arsenal/AUTHORS.md +0 -8
  39. package/vendor/theus-arsenal/CLEANROOM_REPLACEMENT_PLAN.md +0 -11
  40. package/vendor/theus-arsenal/LICENSE +0 -26
  41. package/vendor/theus-arsenal/PROVENANCE.md +0 -9
  42. package/vendor/theus-arsenal/README.md +0 -41
  43. package/vendor/theus-arsenal/THIRD_PARTY.md +0 -80
  44. package/vendor/theus-arsenal/agents/actualizador-docs.md +0 -123
  45. package/vendor/theus-arsenal/agents/analista-comentarios.md +0 -53
  46. package/vendor/theus-arsenal/agents/analista-conversaciones.md +0 -60
  47. package/vendor/theus-arsenal/agents/analista-tests-pr.md +0 -53
  48. package/vendor/theus-arsenal/agents/analista-tipos.md +0 -49
  49. package/vendor/theus-arsenal/agents/arquitecto-accesibilidad.md +0 -148
  50. package/vendor/theus-arsenal/agents/arquitecto-codigo.md +0 -79
  51. package/vendor/theus-arsenal/agents/arquitecto-homelab.md +0 -90
  52. package/vendor/theus-arsenal/agents/arquitecto-redes.md +0 -116
  53. package/vendor/theus-arsenal/agents/arquitecto-sistemas.md +0 -219
  54. package/vendor/theus-arsenal/agents/bifurcador-opensource.md +0 -206
  55. package/vendor/theus-arsenal/agents/cazador-fallos-silenciosos.md +0 -58
  56. package/vendor/theus-arsenal/agents/consultor-documentacion.md +0 -76
  57. package/vendor/theus-arsenal/agents/diagnosticador-redes.md +0 -135
  58. package/vendor/theus-arsenal/agents/ejecutor-e2e.md +0 -96
  59. package/vendor/theus-arsenal/agents/empaquetador-opensource.md +0 -257
  60. package/vendor/theus-arsenal/agents/escaner-secretos.md +0 -196
  61. package/vendor/theus-arsenal/agents/especialista-seo.md +0 -70
  62. package/vendor/theus-arsenal/agents/estratega-marketing.md +0 -158
  63. package/vendor/theus-arsenal/agents/evaluador-agentes.md +0 -214
  64. package/vendor/theus-arsenal/agents/evaluador-harness.md +0 -217
  65. package/vendor/theus-arsenal/agents/explorador-codigo.md +0 -79
  66. package/vendor/theus-arsenal/agents/extractor-specs.md +0 -214
  67. package/vendor/theus-arsenal/agents/generador-harness.md +0 -139
  68. package/vendor/theus-arsenal/agents/guia-tdd.md +0 -99
  69. package/vendor/theus-arsenal/agents/jefe-de-gabinete.md +0 -162
  70. package/vendor/theus-arsenal/agents/limpiador-refactors.md +0 -93
  71. package/vendor/theus-arsenal/agents/optimizador-rendimiento.md +0 -454
  72. package/vendor/theus-arsenal/agents/planificador-harness.md +0 -107
  73. package/vendor/theus-arsenal/agents/planificador.md +0 -221
  74. package/vendor/theus-arsenal/agents/resolutor-apps-harmonyos.md +0 -181
  75. package/vendor/theus-arsenal/agents/resolutor-builds-cpp.md +0 -98
  76. package/vendor/theus-arsenal/agents/resolutor-builds-dart.md +0 -209
  77. package/vendor/theus-arsenal/agents/resolutor-builds-django.md +0 -253
  78. package/vendor/theus-arsenal/agents/resolutor-builds-go.md +0 -102
  79. package/vendor/theus-arsenal/agents/resolutor-builds-java.md +0 -274
  80. package/vendor/theus-arsenal/agents/resolutor-builds-kotlin.md +0 -128
  81. package/vendor/theus-arsenal/agents/resolutor-builds-pytorch.md +0 -128
  82. package/vendor/theus-arsenal/agents/resolutor-builds-react.md +0 -214
  83. package/vendor/theus-arsenal/agents/resolutor-builds-rust.md +0 -156
  84. package/vendor/theus-arsenal/agents/resolutor-builds-swift.md +0 -169
  85. package/vendor/theus-arsenal/agents/resolutor-builds.md +0 -122
  86. package/vendor/theus-arsenal/agents/revisor-bd.md +0 -97
  87. package/vendor/theus-arsenal/agents/revisor-codigo.md +0 -291
  88. package/vendor/theus-arsenal/agents/revisor-config-red.md +0 -116
  89. package/vendor/theus-arsenal/agents/revisor-cpp.md +0 -80
  90. package/vendor/theus-arsenal/agents/revisor-csharp.md +0 -109
  91. package/vendor/theus-arsenal/agents/revisor-django.md +0 -168
  92. package/vendor/theus-arsenal/agents/revisor-fastapi.md +0 -82
  93. package/vendor/theus-arsenal/agents/revisor-flutter.md +0 -251
  94. package/vendor/theus-arsenal/agents/revisor-go.md +0 -84
  95. package/vendor/theus-arsenal/agents/revisor-java.md +0 -192
  96. package/vendor/theus-arsenal/agents/revisor-kotlin.md +0 -171
  97. package/vendor/theus-arsenal/agents/revisor-ml.md +0 -159
  98. package/vendor/theus-arsenal/agents/revisor-php.md +0 -108
  99. package/vendor/theus-arsenal/agents/revisor-python.md +0 -106
  100. package/vendor/theus-arsenal/agents/revisor-react.md +0 -166
  101. package/vendor/theus-arsenal/agents/revisor-rust.md +0 -102
  102. package/vendor/theus-arsenal/agents/revisor-seguridad.md +0 -116
  103. package/vendor/theus-arsenal/agents/revisor-software-clinico.md +0 -95
  104. package/vendor/theus-arsenal/agents/revisor-swift.md +0 -115
  105. package/vendor/theus-arsenal/agents/revisor-typescript.md +0 -123
  106. package/vendor/theus-arsenal/agents/revisor-vue.md +0 -205
  107. package/vendor/theus-arsenal/agents/simplificador-codigo.md +0 -55
  108. package/vendor/theus-arsenal/commands/actualizar-docs.md +0 -88
  109. package/vendor/theus-arsenal/commands/actualizar-mapas-codigo.md +0 -78
  110. package/vendor/theus-arsenal/commands/aprender-evaluar.md +0 -109
  111. package/vendor/theus-arsenal/commands/aprender.md +0 -74
  112. package/vendor/theus-arsenal/commands/arreglar-build.md +0 -66
  113. package/vendor/theus-arsenal/commands/bucle-multiperspectiva.md +0 -153
  114. package/vendor/theus-arsenal/commands/build-cpp.md +0 -173
  115. package/vendor/theus-arsenal/commands/build-flutter.md +0 -164
  116. package/vendor/theus-arsenal/commands/build-go.md +0 -183
  117. package/vendor/theus-arsenal/commands/build-gradle.md +0 -75
  118. package/vendor/theus-arsenal/commands/build-kotlin.md +0 -176
  119. package/vendor/theus-arsenal/commands/build-react.md +0 -187
  120. package/vendor/theus-arsenal/commands/build-rust.md +0 -187
  121. package/vendor/theus-arsenal/commands/campana-marketing.md +0 -125
  122. package/vendor/theus-arsenal/commands/checkpoint.md +0 -78
  123. package/vendor/theus-arsenal/commands/cobertura-tests.md +0 -73
  124. package/vendor/theus-arsenal/commands/configurar-gestor-paquetes.md +0 -86
  125. package/vendor/theus-arsenal/commands/consulta.md +0 -164
  126. package/vendor/theus-arsenal/commands/control-calidad.md +0 -50
  127. package/vendor/theus-arsenal/commands/crear-skill.md +0 -77
  128. package/vendor/theus-arsenal/commands/enrutar-modelo.md +0 -30
  129. package/vendor/theus-arsenal/commands/epica-desbloquear.md +0 -17
  130. package/vendor/theus-arsenal/commands/epica-descomponer.md +0 -22
  131. package/vendor/theus-arsenal/commands/epica-publicar.md +0 -21
  132. package/vendor/theus-arsenal/commands/epica-reclamar.md +0 -25
  133. package/vendor/theus-arsenal/commands/epica-revisar.md +0 -22
  134. package/vendor/theus-arsenal/commands/epica-sincronizar.md +0 -21
  135. package/vendor/theus-arsenal/commands/epica-validar.md +0 -21
  136. package/vendor/theus-arsenal/commands/evolucionar.md +0 -176
  137. package/vendor/theus-arsenal/commands/funcionalidad.md +0 -49
  138. package/vendor/theus-arsenal/commands/guardar-sesion.md +0 -278
  139. package/vendor/theus-arsenal/commands/harness-build.md +0 -111
  140. package/vendor/theus-arsenal/commands/harness-diseno.md +0 -46
  141. package/vendor/theus-arsenal/commands/informe-costos.md +0 -90
  142. package/vendor/theus-arsenal/commands/iniciar-bucle.md +0 -36
  143. package/vendor/theus-arsenal/commands/instintos-estado.md +0 -53
  144. package/vendor/theus-arsenal/commands/instintos-exportar.md +0 -69
  145. package/vendor/theus-arsenal/commands/instintos-importar.md +0 -120
  146. package/vendor/theus-arsenal/commands/lienzo-planes.md +0 -48
  147. package/vendor/theus-arsenal/commands/limpiar-refactor.md +0 -84
  148. package/vendor/theus-arsenal/commands/modo-seguro.md +0 -96
  149. package/vendor/theus-arsenal/commands/orq-agregar-funcionalidad.md +0 -36
  150. package/vendor/theus-arsenal/commands/orq-cambiar-funcionalidad.md +0 -38
  151. package/vendor/theus-arsenal/commands/orq-construir-mvp.md +0 -36
  152. package/vendor/theus-arsenal/commands/orq-corregir-defecto.md +0 -40
  153. package/vendor/theus-arsenal/commands/orq-refinar-codigo.md +0 -44
  154. package/vendor/theus-arsenal/commands/orq-revisar.md +0 -135
  155. package/vendor/theus-arsenal/commands/plan-prd.md +0 -160
  156. package/vendor/theus-arsenal/commands/plan.md +0 -198
  157. package/vendor/theus-arsenal/commands/podar.md +0 -31
  158. package/vendor/theus-arsenal/commands/pr.md +0 -181
  159. package/vendor/theus-arsenal/commands/promover.md +0 -41
  160. package/vendor/theus-arsenal/commands/proyectos.md +0 -33
  161. package/vendor/theus-arsenal/commands/prp-commit.md +0 -112
  162. package/vendor/theus-arsenal/commands/prp-implementar.md +0 -385
  163. package/vendor/theus-arsenal/commands/prp-planificar.md +0 -501
  164. package/vendor/theus-arsenal/commands/reanudar-sesion.md +0 -160
  165. package/vendor/theus-arsenal/commands/revisar-codigo.md +0 -289
  166. package/vendor/theus-arsenal/commands/revisar-cpp.md +0 -132
  167. package/vendor/theus-arsenal/commands/revisar-fastapi.md +0 -39
  168. package/vendor/theus-arsenal/commands/revisar-flutter.md +0 -115
  169. package/vendor/theus-arsenal/commands/revisar-go.md +0 -148
  170. package/vendor/theus-arsenal/commands/revisar-kotlin.md +0 -140
  171. package/vendor/theus-arsenal/commands/revisar-pr.md +0 -37
  172. package/vendor/theus-arsenal/commands/revisar-python.md +0 -297
  173. package/vendor/theus-arsenal/commands/revisar-react.md +0 -170
  174. package/vendor/theus-arsenal/commands/revisar-rust.md +0 -142
  175. package/vendor/theus-arsenal/commands/revisar-vue.md +0 -174
  176. package/vendor/theus-arsenal/commands/salud-skills.md +0 -52
  177. package/vendor/theus-arsenal/commands/sesiones.md +0 -345
  178. package/vendor/theus-arsenal/commands/test-cpp.md +0 -252
  179. package/vendor/theus-arsenal/commands/test-flutter.md +0 -143
  180. package/vendor/theus-arsenal/commands/test-go.md +0 -268
  181. package/vendor/theus-arsenal/commands/test-kotlin.md +0 -312
  182. package/vendor/theus-arsenal/commands/test-react.md +0 -265
  183. package/vendor/theus-arsenal/commands/test-rust.md +0 -309
  184. package/vendor/theus-arsenal/commands/vigilancia-canary.md +0 -43
  185. package/vendor/theus-arsenal/hooks/hooks.json +0 -274
  186. package/vendor/theus-arsenal/package.json +0 -6
  187. package/vendor/theus-arsenal/rules/angular/coding-style.md +0 -182
  188. package/vendor/theus-arsenal/rules/angular/hooks.md +0 -25
  189. package/vendor/theus-arsenal/rules/angular/patterns.md +0 -249
  190. package/vendor/theus-arsenal/rules/angular/security.md +0 -87
  191. package/vendor/theus-arsenal/rules/angular/testing.md +0 -164
  192. package/vendor/theus-arsenal/rules/arkts/coding-style.md +0 -153
  193. package/vendor/theus-arsenal/rules/arkts/hooks.md +0 -80
  194. package/vendor/theus-arsenal/rules/arkts/patterns.md +0 -236
  195. package/vendor/theus-arsenal/rules/arkts/security.md +0 -141
  196. package/vendor/theus-arsenal/rules/arkts/testing.md +0 -126
  197. package/vendor/theus-arsenal/rules/common/agents.md +0 -49
  198. package/vendor/theus-arsenal/rules/common/code-review.md +0 -123
  199. package/vendor/theus-arsenal/rules/common/coding-style.md +0 -90
  200. package/vendor/theus-arsenal/rules/common/development-workflow.md +0 -44
  201. package/vendor/theus-arsenal/rules/common/git-workflow.md +0 -24
  202. package/vendor/theus-arsenal/rules/common/hooks.md +0 -32
  203. package/vendor/theus-arsenal/rules/common/patterns.md +0 -31
  204. package/vendor/theus-arsenal/rules/common/performance.md +0 -53
  205. package/vendor/theus-arsenal/rules/common/security.md +0 -29
  206. package/vendor/theus-arsenal/rules/common/testing.md +0 -57
  207. package/vendor/theus-arsenal/rules/cpp/coding-style.md +0 -44
  208. package/vendor/theus-arsenal/rules/cpp/hooks.md +0 -39
  209. package/vendor/theus-arsenal/rules/cpp/patterns.md +0 -51
  210. package/vendor/theus-arsenal/rules/cpp/security.md +0 -51
  211. package/vendor/theus-arsenal/rules/cpp/testing.md +0 -44
  212. package/vendor/theus-arsenal/rules/csharp/coding-style.md +0 -72
  213. package/vendor/theus-arsenal/rules/csharp/hooks.md +0 -25
  214. package/vendor/theus-arsenal/rules/csharp/patterns.md +0 -50
  215. package/vendor/theus-arsenal/rules/csharp/security.md +0 -58
  216. package/vendor/theus-arsenal/rules/csharp/testing.md +0 -50
  217. package/vendor/theus-arsenal/rules/dart/coding-style.md +0 -159
  218. package/vendor/theus-arsenal/rules/dart/hooks.md +0 -68
  219. package/vendor/theus-arsenal/rules/dart/patterns.md +0 -261
  220. package/vendor/theus-arsenal/rules/dart/security.md +0 -135
  221. package/vendor/theus-arsenal/rules/dart/testing.md +0 -215
  222. package/vendor/theus-arsenal/rules/fsharp/coding-style.md +0 -112
  223. package/vendor/theus-arsenal/rules/fsharp/hooks.md +0 -26
  224. package/vendor/theus-arsenal/rules/fsharp/patterns.md +0 -111
  225. package/vendor/theus-arsenal/rules/fsharp/security.md +0 -76
  226. package/vendor/theus-arsenal/rules/fsharp/testing.md +0 -62
  227. package/vendor/theus-arsenal/rules/golang/coding-style.md +0 -32
  228. package/vendor/theus-arsenal/rules/golang/hooks.md +0 -17
  229. package/vendor/theus-arsenal/rules/golang/patterns.md +0 -45
  230. package/vendor/theus-arsenal/rules/golang/security.md +0 -34
  231. package/vendor/theus-arsenal/rules/golang/testing.md +0 -31
  232. package/vendor/theus-arsenal/rules/java/coding-style.md +0 -114
  233. package/vendor/theus-arsenal/rules/java/hooks.md +0 -18
  234. package/vendor/theus-arsenal/rules/java/patterns.md +0 -147
  235. package/vendor/theus-arsenal/rules/java/security.md +0 -101
  236. package/vendor/theus-arsenal/rules/java/testing.md +0 -133
  237. package/vendor/theus-arsenal/rules/kotlin/coding-style.md +0 -86
  238. package/vendor/theus-arsenal/rules/kotlin/hooks.md +0 -17
  239. package/vendor/theus-arsenal/rules/kotlin/patterns.md +0 -146
  240. package/vendor/theus-arsenal/rules/kotlin/security.md +0 -82
  241. package/vendor/theus-arsenal/rules/kotlin/testing.md +0 -128
  242. package/vendor/theus-arsenal/rules/nuxt/coding-style.md +0 -47
  243. package/vendor/theus-arsenal/rules/nuxt/hooks.md +0 -39
  244. package/vendor/theus-arsenal/rules/nuxt/patterns.md +0 -54
  245. package/vendor/theus-arsenal/rules/nuxt/security.md +0 -48
  246. package/vendor/theus-arsenal/rules/nuxt/testing.md +0 -49
  247. package/vendor/theus-arsenal/rules/php/coding-style.md +0 -40
  248. package/vendor/theus-arsenal/rules/php/hooks.md +0 -24
  249. package/vendor/theus-arsenal/rules/php/patterns.md +0 -33
  250. package/vendor/theus-arsenal/rules/php/security.md +0 -37
  251. package/vendor/theus-arsenal/rules/php/testing.md +0 -38
  252. package/vendor/theus-arsenal/rules/python/coding-style.md +0 -42
  253. package/vendor/theus-arsenal/rules/python/fastapi.md +0 -58
  254. package/vendor/theus-arsenal/rules/python/hooks.md +0 -19
  255. package/vendor/theus-arsenal/rules/python/patterns.md +0 -39
  256. package/vendor/theus-arsenal/rules/python/security.md +0 -30
  257. package/vendor/theus-arsenal/rules/python/testing.md +0 -38
  258. package/vendor/theus-arsenal/rules/react/coding-style.md +0 -109
  259. package/vendor/theus-arsenal/rules/react/hooks.md +0 -187
  260. package/vendor/theus-arsenal/rules/react/patterns.md +0 -194
  261. package/vendor/theus-arsenal/rules/react/security.md +0 -180
  262. package/vendor/theus-arsenal/rules/react/testing.md +0 -208
  263. package/vendor/theus-arsenal/rules/react-native/accessibility.md +0 -55
  264. package/vendor/theus-arsenal/rules/react-native/coding-style.md +0 -71
  265. package/vendor/theus-arsenal/rules/react-native/hooks.md +0 -28
  266. package/vendor/theus-arsenal/rules/react-native/patterns.md +0 -88
  267. package/vendor/theus-arsenal/rules/react-native/performance.md +0 -45
  268. package/vendor/theus-arsenal/rules/react-native/production-readiness.md +0 -51
  269. package/vendor/theus-arsenal/rules/react-native/security.md +0 -43
  270. package/vendor/theus-arsenal/rules/react-native/testing.md +0 -52
  271. package/vendor/theus-arsenal/rules/ruby/coding-style.md +0 -46
  272. package/vendor/theus-arsenal/rules/ruby/hooks.md +0 -37
  273. package/vendor/theus-arsenal/rules/ruby/patterns.md +0 -44
  274. package/vendor/theus-arsenal/rules/ruby/security.md +0 -51
  275. package/vendor/theus-arsenal/rules/ruby/testing.md +0 -51
  276. package/vendor/theus-arsenal/rules/rust/coding-style.md +0 -151
  277. package/vendor/theus-arsenal/rules/rust/hooks.md +0 -16
  278. package/vendor/theus-arsenal/rules/rust/patterns.md +0 -168
  279. package/vendor/theus-arsenal/rules/rust/security.md +0 -141
  280. package/vendor/theus-arsenal/rules/rust/testing.md +0 -154
  281. package/vendor/theus-arsenal/rules/swift/coding-style.md +0 -47
  282. package/vendor/theus-arsenal/rules/swift/hooks.md +0 -20
  283. package/vendor/theus-arsenal/rules/swift/patterns.md +0 -66
  284. package/vendor/theus-arsenal/rules/swift/security.md +0 -33
  285. package/vendor/theus-arsenal/rules/swift/testing.md +0 -45
  286. package/vendor/theus-arsenal/rules/typescript/coding-style.md +0 -199
  287. package/vendor/theus-arsenal/rules/typescript/hooks.md +0 -22
  288. package/vendor/theus-arsenal/rules/typescript/patterns.md +0 -52
  289. package/vendor/theus-arsenal/rules/typescript/security.md +0 -28
  290. package/vendor/theus-arsenal/rules/typescript/testing.md +0 -18
  291. package/vendor/theus-arsenal/rules/vue/coding-style.md +0 -54
  292. package/vendor/theus-arsenal/rules/vue/hooks.md +0 -45
  293. package/vendor/theus-arsenal/rules/vue/patterns.md +0 -56
  294. package/vendor/theus-arsenal/rules/vue/security.md +0 -46
  295. package/vendor/theus-arsenal/rules/vue/testing.md +0 -53
  296. package/vendor/theus-arsenal/rules/web/coding-style.md +0 -108
  297. package/vendor/theus-arsenal/rules/web/design-quality.md +0 -75
  298. package/vendor/theus-arsenal/rules/web/hooks.md +0 -141
  299. package/vendor/theus-arsenal/rules/web/patterns.md +0 -91
  300. package/vendor/theus-arsenal/rules/web/performance.md +0 -76
  301. package/vendor/theus-arsenal/rules/web/security.md +0 -69
  302. package/vendor/theus-arsenal/rules/web/testing.md +0 -67
  303. package/vendor/theus-arsenal/scripts/hooks/bash-hook-dispatcher.js +0 -192
  304. package/vendor/theus-arsenal/scripts/hooks/block-no-verify.js +0 -556
  305. package/vendor/theus-arsenal/scripts/hooks/check-console-log.js +0 -92
  306. package/vendor/theus-arsenal/scripts/hooks/check-hook-enabled.js +0 -16
  307. package/vendor/theus-arsenal/scripts/hooks/config-protection.js +0 -171
  308. package/vendor/theus-arsenal/scripts/hooks/design-quality-check.js +0 -132
  309. package/vendor/theus-arsenal/scripts/hooks/doc-file-warning.js +0 -110
  310. package/vendor/theus-arsenal/scripts/hooks/modo-seguro.js +0 -158
  311. package/vendor/theus-arsenal/scripts/hooks/notificacion-escritorio.js +0 -208
  312. package/vendor/theus-arsenal/scripts/hooks/plugin-hook-bootstrap.js +0 -280
  313. package/vendor/theus-arsenal/scripts/hooks/post-bash-command-log.js +0 -87
  314. package/vendor/theus-arsenal/scripts/hooks/post-bash-dispatcher.js +0 -24
  315. package/vendor/theus-arsenal/scripts/hooks/post-bash-pr-created.js +0 -60
  316. package/vendor/theus-arsenal/scripts/hooks/post-edit-accumulator.js +0 -94
  317. package/vendor/theus-arsenal/scripts/hooks/post-edit-console-warn.js +0 -54
  318. package/vendor/theus-arsenal/scripts/hooks/post-edit-format.js +0 -110
  319. package/vendor/theus-arsenal/scripts/hooks/post-edit-typecheck.js +0 -98
  320. package/vendor/theus-arsenal/scripts/hooks/pre-bash-commit-quality.js +0 -447
  321. package/vendor/theus-arsenal/scripts/hooks/pre-bash-dev-server-block.js +0 -232
  322. package/vendor/theus-arsenal/scripts/hooks/pre-bash-dispatcher.js +0 -27
  323. package/vendor/theus-arsenal/scripts/hooks/pre-compact.js +0 -101
  324. package/vendor/theus-arsenal/scripts/hooks/pretooluse-visible-output.js +0 -49
  325. package/vendor/theus-arsenal/scripts/hooks/quality-gate.js +0 -168
  326. package/vendor/theus-arsenal/scripts/hooks/run-with-flags.js +0 -271
  327. package/vendor/theus-arsenal/scripts/hooks/salud-mcp.js +0 -749
  328. package/vendor/theus-arsenal/scripts/hooks/seguimiento-costos.js +0 -161
  329. package/vendor/theus-arsenal/scripts/hooks/session-end.js +0 -340
  330. package/vendor/theus-arsenal/scripts/hooks/session-start.js +0 -906
  331. package/vendor/theus-arsenal/scripts/hooks/stop-format-typecheck.js +0 -263
  332. package/vendor/theus-arsenal/scripts/hooks/suggest-compact.js +0 -286
  333. package/vendor/theus-arsenal/scripts/hooks/theus-context-monitor.js +0 -289
  334. package/vendor/theus-arsenal/scripts/hooks/verificacion-factual.js +0 -1278
  335. package/vendor/theus-arsenal/scripts/lib/agent-data-home.js +0 -96
  336. package/vendor/theus-arsenal/scripts/lib/cost-estimate.js +0 -66
  337. package/vendor/theus-arsenal/scripts/lib/hook-flags.js +0 -79
  338. package/vendor/theus-arsenal/scripts/lib/llm-summary.js +0 -185
  339. package/vendor/theus-arsenal/scripts/lib/loopback-guard.js +0 -54
  340. package/vendor/theus-arsenal/scripts/lib/observer-sessions.js +0 -237
  341. package/vendor/theus-arsenal/scripts/lib/package-manager.d.ts +0 -120
  342. package/vendor/theus-arsenal/scripts/lib/package-manager.js +0 -434
  343. package/vendor/theus-arsenal/scripts/lib/path-safety.js +0 -86
  344. package/vendor/theus-arsenal/scripts/lib/plan-canvas/markdown.js +0 -285
  345. package/vendor/theus-arsenal/scripts/lib/plan-canvas/sdk.js +0 -238
  346. package/vendor/theus-arsenal/scripts/lib/plan-canvas/server.js +0 -534
  347. package/vendor/theus-arsenal/scripts/lib/plan-canvas/sessions.js +0 -274
  348. package/vendor/theus-arsenal/scripts/lib/plan-canvas/ui.js +0 -546
  349. package/vendor/theus-arsenal/scripts/lib/project-detect.js +0 -451
  350. package/vendor/theus-arsenal/scripts/lib/resolve-formatter.js +0 -193
  351. package/vendor/theus-arsenal/scripts/lib/resolve-theus-root.js +0 -101
  352. package/vendor/theus-arsenal/scripts/lib/session-aliases.d.ts +0 -138
  353. package/vendor/theus-arsenal/scripts/lib/session-aliases.js +0 -483
  354. package/vendor/theus-arsenal/scripts/lib/session-bridge.js +0 -155
  355. package/vendor/theus-arsenal/scripts/lib/session-manager.d.ts +0 -137
  356. package/vendor/theus-arsenal/scripts/lib/session-manager.js +0 -555
  357. package/vendor/theus-arsenal/scripts/lib/shell-split.js +0 -86
  358. package/vendor/theus-arsenal/scripts/lib/shell-substitution.js +0 -501
  359. package/vendor/theus-arsenal/scripts/lib/skill-evolution/dashboard.js +0 -414
  360. package/vendor/theus-arsenal/scripts/lib/skill-evolution/health.js +0 -285
  361. package/vendor/theus-arsenal/scripts/lib/skill-evolution/index.js +0 -22
  362. package/vendor/theus-arsenal/scripts/lib/skill-evolution/provenance.js +0 -200
  363. package/vendor/theus-arsenal/scripts/lib/skill-evolution/tracker.js +0 -157
  364. package/vendor/theus-arsenal/scripts/lib/skill-evolution/versioning.js +0 -246
  365. package/vendor/theus-arsenal/scripts/lib/transcript-context.js +0 -232
  366. package/vendor/theus-arsenal/scripts/lib/utils.d.ts +0 -205
  367. package/vendor/theus-arsenal/scripts/lib/utils.js +0 -658
  368. package/vendor/theus-arsenal/scripts/plan-canvas.js +0 -341
  369. package/vendor/theus-arsenal/scripts/setup-package-manager.js +0 -46
  370. package/vendor/theus-arsenal/scripts/skills-health.js +0 -143
  371. package/vendor/theus-arsenal/skills/a11y-frontend/SKILL.md +0 -444
  372. package/vendor/theus-arsenal/skills/accesibilidad/SKILL.md +0 -144
  373. package/vendor/theus-arsenal/skills/acelerador-throughput-datos/SKILL.md +0 -72
  374. package/vendor/theus-arsenal/skills/agente-scraper-datos/SKILL.md +0 -771
  375. package/vendor/theus-arsenal/skills/analisis-plataformas-competencia/SKILL.md +0 -137
  376. package/vendor/theus-arsenal/skills/api-x/SKILL.md +0 -233
  377. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/SKILL.md +0 -335
  378. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer-loop.sh +0 -374
  379. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer.md +0 -188
  380. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/session-guardian.sh +0 -153
  381. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/start-observer.sh +0 -253
  382. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/config.json +0 -8
  383. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/hooks/observe.sh +0 -594
  384. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/detect-project.sh +0 -325
  385. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/instinct-cli.py +0 -1918
  386. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/lib/arsenal-dir.sh +0 -23
  387. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/test_parse_instinct.py +0 -1420
  388. package/vendor/theus-arsenal/skills/arquitectura-hexagonal/SKILL.md +0 -275
  389. package/vendor/theus-arsenal/skills/arquitectura-limpia-android/SKILL.md +0 -340
  390. package/vendor/theus-arsenal/skills/arquitectura-pipelines-recomendacion/SKILL.md +0 -112
  391. package/vendor/theus-arsenal/skills/atlas-datos-prediccion/SKILL.md +0 -53
  392. package/vendor/theus-arsenal/skills/auditoria-arquitectura-agentes/SKILL.md +0 -257
  393. package/vendor/theus-arsenal/skills/auditoria-produccion/SKILL.md +0 -180
  394. package/vendor/theus-arsenal/skills/auditoria-rutas-clic/SKILL.md +0 -243
  395. package/vendor/theus-arsenal/skills/autoevaluacion-agente/SKILL.md +0 -180
  396. package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/high-score-example.md +0 -89
  397. package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/low-score-example.md +0 -87
  398. package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/evaluation-criteria.md +0 -71
  399. package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/hook-integration.md +0 -90
  400. package/vendor/theus-arsenal/skills/autoevaluacion-agente/scripts/evaluate.py +0 -409
  401. package/vendor/theus-arsenal/skills/autoevaluacion-agente/templates/evaluation-report.md +0 -86
  402. package/vendor/theus-arsenal/skills/automatizacion-ssh-netmiko/SKILL.md +0 -176
  403. package/vendor/theus-arsenal/skills/base-datos-pubmed/SKILL.md +0 -174
  404. package/vendor/theus-arsenal/skills/base-datos-uspto/SKILL.md +0 -173
  405. package/vendor/theus-arsenal/skills/benchmark/SKILL.md +0 -92
  406. package/vendor/theus-arsenal/skills/bitacora-crecimiento/SKILL.md +0 -120
  407. package/vendor/theus-arsenal/skills/bucle-verificacion/SKILL.md +0 -125
  408. package/vendor/theus-arsenal/skills/buscar-primero/SKILL.md +0 -184
  409. package/vendor/theus-arsenal/skills/busqueda-exa/SKILL.md +0 -111
  410. package/vendor/theus-arsenal/skills/cache-por-hash/SKILL.md +0 -160
  411. package/vendor/theus-arsenal/skills/campana-marketing/SKILL.md +0 -118
  412. package/vendor/theus-arsenal/skills/capacidad-producto/SKILL.md +0 -138
  413. package/vendor/theus-arsenal/skills/cazador-vulnerabilidades/SKILL.md +0 -104
  414. package/vendor/theus-arsenal/skills/compactacion-estrategica/SKILL.md +0 -115
  415. package/vendor/theus-arsenal/skills/comparador-baskets-prediccion/SKILL.md +0 -69
  416. package/vendor/theus-arsenal/skills/compras-energia/SKILL.md +0 -206
  417. package/vendor/theus-arsenal/skills/concurrencia-swift-6-2/SKILL.md +0 -224
  418. package/vendor/theus-arsenal/skills/consejo/SKILL.md +0 -201
  419. package/vendor/theus-arsenal/skills/construccion-harness-agentes/SKILL.md +0 -78
  420. package/vendor/theus-arsenal/skills/constructor-conectores/SKILL.md +0 -119
  421. package/vendor/theus-arsenal/skills/constructor-equipos/SKILL.md +0 -169
  422. package/vendor/theus-arsenal/skills/consulta-documentacion/SKILL.md +0 -92
  423. package/vendor/theus-arsenal/skills/consultas-genomicas-gget/SKILL.md +0 -170
  424. package/vendor/theus-arsenal/skills/contacto-inversores/SKILL.md +0 -95
  425. package/vendor/theus-arsenal/skills/corrutinas-flows-kotlin/SKILL.md +0 -284
  426. package/vendor/theus-arsenal/skills/creacion-video-remotion/SKILL.md +0 -46
  427. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/3d.md +0 -86
  428. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/animations.md +0 -29
  429. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/charts-bar-chart.tsx +0 -173
  430. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-typewriter.tsx +0 -100
  431. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-word-highlight.tsx +0 -108
  432. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets.md +0 -78
  433. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/audio.md +0 -172
  434. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/calculate-metadata.md +0 -104
  435. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/can-decode.md +0 -75
  436. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/charts.md +0 -58
  437. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/compositions.md +0 -146
  438. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/display-captions.md +0 -126
  439. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/extract-frames.md +0 -229
  440. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/fonts.md +0 -152
  441. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-audio-duration.md +0 -58
  442. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-dimensions.md +0 -68
  443. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-duration.md +0 -58
  444. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/gifs.md +0 -138
  445. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/images.md +0 -130
  446. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/import-srt-captions.md +0 -67
  447. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/lottie.md +0 -67
  448. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-dom-nodes.md +0 -34
  449. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-text.md +0 -143
  450. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/sequencing.md +0 -106
  451. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/tailwind.md +0 -11
  452. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/text-animations.md +0 -20
  453. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/timing.md +0 -179
  454. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transcribe-captions.md +0 -19
  455. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transitions.md +0 -122
  456. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/trimming.md +0 -52
  457. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/videos.md +0 -171
  458. package/vendor/theus-arsenal/skills/cumplimiento-aduanero/SKILL.md +0 -270
  459. package/vendor/theus-arsenal/skills/cumplimiento-hipaa/SKILL.md +0 -78
  460. package/vendor/theus-arsenal/skills/datos-sensibles-salud/SKILL.md +0 -149
  461. package/vendor/theus-arsenal/skills/decimales-tokens-evm/SKILL.md +0 -128
  462. package/vendor/theus-arsenal/skills/decisiones-arquitectura/SKILL.md +0 -206
  463. package/vendor/theus-arsenal/skills/demos-ui/SKILL.md +0 -469
  464. package/vendor/theus-arsenal/skills/desarrollo-por-intencion/SKILL.md +0 -369
  465. package/vendor/theus-arsenal/skills/descubrimiento-marca/SKILL.md +0 -149
  466. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/10_proposito-porque.md +0 -42
  467. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/20_posicionamiento.md +0 -47
  468. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/30_audiencia-nicho.md +0 -53
  469. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/40_personalidad-arquetipo.md +0 -61
  470. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/50_voz-tono.md +0 -62
  471. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/60_narrativa-historia.md +0 -54
  472. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/70_tension-fundador.md +0 -50
  473. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/90_SINTESIS.md +0 -133
  474. package/vendor/theus-arsenal/skills/descubrimiento-plugins-laravel/SKILL.md +0 -230
  475. package/vendor/theus-arsenal/skills/destilado-reglas/SKILL.md +0 -263
  476. package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-rules.sh +0 -58
  477. package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-skills.sh +0 -129
  478. package/vendor/theus-arsenal/skills/diagnostico-bgp/SKILL.md +0 -170
  479. package/vendor/theus-arsenal/skills/direccion-estetica/SKILL.md +0 -220
  480. package/vendor/theus-arsenal/skills/direccion-estetica/references/taxonomia-generos.md +0 -89
  481. package/vendor/theus-arsenal/skills/diseno-api/SKILL.md +0 -522
  482. package/vendor/theus-arsenal/skills/diseno-liquid-glass/SKILL.md +0 -282
  483. package/vendor/theus-arsenal/skills/e2e-escritorio-windows/SKILL.md +0 -879
  484. package/vendor/theus-arsenal/skills/edicion-video/SKILL.md +0 -289
  485. package/vendor/theus-arsenal/skills/enfoque-producto/SKILL.md +0 -91
  486. package/vendor/theus-arsenal/skills/entornos-flox/SKILL.md +0 -493
  487. package/vendor/theus-arsenal/skills/estandares-codigo/SKILL.md +0 -548
  488. package/vendor/theus-arsenal/skills/estandares-cpp/SKILL.md +0 -727
  489. package/vendor/theus-arsenal/skills/estandares-java/SKILL.md +0 -389
  490. package/vendor/theus-arsenal/skills/estilo-legado/SKILL.md +0 -149
  491. package/vendor/theus-arsenal/skills/estructura-informes-competencia/SKILL.md +0 -95
  492. package/vendor/theus-arsenal/skills/evaluacion-academica/SKILL.md +0 -165
  493. package/vendor/theus-arsenal/skills/explorador-skills/SKILL.md +0 -129
  494. package/vendor/theus-arsenal/skills/flujo-git/SKILL.md +0 -714
  495. package/vendor/theus-arsenal/skills/flujo-ingenieria-ml/SKILL.md +0 -343
  496. package/vendor/theus-arsenal/skills/flujo-tdd/SKILL.md +0 -584
  497. package/vendor/theus-arsenal/skills/forja-personas/SKILL.md +0 -298
  498. package/vendor/theus-arsenal/skills/forja-personas/gacha.py +0 -224
  499. package/vendor/theus-arsenal/skills/forja-personas/gacha.sh +0 -5
  500. package/vendor/theus-arsenal/skills/forja-personas/references/avatar-style.md +0 -123
  501. package/vendor/theus-arsenal/skills/forja-personas/references/boundary-rules.md +0 -53
  502. package/vendor/theus-arsenal/skills/forja-personas/references/error-handling.md +0 -53
  503. package/vendor/theus-arsenal/skills/forja-personas/references/identity-tension.md +0 -48
  504. package/vendor/theus-arsenal/skills/forja-personas/references/naming-system.md +0 -39
  505. package/vendor/theus-arsenal/skills/forja-personas/references/output-template.md +0 -166
  506. package/vendor/theus-arsenal/skills/fundamentos-animacion/SKILL.md +0 -296
  507. package/vendor/theus-arsenal/skills/generacion-iconos-ios/SKILL.md +0 -156
  508. package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/generate_icons.swift +0 -258
  509. package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/iconify_gen.sh +0 -235
  510. package/vendor/theus-arsenal/skills/gestion-excepciones-logisticas/SKILL.md +0 -208
  511. package/vendor/theus-arsenal/skills/gestion-transportistas/SKILL.md +0 -204
  512. package/vendor/theus-arsenal/skills/guia-adopcion-ml/SKILL.md +0 -64
  513. package/vendor/theus-arsenal/skills/harness-agente-autonomo/SKILL.md +0 -252
  514. package/vendor/theus-arsenal/skills/harness-eval-clinico/SKILL.md +0 -212
  515. package/vendor/theus-arsenal/skills/harness-evaluacion/SKILL.md +0 -296
  516. package/vendor/theus-arsenal/skills/harness-generador-evaluador/SKILL.md +0 -254
  517. package/vendor/theus-arsenal/skills/homelab-diseno-red/SKILL.md +0 -120
  518. package/vendor/theus-arsenal/skills/homelab-dns-pihole/SKILL.md +0 -264
  519. package/vendor/theus-arsenal/skills/homelab-preparacion-red/SKILL.md +0 -144
  520. package/vendor/theus-arsenal/skills/homelab-segmentacion-vlan/SKILL.md +0 -307
  521. package/vendor/theus-arsenal/skills/homelab-vpn-wireguard/SKILL.md +0 -299
  522. package/vendor/theus-arsenal/skills/ingenieria-agentica/SKILL.md +0 -96
  523. package/vendor/theus-arsenal/skills/inspeccion-animaciones-blender/SKILL.md +0 -164
  524. package/vendor/theus-arsenal/skills/instaladores-python/SKILL.md +0 -820
  525. package/vendor/theus-arsenal/skills/integracion-jira/SKILL.md +0 -307
  526. package/vendor/theus-arsenal/skills/inteligencia-leads/SKILL.md +0 -312
  527. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/agente-enriquecimiento.md +0 -87
  528. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/mapeador-mutuos.md +0 -76
  529. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/puntuador-senales.md +0 -60
  530. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/redactor-outreach.md +0 -102
  531. package/vendor/theus-arsenal/skills/inteligencia-mercados-prediccion/SKILL.md +0 -53
  532. package/vendor/theus-arsenal/skills/introspeccion-agente/SKILL.md +0 -150
  533. package/vendor/theus-arsenal/skills/inventario-skills/SKILL.md +0 -194
  534. package/vendor/theus-arsenal/skills/inventario-skills/scripts/quick-diff.sh +0 -87
  535. package/vendor/theus-arsenal/skills/inventario-skills/scripts/save-results.sh +0 -57
  536. package/vendor/theus-arsenal/skills/inventario-skills/scripts/scan.sh +0 -170
  537. package/vendor/theus-arsenal/skills/investigacion-mercado/SKILL.md +0 -87
  538. package/vendor/theus-arsenal/skills/investigacion-oraculos-prediccion/SKILL.md +0 -59
  539. package/vendor/theus-arsenal/skills/investigacion-profunda/SKILL.md +0 -161
  540. package/vendor/theus-arsenal/skills/keccak256-nodejs/SKILL.md +0 -100
  541. package/vendor/theus-arsenal/skills/libro-decisiones-recursivo/SKILL.md +0 -82
  542. package/vendor/theus-arsenal/skills/lienzo-planes/SKILL.md +0 -111
  543. package/vendor/theus-arsenal/skills/limpieza-configuracion/SKILL.md +0 -116
  544. package/vendor/theus-arsenal/skills/logistica-inversa-devoluciones/SKILL.md +0 -241
  545. package/vendor/theus-arsenal/skills/manejo-errores/SKILL.md +0 -375
  546. package/vendor/theus-arsenal/skills/materiales-inversores/SKILL.md +0 -101
  547. package/vendor/theus-arsenal/skills/memoria-por-proyecto/SKILL.md +0 -170
  548. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/forget.mjs +0 -44
  549. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/info.mjs +0 -24
  550. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/init.mjs +0 -152
  551. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/list.mjs +0 -40
  552. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/migrate.mjs +0 -214
  553. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/resume.mjs +0 -36
  554. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/save.mjs +0 -210
  555. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/shared.mjs +0 -402
  556. package/vendor/theus-arsenal/skills/memoria-por-proyecto/hooks/session-start.mjs +0 -238
  557. package/vendor/theus-arsenal/skills/metodologia-benchmarks/SKILL.md +0 -131
  558. package/vendor/theus-arsenal/skills/migraciones-bd/SKILL.md +0 -430
  559. package/vendor/theus-arsenal/skills/modelos-fundacionales-en-dispositivo/SKILL.md +0 -244
  560. package/vendor/theus-arsenal/skills/modo-seguro/SKILL.md +0 -86
  561. package/vendor/theus-arsenal/skills/motion-avanzado/SKILL.md +0 -592
  562. package/vendor/theus-arsenal/skills/motor-de-contenido/SKILL.md +0 -136
  563. package/vendor/theus-arsenal/skills/no-conformidades-calidad/SKILL.md +0 -249
  564. package/vendor/theus-arsenal/skills/onboarding-codigo/SKILL.md +0 -244
  565. package/vendor/theus-arsenal/skills/operaciones-agentes-empresariales/SKILL.md +0 -51
  566. package/vendor/theus-arsenal/skills/operaciones-conocimiento/SKILL.md +0 -159
  567. package/vendor/theus-arsenal/skills/operaciones-correo/SKILL.md +0 -123
  568. package/vendor/theus-arsenal/skills/operaciones-facturacion/SKILL.md +0 -142
  569. package/vendor/theus-arsenal/skills/operaciones-flujo-proyectos/SKILL.md +0 -110
  570. package/vendor/theus-arsenal/skills/operaciones-github/SKILL.md +0 -143
  571. package/vendor/theus-arsenal/skills/operaciones-google-workspace/SKILL.md +0 -100
  572. package/vendor/theus-arsenal/skills/operaciones-investigacion/SKILL.md +0 -124
  573. package/vendor/theus-arsenal/skills/operaciones-mensajes/SKILL.md +0 -103
  574. package/vendor/theus-arsenal/skills/operaciones-terminal/SKILL.md +0 -110
  575. package/vendor/theus-arsenal/skills/optimizacion-benchmark/SKILL.md +0 -70
  576. package/vendor/theus-arsenal/skills/optimizacion-costos-llm/SKILL.md +0 -182
  577. package/vendor/theus-arsenal/skills/optimizador-conexiones/SKILL.md +0 -191
  578. package/vendor/theus-arsenal/skills/optimizador-paralelismo/SKILL.md +0 -75
  579. package/vendor/theus-arsenal/skills/optimizador-prompts/SKILL.md +0 -374
  580. package/vendor/theus-arsenal/skills/orq-agregar-funcionalidad/SKILL.md +0 -37
  581. package/vendor/theus-arsenal/skills/orq-cambiar-funcionalidad/SKILL.md +0 -36
  582. package/vendor/theus-arsenal/skills/orq-construir-mvp/SKILL.md +0 -38
  583. package/vendor/theus-arsenal/skills/orq-corregir-defecto/SKILL.md +0 -36
  584. package/vendor/theus-arsenal/skills/orq-pipeline/SKILL.md +0 -134
  585. package/vendor/theus-arsenal/skills/orq-refinar-codigo/SKILL.md +0 -49
  586. package/vendor/theus-arsenal/skills/orquestacion-equipos-agentes/SKILL.md +0 -117
  587. package/vendor/theus-arsenal/skills/pagos-agentes-x402/SKILL.md +0 -237
  588. package/vendor/theus-arsenal/skills/patrones-angular/SKILL.md +0 -153
  589. package/vendor/theus-arsenal/skills/patrones-angular/references/angular-animations.md +0 -160
  590. package/vendor/theus-arsenal/skills/patrones-angular/references/angular-aria.md +0 -410
  591. package/vendor/theus-arsenal/skills/patrones-angular/references/cli.md +0 -86
  592. package/vendor/theus-arsenal/skills/patrones-angular/references/component-harnesses.md +0 -59
  593. package/vendor/theus-arsenal/skills/patrones-angular/references/component-styling.md +0 -91
  594. package/vendor/theus-arsenal/skills/patrones-angular/references/components.md +0 -117
  595. package/vendor/theus-arsenal/skills/patrones-angular/references/creating-services.md +0 -97
  596. package/vendor/theus-arsenal/skills/patrones-angular/references/data-resolvers.md +0 -69
  597. package/vendor/theus-arsenal/skills/patrones-angular/references/define-routes.md +0 -67
  598. package/vendor/theus-arsenal/skills/patrones-angular/references/defining-providers.md +0 -72
  599. package/vendor/theus-arsenal/skills/patrones-angular/references/di-fundamentals.md +0 -120
  600. package/vendor/theus-arsenal/skills/patrones-angular/references/e2e-testing.md +0 -56
  601. package/vendor/theus-arsenal/skills/patrones-angular/references/effects.md +0 -83
  602. package/vendor/theus-arsenal/skills/patrones-angular/references/hierarchical-injectors.md +0 -43
  603. package/vendor/theus-arsenal/skills/patrones-angular/references/host-elements.md +0 -80
  604. package/vendor/theus-arsenal/skills/patrones-angular/references/injection-context.md +0 -63
  605. package/vendor/theus-arsenal/skills/patrones-angular/references/inputs.md +0 -101
  606. package/vendor/theus-arsenal/skills/patrones-angular/references/linked-signal.md +0 -59
  607. package/vendor/theus-arsenal/skills/patrones-angular/references/loading-strategies.md +0 -61
  608. package/vendor/theus-arsenal/skills/patrones-angular/references/mcp.md +0 -67
  609. package/vendor/theus-arsenal/skills/patrones-angular/references/navigate-to-routes.md +0 -69
  610. package/vendor/theus-arsenal/skills/patrones-angular/references/outputs.md +0 -86
  611. package/vendor/theus-arsenal/skills/patrones-angular/references/reactive-forms.md +0 -122
  612. package/vendor/theus-arsenal/skills/patrones-angular/references/rendering-strategies.md +0 -44
  613. package/vendor/theus-arsenal/skills/patrones-angular/references/resource.md +0 -77
  614. package/vendor/theus-arsenal/skills/patrones-angular/references/route-animations.md +0 -56
  615. package/vendor/theus-arsenal/skills/patrones-angular/references/route-guards.md +0 -52
  616. package/vendor/theus-arsenal/skills/patrones-angular/references/router-lifecycle.md +0 -45
  617. package/vendor/theus-arsenal/skills/patrones-angular/references/router-testing.md +0 -87
  618. package/vendor/theus-arsenal/skills/patrones-angular/references/show-routes-with-outlets.md +0 -68
  619. package/vendor/theus-arsenal/skills/patrones-angular/references/signal-forms.md +0 -795
  620. package/vendor/theus-arsenal/skills/patrones-angular/references/signals-overview.md +0 -94
  621. package/vendor/theus-arsenal/skills/patrones-angular/references/tailwind-css.md +0 -69
  622. package/vendor/theus-arsenal/skills/patrones-angular/references/template-driven-forms.md +0 -114
  623. package/vendor/theus-arsenal/skills/patrones-angular/references/testing-fundamentals.md +0 -65
  624. package/vendor/theus-arsenal/skills/patrones-animacion/SKILL.md +0 -432
  625. package/vendor/theus-arsenal/skills/patrones-backend/SKILL.md +0 -560
  626. package/vendor/theus-arsenal/skills/patrones-cdss-clinico/SKILL.md +0 -249
  627. package/vendor/theus-arsenal/skills/patrones-cisco-ios/SKILL.md +0 -148
  628. package/vendor/theus-arsenal/skills/patrones-clickhouse/SKILL.md +0 -443
  629. package/vendor/theus-arsenal/skills/patrones-compose-multiplatform/SKILL.md +0 -298
  630. package/vendor/theus-arsenal/skills/patrones-dart-flutter/SKILL.md +0 -566
  631. package/vendor/theus-arsenal/skills/patrones-despliegue/SKILL.md +0 -426
  632. package/vendor/theus-arsenal/skills/patrones-django/SKILL.md +0 -739
  633. package/vendor/theus-arsenal/skills/patrones-django-celery/SKILL.md +0 -456
  634. package/vendor/theus-arsenal/skills/patrones-docker/SKILL.md +0 -363
  635. package/vendor/theus-arsenal/skills/patrones-dotnet/SKILL.md +0 -321
  636. package/vendor/theus-arsenal/skills/patrones-emr-clinico/SKILL.md +0 -168
  637. package/vendor/theus-arsenal/skills/patrones-fastapi/SKILL.md +0 -513
  638. package/vendor/theus-arsenal/skills/patrones-frontend/SKILL.md +0 -655
  639. package/vendor/theus-arsenal/skills/patrones-go/SKILL.md +0 -674
  640. package/vendor/theus-arsenal/skills/patrones-jpa/SKILL.md +0 -156
  641. package/vendor/theus-arsenal/skills/patrones-kotlin/SKILL.md +0 -717
  642. package/vendor/theus-arsenal/skills/patrones-kotlin-exposed/SKILL.md +0 -724
  643. package/vendor/theus-arsenal/skills/patrones-kotlin-ktor/SKILL.md +0 -694
  644. package/vendor/theus-arsenal/skills/patrones-kubernetes/SKILL.md +0 -754
  645. package/vendor/theus-arsenal/skills/patrones-laravel/SKILL.md +0 -414
  646. package/vendor/theus-arsenal/skills/patrones-mcp/SKILL.md +0 -66
  647. package/vendor/theus-arsenal/skills/patrones-mysql/SKILL.md +0 -420
  648. package/vendor/theus-arsenal/skills/patrones-nestjs/SKILL.md +0 -229
  649. package/vendor/theus-arsenal/skills/patrones-nextjs/SKILL.md +0 -56
  650. package/vendor/theus-arsenal/skills/patrones-nuxt4/SKILL.md +0 -99
  651. package/vendor/theus-arsenal/skills/patrones-perl/SKILL.md +0 -508
  652. package/vendor/theus-arsenal/skills/patrones-postgres/SKILL.md +0 -141
  653. package/vendor/theus-arsenal/skills/patrones-prisma/SKILL.md +0 -399
  654. package/vendor/theus-arsenal/skills/patrones-python/SKILL.md +0 -750
  655. package/vendor/theus-arsenal/skills/patrones-pytorch/SKILL.md +0 -400
  656. package/vendor/theus-arsenal/skills/patrones-quarkus/SKILL.md +0 -729
  657. package/vendor/theus-arsenal/skills/patrones-react/SKILL.md +0 -339
  658. package/vendor/theus-arsenal/skills/patrones-react-native/SKILL.md +0 -325
  659. package/vendor/theus-arsenal/skills/patrones-redis/SKILL.md +0 -401
  660. package/vendor/theus-arsenal/skills/patrones-rust/SKILL.md +0 -504
  661. package/vendor/theus-arsenal/skills/patrones-springboot/SKILL.md +0 -314
  662. package/vendor/theus-arsenal/skills/patrones-swiftui/SKILL.md +0 -261
  663. package/vendor/theus-arsenal/skills/patrones-tinystruct/SKILL.md +0 -281
  664. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/architecture.md +0 -90
  665. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/data-handling.md +0 -60
  666. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/database.md +0 -99
  667. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/routing.md +0 -64
  668. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/system-usage.md +0 -97
  669. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/testing.md +0 -72
  670. package/vendor/theus-arsenal/skills/patrones-vite/SKILL.md +0 -449
  671. package/vendor/theus-arsenal/skills/patrones-vue/SKILL.md +0 -469
  672. package/vendor/theus-arsenal/skills/persistencia-actores-swift/SKILL.md +0 -148
  673. package/vendor/theus-arsenal/skills/pipeline-opensource/SKILL.md +0 -254
  674. package/vendor/theus-arsenal/skills/planificacion-demanda-inventario/SKILL.md +0 -236
  675. package/vendor/theus-arsenal/skills/planificador-trades-prediccion/SKILL.md +0 -60
  676. package/vendor/theus-arsenal/skills/plano-tecnico/SKILL.md +0 -102
  677. package/vendor/theus-arsenal/skills/presentaciones-html/SKILL.md +0 -189
  678. package/vendor/theus-arsenal/skills/presentaciones-html/STYLE_PRESETS.md +0 -330
  679. package/vendor/theus-arsenal/skills/presentaciones-html/animation-patterns.md +0 -122
  680. package/vendor/theus-arsenal/skills/presentaciones-html/html-template.md +0 -419
  681. package/vendor/theus-arsenal/skills/presentaciones-html/scripts/export-pdf.sh +0 -418
  682. package/vendor/theus-arsenal/skills/presentaciones-html/scripts/extract-pptx.py +0 -96
  683. package/vendor/theus-arsenal/skills/presentaciones-html/viewport-base.css +0 -153
  684. package/vendor/theus-arsenal/skills/presupuesto-contexto/SKILL.md +0 -263
  685. package/vendor/theus-arsenal/skills/procesamiento-documentos-nutrient/SKILL.md +0 -168
  686. package/vendor/theus-arsenal/skills/programacion-produccion/SKILL.md +0 -220
  687. package/vendor/theus-arsenal/skills/publicacion-cruzada/SKILL.md +0 -117
  688. package/vendor/theus-arsenal/skills/publicador-social/SKILL.md +0 -121
  689. package/vendor/theus-arsenal/skills/puerta-de-entrega/SKILL.md +0 -164
  690. package/vendor/theus-arsenal/skills/puerta-de-entrega/hooks/puerta-entrega.py +0 -242
  691. package/vendor/theus-arsenal/skills/pulir-interfaces/SKILL.md +0 -126
  692. package/vendor/theus-arsenal/skills/qa-navegador/SKILL.md +0 -103
  693. package/vendor/theus-arsenal/skills/ranking-grafo-social/SKILL.md +0 -153
  694. package/vendor/theus-arsenal/skills/recuperacion-iterativa/SKILL.md +0 -236
  695. package/vendor/theus-arsenal/skills/redaccion-articulos/SKILL.md +0 -81
  696. package/vendor/theus-arsenal/skills/regex-vs-llm/SKILL.md +0 -221
  697. package/vendor/theus-arsenal/skills/regresion-codigo-ia/SKILL.md +0 -388
  698. package/vendor/theus-arsenal/skills/rendimiento-react/SKILL.md +0 -567
  699. package/vendor/theus-arsenal/skills/revision-codigo-flutter-dart/SKILL.md +0 -442
  700. package/vendor/theus-arsenal/skills/revision-literatura-cientifica/SKILL.md +0 -191
  701. package/vendor/theus-arsenal/skills/revision-multiperspectiva/SKILL.md +0 -302
  702. package/vendor/theus-arsenal/skills/revision-riesgo-prediccion/SKILL.md +0 -56
  703. package/vendor/theus-arsenal/skills/revision-seguridad/SKILL.md +0 -505
  704. package/vendor/theus-arsenal/skills/revision-seguridad/cloud-infrastructure-security.md +0 -361
  705. package/vendor/theus-arsenal/skills/runtime-bun/SKILL.md +0 -83
  706. package/vendor/theus-arsenal/skills/salud-codigo-codescene/SKILL.md +0 -163
  707. package/vendor/theus-arsenal/skills/salud-interfaces-red/SKILL.md +0 -143
  708. package/vendor/theus-arsenal/skills/seguimiento-costos/SKILL.md +0 -122
  709. package/vendor/theus-arsenal/skills/seguridad-agentes-trading/SKILL.md +0 -145
  710. package/vendor/theus-arsenal/skills/seguridad-amm-defi/SKILL.md +0 -166
  711. package/vendor/theus-arsenal/skills/seguridad-django/SKILL.md +0 -648
  712. package/vendor/theus-arsenal/skills/seguridad-laravel/SKILL.md +0 -945
  713. package/vendor/theus-arsenal/skills/seguridad-perl/SKILL.md +0 -507
  714. package/vendor/theus-arsenal/skills/seguridad-quarkus/SKILL.md +0 -474
  715. package/vendor/theus-arsenal/skills/seguridad-springboot/SKILL.md +0 -279
  716. package/vendor/theus-arsenal/skills/seo/SKILL.md +0 -166
  717. package/vendor/theus-arsenal/skills/sistema-diseno/SKILL.md +0 -81
  718. package/vendor/theus-arsenal/skills/sistema-operativo-agentico/SKILL.md +0 -375
  719. package/vendor/theus-arsenal/skills/sistemas-latencia-critica/SKILL.md +0 -65
  720. package/vendor/theus-arsenal/skills/tdd-django/SKILL.md +0 -735
  721. package/vendor/theus-arsenal/skills/tdd-laravel/SKILL.md +0 -673
  722. package/vendor/theus-arsenal/skills/tdd-quarkus/SKILL.md +0 -817
  723. package/vendor/theus-arsenal/skills/tdd-springboot/SKILL.md +0 -165
  724. package/vendor/theus-arsenal/skills/testing-cpp/SKILL.md +0 -328
  725. package/vendor/theus-arsenal/skills/testing-csharp/SKILL.md +0 -321
  726. package/vendor/theus-arsenal/skills/testing-e2e/SKILL.md +0 -325
  727. package/vendor/theus-arsenal/skills/testing-go/SKILL.md +0 -719
  728. package/vendor/theus-arsenal/skills/testing-kotlin/SKILL.md +0 -829
  729. package/vendor/theus-arsenal/skills/testing-perl/SKILL.md +0 -479
  730. package/vendor/theus-arsenal/skills/testing-protocolos-di-swift/SKILL.md +0 -196
  731. package/vendor/theus-arsenal/skills/testing-python/SKILL.md +0 -815
  732. package/vendor/theus-arsenal/skills/testing-react/SKILL.md +0 -421
  733. package/vendor/theus-arsenal/skills/testing-rust/SKILL.md +0 -506
  734. package/vendor/theus-arsenal/skills/tours-de-codigo/SKILL.md +0 -252
  735. package/vendor/theus-arsenal/skills/traduccion-documentos-visado/SKILL.md +0 -121
  736. package/vendor/theus-arsenal/skills/ui-a-vue/SKILL.md +0 -141
  737. package/vendor/theus-arsenal/skills/validacion-config-red/SKILL.md +0 -211
  738. package/vendor/theus-arsenal/skills/verificacion-django/SKILL.md +0 -470
  739. package/vendor/theus-arsenal/skills/verificacion-factual/SKILL.md +0 -142
  740. package/vendor/theus-arsenal/skills/verificacion-laravel/SKILL.md +0 -184
  741. package/vendor/theus-arsenal/skills/verificacion-quarkus/SKILL.md +0 -485
  742. package/vendor/theus-arsenal/skills/verificacion-springboot/SKILL.md +0 -237
  743. package/vendor/theus-arsenal/skills/videodb/SKILL.md +0 -374
  744. package/vendor/theus-arsenal/skills/videodb/reference/api-reference.md +0 -550
  745. package/vendor/theus-arsenal/skills/videodb/reference/capture-reference.md +0 -407
  746. package/vendor/theus-arsenal/skills/videodb/reference/capture.md +0 -101
  747. package/vendor/theus-arsenal/skills/videodb/reference/editor.md +0 -443
  748. package/vendor/theus-arsenal/skills/videodb/reference/generative.md +0 -331
  749. package/vendor/theus-arsenal/skills/videodb/reference/rtstream-reference.md +0 -564
  750. package/vendor/theus-arsenal/skills/videodb/reference/rtstream.md +0 -65
  751. package/vendor/theus-arsenal/skills/videodb/reference/search.md +0 -230
  752. package/vendor/theus-arsenal/skills/videodb/reference/streaming.md +0 -406
  753. package/vendor/theus-arsenal/skills/videodb/reference/use-cases.md +0 -118
  754. package/vendor/theus-arsenal/skills/videodb/scripts/ws_listener.py +0 -279
  755. package/vendor/theus-arsenal/skills/videos-manim/SKILL.md +0 -91
  756. package/vendor/theus-arsenal/skills/videos-manim/assets/network_graph_scene.py +0 -52
  757. package/vendor/theus-arsenal/skills/vigilancia-canary/SKILL.md +0 -108
  758. package/vendor/theus-arsenal/skills/voz-de-marca/SKILL.md +0 -106
@@ -0,0 +1,2171 @@
1
+ """Detect and strip C2PA / AI-related metadata from raster images (stdlib).
2
+
3
+ Supported formats: PNG, JPEG, WebP, AVIF/HEIC (ISOBMFF), BMP, GIF, and TIFF
4
+ (classic and BigTIFF).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+ import os
12
+ import re
13
+ import struct
14
+ import subprocess
15
+ import sys
16
+ import urllib.error
17
+ import urllib.request
18
+ import zlib
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any
22
+ from urllib.parse import urlparse
23
+
24
+ from common import (
25
+ c2patool_probe_note,
26
+ classify_finding_confidence,
27
+ safe_arg,
28
+ safe_write_bytes,
29
+ subprocess_creationflags,
30
+ subprocess_preexec_fn,
31
+ which,
32
+ )
33
+
34
+ SCRIPTS_DIR = Path(__file__).resolve().parent
35
+
36
+ # Optional HTTP SynthID scorer sidecar (synthid_score_server.py). When
37
+ # WATERMARKS_SYNTHID_SCORER_URL is set, run_synthid_score calls the sidecar
38
+ # instead of a local reverse-SynthID checkout — this keeps the published core
39
+ # image free of the non-commercial upstream code. Read per call so tests can
40
+ # set the env vars after import.
41
+ DEFAULT_SYNTHID_SCORER_TIMEOUT = 60.0
42
+
43
+ # CtrlRegen is torch-based and needs far more address space than the stdlib
44
+ # parsers. The invoking clean_image.py subprocess applies these higher,
45
+ # env-overridable caps instead of the default child limits in common.py.
46
+ _CTRLREGEN_RLIMIT_AS = int(os.environ.get("WATERMARKS_CTRLREGEN_RLIMIT_AS", str(32 << 30)))
47
+ _CTRLREGEN_RLIMIT_FSIZE = int(os.environ.get("WATERMARKS_CTRLREGEN_RLIMIT_FSIZE", str(2 << 30)))
48
+
49
+
50
+ def ctrlregen_preexec_fn() -> None:
51
+ """Higher resource caps for the CtrlRegen subprocess (torch memory)."""
52
+ try:
53
+ import resource
54
+
55
+ resource.setrlimit(resource.RLIMIT_AS, (_CTRLREGEN_RLIMIT_AS, _CTRLREGEN_RLIMIT_AS))
56
+ resource.setrlimit(
57
+ resource.RLIMIT_FSIZE, (_CTRLREGEN_RLIMIT_FSIZE, _CTRLREGEN_RLIMIT_FSIZE)
58
+ )
59
+ except (ImportError, OSError, ValueError):
60
+ pass
61
+
62
+
63
+ ctrlregen_subprocess_preexec_fn = ctrlregen_preexec_fn if os.name == "posix" else None
64
+
65
+ PNG_SIG = b"\x89PNG\r\n\x1a\n"
66
+ JPEG_SOI = b"\xff\xd8"
67
+ WEBP_RIFF = b"RIFF"
68
+ WEBP_SIG = b"WEBP"
69
+ BMP_SIG = b"BM"
70
+ GIF_SIGS = (b"GIF87a", b"GIF89a")
71
+ TIFF_LE_SIG = b"II*\x00"
72
+ TIFF_BE_SIG = b"MM\x00*"
73
+ TIFF_LE_BIG_SIG = b"II+\x00"
74
+ TIFF_BE_BIG_SIG = b"MM\x00+"
75
+
76
+ # Chunk types / content patterns associated with C2PA / AI provenance
77
+ C2PA_MARKERS = (
78
+ b"c2pa",
79
+ b"C2PA",
80
+ b"jumb",
81
+ b"JUMB",
82
+ b"c2ma",
83
+ b"contentcredentials",
84
+ b"contentauth",
85
+ b"cai:",
86
+ b"http://ns.adobe.com/xmp/InstanceID/", # not solely C2PA
87
+ )
88
+
89
+ AI_META_HINTS = (
90
+ b"c2pa",
91
+ b"C2PA",
92
+ b"contentcredentials",
93
+ b"ContentCredentials",
94
+ b"digitalSourceType",
95
+ b"trainedAlgorithmicMedia",
96
+ b"compositeWithTrainedAlgorithmicMedia",
97
+ b"algorithmicMedia",
98
+ b"AIGC",
99
+ b"aigc",
100
+ b"AI generated",
101
+ b"Generated by",
102
+ b"Claude",
103
+ b"Anthropic",
104
+ b"OpenAI",
105
+ b"SynthID",
106
+ b"dcterms:provenance",
107
+ )
108
+
109
+ # JPEG COM is unkeyed free text, so only unambiguous provenance markers are
110
+ # evidence of AI metadata. Bare vendor names and "Generated by" are ordinary
111
+ # comment text too (for example, "Claude Monet" or "Generated by ImageMagick").
112
+ JPEG_C2PA_MARKERS = (
113
+ b"c2pa",
114
+ b"c2ma",
115
+ b"contentcredentials",
116
+ b"contentauth",
117
+ b"cai:",
118
+ )
119
+ JPEG_COM_AI_HINTS = (
120
+ *JPEG_C2PA_MARKERS,
121
+ b"digitalSourceType",
122
+ b"trainedAlgorithmicMedia",
123
+ b"compositeWithTrainedAlgorithmicMedia",
124
+ b"algorithmicMedia",
125
+ b"AIGC",
126
+ b"AI generated",
127
+ b"Generated by AI",
128
+ b"SynthID",
129
+ b"dcterms:provenance",
130
+ )
131
+
132
+ # Well-known AI generator product/model names. These are matched ONLY
133
+ # against the values of generator-bearing PNG text-chunk keys (Software,
134
+ # Creator, parameters) — see _generator_product_hits — and deliberately
135
+ # kept out of the flat AI_META_HINTS blob scan: bare "Gemini", "Sora",
136
+ # or "Firefly" are ordinary words that can appear in captions, and only a
137
+ # generator field makes them evidence of AI provenance.
138
+ AI_GENERATOR_PRODUCTS = (
139
+ b"ChatGPT",
140
+ b"DALL-E",
141
+ b"Midjourney",
142
+ b"Stable Diffusion",
143
+ b"SDXL",
144
+ b"FLUX",
145
+ b"DreamStudio",
146
+ b"Leonardo AI",
147
+ b"Leonardo.Ai",
148
+ b"Craiyon",
149
+ b"NovelAI",
150
+ b"Ideogram",
151
+ b"TensorArt",
152
+ b"Recraft",
153
+ b"Clipdrop",
154
+ b"DeepAI",
155
+ b"NightCafe",
156
+ b"Bing Image Creator",
157
+ b"Adobe Firefly",
158
+ b"Firefly",
159
+ b"Gemini",
160
+ b"Imagen",
161
+ b"Grok",
162
+ b"Sora",
163
+ b"Veo",
164
+ b"Kling",
165
+ b"Runway",
166
+ b"Luma",
167
+ b"Qwen",
168
+ b"GPT-4",
169
+ b"GPT-5",
170
+ )
171
+
172
+ # PNG text-chunk keys whose value can name the generating model/tool.
173
+ # tEXt/iTXt "Software" and "Creator" are the classic generator fields;
174
+ # "parameters" is what Stable Diffusion WebUI writes with the full
175
+ # generation string. Values of other keys (Comment, Description, Title,
176
+ # ...) are free text and are never scanned for product names.
177
+ _GENERATOR_TEXT_KEYS = ("software", "creator", "parameters")
178
+
179
+
180
+ @dataclass
181
+ class ImageInspectReport:
182
+ path: str
183
+ format: str # png | jpeg | webp | avif | heic | bmp | gif | tiff | unknown
184
+ has_c2pa: bool
185
+ has_ai_metadata: bool
186
+ findings: list[str] = field(default_factory=list)
187
+ tools: dict[str, Any] = field(default_factory=dict)
188
+ synthid: dict[str, Any] | None = None
189
+ notes: list[str] = field(default_factory=list)
190
+
191
+ def to_dict(self) -> dict:
192
+ return {
193
+ "path": self.path,
194
+ "format": self.format,
195
+ "has_c2pa": self.has_c2pa,
196
+ "has_ai_metadata": self.has_ai_metadata,
197
+ "findings": self.findings,
198
+ "findings_confidence": [classify_finding_confidence(f) for f in self.findings],
199
+ "tools": self.tools,
200
+ "synthid": self.synthid,
201
+ "notes": self.notes,
202
+ }
203
+
204
+
205
+ def detect_format(data: bytes) -> str:
206
+ if data.startswith(PNG_SIG):
207
+ return "png"
208
+ if data.startswith(JPEG_SOI):
209
+ return "jpeg"
210
+ if len(data) >= 12 and data[:4] == WEBP_RIFF and data[8:12] == WEBP_SIG:
211
+ return "webp"
212
+ if len(data) >= 12 and data[4:8] == b"ftyp":
213
+ box_size = struct.unpack(">I", data[0:4])[0]
214
+ header_chunk = (
215
+ data[8 : min(box_size, len(data), 64)]
216
+ if box_size >= 8
217
+ else data[8 : min(len(data), 64)]
218
+ )
219
+ if any(b in header_chunk for b in (b"avif", b"avis", b"avio")):
220
+ return "avif"
221
+ if any(
222
+ b in header_chunk
223
+ for b in (b"heic", b"heix", b"hevc", b"heim", b"heis", b"mif1", b"msf1", b"heif")
224
+ ):
225
+ return "heic"
226
+ if data[:2] == BMP_SIG:
227
+ return "bmp"
228
+ if data[:6] in GIF_SIGS:
229
+ return "gif"
230
+ if data[:4] in (TIFF_LE_SIG, TIFF_BE_SIG, TIFF_LE_BIG_SIG, TIFF_BE_BIG_SIG):
231
+ return "tiff"
232
+ return "unknown"
233
+
234
+
235
+ def _contains_any(blob: bytes, needles: tuple[bytes, ...]) -> list[str]:
236
+ found = []
237
+ lower = blob.lower()
238
+ for n in needles:
239
+ if n.lower() in lower:
240
+ try:
241
+ found.append(n.decode("ascii", errors="replace"))
242
+ except Exception:
243
+ found.append(repr(n))
244
+ return found
245
+
246
+
247
+ def _png_text_entries(payload: bytes, ctype: bytes) -> list[tuple[str, str]]:
248
+ """Parse a PNG text-chunk payload into (key, value) pairs.
249
+
250
+ Handles tEXt (latin-1), zTXt (zlib-compressed text), and iTXt
251
+ (UTF-8, optionally compressed). Malformed or undecodable chunks
252
+ yield whatever pairs are recoverable; nothing is raised.
253
+ """
254
+ entries: list[tuple[str, str]] = []
255
+ if ctype == b"tEXt":
256
+ key, sep, text = payload.partition(b"\x00")
257
+ if sep:
258
+ entries.append(
259
+ (
260
+ key.decode("latin-1", errors="replace"),
261
+ text.decode("latin-1", errors="replace"),
262
+ )
263
+ )
264
+ elif ctype == b"zTXt":
265
+ key, sep, rest = payload.partition(b"\x00")
266
+ if not sep or len(rest) < 2:
267
+ return entries
268
+ try:
269
+ text = zlib.decompress(rest[1:])
270
+ except zlib.error:
271
+ return entries
272
+ entries.append(
273
+ (
274
+ key.decode("latin-1", errors="replace"),
275
+ text.decode("latin-1", errors="replace"),
276
+ )
277
+ )
278
+ elif ctype == b"iTXt":
279
+ key, sep, rest = payload.partition(b"\x00")
280
+ if not sep or len(rest) < 4:
281
+ return entries
282
+ comp_flag = rest[0]
283
+ rest = rest[2:] # skip compression flag + method
284
+ _lang, sep2, rest = rest.partition(b"\x00")
285
+ if not sep2:
286
+ return entries
287
+ _tkey, sep3, text = rest.partition(b"\x00")
288
+ if not sep3:
289
+ return entries
290
+ if comp_flag == 1:
291
+ try:
292
+ text = zlib.decompress(text)
293
+ except zlib.error:
294
+ return entries
295
+ entries.append(
296
+ (
297
+ key.decode("latin-1", errors="replace"),
298
+ text.decode("utf-8", errors="replace"),
299
+ )
300
+ )
301
+ return entries
302
+
303
+
304
+ def _generator_product_hits(entries: list[tuple[str, str]]) -> list[str]:
305
+ """Product-name hits scoped to generator-bearing text-chunk keys.
306
+
307
+ Returns labels like "Software=ChatGPT" (one per matching product).
308
+ Matching is a case-insensitive substring check on the value, mirroring
309
+ _contains_any, but only for _GENERATOR_TEXT_KEYS values.
310
+ """
311
+ hits: list[str] = []
312
+ for key, value in entries:
313
+ if key.strip().lower() not in _GENERATOR_TEXT_KEYS:
314
+ continue
315
+ low = value.lower()
316
+ for product in AI_GENERATOR_PRODUCTS:
317
+ label = product.decode("ascii", errors="replace")
318
+ if label.lower() in low:
319
+ hits.append(f"{key.strip()}={label}")
320
+ return hits
321
+
322
+
323
+ def _png_text_hits(payload: bytes, ctype: bytes) -> tuple[list[str], list[str]]:
324
+ """Return flat-marker and product-name hits from a PNG text chunk."""
325
+ entries = _png_text_entries(payload, ctype)
326
+ decoded = "\n".join(value for _key, value in entries).encode("utf-8")
327
+ hits = _contains_any(payload + b"\n" + decoded, AI_META_HINTS + C2PA_MARKERS)
328
+ return hits, _generator_product_hits(entries)
329
+
330
+
331
+ def _text_chunk_is_ai(payload: bytes, ctype: bytes) -> bool:
332
+ """True when a PNG text chunk carries AI/C2PA markers.
333
+
334
+ Flat markers (AI_META_HINTS + C2PA_MARKERS) match the raw payload and
335
+ decoded text; generator product names only match generator-bearing key
336
+ values (see _generator_product_hits).
337
+ """
338
+ hits, product_hits = _png_text_hits(payload, ctype)
339
+ return bool(hits or product_hits)
340
+
341
+
342
+ def inspect_png(data: bytes) -> tuple[bool, bool, list[str]]:
343
+ findings: list[str] = []
344
+ has_c2pa = False
345
+ has_ai = False
346
+ if not data.startswith(PNG_SIG):
347
+ return False, False, ["not a PNG"]
348
+ pos = 8
349
+ while pos + 8 <= len(data):
350
+ length = struct.unpack(">I", data[pos : pos + 4])[0]
351
+ ctype = data[pos + 4 : pos + 8]
352
+ chunk_start = pos + 8
353
+ chunk_end = chunk_start + length
354
+ if chunk_end + 4 > len(data):
355
+ findings.append(f"truncated chunk {ctype!r}")
356
+ break
357
+ payload = data[chunk_start:chunk_end]
358
+ name = ctype.decode("latin-1", errors="replace")
359
+ # Private/ancillary chunks sometimes used for JUMBF/C2PA
360
+ if ctype in (b"caBX", b"juMB", b"jumb") or ctype.startswith(b"c2"):
361
+ has_c2pa = True
362
+ findings.append(f"PNG chunk {name} (possible C2PA container)")
363
+ if ctype in (b"tEXt", b"zTXt", b"iTXt", b"eXIf"):
364
+ if ctype in (b"tEXt", b"zTXt", b"iTXt"):
365
+ hits, product_hits = _png_text_hits(payload, ctype)
366
+ else:
367
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
368
+ product_hits = []
369
+ if hits or product_hits:
370
+ has_ai = True
371
+ if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
372
+ has_c2pa = True
373
+ parts = [", ".join(hits[:8])] if hits else []
374
+ if product_hits:
375
+ parts.append(f"AI generator ({', '.join(product_hits[:8])})")
376
+ findings.append(f"PNG {name}: {'; '.join(parts)}")
377
+ if ctype == b"IEND":
378
+ break
379
+ pos = chunk_end + 4 # skip CRC
380
+ # Whole-file scan fallback
381
+ whole = _contains_any(data, C2PA_MARKERS)
382
+ if whole and not has_c2pa:
383
+ has_c2pa = True
384
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
385
+ return has_c2pa, has_ai or has_c2pa, findings
386
+
387
+
388
+ def inspect_jpeg(data: bytes) -> tuple[bool, bool, list[str]]:
389
+ findings: list[str] = []
390
+ has_c2pa = False
391
+ has_ai = False
392
+ if not data.startswith(JPEG_SOI):
393
+ return False, False, ["not a JPEG"]
394
+ i = 2
395
+ n = len(data)
396
+ while i + 4 <= n:
397
+ if data[i] != 0xFF:
398
+ i += 1
399
+ continue
400
+ # skip fill
401
+ while i < n and data[i] == 0xFF:
402
+ i += 1
403
+ if i >= n:
404
+ break
405
+ marker = data[i]
406
+ i += 1
407
+ if marker in (0xD8, 0xD9): # SOI/EOI
408
+ continue
409
+ if marker == 0xDA: # SOS — image data follows
410
+ break
411
+ if marker >= 0xD0 and marker <= 0xD7: # RSTn
412
+ continue
413
+ if i + 2 > n:
414
+ break
415
+ seglen = struct.unpack(">H", data[i : i + 2])[0]
416
+ if seglen < 2 or i + seglen > n:
417
+ findings.append(f"bad segment length at marker 0x{marker:02X}")
418
+ break
419
+ payload = data[i + 2 : i + seglen]
420
+ i += seglen
421
+
422
+ if marker == 0xFE: # COM
423
+ hits = _contains_any(payload, JPEG_COM_AI_HINTS)
424
+ if hits:
425
+ has_ai = True
426
+ if any(
427
+ h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
428
+ ):
429
+ has_c2pa = True
430
+ findings.append(f"JPEG COM: {', '.join(hits[:8])}")
431
+ # APP11 (0xEB) often holds JUMBF/C2PA
432
+ if marker == 0xEB:
433
+ has_c2pa = True
434
+ findings.append("JPEG APP11 segment (JUMBF/C2PA common)")
435
+ if marker in (0xE1, 0xE2, 0xED, 0xEE, 0xEB): # APP1,2,13,14,11
436
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
437
+ if hits:
438
+ has_ai = True
439
+ if any(
440
+ h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
441
+ ):
442
+ has_c2pa = True
443
+ findings.append(f"JPEG APP{marker - 0xE0}: {', '.join(hits[:8])}")
444
+
445
+ whole = _contains_any(data, JPEG_C2PA_MARKERS)
446
+ if whole and not has_c2pa:
447
+ has_c2pa = True
448
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
449
+ return has_c2pa, has_ai or has_c2pa, findings
450
+
451
+
452
+ def _webp_chunks(data: bytes) -> tuple[list[tuple[bytes, bytes, bytes]], list[str]]:
453
+ if detect_format(data) != "webp":
454
+ return [], ["not a WebP"]
455
+
456
+ notes: list[str] = []
457
+ declared_size = struct.unpack("<I", data[4:8])[0]
458
+ if declared_size + 8 != len(data):
459
+ notes.append(f"RIFF size mismatch: header={declared_size + 8} actual={len(data)}")
460
+
461
+ chunks: list[tuple[bytes, bytes, bytes]] = []
462
+ pos = 12
463
+ while pos + 8 <= len(data):
464
+ fourcc = data[pos : pos + 4]
465
+ length = struct.unpack("<I", data[pos + 4 : pos + 8])[0]
466
+ payload_start = pos + 8
467
+ payload_end = payload_start + length
468
+ padded_end = payload_end + (length & 1)
469
+ if padded_end > len(data):
470
+ name = fourcc.decode("latin-1", errors="replace")
471
+ notes.append(f"truncated WebP chunk {name}")
472
+ break
473
+ chunks.append((fourcc, data[payload_start:payload_end], data[payload_end:padded_end]))
474
+ pos = padded_end
475
+ if pos != len(data) and not any("truncated" in note for note in notes):
476
+ notes.append(f"trailing WebP bytes: {len(data) - pos}")
477
+ return chunks, notes
478
+
479
+
480
+ def inspect_webp(data: bytes) -> tuple[bool, bool, list[str]]:
481
+ chunks, findings = _webp_chunks(data)
482
+ if not chunks and findings == ["not a WebP"]:
483
+ return False, False, findings
484
+
485
+ has_c2pa = False
486
+ has_ai = False
487
+ for fourcc, payload, _padding in chunks:
488
+ name = fourcc.decode("latin-1", errors="replace")
489
+ if fourcc.upper() == b"C2PA":
490
+ has_c2pa = True
491
+ has_ai = True
492
+ findings.append("WebP C2PA chunk")
493
+ continue
494
+ if fourcc in (b"XMP ", b"EXIF"):
495
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
496
+ if hits:
497
+ has_ai = True
498
+ if any(
499
+ hit.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth")
500
+ for hit in hits
501
+ ):
502
+ has_c2pa = True
503
+ findings.append(f"WebP {name}: {', '.join(hits[:8])}")
504
+ return has_c2pa, has_ai or has_c2pa, findings
505
+
506
+
507
+ XMP_UUID = b"\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac"
508
+
509
+
510
+ def _parse_isobmff_boxes(
511
+ data: bytes, start: int = 0, end: int | None = None
512
+ ) -> tuple[list[tuple[bytes, bytes, int, int]], int]:
513
+ """Parse top-level or container ISOBMFF boxes.
514
+
515
+ Returns (boxes, scanned_end): the list of (fourcc, payload,
516
+ total_box_size, header_size) tuples and the offset where the walk
517
+ stopped -- the start of the first box that could not be parsed (an
518
+ overrunning box, or a run of fewer than 8 trailing bytes), or `end`
519
+ when the whole buffer parsed.
520
+ """
521
+ if end is None:
522
+ end = len(data)
523
+ boxes = []
524
+ pos = start
525
+ while pos + 8 <= end:
526
+ size = struct.unpack(">I", data[pos : pos + 4])[0]
527
+ fourcc = data[pos + 4 : pos + 8]
528
+ header_size = 8
529
+ if size == 1:
530
+ if pos + 16 > end:
531
+ break
532
+ size = struct.unpack(">Q", data[pos + 8 : pos + 16])[0]
533
+ header_size = 16
534
+ elif size == 0:
535
+ size = end - pos
536
+
537
+ if size < header_size or pos + size > end:
538
+ break
539
+ payload = data[pos + header_size : pos + size]
540
+ boxes.append((fourcc, payload, size, header_size))
541
+ pos += size
542
+ return boxes, pos
543
+
544
+
545
+ def _build_isobmff_box(fourcc: bytes, payload: bytes, header_size: int = 8) -> bytes:
546
+ """Serialize an ISOBMFF box while preserving its header width."""
547
+ size = len(payload) + header_size
548
+ if header_size == 16:
549
+ return struct.pack(">I4sQ", 1, fourcc, size) + payload
550
+ return struct.pack(">I4s", size, fourcc) + payload
551
+
552
+
553
+ def _isobmff_free_box(size: int, header_size: int = 8) -> bytes:
554
+ """Return an equal-size free box so later absolute offsets stay valid."""
555
+ return _build_isobmff_box(b"free", b"\x00" * (size - header_size), header_size)
556
+
557
+
558
+ def inspect_isobmff(data: bytes, fmt: str = "avif") -> tuple[bool, bool, list[str]]:
559
+ findings: list[str] = []
560
+ has_c2pa = False
561
+ has_ai = False
562
+
563
+ boxes, _ = _parse_isobmff_boxes(data)
564
+ if not boxes:
565
+ # Box parsing failed (e.g. the first box's size overruns a truncated
566
+ # download) — that is exactly when the whole-file byte scan below is
567
+ # most useful, and every sibling inspector (png/jpeg/gif/tiff) still
568
+ # runs its equivalent after a truncation. Run the fallback, then
569
+ # report the parse failure alongside whatever it found (#167).
570
+ whole = _contains_any(data, C2PA_MARKERS)
571
+ if whole:
572
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
573
+ has_c2pa = True
574
+ findings.append(f"not a valid {fmt.upper()} (no ISOBMFF boxes found)")
575
+ return has_c2pa, has_ai or has_c2pa, findings
576
+
577
+ for fourcc, payload, _, _ in boxes:
578
+ name = fourcc.decode("latin-1", errors="replace")
579
+ if fourcc in (b"jumb", b"c2pa") or name.lower().startswith("c2"):
580
+ has_c2pa = True
581
+ findings.append(f"{fmt.upper()} top-level box {name} (C2PA/JUMBF manifest)")
582
+ elif fourcc == b"uuid":
583
+ if payload.startswith(XMP_UUID):
584
+ has_ai = True
585
+ xmp_text = payload[16:]
586
+ hits = _contains_any(xmp_text, AI_META_HINTS + C2PA_MARKERS)
587
+ if hits:
588
+ findings.append(f"{fmt.upper()} XMP uuid box: {', '.join(hits[:8])}")
589
+ else:
590
+ findings.append(f"{fmt.upper()} XMP uuid box")
591
+ if any(
592
+ h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
593
+ ):
594
+ has_c2pa = True
595
+ else:
596
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
597
+ if hits:
598
+ has_ai = True
599
+ findings.append(f"{fmt.upper()} uuid box: {', '.join(hits[:8])}")
600
+ if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
601
+ has_c2pa = True
602
+ elif fourcc == b"meta":
603
+ meta_sub, _ = _parse_isobmff_boxes(payload, start=4)
604
+ for s_fourcc, s_payload, _, _ in meta_sub:
605
+ s_name = s_fourcc.decode("latin-1", errors="replace")
606
+ if s_fourcc in (b"jumb", b"c2pa") or s_name.lower().startswith("c2"):
607
+ has_c2pa = True
608
+ findings.append(f"{fmt.upper()} meta sub-box {s_name} (C2PA/JUMBF container)")
609
+ elif s_fourcc == b"uuid":
610
+ if s_payload.startswith(XMP_UUID):
611
+ has_ai = True
612
+ hits = _contains_any(s_payload[16:], AI_META_HINTS + C2PA_MARKERS)
613
+ if hits:
614
+ findings.append(f"{fmt.upper()} meta XMP uuid: {', '.join(hits[:8])}")
615
+ else:
616
+ findings.append(f"{fmt.upper()} meta XMP uuid box")
617
+ if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
618
+ has_c2pa = True
619
+ else:
620
+ hits = _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
621
+ if hits:
622
+ has_ai = True
623
+ findings.append(f"{fmt.upper()} meta uuid: {', '.join(hits[:8])}")
624
+ elif s_fourcc in (b"iinf", b"infe", b"iref", b"iloc", b"xml ", b"bxml"):
625
+ hits = _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
626
+ if hits:
627
+ has_ai = True
628
+ if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
629
+ has_c2pa = True
630
+ findings.append(f"{fmt.upper()} meta/{s_name}: {', '.join(hits[:8])}")
631
+
632
+ whole = _contains_any(data, C2PA_MARKERS)
633
+ if whole and not has_c2pa:
634
+ has_c2pa = True
635
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
636
+
637
+ return has_c2pa, has_ai or has_c2pa, findings
638
+
639
+
640
+ # ---------------------------------------------------------------------------
641
+ # BMP
642
+ # ---------------------------------------------------------------------------
643
+
644
+
645
+ def _bmp_payload_extent(data: bytes) -> tuple[int, int] | None:
646
+ """Return (pixel_offset, pixel_size) for a BMP, or None.
647
+
648
+ Locates the true image payload by parsing the BITMAPFILEHEADER and the DIB
649
+ header (BITMAPINFOHEADER 40, V4 108, or V5 124 bytes), so trailing
650
+ non-image bytes can be told apart from pixel data. Uncompressed rows are
651
+ padded to 4-byte boundaries; compressed or embedded-image BMPs (RLE, JPEG,
652
+ PNG) rely on the declared biSizeImage, and anything unparseable returns
653
+ None so callers stay conservative.
654
+ """
655
+ if len(data) < 30 or data[:2] != BMP_SIG:
656
+ return None
657
+ pixel_offset = struct.unpack("<I", data[10:14])[0]
658
+ dib_size = struct.unpack("<I", data[14:18])[0]
659
+ if dib_size < 40 or 14 + dib_size > len(data):
660
+ return None
661
+ width = struct.unpack("<i", data[18:22])[0]
662
+ height = struct.unpack("<i", data[22:26])[0]
663
+ bpp = struct.unpack("<H", data[28:30])[0]
664
+ compression = struct.unpack("<I", data[30:34])[0]
665
+ size_image = struct.unpack("<I", data[34:38])[0]
666
+ if width <= 0 or bpp == 0 or pixel_offset > len(data):
667
+ return None
668
+ if compression == 0: # BI_RGB
669
+ row_size = ((width * bpp + 31) // 32) * 4
670
+ size = row_size * abs(height)
671
+ elif size_image:
672
+ size = size_image
673
+ else:
674
+ return None
675
+ return pixel_offset, size
676
+
677
+
678
+ def _bmp_trailing(data: bytes) -> bytes:
679
+ """Bytes after the image payload, which is where non-standard BMP metadata lives."""
680
+ extent = _bmp_payload_extent(data)
681
+ if extent is None:
682
+ return b""
683
+ pixel_offset, size = extent
684
+ end = pixel_offset + size
685
+ return data[end:] if end < len(data) else b""
686
+
687
+
688
+ def inspect_bmp(data: bytes) -> tuple[bool, bool, list[str]]:
689
+ """Inspect a BMP for trailing (non-standard) metadata.
690
+
691
+ BMP has no standardized metadata container; the only realistic place
692
+ provenance data can live is after the image payload. Pixel bytes are never
693
+ scanned, so compressed/embedded-image data cannot false-positive.
694
+ """
695
+ findings: list[str] = []
696
+ if len(data) < 14 or data[:2] != BMP_SIG:
697
+ return False, False, ["not a BMP"]
698
+ trailing = _bmp_trailing(data)
699
+ has_c2pa = False
700
+ has_ai = False
701
+ if trailing:
702
+ hits = _contains_any(trailing, AI_META_HINTS + C2PA_MARKERS)
703
+ if hits:
704
+ has_ai = True
705
+ if any(
706
+ h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
707
+ ):
708
+ has_c2pa = True
709
+ findings.append(f"BMP trailing metadata: {', '.join(hits[:6])}")
710
+ else:
711
+ findings.append(f"BMP has {len(trailing)} unrecognized trailing byte(s)")
712
+ else:
713
+ findings.append("BMP has no metadata (header-only raster format)")
714
+ return has_c2pa, has_ai or has_c2pa, findings
715
+
716
+
717
+ def strip_bmp(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
718
+ """Strip trailing non-image bytes from a BMP and fix the file-size field.
719
+
720
+ The pixel payload is located via the DIB header and left byte-identical;
721
+ anything after it is treated as metadata. The 4-byte file-size field at
722
+ offset 2 is rewritten to the truncated length.
723
+ """
724
+ if len(data) < 14 or data[:2] != BMP_SIG:
725
+ raise ValueError("not BMP")
726
+ extent = _bmp_payload_extent(data)
727
+ if extent is None:
728
+ return data, ["BMP header not fully parsed; left unchanged"]
729
+ pixel_offset, size = extent
730
+ end = pixel_offset + size
731
+ if end >= len(data):
732
+ return data, ["no BMP trailing metadata to strip"]
733
+ trailing = data[end:]
734
+ hits = _contains_any(trailing, AI_META_HINTS + C2PA_MARKERS)
735
+ if not strip_all_metadata and not hits:
736
+ return data, ["BMP trailing bytes kept (keep-non-ai-metadata)"]
737
+ out = bytearray(data[:end])
738
+ out[2:6] = struct.pack("<I", end)
739
+ reason = f" ({', '.join(hits[:4])})" if hits else ""
740
+ return bytes(out), [f"drop {len(trailing)} BMP trailing byte(s){reason}"]
741
+
742
+
743
+ # ---------------------------------------------------------------------------
744
+ # GIF
745
+ # ---------------------------------------------------------------------------
746
+
747
+
748
+ def _gif_extension_info(data: bytes, start: int, n: int) -> tuple[int, int, bytes] | None:
749
+ """Return (end, label, payload) for the extension block at *start* (0x21).
750
+
751
+ The payload is the concatenation of all sub-blocks after the label; *end*
752
+ is the offset just past the terminating 0x00.
753
+ """
754
+ if start + 2 > n:
755
+ return None
756
+ label = data[start + 1]
757
+ pos = start + 2
758
+ payload = bytearray()
759
+ while pos < n:
760
+ size = data[pos]
761
+ pos += 1
762
+ if size == 0:
763
+ return pos, label, bytes(payload)
764
+ if pos + size > n:
765
+ return None
766
+ payload.extend(data[pos : pos + size])
767
+ pos += size
768
+ return None
769
+
770
+
771
+ def _gif_image_end(data: bytes, start: int, n: int) -> int | None:
772
+ """Return the offset just past the image block starting at *start* (0x2C)."""
773
+ pos = start + 1
774
+ if pos + 9 > n:
775
+ return None
776
+ packed = data[pos + 8]
777
+ pos += 9
778
+ if packed & 0x80: # local color table present
779
+ pos += 3 * (1 << ((packed & 0x07) + 1))
780
+ if pos >= n:
781
+ return None
782
+ pos += 1 # LZW minimum code size
783
+ while pos < n:
784
+ size = data[pos]
785
+ pos += 1
786
+ if size == 0:
787
+ return pos
788
+ if pos + size > n:
789
+ return None
790
+ pos += size
791
+ return None
792
+
793
+
794
+ GIF_XMP_APPLICATION_ID = b"XMP DataXMP"
795
+ # Application extensions that control rendering rather than carrying provenance;
796
+ # dropping them would change animation looping or color, so they are kept.
797
+ _GIF_CONTROL_APPLICATION_IDS = (b"NETSCAPE2.0", b"ICCRGBG1012")
798
+
799
+
800
+ def inspect_gif(data: bytes) -> tuple[bool, bool, list[str]]:
801
+ """Inspect GIF comment / application extensions and XMP payloads."""
802
+ findings: list[str] = []
803
+ has_c2pa = False
804
+ has_ai = False
805
+ if data[:6] not in GIF_SIGS:
806
+ return False, False, ["not a GIF"]
807
+ n = len(data)
808
+ pos = 6
809
+ if pos + 7 > n:
810
+ return False, False, ["truncated GIF header"]
811
+ packed = data[pos + 4]
812
+ pos += 7
813
+ if packed & 0x80: # global color table present
814
+ pos += 3 * (1 << ((packed & 0x07) + 1))
815
+ while pos < n:
816
+ block = data[pos]
817
+ if block == 0x3B: # trailer
818
+ break
819
+ if block == 0x21:
820
+ info = _gif_extension_info(data, pos, n)
821
+ if info is None:
822
+ findings.append("truncated GIF extension")
823
+ break
824
+ end, label, payload = info
825
+ if label == 0xFE:
826
+ findings.append("GIF comment extension present")
827
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
828
+ if hits:
829
+ has_ai = True
830
+ findings.append(f"GIF comment: {', '.join(hits[:6])}")
831
+ elif label == 0xFF:
832
+ if payload.startswith(GIF_XMP_APPLICATION_ID):
833
+ findings.append("GIF XMP application extension present")
834
+ hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
835
+ if hits:
836
+ has_ai = True
837
+ findings.append(f"GIF XMP: {', '.join(hits[:6])}")
838
+ elif any(m in payload[:11] for m in (b"c2pa", b"jumb", b"C2PA", b"JUMB")):
839
+ has_c2pa = True
840
+ findings.append("GIF application extension (possible C2PA)")
841
+ pos = end
842
+ elif block == 0x2C:
843
+ end = _gif_image_end(data, pos, n)
844
+ if end is None:
845
+ findings.append("truncated GIF image block")
846
+ break
847
+ pos = end
848
+ else:
849
+ pos += 1
850
+ whole = _contains_any(data, C2PA_MARKERS)
851
+ if whole and not has_c2pa:
852
+ has_c2pa = True
853
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
854
+ if not findings:
855
+ findings.append("no GIF metadata extensions found")
856
+ return has_c2pa, has_ai or has_c2pa, findings
857
+
858
+
859
+ def strip_gif(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
860
+ """Rebuild the GIF without comment/XMP metadata, keeping control blocks.
861
+
862
+ Comment (0xFE) and XMP application (0xFF "XMP DataXMP") extensions carry
863
+ metadata and are dropped in strip-all mode; unknown application extensions
864
+ are dropped too. NETSCAPE2.0 (loop count) and ICC (color) extensions are
865
+ rendering control, not provenance, so they are preserved unless they
866
+ themselves contain AI/C2PA markers. Graphic-control, plain-text, and image
867
+ blocks are always copied verbatim.
868
+ """
869
+ if data[:6] not in GIF_SIGS:
870
+ raise ValueError("not GIF")
871
+ actions: list[str] = []
872
+ out = bytearray(data[:6])
873
+ n = len(data)
874
+ pos = 6
875
+ if pos + 7 > n:
876
+ raise ValueError("truncated GIF header")
877
+ packed = data[pos + 4]
878
+ out.extend(data[pos : pos + 7])
879
+ pos += 7
880
+ if packed & 0x80: # global color table present
881
+ gct_size = 3 * (1 << ((packed & 0x07) + 1))
882
+ out.extend(data[pos : pos + gct_size])
883
+ pos += gct_size
884
+
885
+ while pos < n:
886
+ block = data[pos]
887
+ if block == 0x3B: # trailer
888
+ out.extend(data[pos:])
889
+ break
890
+ if block == 0x21:
891
+ info = _gif_extension_info(data, pos, n)
892
+ if info is None:
893
+ out.extend(data[pos:])
894
+ break
895
+ end, label, payload = info
896
+ drop = False
897
+ name = "extension"
898
+ if label == 0xFE:
899
+ name = "comment"
900
+ drop = strip_all_metadata or bool(
901
+ _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
902
+ )
903
+ elif label == 0xFF:
904
+ ident = payload[:11]
905
+ marker_hit = bool(_contains_any(payload, AI_META_HINTS + C2PA_MARKERS))
906
+ if ident == GIF_XMP_APPLICATION_ID:
907
+ name = "XMP application"
908
+ drop = strip_all_metadata or marker_hit
909
+ elif ident in _GIF_CONTROL_APPLICATION_IDS:
910
+ name = "control application"
911
+ drop = marker_hit # keep looping/ICC unless it carries markers
912
+ else:
913
+ name = "application"
914
+ drop = strip_all_metadata or marker_hit
915
+ if drop:
916
+ actions.append(f"drop GIF {name} extension")
917
+ else:
918
+ out.extend(data[pos:end])
919
+ pos = end
920
+ elif block == 0x2C:
921
+ end = _gif_image_end(data, pos, n)
922
+ if end is None:
923
+ out.extend(data[pos:])
924
+ break
925
+ out.extend(data[pos:end])
926
+ pos = end
927
+ else:
928
+ out.extend(data[pos : pos + 1])
929
+ pos += 1
930
+
931
+ if not actions:
932
+ actions.append("no GIF metadata blocks removed (already clean or none matched)")
933
+ return bytes(out), actions
934
+
935
+
936
+ # ---------------------------------------------------------------------------
937
+ # TIFF (classic + BigTIFF)
938
+ # ---------------------------------------------------------------------------
939
+
940
+ _TIFF_TYPE_SIZES = {1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8}
941
+
942
+ # Provenance / descriptive metadata tags stripped when cleaning TIFF.
943
+ _TIFF_META_TAG_NAMES = {
944
+ 269: "DocumentName",
945
+ 270: "ImageDescription",
946
+ 271: "Make",
947
+ 272: "Model",
948
+ 305: "Software",
949
+ 306: "DateTime",
950
+ 315: "Artist",
951
+ 316: "HostComputer",
952
+ 33432: "Copyright",
953
+ 40091: "XPTitle",
954
+ 40092: "XPComment",
955
+ 40093: "XPAuthor",
956
+ 40094: "XPKeywords",
957
+ 40095: "XPSubject",
958
+ 700: "XMP",
959
+ 33723: "IPTC/NAA",
960
+ 34377: "Photoshop",
961
+ 34665: "ExifIFD",
962
+ 34853: "GPSInfo",
963
+ 37500: "MakerNote",
964
+ }
965
+
966
+ _TIFF_DROP_TAGS = frozenset(_TIFF_META_TAG_NAMES)
967
+
968
+ # Structural tags that must survive even when their payload looks marker-ish
969
+ # (offset lists, color tables, compression tables, DNG core tags).
970
+ _TIFF_KEEP_TAGS = frozenset(
971
+ {
972
+ 254,
973
+ 255,
974
+ 256,
975
+ 257,
976
+ 258,
977
+ 259,
978
+ 262,
979
+ 263,
980
+ 264,
981
+ 265,
982
+ 266,
983
+ 273,
984
+ 274,
985
+ 277,
986
+ 278,
987
+ 279,
988
+ 282,
989
+ 283,
990
+ 284,
991
+ 285,
992
+ 286,
993
+ 287,
994
+ 288,
995
+ 289,
996
+ 290,
997
+ 291,
998
+ 292,
999
+ 293,
1000
+ 294,
1001
+ 295,
1002
+ 296,
1003
+ 297,
1004
+ 301,
1005
+ 302,
1006
+ 304,
1007
+ 320,
1008
+ 321,
1009
+ 322,
1010
+ 323,
1011
+ 324,
1012
+ 325,
1013
+ 326,
1014
+ 327,
1015
+ 328,
1016
+ 329,
1017
+ 330,
1018
+ 331,
1019
+ 332,
1020
+ 333,
1021
+ 334,
1022
+ 336,
1023
+ 338,
1024
+ 339,
1025
+ 340,
1026
+ 341,
1027
+ 342,
1028
+ 343,
1029
+ 344,
1030
+ 345,
1031
+ 346,
1032
+ 347,
1033
+ 512,
1034
+ 513,
1035
+ 514,
1036
+ 515,
1037
+ 516,
1038
+ 517,
1039
+ 518,
1040
+ 519,
1041
+ 520,
1042
+ 521,
1043
+ 529,
1044
+ 530,
1045
+ 531,
1046
+ 532,
1047
+ 533,
1048
+ 33421,
1049
+ 33422,
1050
+ 33423,
1051
+ 34675,
1052
+ 34676,
1053
+ *range(50706, 50742), # DNG structural tags
1054
+ }
1055
+ )
1056
+
1057
+ MAX_TIFF_IFDS = 4096
1058
+
1059
+
1060
+ def _tiff_layout(data: bytes) -> tuple[str, bool] | None:
1061
+ """Return (byte_order, bigtiff) for a classic or BigTIFF, else None."""
1062
+ if data[:2] == b"II" and data[2:4] == b"*\x00":
1063
+ return "<", False
1064
+ if data[:2] == b"MM" and data[2:4] == b"\x00*":
1065
+ return ">", False
1066
+ if data[:2] == b"II" and data[2:4] == b"+\x00":
1067
+ return "<", True
1068
+ if data[:2] == b"MM" and data[2:4] == b"\x00+":
1069
+ return ">", True
1070
+ return None
1071
+
1072
+
1073
+ def _parse_tiff_ifds(data: bytes) -> tuple[str, bool, dict[int, dict[str, Any]]]:
1074
+ """Parse every reachable TIFF IFD (classic 12-byte or BigTIFF 20-byte entries).
1075
+
1076
+ Returns (byte_order, bigtiff, {offset: {count, entries, next, block_len}}).
1077
+ Each entry is {tag, type, count, value (inline field bytes), byte_size,
1078
+ value_offset}; value_offset is None when the value is inline. Cycle-
1079
+ protected with a hard cap on the number of IFDs.
1080
+ """
1081
+ layout = _tiff_layout(data)
1082
+ if layout is None:
1083
+ raise ValueError("not a TIFF")
1084
+ bo, bigtiff = layout
1085
+ n = len(data)
1086
+ count_len = 8 if bigtiff else 2
1087
+ off_len = 8 if bigtiff else 4
1088
+ entry_size = 20 if bigtiff else 12
1089
+ header_size = 16 if bigtiff else 8
1090
+ count_fmt = bo + ("Q" if bigtiff else "H")
1091
+ off_fmt = bo + ("Q" if bigtiff else "I")
1092
+ if n < header_size:
1093
+ return bo, bigtiff, {}
1094
+ first = struct.unpack(off_fmt, data[header_size - off_len : header_size])[0]
1095
+ if first == 0 or first + count_len > n:
1096
+ return bo, bigtiff, {}
1097
+ ifds: dict[int, dict[str, Any]] = {}
1098
+ seen: set[int] = set()
1099
+ todo = [first]
1100
+ while todo and len(ifds) < MAX_TIFF_IFDS:
1101
+ off = todo.pop()
1102
+ if off in seen or off + count_len > n:
1103
+ continue
1104
+ seen.add(off)
1105
+ count = struct.unpack(count_fmt, data[off : off + count_len])[0]
1106
+ block_len = count_len + count * entry_size
1107
+ entries: list[dict[str, Any]] = []
1108
+ if off + block_len + off_len <= n:
1109
+ next_ptr = struct.unpack(off_fmt, data[off + block_len : off + block_len + off_len])[0]
1110
+ else:
1111
+ block_len = max(0, n - off - off_len)
1112
+ next_ptr = 0
1113
+ for i in range(count):
1114
+ e = off + count_len + i * entry_size
1115
+ if e + entry_size > n:
1116
+ break
1117
+ tag = struct.unpack(bo + "H", data[e : e + 2])[0]
1118
+ ftype = struct.unpack(bo + "H", data[e + 2 : e + 4])[0]
1119
+ fcount = struct.unpack(off_fmt, data[e + 4 : e + 4 + off_len])[0]
1120
+ value = data[e + 4 + off_len : e + entry_size]
1121
+ byte_size = fcount * _TIFF_TYPE_SIZES.get(ftype, 1)
1122
+ value_offset = None
1123
+ if byte_size > len(value):
1124
+ value_offset = struct.unpack(off_fmt, value[:off_len])[0]
1125
+ entries.append(
1126
+ {
1127
+ "tag": tag,
1128
+ "type": ftype,
1129
+ "count": fcount,
1130
+ "value": value,
1131
+ "byte_size": byte_size,
1132
+ "value_offset": value_offset,
1133
+ }
1134
+ )
1135
+ ifds[off] = {
1136
+ "count": count,
1137
+ "entries": entries,
1138
+ "next": next_ptr,
1139
+ "block_len": block_len,
1140
+ }
1141
+ if next_ptr:
1142
+ todo.append(next_ptr)
1143
+ for ent in entries:
1144
+ if ent["tag"] in (34665, 34853, 40965):
1145
+ ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
1146
+ if ptr:
1147
+ todo.append(ptr)
1148
+ return bo, bigtiff, ifds
1149
+
1150
+
1151
+ def _tiff_entry_payload(data: bytes, ent: dict[str, Any]) -> bytes | None:
1152
+ if ent["value_offset"] is None:
1153
+ return ent["value"][: ent["byte_size"]] if ent["byte_size"] <= len(ent["value"]) else None
1154
+ vo = ent["value_offset"]
1155
+ return data[vo : vo + ent["byte_size"]]
1156
+
1157
+
1158
+ def inspect_tiff(data: bytes) -> tuple[bool, bool, list[str]]:
1159
+ """Walk the IFD chains (classic or BigTIFF) and report metadata tags."""
1160
+ findings: list[str] = []
1161
+ has_c2pa = False
1162
+ has_ai = False
1163
+ try:
1164
+ _bo, _big, ifds = _parse_tiff_ifds(data)
1165
+ except ValueError:
1166
+ return False, False, ["not a valid TIFF"]
1167
+ if not ifds:
1168
+ return False, False, ["TIFF with no image file directories"]
1169
+ for _off, ifd in sorted(ifds.items()):
1170
+ for ent in ifd["entries"]:
1171
+ tag = ent["tag"]
1172
+ payload = _tiff_entry_payload(data, ent)
1173
+ hits = _contains_any(payload or b"", AI_META_HINTS + C2PA_MARKERS)
1174
+ if hits:
1175
+ if any(
1176
+ h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
1177
+ ):
1178
+ has_c2pa = True
1179
+ has_ai = True
1180
+ findings.append(f"TIFF tag {tag}: {', '.join(hits[:6])}")
1181
+ name = _TIFF_META_TAG_NAMES.get(tag)
1182
+ if name:
1183
+ label = "sub-IFD" if tag in (34665, 34853, 40965) else "tag"
1184
+ findings.append(f"TIFF {label} {tag} ({name}) present")
1185
+ whole = _contains_any(data, C2PA_MARKERS)
1186
+ if whole and not has_c2pa:
1187
+ has_c2pa = True
1188
+ findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
1189
+ if not findings:
1190
+ findings.append("no TIFF metadata tags found")
1191
+ return has_c2pa, has_ai or has_c2pa, findings
1192
+
1193
+
1194
+ def _collect_tiff_sub_ifd_drops(
1195
+ off_fmt: str,
1196
+ off_len: int,
1197
+ ifds: dict[int, dict[str, Any]],
1198
+ data_len: int,
1199
+ ptr: int,
1200
+ drop_ranges: list[tuple[int, int]],
1201
+ drop_ifd_ranges: list[tuple[int, int]],
1202
+ seen: set[int],
1203
+ ) -> None:
1204
+ """Record the region and value payloads of a dropped sub-IFD chain."""
1205
+ sub = ifds.get(ptr)
1206
+ if sub is None or ptr in seen:
1207
+ return
1208
+ seen.add(ptr)
1209
+ drop_ifd_ranges.append((ptr, min(ptr + sub["block_len"] + off_len, data_len)))
1210
+ for ent in sub["entries"]:
1211
+ if ent["tag"] in (34665, 34853):
1212
+ p2 = struct.unpack(off_fmt, ent["value"][:off_len])[0]
1213
+ if p2:
1214
+ _collect_tiff_sub_ifd_drops(
1215
+ off_fmt, off_len, ifds, data_len, p2, drop_ranges, drop_ifd_ranges, seen
1216
+ )
1217
+ elif ent["value_offset"] is not None:
1218
+ vo, vs = ent["value_offset"], ent["byte_size"]
1219
+ if vo + vs <= data_len:
1220
+ drop_ranges.append((vo, vo + vs))
1221
+
1222
+
1223
+ def strip_tiff(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
1224
+ """Drop TIFF metadata tags (classic or BigTIFF) without moving referenced data.
1225
+
1226
+ Each IFD entry region is patched in place — kept entries copied verbatim,
1227
+ dropped entries removed, the block zero-padded to its original length — so
1228
+ every offset (strip/tile offsets, next-IFD pointers, color tables) stays
1229
+ valid. Payloads of dropped tags and orphaned sub-IFD chains are zeroed
1230
+ unless a kept entry still references them.
1231
+ """
1232
+ bo, bigtiff, ifds = _parse_tiff_ifds(data)
1233
+ if not ifds:
1234
+ raise ValueError("not a valid TIFF (no image file directories)")
1235
+ n = len(data)
1236
+ off_len = 8 if bigtiff else 4
1237
+ off_fmt = bo + ("Q" if bigtiff else "I")
1238
+ count_fmt = bo + ("Q" if bigtiff else "H")
1239
+ actions: list[str] = []
1240
+ kept: dict[int, list[dict[str, Any]]] = {}
1241
+ drop_ranges: list[tuple[int, int]] = []
1242
+ drop_ifd_ranges: list[tuple[int, int]] = []
1243
+
1244
+ for off, ifd in sorted(ifds.items()):
1245
+ keep_here: list[dict[str, Any]] = []
1246
+ for ent in ifd["entries"]:
1247
+ tag = ent["tag"]
1248
+ payload = _tiff_entry_payload(data, ent)
1249
+ marker_hit = bool(_contains_any(payload or b"", AI_META_HINTS + C2PA_MARKERS))
1250
+ if tag in (34665, 34853):
1251
+ ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
1252
+ sub = ifds.get(ptr)
1253
+ if sub is not None:
1254
+ sub_blob = data[ptr : min(ptr + sub["block_len"] + off_len, n)]
1255
+ marker_hit = marker_hit or bool(
1256
+ _contains_any(sub_blob, AI_META_HINTS + C2PA_MARKERS)
1257
+ )
1258
+ for sent in sub["entries"]:
1259
+ s_payload = _tiff_entry_payload(data, sent)
1260
+ if _contains_any(s_payload or b"", AI_META_HINTS + C2PA_MARKERS):
1261
+ marker_hit = True
1262
+ break
1263
+ drop = False
1264
+ if tag in _TIFF_DROP_TAGS:
1265
+ drop = strip_all_metadata or marker_hit
1266
+ elif marker_hit and tag not in _TIFF_KEEP_TAGS:
1267
+ drop = True
1268
+ if not drop:
1269
+ keep_here.append(ent)
1270
+ continue
1271
+ name = _TIFF_META_TAG_NAMES.get(tag)
1272
+ actions.append(
1273
+ f"drop TIFF tag {tag} ({name})" if name else f"drop TIFF tag {tag} (AI markers)"
1274
+ )
1275
+ if tag in (34665, 34853):
1276
+ ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
1277
+ _collect_tiff_sub_ifd_drops(
1278
+ off_fmt, off_len, ifds, n, ptr, drop_ranges, drop_ifd_ranges, set()
1279
+ )
1280
+ elif ent["value_offset"] is not None and ent["value_offset"] + ent["byte_size"] <= n:
1281
+ drop_ranges.append((ent["value_offset"], ent["value_offset"] + ent["byte_size"]))
1282
+ kept[off] = keep_here
1283
+
1284
+ # Ranges still referenced from the root through kept entries must never be
1285
+ # zeroed. An orphaned sub-IFD (its pointer dropped) contributes nothing.
1286
+ reachable: set[int] = set()
1287
+
1288
+ def _mark_reachable(off: int) -> None:
1289
+ if off in reachable or off not in ifds:
1290
+ return
1291
+ reachable.add(off)
1292
+ for ent in kept.get(off, []):
1293
+ if ent["tag"] in (34665, 34853, 40965):
1294
+ _mark_reachable(struct.unpack(off_fmt, ent["value"][:off_len])[0])
1295
+ if ifds[off]["next"]:
1296
+ _mark_reachable(ifds[off]["next"])
1297
+
1298
+ root = (
1299
+ struct.unpack(off_fmt, data[16 - off_len : 16])[0]
1300
+ if bigtiff
1301
+ else struct.unpack(off_fmt, data[8 - off_len : 8])[0]
1302
+ )
1303
+ _mark_reachable(root)
1304
+
1305
+ referenced: list[tuple[int, int]] = []
1306
+ for off in sorted(reachable):
1307
+ for ent in kept.get(off, []):
1308
+ if ent["value_offset"] is not None and ent["value_offset"] + ent["byte_size"] <= n:
1309
+ referenced.append((ent["value_offset"], ent["value_offset"] + ent["byte_size"]))
1310
+ if ent["tag"] in (34665, 34853, 40965):
1311
+ ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
1312
+ sub = ifds.get(ptr)
1313
+ if sub is not None:
1314
+ referenced.append((ptr, min(ptr + sub["block_len"] + off_len, n)))
1315
+
1316
+ def _covered(ranges: list[tuple[int, int]], start: int, end: int) -> bool:
1317
+ return any(s <= start and end <= e for s, e in ranges)
1318
+
1319
+ zero: list[tuple[int, int]] = []
1320
+ for s, e in drop_ranges:
1321
+ if not _covered(referenced, s, e):
1322
+ zero.append((s, e))
1323
+ for s, e in drop_ifd_ranges:
1324
+ if not _covered(referenced, s, e):
1325
+ zero.append((s, e))
1326
+
1327
+ # Patch IFD entry regions in place, then zero dropped payloads.
1328
+ out = bytearray(data)
1329
+ for off, ifd in sorted(ifds.items()):
1330
+ entries = kept.get(off, [])
1331
+ block = bytearray()
1332
+ block.extend(struct.pack(count_fmt, len(entries)))
1333
+ for ent in entries:
1334
+ block.extend(struct.pack(bo + "H", ent["tag"]))
1335
+ block.extend(struct.pack(bo + "H", ent["type"]))
1336
+ block.extend(struct.pack(off_fmt, ent["count"]))
1337
+ block.extend(ent["value"])
1338
+ block.extend(struct.pack(off_fmt, ifd["next"]))
1339
+ region_len = ifd["block_len"] + off_len
1340
+ if off + region_len <= n:
1341
+ if len(block) > region_len:
1342
+ block = block[:region_len]
1343
+ out[off : off + region_len] = block + b"\x00" * (region_len - len(block))
1344
+ for s, e in zero:
1345
+ out[s:e] = b"\x00" * (e - s)
1346
+
1347
+ if not actions:
1348
+ actions.append("no TIFF metadata tags removed (already clean or none matched)")
1349
+ return bytes(out), actions
1350
+
1351
+
1352
+ def run_optional_tools(path: Path) -> dict[str, Any]:
1353
+ tools: dict[str, Any] = {}
1354
+ c2patool = which("c2patool")
1355
+ if c2patool:
1356
+ try:
1357
+ r = subprocess.run(
1358
+ [c2patool, safe_arg(str(path))],
1359
+ capture_output=True,
1360
+ text=True,
1361
+ timeout=30,
1362
+ preexec_fn=subprocess_preexec_fn,
1363
+ check=False,
1364
+ creationflags=subprocess_creationflags,
1365
+ )
1366
+ out = (r.stdout or "") + (r.stderr or "")
1367
+ low = out.lower()
1368
+ # Negative markers must veto every positive branch, so the
1369
+ # positive alternatives are parenthesised: c2patool reports a
1370
+ # missing manifest as "Error: No claim found", which contains
1371
+ # the substring "claim" and would otherwise read as a hit.
1372
+ no_manifest = "no claim" in low or "no jumbf" in low
1373
+ has_manifest = (
1374
+ "claim" in low or "c2pa" in low or "manifest" in low
1375
+ ) and not no_manifest
1376
+ # c2patool exits non-zero for a missing manifest too, so the exit
1377
+ # code alone cannot separate "asset is clean" from "the probe
1378
+ # never ran". Treat the run as conclusive only when it either
1379
+ # found a manifest or said in so many words that there is none.
1380
+ # Anything else -- a crash before main(), a kill, an unrecognized
1381
+ # error -- leaves the C2PA question unanswered, and callers must
1382
+ # not read that as a negative.
1383
+ conclusive = has_manifest or no_manifest
1384
+ entry: dict[str, Any] = {
1385
+ "available": True,
1386
+ "returncode": r.returncode,
1387
+ "snippet": out[:2000],
1388
+ "has_manifest": has_manifest,
1389
+ "ok": conclusive,
1390
+ }
1391
+ if not conclusive:
1392
+ entry["error"] = f"exit {r.returncode}, unrecognized output"
1393
+ tools["c2patool"] = entry
1394
+ except Exception as e:
1395
+ tools["c2patool"] = {
1396
+ "available": True,
1397
+ "ok": False,
1398
+ "has_manifest": False,
1399
+ "error": str(e),
1400
+ }
1401
+ else:
1402
+ tools["c2patool"] = {"available": False}
1403
+
1404
+ exiftool = which("exiftool")
1405
+ if exiftool:
1406
+ try:
1407
+ r = subprocess.run(
1408
+ [exiftool, "-G1", "-a", "-s", safe_arg(str(path))],
1409
+ capture_output=True,
1410
+ text=True,
1411
+ timeout=30,
1412
+ preexec_fn=subprocess_preexec_fn,
1413
+ check=False,
1414
+ creationflags=subprocess_creationflags,
1415
+ )
1416
+ out = r.stdout or ""
1417
+ interesting = [
1418
+ line
1419
+ for line in out.splitlines()
1420
+ if re.search(
1421
+ r"c2pa|content.?credential|AIGC|digitalSource|XMP|EXIF|IPTC|jumb",
1422
+ line,
1423
+ re.I,
1424
+ )
1425
+ ]
1426
+ tools["exiftool"] = {
1427
+ "available": True,
1428
+ "interesting_lines": interesting[:50],
1429
+ }
1430
+ except Exception as e:
1431
+ tools["exiftool"] = {"available": True, "error": str(e)}
1432
+ else:
1433
+ tools["exiftool"] = {"available": False}
1434
+ return tools
1435
+
1436
+
1437
+ def _synthid_score_http(
1438
+ path: Path, base_url: str, api_key: str, timeout: float
1439
+ ) -> dict[str, Any] | None:
1440
+ """Score *path* via the HTTP sidecar (synthid_score_server.py)."""
1441
+ try:
1442
+ data = path.read_bytes()
1443
+ except OSError as e:
1444
+ return {"available": False, "error": f"cannot read {path}: {e}"}
1445
+ body = json.dumps({"file": base64.b64encode(data).decode("ascii")}).encode("utf-8")
1446
+ headers = {"Content-Type": "application/json"}
1447
+ if api_key:
1448
+ headers["Authorization"] = f"Bearer {api_key}"
1449
+ if urlparse(base_url).scheme not in ("http", "https"):
1450
+ return {"available": False, "error": f"refusing non-http(s) scorer endpoint: {base_url}"}
1451
+ # S310: URL scheme is restricted to http/https just above.
1452
+ req = urllib.request.Request( # noqa: S310
1453
+ base_url.rstrip("/") + "/score",
1454
+ data=body,
1455
+ headers=headers,
1456
+ method="POST",
1457
+ )
1458
+ try:
1459
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
1460
+ payload = json.loads(resp.read().decode("utf-8"))
1461
+ except (
1462
+ urllib.error.HTTPError,
1463
+ urllib.error.URLError,
1464
+ TimeoutError,
1465
+ OSError,
1466
+ json.JSONDecodeError,
1467
+ ) as e:
1468
+ return {"available": False, "error": f"SynthID scorer sidecar unreachable: {e}"}
1469
+ if not isinstance(payload, dict):
1470
+ return {"available": False, "error": "bad scorer sidecar response"}
1471
+ return payload
1472
+
1473
+
1474
+ def _synthid_python(upstream: Path) -> str:
1475
+ """Prefer the checkout venv so the scorer deps (cv2, sklearn) are importable."""
1476
+ if os.name == "nt":
1477
+ venv = upstream / ".venv" / "Scripts" / "python.exe"
1478
+ else:
1479
+ venv = upstream / ".venv" / "bin" / "python"
1480
+ if venv.is_file():
1481
+ return str(venv)
1482
+ return sys.executable
1483
+
1484
+
1485
+ def run_synthid_score(
1486
+ path: Path,
1487
+ upstream_dir: str | None = None,
1488
+ ) -> dict[str, Any] | None:
1489
+ """Run the optional reverse-SynthID scorer.
1490
+
1491
+ Uses the HTTP sidecar when WATERMARKS_SYNTHID_SCORER_URL is set,
1492
+ otherwise a subprocess against a local checkout. Returns None when the
1493
+ scorer is not configured; a dict with "available": False and an "error"
1494
+ when it is configured but unavailable (e.g. exit 3), so callers can
1495
+ distinguish "not scored" from "scored and clean".
1496
+ """
1497
+ scorer_url = os.environ.get("WATERMARKS_SYNTHID_SCORER_URL", "").strip()
1498
+ if scorer_url:
1499
+ api_key = os.environ.get("WATERMARKS_SYNTHID_SCORER_API_KEY", "").strip()
1500
+ try:
1501
+ timeout = float(os.environ.get("WATERMARKS_SYNTHID_SCORER_TIMEOUT", "60"))
1502
+ except ValueError:
1503
+ timeout = DEFAULT_SYNTHID_SCORER_TIMEOUT
1504
+ return _synthid_score_http(path, scorer_url, api_key, timeout)
1505
+ if upstream_dir is None:
1506
+ upstream_dir = os.environ.get("REVERSE_SYNTHID_DIR")
1507
+ if not upstream_dir:
1508
+ return None
1509
+
1510
+ script = SCRIPTS_DIR / "score_synthid.py"
1511
+ cmd = [
1512
+ _synthid_python(Path(upstream_dir)),
1513
+ str(script),
1514
+ str(path),
1515
+ "--upstream-dir",
1516
+ str(upstream_dir),
1517
+ "--json",
1518
+ ]
1519
+ try:
1520
+ r = subprocess.run(
1521
+ cmd,
1522
+ capture_output=True,
1523
+ text=True,
1524
+ timeout=180,
1525
+ preexec_fn=subprocess_preexec_fn,
1526
+ check=False,
1527
+ creationflags=subprocess_creationflags,
1528
+ )
1529
+ except Exception as e:
1530
+ return {"available": False, "error": str(e)}
1531
+
1532
+ if r.returncode == 3:
1533
+ return {
1534
+ "available": False,
1535
+ "error": (r.stderr or "SynthID scorer unavailable (exit 3)").strip()[:2000],
1536
+ }
1537
+ if r.returncode != 0:
1538
+ return {"available": False, "error": (r.stderr or "").strip()[:2000]}
1539
+ try:
1540
+ return json.loads(r.stdout or "{}")
1541
+ except json.JSONDecodeError as e:
1542
+ return {"available": False, "error": f"bad scorer JSON: {e}"}
1543
+
1544
+
1545
+ def _ctrlregen_python(upstream: Path) -> str:
1546
+ """Prefer the checkout venv so torch/diffusers are importable."""
1547
+ if os.name == "nt":
1548
+ venv = upstream / ".venv" / "Scripts" / "python.exe"
1549
+ else:
1550
+ venv = upstream / ".venv" / "bin" / "python"
1551
+ if venv.is_file():
1552
+ return str(venv)
1553
+ return sys.executable
1554
+
1555
+
1556
+ def _markdiffusion_python(upstream: Path | None) -> str:
1557
+ """Prefer the bootstrap venv so torch/diffusers/markdiffusion importable."""
1558
+ if upstream is not None:
1559
+ if os.name == "nt":
1560
+ venv = upstream / ".venv" / "Scripts" / "python.exe"
1561
+ else:
1562
+ venv = upstream / ".venv" / "bin" / "python"
1563
+ if venv.is_file():
1564
+ return str(venv)
1565
+ return sys.executable
1566
+
1567
+
1568
+ def run_markdiffusion_purify(
1569
+ path: Path,
1570
+ output: Path,
1571
+ *,
1572
+ upstream_dir: str | None = None,
1573
+ strength: float = 0.3,
1574
+ model: str | None = None,
1575
+ size: int = 512,
1576
+ steps: int = 50,
1577
+ device: str | None = None,
1578
+ timeout: int = 3600,
1579
+ ) -> dict[str, Any]:
1580
+ """Run the optional MarkDiffusion DiffusionPurification remover in a subprocess.
1581
+
1582
+ Returns ``{"available": False, "error": ...}`` when the backend is not
1583
+ configured, its dependencies are missing, or it fails at runtime; a
1584
+ successful run is ``{"available": True, ...}``.
1585
+ """
1586
+ if upstream_dir is None:
1587
+ upstream_dir = os.environ.get("MARKDIFFUSION_DIR")
1588
+
1589
+ upstream = Path(upstream_dir).expanduser().resolve() if upstream_dir else None
1590
+ if upstream is not None and not upstream.is_dir():
1591
+ return {
1592
+ "available": False,
1593
+ "error": f"MarkDiffusion dir not found: {upstream}",
1594
+ }
1595
+
1596
+ script = SCRIPTS_DIR / "markdiffusion_harness.py"
1597
+ cmd = [
1598
+ _markdiffusion_python(upstream),
1599
+ str(script),
1600
+ "purify",
1601
+ str(path),
1602
+ "-o",
1603
+ str(output),
1604
+ "--purification-strength",
1605
+ str(strength),
1606
+ "--size",
1607
+ str(size),
1608
+ "--steps",
1609
+ str(steps),
1610
+ "--json",
1611
+ ]
1612
+ if upstream is not None:
1613
+ cmd += ["--upstream-dir", str(upstream)]
1614
+ if model:
1615
+ cmd += ["--model", str(model)]
1616
+ if device:
1617
+ cmd += ["--device", str(device)]
1618
+
1619
+ try:
1620
+ r = subprocess.run(
1621
+ cmd,
1622
+ capture_output=True,
1623
+ text=True,
1624
+ timeout=timeout,
1625
+ preexec_fn=ctrlregen_subprocess_preexec_fn,
1626
+ check=False,
1627
+ creationflags=subprocess_creationflags,
1628
+ )
1629
+ except subprocess.TimeoutExpired:
1630
+ return {
1631
+ "available": False,
1632
+ "error": f"DiffusionPurification timed out after {timeout}s",
1633
+ }
1634
+ except Exception as e:
1635
+ return {"available": False, "error": str(e)}
1636
+
1637
+ if r.returncode != 0:
1638
+ return {"available": False, "error": (r.stderr or "").strip()[:2000]}
1639
+ try:
1640
+ payload = json.loads(r.stdout or "{}")
1641
+ except json.JSONDecodeError as e:
1642
+ return {
1643
+ "available": False,
1644
+ "error": f"bad MarkDiffusion adapter JSON: {e}",
1645
+ }
1646
+ payload["available"] = True
1647
+ return payload
1648
+
1649
+
1650
+ def run_ctrlregen_clean(
1651
+ path: Path,
1652
+ output: Path,
1653
+ *,
1654
+ upstream_dir: str | None = None,
1655
+ strength: float = 0.25,
1656
+ steps: int = 50,
1657
+ device: str | None = None,
1658
+ seed: int | None = None,
1659
+ timeout: int = 3600,
1660
+ ) -> dict[str, Any]:
1661
+ """Run the optional CtrlRegen remover in a subprocess.
1662
+
1663
+ Returns ``{"available": False, "error": ...}`` when the remover is not
1664
+ configured, its dependencies are missing, or it fails at runtime; a
1665
+ successful run is ``{"available": True, ...}``.
1666
+ """
1667
+ if upstream_dir is None:
1668
+ upstream_dir = os.environ.get("NOAI_WATERMARK_DIR")
1669
+ if not upstream_dir:
1670
+ return {
1671
+ "available": False,
1672
+ "error": "CtrlRegen not configured (set NOAI_WATERMARK_DIR or pass --ctrlregen-dir)",
1673
+ }
1674
+
1675
+ upstream = Path(upstream_dir).expanduser().resolve()
1676
+ if not upstream.is_dir():
1677
+ return {"available": False, "error": f"CtrlRegen dir not found: {upstream}"}
1678
+
1679
+ script = SCRIPTS_DIR / "clean_ctrlregen.py"
1680
+ cmd = [
1681
+ _ctrlregen_python(upstream),
1682
+ str(script),
1683
+ str(path),
1684
+ "-o",
1685
+ str(output),
1686
+ "--upstream-dir",
1687
+ str(upstream),
1688
+ "--strength",
1689
+ str(strength),
1690
+ "--steps",
1691
+ str(steps),
1692
+ "--json",
1693
+ ]
1694
+ if device:
1695
+ cmd += ["--device", str(device)]
1696
+ if seed is not None:
1697
+ cmd += ["--seed", str(seed)]
1698
+
1699
+ try:
1700
+ r = subprocess.run(
1701
+ cmd,
1702
+ capture_output=True,
1703
+ text=True,
1704
+ timeout=timeout,
1705
+ preexec_fn=ctrlregen_subprocess_preexec_fn,
1706
+ check=False,
1707
+ creationflags=subprocess_creationflags,
1708
+ )
1709
+ except subprocess.TimeoutExpired:
1710
+ return {"available": False, "error": f"CtrlRegen timed out after {timeout}s"}
1711
+ except Exception as e:
1712
+ return {"available": False, "error": str(e)}
1713
+
1714
+ if r.returncode != 0:
1715
+ return {"available": False, "error": (r.stderr or "").strip()[:2000]}
1716
+ try:
1717
+ payload = json.loads(r.stdout or "{}")
1718
+ except json.JSONDecodeError as e:
1719
+ return {"available": False, "error": f"bad CtrlRegen adapter JSON: {e}"}
1720
+ payload["available"] = True
1721
+ return payload
1722
+
1723
+
1724
+ def inspect_image(
1725
+ path: Path,
1726
+ synthid_dir: str | None = None,
1727
+ ) -> ImageInspectReport:
1728
+ data = path.read_bytes()
1729
+ fmt = detect_format(data)
1730
+ if fmt == "png":
1731
+ has_c2pa, has_ai, findings = inspect_png(data)
1732
+ elif fmt == "jpeg":
1733
+ has_c2pa, has_ai, findings = inspect_jpeg(data)
1734
+ elif fmt == "webp":
1735
+ has_c2pa, has_ai, findings = inspect_webp(data)
1736
+ elif fmt in ("avif", "heic"):
1737
+ has_c2pa, has_ai, findings = inspect_isobmff(data, fmt)
1738
+ elif fmt == "bmp":
1739
+ has_c2pa, has_ai, findings = inspect_bmp(data)
1740
+ elif fmt == "gif":
1741
+ has_c2pa, has_ai, findings = inspect_gif(data)
1742
+ elif fmt == "tiff":
1743
+ has_c2pa, has_ai, findings = inspect_tiff(data)
1744
+ else:
1745
+ has_c2pa, has_ai, findings = (
1746
+ False,
1747
+ False,
1748
+ ["unsupported format (PNG/JPEG/WebP/AVIF/HEIC/BMP/GIF/TIFF)"],
1749
+ )
1750
+
1751
+ notes: list[str] = []
1752
+ if fmt == "unknown":
1753
+ notes.append(
1754
+ "format not fully inspected; only PNG/JPEG/WebP/AVIF/HEIC/BMP/GIF/TIFF are supported"
1755
+ )
1756
+
1757
+ tools = run_optional_tools(path)
1758
+ # Elevate flags from tools
1759
+ ct = tools.get("c2patool") or {}
1760
+ if ct.get("has_manifest"):
1761
+ has_c2pa = True
1762
+ findings.append("c2patool reports a C2PA-related manifest")
1763
+ probe_note = c2patool_probe_note(tools)
1764
+ if probe_note:
1765
+ notes.append(probe_note)
1766
+
1767
+ return ImageInspectReport(
1768
+ path=str(path),
1769
+ format=fmt,
1770
+ has_c2pa=has_c2pa,
1771
+ has_ai_metadata=has_ai,
1772
+ findings=findings,
1773
+ tools=tools,
1774
+ synthid=run_synthid_score(path, synthid_dir),
1775
+ notes=notes,
1776
+ )
1777
+
1778
+
1779
+ def _png_chunk(ctype: bytes, payload: bytes) -> bytes:
1780
+ crc = zlib.crc32(ctype)
1781
+ crc = zlib.crc32(payload, crc) & 0xFFFFFFFF
1782
+ return struct.pack(">I", len(payload)) + ctype + payload + struct.pack(">I", crc)
1783
+
1784
+
1785
+ def strip_png(data: bytes, *, strip_all_text: bool = True) -> tuple[bytes, list[str]]:
1786
+ if not data.startswith(PNG_SIG):
1787
+ raise ValueError("not PNG")
1788
+ actions: list[str] = []
1789
+ out = bytearray(PNG_SIG)
1790
+ pos = 8
1791
+ while pos + 8 <= len(data):
1792
+ length = struct.unpack(">I", data[pos : pos + 4])[0]
1793
+ ctype = data[pos + 4 : pos + 8]
1794
+ chunk_start = pos + 8
1795
+ chunk_end = chunk_start + length
1796
+ if chunk_end + 4 > len(data):
1797
+ # A truncated chunk (interrupted download): copy the remainder
1798
+ # verbatim instead of dropping it — dropping turned a recoverable
1799
+ # image into an unopenable husk while reporting "already clean"
1800
+ # (#170). Note it as an action so the run is never mistaken for
1801
+ # an ordinary no-op clean.
1802
+ out.extend(data[pos:])
1803
+ actions.append(
1804
+ f"kept {len(data) - pos} bytes of truncated chunk {ctype.decode('latin-1', errors='replace')} tail (file truncated)"
1805
+ )
1806
+ break
1807
+ payload = data[chunk_start:chunk_end]
1808
+ crc_bytes = data[chunk_end : chunk_end + 4]
1809
+ pos = chunk_end + 4
1810
+ name = ctype.decode("latin-1", errors="replace")
1811
+
1812
+ drop = False
1813
+ if ctype in (b"eXIf", b"caBX") or ctype.startswith(b"c2"):
1814
+ drop = True
1815
+ actions.append(f"drop chunk {name}")
1816
+ elif ctype in (b"tEXt", b"zTXt", b"iTXt"):
1817
+ if strip_all_text or _text_chunk_is_ai(payload, ctype):
1818
+ drop = True
1819
+ actions.append(f"drop chunk {name}")
1820
+ elif _contains_any(ctype + payload, C2PA_MARKERS) and ctype not in (
1821
+ b"IHDR",
1822
+ b"IDAT",
1823
+ b"IEND",
1824
+ b"PLTE",
1825
+ b"tRNS",
1826
+ b"gAMA",
1827
+ b"pHYs",
1828
+ b"sRGB",
1829
+ b"cHRM",
1830
+ b"iCCP",
1831
+ ):
1832
+ drop = True
1833
+ actions.append(f"drop chunk {name} (C2PA marker in payload)")
1834
+
1835
+ if not drop:
1836
+ out.extend(struct.pack(">I", length) + ctype + payload + crc_bytes)
1837
+ if ctype == b"IEND":
1838
+ break
1839
+ if not actions:
1840
+ actions.append("no PNG metadata chunks removed (already clean or none matched)")
1841
+ return bytes(out), actions
1842
+
1843
+
1844
+ def strip_jpeg(data: bytes, *, strip_all_app: bool = True) -> tuple[bytes, list[str]]:
1845
+ if not data.startswith(JPEG_SOI):
1846
+ raise ValueError("not JPEG")
1847
+ actions: list[str] = []
1848
+ out = bytearray(JPEG_SOI)
1849
+ i = 2
1850
+ n = len(data)
1851
+ while i < n:
1852
+ if data[i] != 0xFF:
1853
+ # unexpected; copy rest
1854
+ out.extend(data[i:])
1855
+ actions.append("copied remainder after non-marker byte")
1856
+ break
1857
+ while i < n and data[i] == 0xFF:
1858
+ i += 1
1859
+ if i >= n:
1860
+ break
1861
+ marker = data[i]
1862
+ i += 1
1863
+
1864
+ if marker == 0xD9: # EOI
1865
+ out.extend(b"\xff\xd9")
1866
+ break
1867
+ if marker == 0xD8: # nested SOI?
1868
+ continue
1869
+ if 0xD0 <= marker <= 0xD7:
1870
+ out.extend(bytes([0xFF, marker]))
1871
+ continue
1872
+ if marker == 0xDA: # SOS — copy through end
1873
+ # need length of SOS header then entropy-coded data to EOI
1874
+ if i + 2 > n:
1875
+ break
1876
+ seglen = struct.unpack(">H", data[i : i + 2])[0]
1877
+ # Find EOI from here carefully: after SOS segment header, scan for FF D9
1878
+ # not preceded by stuffed FF 00 issues — simple approach: copy from FF DA to end
1879
+ # Reconstruct: FF DA + rest of file
1880
+ out.extend(b"\xff\xda")
1881
+ out.extend(data[i:])
1882
+ actions.append("preserved entropy-coded scan (SOS→EOF)")
1883
+ break
1884
+
1885
+ if i + 2 > n:
1886
+ break
1887
+ seglen = struct.unpack(">H", data[i : i + 2])[0]
1888
+ if seglen < 2 or i + seglen > n:
1889
+ out.extend(data[i - 2 :]) # best effort
1890
+ actions.append("truncated segment; copied remainder")
1891
+ break
1892
+ payload = data[i + 2 : i + seglen]
1893
+ next_i = i + seglen
1894
+
1895
+ # APP0 is JFIF — keep for compatibility unless full strip of all APP
1896
+ keep = False
1897
+ drop = False
1898
+ if 0xE0 <= marker <= 0xEF: # APPn
1899
+ if marker == 0xEB: # APP11 JUMBF/C2PA
1900
+ drop = True
1901
+ actions.append("drop APP11 (C2PA/JUMBF)")
1902
+ elif strip_all_app and marker != 0xE0:
1903
+ # keep APP0 (JFIF) by default
1904
+ drop = True
1905
+ actions.append(f"drop APP{marker - 0xE0}")
1906
+ elif _contains_any(payload, AI_META_HINTS + C2PA_MARKERS):
1907
+ drop = True
1908
+ actions.append(f"drop APP{marker - 0xE0} (AI/C2PA markers)")
1909
+ else:
1910
+ keep = True
1911
+ elif marker == 0xFE: # COM
1912
+ hits = _contains_any(payload, JPEG_COM_AI_HINTS)
1913
+ if strip_all_app or hits:
1914
+ drop = True
1915
+ actions.append("drop COM comment")
1916
+ else:
1917
+ keep = True
1918
+ else:
1919
+ keep = True
1920
+
1921
+ if keep and not drop:
1922
+ out.extend(bytes([0xFF, marker]))
1923
+ out.extend(data[i : i + seglen])
1924
+ i = next_i
1925
+
1926
+ if not actions:
1927
+ actions.append("no JPEG APP segments removed")
1928
+ return bytes(out), actions
1929
+
1930
+
1931
+ def strip_webp(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
1932
+ chunks, notes = _webp_chunks(data)
1933
+ if not chunks and notes == ["not a WebP"]:
1934
+ raise ValueError("not WebP")
1935
+ if notes:
1936
+ raise ValueError("malformed WebP: " + "; ".join(notes))
1937
+
1938
+ actions: list[str] = []
1939
+ kept: list[tuple[bytes, bytes, bytes]] = []
1940
+ removed_flags = 0
1941
+ metadata_flags = {b"ICCP": 0x20, b"EXIF": 0x08, b"XMP ": 0x04}
1942
+
1943
+ for fourcc, payload, padding in chunks:
1944
+ drop = fourcc.upper() == b"C2PA"
1945
+ if fourcc in metadata_flags:
1946
+ drop = strip_all_metadata or bool(_contains_any(payload, AI_META_HINTS + C2PA_MARKERS))
1947
+ if drop:
1948
+ name = fourcc.decode("latin-1", errors="replace")
1949
+ actions.append(f"drop WebP chunk {name}")
1950
+ removed_flags |= metadata_flags.get(fourcc, 0)
1951
+ else:
1952
+ kept.append((fourcc, payload, padding))
1953
+
1954
+ body = bytearray(WEBP_SIG)
1955
+ for fourcc, payload, padding in kept:
1956
+ chunk = payload
1957
+ if fourcc == b"VP8X" and len(chunk) >= 1 and removed_flags:
1958
+ chunk = bytes([chunk[0] & ~removed_flags]) + chunk[1:]
1959
+ body.extend(fourcc)
1960
+ body.extend(struct.pack("<I", len(chunk)))
1961
+ body.extend(chunk)
1962
+ body.extend(padding if len(chunk) & 1 else b"")
1963
+
1964
+ if not actions:
1965
+ actions.append("no WebP metadata chunks removed (already clean or none matched)")
1966
+ return WEBP_RIFF + struct.pack("<I", len(body)) + bytes(body), actions
1967
+
1968
+
1969
+ def strip_isobmff(
1970
+ data: bytes, fmt: str = "avif", *, strip_all_metadata: bool = True
1971
+ ) -> tuple[bytes, list[str]]:
1972
+ boxes, scanned_end = _parse_isobmff_boxes(data)
1973
+ if not boxes:
1974
+ raise ValueError(f"not a valid {fmt.upper()} (no ISOBMFF boxes)")
1975
+
1976
+ # scanned_end is where the box walk stopped: the parser halts at the first
1977
+ # box whose declared size overruns the data (a truncated download). The
1978
+ # rebuild below used to emit only the boxes that parsed — dropping a
1979
+ # truncated mdat, the actual coded image, while reporting "already clean"
1980
+ # (#170); the tail is appended verbatim afterwards instead.
1981
+ actions: list[str] = []
1982
+ out = bytearray()
1983
+
1984
+ for fourcc, payload, size, header_size in boxes:
1985
+ name = fourcc.decode("latin-1", errors="replace")
1986
+ if fourcc in (b"jumb", b"c2pa") or name.lower().startswith("c2"):
1987
+ actions.append(f"drop top-level {name} box (C2PA/JUMBF)")
1988
+ out.extend(_isobmff_free_box(size, header_size))
1989
+ continue
1990
+
1991
+ if fourcc == b"uuid":
1992
+ if payload.startswith(XMP_UUID):
1993
+ actions.append(f"drop top-level {name} box (XMP metadata)")
1994
+ out.extend(_isobmff_free_box(size, header_size))
1995
+ continue
1996
+ if strip_all_metadata or _contains_any(payload, AI_META_HINTS + C2PA_MARKERS):
1997
+ actions.append(f"drop top-level {name} box (UUID metadata)")
1998
+ out.extend(_isobmff_free_box(size, header_size))
1999
+ continue
2000
+
2001
+ if fourcc == b"meta":
2002
+ meta_verflags = payload[:4] if len(payload) >= 4 else b"\x00\x00\x00\x00"
2003
+ sub_boxes, _ = _parse_isobmff_boxes(payload, start=4)
2004
+ clean_sub = bytearray()
2005
+ for s_fourcc, s_payload, s_size, s_hdr in sub_boxes:
2006
+ s_name = s_fourcc.decode("latin-1", errors="replace")
2007
+ if s_fourcc in (b"jumb", b"c2pa") or s_name.lower().startswith("c2"):
2008
+ actions.append(f"drop meta sub-box {s_name} (C2PA/JUMBF)")
2009
+ clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
2010
+ continue
2011
+ if s_fourcc == b"uuid":
2012
+ if s_payload.startswith(XMP_UUID):
2013
+ actions.append(f"drop meta sub-box {s_name} (XMP metadata)")
2014
+ clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
2015
+ continue
2016
+ if strip_all_metadata or _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS):
2017
+ actions.append(f"drop meta sub-box {s_name} (UUID metadata)")
2018
+ clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
2019
+ continue
2020
+ if s_fourcc in (b"xml ", b"bxml") and (
2021
+ strip_all_metadata or _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
2022
+ ):
2023
+ actions.append(f"drop meta sub-box {s_name} (XML metadata)")
2024
+ clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
2025
+ continue
2026
+ clean_sub.extend(_build_isobmff_box(s_fourcc, s_payload, s_hdr))
2027
+
2028
+ new_meta_payload = meta_verflags + clean_sub
2029
+ out.extend(_build_isobmff_box(b"meta", new_meta_payload, header_size))
2030
+ continue
2031
+
2032
+ out.extend(_build_isobmff_box(fourcc, payload, header_size))
2033
+
2034
+ if scanned_end < len(data):
2035
+ tail = data[scanned_end:]
2036
+ out.extend(tail)
2037
+ # An 8-byte header whose box overruns the data is a truncated download
2038
+ # worth reporting; fewer than 8 leftover bytes is merely trailing junk
2039
+ # and is kept verbatim without claiming the file was truncated (#170).
2040
+ if len(tail) >= 8:
2041
+ actions.append(f"kept {len(tail)} bytes of truncated tail (file truncated)")
2042
+
2043
+ if not actions:
2044
+ actions.append(f"no {fmt.upper()} metadata boxes removed (already clean or none matched)")
2045
+
2046
+ return bytes(out), actions
2047
+
2048
+
2049
+ def clean_image(
2050
+ path: Path,
2051
+ dest: Path,
2052
+ *,
2053
+ strip_all_metadata: bool = True,
2054
+ synthid_dir: str | None = None,
2055
+ remove_pixel: str | None = None,
2056
+ ctrlregen_dir: str | None = None,
2057
+ ctrlregen_strength: float = 0.25,
2058
+ ctrlregen_steps: int = 50,
2059
+ ctrlregen_device: str | None = None,
2060
+ ctrlregen_seed: int | None = None,
2061
+ ctrlregen_timeout: int = 3600,
2062
+ markdiffusion_dir: str | None = None,
2063
+ markdiffusion_strength: float = 0.3,
2064
+ markdiffusion_model: str | None = None,
2065
+ markdiffusion_size: int = 512,
2066
+ markdiffusion_steps: int = 50,
2067
+ markdiffusion_device: str | None = None,
2068
+ markdiffusion_timeout: int = 3600,
2069
+ ) -> dict[str, Any]:
2070
+ synthid_before = run_synthid_score(path, synthid_dir)
2071
+ data = path.read_bytes()
2072
+ fmt = detect_format(data)
2073
+ if fmt == "png":
2074
+ cleaned, actions = strip_png(data, strip_all_text=strip_all_metadata)
2075
+ elif fmt == "jpeg":
2076
+ cleaned, actions = strip_jpeg(data, strip_all_app=strip_all_metadata)
2077
+ elif fmt == "webp":
2078
+ cleaned, actions = strip_webp(data, strip_all_metadata=strip_all_metadata)
2079
+ elif fmt in ("avif", "heic"):
2080
+ cleaned, actions = strip_isobmff(data, fmt, strip_all_metadata=strip_all_metadata)
2081
+ elif fmt == "bmp":
2082
+ cleaned, actions = strip_bmp(data, strip_all_metadata=strip_all_metadata)
2083
+ elif fmt == "gif":
2084
+ cleaned, actions = strip_gif(data, strip_all_metadata=strip_all_metadata)
2085
+ elif fmt == "tiff":
2086
+ cleaned, actions = strip_tiff(data, strip_all_metadata=strip_all_metadata)
2087
+ else:
2088
+ raise ValueError(f"unsupported format: {fmt}")
2089
+
2090
+ # Optional exiftool pass for residual tags
2091
+ safe_write_bytes(dest, cleaned)
2092
+ exiftool = which("exiftool")
2093
+ if exiftool and strip_all_metadata:
2094
+ try:
2095
+ subprocess.run(
2096
+ [
2097
+ exiftool,
2098
+ "-all=",
2099
+ "-overwrite_original",
2100
+ safe_arg(str(dest)),
2101
+ ],
2102
+ capture_output=True,
2103
+ text=True,
2104
+ timeout=60,
2105
+ check=False,
2106
+ preexec_fn=subprocess_preexec_fn,
2107
+ creationflags=subprocess_creationflags,
2108
+ )
2109
+ actions.append("exiftool -all= pass")
2110
+ except Exception as e:
2111
+ actions.append(f"exiftool failed: {e}")
2112
+
2113
+ pixel_removal: dict[str, Any] | None = None
2114
+ if remove_pixel:
2115
+ if remove_pixel == "ctrlregen":
2116
+ pixel_removal = run_ctrlregen_clean(
2117
+ dest,
2118
+ dest,
2119
+ upstream_dir=ctrlregen_dir,
2120
+ strength=ctrlregen_strength,
2121
+ steps=ctrlregen_steps,
2122
+ device=ctrlregen_device,
2123
+ seed=ctrlregen_seed,
2124
+ timeout=ctrlregen_timeout,
2125
+ )
2126
+ if pixel_removal.get("available"):
2127
+ actions.append(f"CtrlRegen pixel removal (strength {ctrlregen_strength})")
2128
+ else:
2129
+ actions.append(
2130
+ "CtrlRegen pixel removal skipped: "
2131
+ f"{pixel_removal.get('error', 'unknown error')}"
2132
+ )
2133
+ elif remove_pixel == "diffusion":
2134
+ pixel_removal = run_markdiffusion_purify(
2135
+ dest,
2136
+ dest,
2137
+ upstream_dir=markdiffusion_dir,
2138
+ strength=markdiffusion_strength,
2139
+ model=markdiffusion_model,
2140
+ size=markdiffusion_size,
2141
+ steps=markdiffusion_steps,
2142
+ device=markdiffusion_device,
2143
+ timeout=markdiffusion_timeout,
2144
+ )
2145
+ if pixel_removal.get("available"):
2146
+ actions.append(
2147
+ f"DiffusionPurification pixel removal (strength {markdiffusion_strength})"
2148
+ )
2149
+ else:
2150
+ actions.append(
2151
+ "DiffusionPurification pixel removal skipped: "
2152
+ f"{pixel_removal.get('error', 'unknown error')}"
2153
+ )
2154
+ else:
2155
+ raise ValueError(f"unknown pixel remover: {remove_pixel}")
2156
+
2157
+ after = inspect_image(dest, synthid_dir=synthid_dir)
2158
+ return {
2159
+ "input": str(path),
2160
+ "output": str(dest),
2161
+ "format": fmt,
2162
+ "actions": actions,
2163
+ "bytes_in": len(data),
2164
+ "bytes_out": dest.stat().st_size,
2165
+ "still_has_c2pa": after.has_c2pa,
2166
+ "still_has_ai_metadata": after.has_ai_metadata,
2167
+ "post_findings": after.findings,
2168
+ "synthid_before": synthid_before,
2169
+ "synthid_after": after.synthid,
2170
+ "pixel_removal": pixel_removal,
2171
+ }