theus-cli-linux-arm64 1.2.1 → 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 (772) hide show
  1. package/README.md +15 -0
  2. package/RELEASE.json +6 -6
  3. package/bin/theus +0 -0
  4. package/package.json +1 -1
  5. package/vendor/chrome-extension/README.md +142 -0
  6. package/vendor/chrome-extension/background.js +540 -0
  7. package/vendor/chrome-extension/cdp-core.js +400 -0
  8. package/vendor/chrome-extension/cdp-core.test.ts +321 -0
  9. package/vendor/chrome-extension/cdp.js +672 -0
  10. package/vendor/chrome-extension/icons/icon128.png +0 -0
  11. package/vendor/chrome-extension/icons/icon16.png +0 -0
  12. package/vendor/chrome-extension/icons/icon32.png +0 -0
  13. package/vendor/chrome-extension/icons/icon48.png +0 -0
  14. package/vendor/chrome-extension/manifest.json +36 -0
  15. package/vendor/chrome-extension/popup.html +78 -0
  16. package/vendor/chrome-extension/popup.js +47 -0
  17. package/vendor/chrome-extension/recorder.js +316 -0
  18. package/vendor/higiene-motor/PARCHES.md +244 -0
  19. package/vendor/higiene-motor/audit_dir.py +166 -0
  20. package/vendor/higiene-motor/audit_lib.py +333 -0
  21. package/vendor/higiene-motor/audit_website.py +635 -0
  22. package/vendor/higiene-motor/av_meta.py +498 -0
  23. package/vendor/higiene-motor/check_staged.py +71 -0
  24. package/vendor/higiene-motor/clean_ctrlregen.py +212 -0
  25. package/vendor/higiene-motor/clean_file.py +237 -0
  26. package/vendor/higiene-motor/clean_image.py +218 -0
  27. package/vendor/higiene-motor/clean_staged.py +143 -0
  28. package/vendor/higiene-motor/clean_text.py +108 -0
  29. package/vendor/higiene-motor/common.py +486 -0
  30. package/vendor/higiene-motor/container_meta.py +2522 -0
  31. package/vendor/higiene-motor/detect_gumbel.py +308 -0
  32. package/vendor/higiene-motor/detect_text_watermark.py +613 -0
  33. package/vendor/higiene-motor/format_dispatch.py +157 -0
  34. package/vendor/higiene-motor/hook_written_file.py +253 -0
  35. package/vendor/higiene-motor/image_meta.py +2171 -0
  36. package/vendor/higiene-motor/inspect_file.py +138 -0
  37. package/vendor/higiene-motor/inspect_image.py +72 -0
  38. package/vendor/higiene-motor/inspect_text.py +86 -0
  39. package/vendor/higiene-motor/markdiffusion_harness.py +533 -0
  40. package/vendor/higiene-motor/rewrite_text.py +737 -0
  41. package/vendor/higiene-motor/score_stylometry.py +460 -0
  42. package/vendor/higiene-motor/score_synthid.py +165 -0
  43. package/vendor/higiene-motor/server.py +1059 -0
  44. package/vendor/higiene-motor/synthid_score_server.py +176 -0
  45. package/vendor/higiene-motor/text_detectors.py +433 -0
  46. package/vendor/higiene-motor/text_unicode.py +730 -0
  47. package/vendor/theus-arsenal/.arsenal-nombres-ola2.json +0 -6
  48. package/vendor/theus-arsenal/.arsenal-nombres-ola3.json +0 -72
  49. package/vendor/theus-arsenal/.mailmap +0 -2
  50. package/vendor/theus-arsenal/.theus-plugin/marketplace.json +0 -25
  51. package/vendor/theus-arsenal/.theus-plugin/plugin.json +0 -20
  52. package/vendor/theus-arsenal/AUTHORS.md +0 -8
  53. package/vendor/theus-arsenal/CLEANROOM_REPLACEMENT_PLAN.md +0 -11
  54. package/vendor/theus-arsenal/LICENSE +0 -26
  55. package/vendor/theus-arsenal/PROVENANCE.md +0 -9
  56. package/vendor/theus-arsenal/README.md +0 -41
  57. package/vendor/theus-arsenal/THIRD_PARTY.md +0 -80
  58. package/vendor/theus-arsenal/agents/actualizador-docs.md +0 -123
  59. package/vendor/theus-arsenal/agents/analista-comentarios.md +0 -53
  60. package/vendor/theus-arsenal/agents/analista-conversaciones.md +0 -60
  61. package/vendor/theus-arsenal/agents/analista-tests-pr.md +0 -53
  62. package/vendor/theus-arsenal/agents/analista-tipos.md +0 -49
  63. package/vendor/theus-arsenal/agents/arquitecto-accesibilidad.md +0 -148
  64. package/vendor/theus-arsenal/agents/arquitecto-codigo.md +0 -79
  65. package/vendor/theus-arsenal/agents/arquitecto-homelab.md +0 -90
  66. package/vendor/theus-arsenal/agents/arquitecto-redes.md +0 -116
  67. package/vendor/theus-arsenal/agents/arquitecto-sistemas.md +0 -219
  68. package/vendor/theus-arsenal/agents/bifurcador-opensource.md +0 -206
  69. package/vendor/theus-arsenal/agents/cazador-fallos-silenciosos.md +0 -58
  70. package/vendor/theus-arsenal/agents/consultor-documentacion.md +0 -76
  71. package/vendor/theus-arsenal/agents/diagnosticador-redes.md +0 -135
  72. package/vendor/theus-arsenal/agents/ejecutor-e2e.md +0 -96
  73. package/vendor/theus-arsenal/agents/empaquetador-opensource.md +0 -257
  74. package/vendor/theus-arsenal/agents/escaner-secretos.md +0 -196
  75. package/vendor/theus-arsenal/agents/especialista-seo.md +0 -70
  76. package/vendor/theus-arsenal/agents/estratega-marketing.md +0 -158
  77. package/vendor/theus-arsenal/agents/evaluador-agentes.md +0 -214
  78. package/vendor/theus-arsenal/agents/evaluador-harness.md +0 -217
  79. package/vendor/theus-arsenal/agents/explorador-codigo.md +0 -79
  80. package/vendor/theus-arsenal/agents/extractor-specs.md +0 -214
  81. package/vendor/theus-arsenal/agents/generador-harness.md +0 -139
  82. package/vendor/theus-arsenal/agents/guia-tdd.md +0 -99
  83. package/vendor/theus-arsenal/agents/jefe-de-gabinete.md +0 -162
  84. package/vendor/theus-arsenal/agents/limpiador-refactors.md +0 -93
  85. package/vendor/theus-arsenal/agents/optimizador-rendimiento.md +0 -454
  86. package/vendor/theus-arsenal/agents/planificador-harness.md +0 -107
  87. package/vendor/theus-arsenal/agents/planificador.md +0 -221
  88. package/vendor/theus-arsenal/agents/resolutor-apps-harmonyos.md +0 -181
  89. package/vendor/theus-arsenal/agents/resolutor-builds-cpp.md +0 -98
  90. package/vendor/theus-arsenal/agents/resolutor-builds-dart.md +0 -209
  91. package/vendor/theus-arsenal/agents/resolutor-builds-django.md +0 -253
  92. package/vendor/theus-arsenal/agents/resolutor-builds-go.md +0 -102
  93. package/vendor/theus-arsenal/agents/resolutor-builds-java.md +0 -274
  94. package/vendor/theus-arsenal/agents/resolutor-builds-kotlin.md +0 -128
  95. package/vendor/theus-arsenal/agents/resolutor-builds-pytorch.md +0 -128
  96. package/vendor/theus-arsenal/agents/resolutor-builds-react.md +0 -214
  97. package/vendor/theus-arsenal/agents/resolutor-builds-rust.md +0 -156
  98. package/vendor/theus-arsenal/agents/resolutor-builds-swift.md +0 -169
  99. package/vendor/theus-arsenal/agents/resolutor-builds.md +0 -122
  100. package/vendor/theus-arsenal/agents/revisor-bd.md +0 -97
  101. package/vendor/theus-arsenal/agents/revisor-codigo.md +0 -291
  102. package/vendor/theus-arsenal/agents/revisor-config-red.md +0 -116
  103. package/vendor/theus-arsenal/agents/revisor-cpp.md +0 -80
  104. package/vendor/theus-arsenal/agents/revisor-csharp.md +0 -109
  105. package/vendor/theus-arsenal/agents/revisor-django.md +0 -168
  106. package/vendor/theus-arsenal/agents/revisor-fastapi.md +0 -82
  107. package/vendor/theus-arsenal/agents/revisor-flutter.md +0 -251
  108. package/vendor/theus-arsenal/agents/revisor-go.md +0 -84
  109. package/vendor/theus-arsenal/agents/revisor-java.md +0 -192
  110. package/vendor/theus-arsenal/agents/revisor-kotlin.md +0 -171
  111. package/vendor/theus-arsenal/agents/revisor-ml.md +0 -159
  112. package/vendor/theus-arsenal/agents/revisor-php.md +0 -108
  113. package/vendor/theus-arsenal/agents/revisor-python.md +0 -106
  114. package/vendor/theus-arsenal/agents/revisor-react.md +0 -166
  115. package/vendor/theus-arsenal/agents/revisor-rust.md +0 -102
  116. package/vendor/theus-arsenal/agents/revisor-seguridad.md +0 -116
  117. package/vendor/theus-arsenal/agents/revisor-software-clinico.md +0 -95
  118. package/vendor/theus-arsenal/agents/revisor-swift.md +0 -115
  119. package/vendor/theus-arsenal/agents/revisor-typescript.md +0 -123
  120. package/vendor/theus-arsenal/agents/revisor-vue.md +0 -205
  121. package/vendor/theus-arsenal/agents/simplificador-codigo.md +0 -55
  122. package/vendor/theus-arsenal/commands/actualizar-docs.md +0 -88
  123. package/vendor/theus-arsenal/commands/actualizar-mapas-codigo.md +0 -78
  124. package/vendor/theus-arsenal/commands/aprender-evaluar.md +0 -109
  125. package/vendor/theus-arsenal/commands/aprender.md +0 -74
  126. package/vendor/theus-arsenal/commands/arreglar-build.md +0 -66
  127. package/vendor/theus-arsenal/commands/bucle-multiperspectiva.md +0 -153
  128. package/vendor/theus-arsenal/commands/build-cpp.md +0 -173
  129. package/vendor/theus-arsenal/commands/build-flutter.md +0 -164
  130. package/vendor/theus-arsenal/commands/build-go.md +0 -183
  131. package/vendor/theus-arsenal/commands/build-gradle.md +0 -75
  132. package/vendor/theus-arsenal/commands/build-kotlin.md +0 -176
  133. package/vendor/theus-arsenal/commands/build-react.md +0 -187
  134. package/vendor/theus-arsenal/commands/build-rust.md +0 -187
  135. package/vendor/theus-arsenal/commands/campana-marketing.md +0 -125
  136. package/vendor/theus-arsenal/commands/checkpoint.md +0 -78
  137. package/vendor/theus-arsenal/commands/cobertura-tests.md +0 -73
  138. package/vendor/theus-arsenal/commands/configurar-gestor-paquetes.md +0 -86
  139. package/vendor/theus-arsenal/commands/consulta.md +0 -164
  140. package/vendor/theus-arsenal/commands/control-calidad.md +0 -50
  141. package/vendor/theus-arsenal/commands/crear-skill.md +0 -77
  142. package/vendor/theus-arsenal/commands/enrutar-modelo.md +0 -30
  143. package/vendor/theus-arsenal/commands/epica-desbloquear.md +0 -17
  144. package/vendor/theus-arsenal/commands/epica-descomponer.md +0 -22
  145. package/vendor/theus-arsenal/commands/epica-publicar.md +0 -21
  146. package/vendor/theus-arsenal/commands/epica-reclamar.md +0 -25
  147. package/vendor/theus-arsenal/commands/epica-revisar.md +0 -22
  148. package/vendor/theus-arsenal/commands/epica-sincronizar.md +0 -21
  149. package/vendor/theus-arsenal/commands/epica-validar.md +0 -21
  150. package/vendor/theus-arsenal/commands/evolucionar.md +0 -176
  151. package/vendor/theus-arsenal/commands/funcionalidad.md +0 -49
  152. package/vendor/theus-arsenal/commands/guardar-sesion.md +0 -278
  153. package/vendor/theus-arsenal/commands/harness-build.md +0 -111
  154. package/vendor/theus-arsenal/commands/harness-diseno.md +0 -46
  155. package/vendor/theus-arsenal/commands/informe-costos.md +0 -90
  156. package/vendor/theus-arsenal/commands/iniciar-bucle.md +0 -36
  157. package/vendor/theus-arsenal/commands/instintos-estado.md +0 -53
  158. package/vendor/theus-arsenal/commands/instintos-exportar.md +0 -69
  159. package/vendor/theus-arsenal/commands/instintos-importar.md +0 -120
  160. package/vendor/theus-arsenal/commands/lienzo-planes.md +0 -48
  161. package/vendor/theus-arsenal/commands/limpiar-refactor.md +0 -84
  162. package/vendor/theus-arsenal/commands/modo-seguro.md +0 -96
  163. package/vendor/theus-arsenal/commands/orq-agregar-funcionalidad.md +0 -36
  164. package/vendor/theus-arsenal/commands/orq-cambiar-funcionalidad.md +0 -38
  165. package/vendor/theus-arsenal/commands/orq-construir-mvp.md +0 -36
  166. package/vendor/theus-arsenal/commands/orq-corregir-defecto.md +0 -40
  167. package/vendor/theus-arsenal/commands/orq-refinar-codigo.md +0 -44
  168. package/vendor/theus-arsenal/commands/orq-revisar.md +0 -135
  169. package/vendor/theus-arsenal/commands/plan-prd.md +0 -160
  170. package/vendor/theus-arsenal/commands/plan.md +0 -198
  171. package/vendor/theus-arsenal/commands/podar.md +0 -31
  172. package/vendor/theus-arsenal/commands/pr.md +0 -181
  173. package/vendor/theus-arsenal/commands/promover.md +0 -41
  174. package/vendor/theus-arsenal/commands/proyectos.md +0 -33
  175. package/vendor/theus-arsenal/commands/prp-commit.md +0 -112
  176. package/vendor/theus-arsenal/commands/prp-implementar.md +0 -385
  177. package/vendor/theus-arsenal/commands/prp-planificar.md +0 -501
  178. package/vendor/theus-arsenal/commands/reanudar-sesion.md +0 -160
  179. package/vendor/theus-arsenal/commands/revisar-codigo.md +0 -289
  180. package/vendor/theus-arsenal/commands/revisar-cpp.md +0 -132
  181. package/vendor/theus-arsenal/commands/revisar-fastapi.md +0 -39
  182. package/vendor/theus-arsenal/commands/revisar-flutter.md +0 -115
  183. package/vendor/theus-arsenal/commands/revisar-go.md +0 -148
  184. package/vendor/theus-arsenal/commands/revisar-kotlin.md +0 -140
  185. package/vendor/theus-arsenal/commands/revisar-pr.md +0 -37
  186. package/vendor/theus-arsenal/commands/revisar-python.md +0 -297
  187. package/vendor/theus-arsenal/commands/revisar-react.md +0 -170
  188. package/vendor/theus-arsenal/commands/revisar-rust.md +0 -142
  189. package/vendor/theus-arsenal/commands/revisar-vue.md +0 -174
  190. package/vendor/theus-arsenal/commands/salud-skills.md +0 -52
  191. package/vendor/theus-arsenal/commands/sesiones.md +0 -345
  192. package/vendor/theus-arsenal/commands/test-cpp.md +0 -252
  193. package/vendor/theus-arsenal/commands/test-flutter.md +0 -143
  194. package/vendor/theus-arsenal/commands/test-go.md +0 -268
  195. package/vendor/theus-arsenal/commands/test-kotlin.md +0 -312
  196. package/vendor/theus-arsenal/commands/test-react.md +0 -265
  197. package/vendor/theus-arsenal/commands/test-rust.md +0 -309
  198. package/vendor/theus-arsenal/commands/vigilancia-canary.md +0 -43
  199. package/vendor/theus-arsenal/hooks/hooks.json +0 -274
  200. package/vendor/theus-arsenal/package.json +0 -6
  201. package/vendor/theus-arsenal/rules/angular/coding-style.md +0 -182
  202. package/vendor/theus-arsenal/rules/angular/hooks.md +0 -25
  203. package/vendor/theus-arsenal/rules/angular/patterns.md +0 -249
  204. package/vendor/theus-arsenal/rules/angular/security.md +0 -87
  205. package/vendor/theus-arsenal/rules/angular/testing.md +0 -164
  206. package/vendor/theus-arsenal/rules/arkts/coding-style.md +0 -153
  207. package/vendor/theus-arsenal/rules/arkts/hooks.md +0 -80
  208. package/vendor/theus-arsenal/rules/arkts/patterns.md +0 -236
  209. package/vendor/theus-arsenal/rules/arkts/security.md +0 -141
  210. package/vendor/theus-arsenal/rules/arkts/testing.md +0 -126
  211. package/vendor/theus-arsenal/rules/common/agents.md +0 -49
  212. package/vendor/theus-arsenal/rules/common/code-review.md +0 -123
  213. package/vendor/theus-arsenal/rules/common/coding-style.md +0 -90
  214. package/vendor/theus-arsenal/rules/common/development-workflow.md +0 -44
  215. package/vendor/theus-arsenal/rules/common/git-workflow.md +0 -24
  216. package/vendor/theus-arsenal/rules/common/hooks.md +0 -32
  217. package/vendor/theus-arsenal/rules/common/patterns.md +0 -31
  218. package/vendor/theus-arsenal/rules/common/performance.md +0 -53
  219. package/vendor/theus-arsenal/rules/common/security.md +0 -29
  220. package/vendor/theus-arsenal/rules/common/testing.md +0 -57
  221. package/vendor/theus-arsenal/rules/cpp/coding-style.md +0 -44
  222. package/vendor/theus-arsenal/rules/cpp/hooks.md +0 -39
  223. package/vendor/theus-arsenal/rules/cpp/patterns.md +0 -51
  224. package/vendor/theus-arsenal/rules/cpp/security.md +0 -51
  225. package/vendor/theus-arsenal/rules/cpp/testing.md +0 -44
  226. package/vendor/theus-arsenal/rules/csharp/coding-style.md +0 -72
  227. package/vendor/theus-arsenal/rules/csharp/hooks.md +0 -25
  228. package/vendor/theus-arsenal/rules/csharp/patterns.md +0 -50
  229. package/vendor/theus-arsenal/rules/csharp/security.md +0 -58
  230. package/vendor/theus-arsenal/rules/csharp/testing.md +0 -50
  231. package/vendor/theus-arsenal/rules/dart/coding-style.md +0 -159
  232. package/vendor/theus-arsenal/rules/dart/hooks.md +0 -68
  233. package/vendor/theus-arsenal/rules/dart/patterns.md +0 -261
  234. package/vendor/theus-arsenal/rules/dart/security.md +0 -135
  235. package/vendor/theus-arsenal/rules/dart/testing.md +0 -215
  236. package/vendor/theus-arsenal/rules/fsharp/coding-style.md +0 -112
  237. package/vendor/theus-arsenal/rules/fsharp/hooks.md +0 -26
  238. package/vendor/theus-arsenal/rules/fsharp/patterns.md +0 -111
  239. package/vendor/theus-arsenal/rules/fsharp/security.md +0 -76
  240. package/vendor/theus-arsenal/rules/fsharp/testing.md +0 -62
  241. package/vendor/theus-arsenal/rules/golang/coding-style.md +0 -32
  242. package/vendor/theus-arsenal/rules/golang/hooks.md +0 -17
  243. package/vendor/theus-arsenal/rules/golang/patterns.md +0 -45
  244. package/vendor/theus-arsenal/rules/golang/security.md +0 -34
  245. package/vendor/theus-arsenal/rules/golang/testing.md +0 -31
  246. package/vendor/theus-arsenal/rules/java/coding-style.md +0 -114
  247. package/vendor/theus-arsenal/rules/java/hooks.md +0 -18
  248. package/vendor/theus-arsenal/rules/java/patterns.md +0 -147
  249. package/vendor/theus-arsenal/rules/java/security.md +0 -101
  250. package/vendor/theus-arsenal/rules/java/testing.md +0 -133
  251. package/vendor/theus-arsenal/rules/kotlin/coding-style.md +0 -86
  252. package/vendor/theus-arsenal/rules/kotlin/hooks.md +0 -17
  253. package/vendor/theus-arsenal/rules/kotlin/patterns.md +0 -146
  254. package/vendor/theus-arsenal/rules/kotlin/security.md +0 -82
  255. package/vendor/theus-arsenal/rules/kotlin/testing.md +0 -128
  256. package/vendor/theus-arsenal/rules/nuxt/coding-style.md +0 -47
  257. package/vendor/theus-arsenal/rules/nuxt/hooks.md +0 -39
  258. package/vendor/theus-arsenal/rules/nuxt/patterns.md +0 -54
  259. package/vendor/theus-arsenal/rules/nuxt/security.md +0 -48
  260. package/vendor/theus-arsenal/rules/nuxt/testing.md +0 -49
  261. package/vendor/theus-arsenal/rules/php/coding-style.md +0 -40
  262. package/vendor/theus-arsenal/rules/php/hooks.md +0 -24
  263. package/vendor/theus-arsenal/rules/php/patterns.md +0 -33
  264. package/vendor/theus-arsenal/rules/php/security.md +0 -37
  265. package/vendor/theus-arsenal/rules/php/testing.md +0 -38
  266. package/vendor/theus-arsenal/rules/python/coding-style.md +0 -42
  267. package/vendor/theus-arsenal/rules/python/fastapi.md +0 -58
  268. package/vendor/theus-arsenal/rules/python/hooks.md +0 -19
  269. package/vendor/theus-arsenal/rules/python/patterns.md +0 -39
  270. package/vendor/theus-arsenal/rules/python/security.md +0 -30
  271. package/vendor/theus-arsenal/rules/python/testing.md +0 -38
  272. package/vendor/theus-arsenal/rules/react/coding-style.md +0 -109
  273. package/vendor/theus-arsenal/rules/react/hooks.md +0 -187
  274. package/vendor/theus-arsenal/rules/react/patterns.md +0 -194
  275. package/vendor/theus-arsenal/rules/react/security.md +0 -180
  276. package/vendor/theus-arsenal/rules/react/testing.md +0 -208
  277. package/vendor/theus-arsenal/rules/react-native/accessibility.md +0 -55
  278. package/vendor/theus-arsenal/rules/react-native/coding-style.md +0 -71
  279. package/vendor/theus-arsenal/rules/react-native/hooks.md +0 -28
  280. package/vendor/theus-arsenal/rules/react-native/patterns.md +0 -88
  281. package/vendor/theus-arsenal/rules/react-native/performance.md +0 -45
  282. package/vendor/theus-arsenal/rules/react-native/production-readiness.md +0 -51
  283. package/vendor/theus-arsenal/rules/react-native/security.md +0 -43
  284. package/vendor/theus-arsenal/rules/react-native/testing.md +0 -52
  285. package/vendor/theus-arsenal/rules/ruby/coding-style.md +0 -46
  286. package/vendor/theus-arsenal/rules/ruby/hooks.md +0 -37
  287. package/vendor/theus-arsenal/rules/ruby/patterns.md +0 -44
  288. package/vendor/theus-arsenal/rules/ruby/security.md +0 -51
  289. package/vendor/theus-arsenal/rules/ruby/testing.md +0 -51
  290. package/vendor/theus-arsenal/rules/rust/coding-style.md +0 -151
  291. package/vendor/theus-arsenal/rules/rust/hooks.md +0 -16
  292. package/vendor/theus-arsenal/rules/rust/patterns.md +0 -168
  293. package/vendor/theus-arsenal/rules/rust/security.md +0 -141
  294. package/vendor/theus-arsenal/rules/rust/testing.md +0 -154
  295. package/vendor/theus-arsenal/rules/swift/coding-style.md +0 -47
  296. package/vendor/theus-arsenal/rules/swift/hooks.md +0 -20
  297. package/vendor/theus-arsenal/rules/swift/patterns.md +0 -66
  298. package/vendor/theus-arsenal/rules/swift/security.md +0 -33
  299. package/vendor/theus-arsenal/rules/swift/testing.md +0 -45
  300. package/vendor/theus-arsenal/rules/typescript/coding-style.md +0 -199
  301. package/vendor/theus-arsenal/rules/typescript/hooks.md +0 -22
  302. package/vendor/theus-arsenal/rules/typescript/patterns.md +0 -52
  303. package/vendor/theus-arsenal/rules/typescript/security.md +0 -28
  304. package/vendor/theus-arsenal/rules/typescript/testing.md +0 -18
  305. package/vendor/theus-arsenal/rules/vue/coding-style.md +0 -54
  306. package/vendor/theus-arsenal/rules/vue/hooks.md +0 -45
  307. package/vendor/theus-arsenal/rules/vue/patterns.md +0 -56
  308. package/vendor/theus-arsenal/rules/vue/security.md +0 -46
  309. package/vendor/theus-arsenal/rules/vue/testing.md +0 -53
  310. package/vendor/theus-arsenal/rules/web/coding-style.md +0 -108
  311. package/vendor/theus-arsenal/rules/web/design-quality.md +0 -75
  312. package/vendor/theus-arsenal/rules/web/hooks.md +0 -141
  313. package/vendor/theus-arsenal/rules/web/patterns.md +0 -91
  314. package/vendor/theus-arsenal/rules/web/performance.md +0 -76
  315. package/vendor/theus-arsenal/rules/web/security.md +0 -69
  316. package/vendor/theus-arsenal/rules/web/testing.md +0 -67
  317. package/vendor/theus-arsenal/scripts/hooks/bash-hook-dispatcher.js +0 -192
  318. package/vendor/theus-arsenal/scripts/hooks/block-no-verify.js +0 -556
  319. package/vendor/theus-arsenal/scripts/hooks/check-console-log.js +0 -92
  320. package/vendor/theus-arsenal/scripts/hooks/check-hook-enabled.js +0 -16
  321. package/vendor/theus-arsenal/scripts/hooks/config-protection.js +0 -171
  322. package/vendor/theus-arsenal/scripts/hooks/design-quality-check.js +0 -132
  323. package/vendor/theus-arsenal/scripts/hooks/doc-file-warning.js +0 -110
  324. package/vendor/theus-arsenal/scripts/hooks/modo-seguro.js +0 -158
  325. package/vendor/theus-arsenal/scripts/hooks/notificacion-escritorio.js +0 -208
  326. package/vendor/theus-arsenal/scripts/hooks/plugin-hook-bootstrap.js +0 -280
  327. package/vendor/theus-arsenal/scripts/hooks/post-bash-command-log.js +0 -87
  328. package/vendor/theus-arsenal/scripts/hooks/post-bash-dispatcher.js +0 -24
  329. package/vendor/theus-arsenal/scripts/hooks/post-bash-pr-created.js +0 -60
  330. package/vendor/theus-arsenal/scripts/hooks/post-edit-accumulator.js +0 -94
  331. package/vendor/theus-arsenal/scripts/hooks/post-edit-console-warn.js +0 -54
  332. package/vendor/theus-arsenal/scripts/hooks/post-edit-format.js +0 -110
  333. package/vendor/theus-arsenal/scripts/hooks/post-edit-typecheck.js +0 -98
  334. package/vendor/theus-arsenal/scripts/hooks/pre-bash-commit-quality.js +0 -447
  335. package/vendor/theus-arsenal/scripts/hooks/pre-bash-dev-server-block.js +0 -232
  336. package/vendor/theus-arsenal/scripts/hooks/pre-bash-dispatcher.js +0 -27
  337. package/vendor/theus-arsenal/scripts/hooks/pre-compact.js +0 -101
  338. package/vendor/theus-arsenal/scripts/hooks/pretooluse-visible-output.js +0 -49
  339. package/vendor/theus-arsenal/scripts/hooks/quality-gate.js +0 -168
  340. package/vendor/theus-arsenal/scripts/hooks/run-with-flags.js +0 -271
  341. package/vendor/theus-arsenal/scripts/hooks/salud-mcp.js +0 -749
  342. package/vendor/theus-arsenal/scripts/hooks/seguimiento-costos.js +0 -161
  343. package/vendor/theus-arsenal/scripts/hooks/session-end.js +0 -340
  344. package/vendor/theus-arsenal/scripts/hooks/session-start.js +0 -906
  345. package/vendor/theus-arsenal/scripts/hooks/stop-format-typecheck.js +0 -263
  346. package/vendor/theus-arsenal/scripts/hooks/suggest-compact.js +0 -286
  347. package/vendor/theus-arsenal/scripts/hooks/theus-context-monitor.js +0 -289
  348. package/vendor/theus-arsenal/scripts/hooks/verificacion-factual.js +0 -1278
  349. package/vendor/theus-arsenal/scripts/lib/agent-data-home.js +0 -96
  350. package/vendor/theus-arsenal/scripts/lib/cost-estimate.js +0 -66
  351. package/vendor/theus-arsenal/scripts/lib/hook-flags.js +0 -79
  352. package/vendor/theus-arsenal/scripts/lib/llm-summary.js +0 -185
  353. package/vendor/theus-arsenal/scripts/lib/loopback-guard.js +0 -54
  354. package/vendor/theus-arsenal/scripts/lib/observer-sessions.js +0 -237
  355. package/vendor/theus-arsenal/scripts/lib/package-manager.d.ts +0 -120
  356. package/vendor/theus-arsenal/scripts/lib/package-manager.js +0 -434
  357. package/vendor/theus-arsenal/scripts/lib/path-safety.js +0 -86
  358. package/vendor/theus-arsenal/scripts/lib/plan-canvas/markdown.js +0 -285
  359. package/vendor/theus-arsenal/scripts/lib/plan-canvas/sdk.js +0 -238
  360. package/vendor/theus-arsenal/scripts/lib/plan-canvas/server.js +0 -534
  361. package/vendor/theus-arsenal/scripts/lib/plan-canvas/sessions.js +0 -274
  362. package/vendor/theus-arsenal/scripts/lib/plan-canvas/ui.js +0 -546
  363. package/vendor/theus-arsenal/scripts/lib/project-detect.js +0 -451
  364. package/vendor/theus-arsenal/scripts/lib/resolve-formatter.js +0 -193
  365. package/vendor/theus-arsenal/scripts/lib/resolve-theus-root.js +0 -101
  366. package/vendor/theus-arsenal/scripts/lib/session-aliases.d.ts +0 -138
  367. package/vendor/theus-arsenal/scripts/lib/session-aliases.js +0 -483
  368. package/vendor/theus-arsenal/scripts/lib/session-bridge.js +0 -155
  369. package/vendor/theus-arsenal/scripts/lib/session-manager.d.ts +0 -137
  370. package/vendor/theus-arsenal/scripts/lib/session-manager.js +0 -555
  371. package/vendor/theus-arsenal/scripts/lib/shell-split.js +0 -86
  372. package/vendor/theus-arsenal/scripts/lib/shell-substitution.js +0 -501
  373. package/vendor/theus-arsenal/scripts/lib/skill-evolution/dashboard.js +0 -414
  374. package/vendor/theus-arsenal/scripts/lib/skill-evolution/health.js +0 -285
  375. package/vendor/theus-arsenal/scripts/lib/skill-evolution/index.js +0 -22
  376. package/vendor/theus-arsenal/scripts/lib/skill-evolution/provenance.js +0 -200
  377. package/vendor/theus-arsenal/scripts/lib/skill-evolution/tracker.js +0 -157
  378. package/vendor/theus-arsenal/scripts/lib/skill-evolution/versioning.js +0 -246
  379. package/vendor/theus-arsenal/scripts/lib/transcript-context.js +0 -232
  380. package/vendor/theus-arsenal/scripts/lib/utils.d.ts +0 -205
  381. package/vendor/theus-arsenal/scripts/lib/utils.js +0 -658
  382. package/vendor/theus-arsenal/scripts/plan-canvas.js +0 -341
  383. package/vendor/theus-arsenal/scripts/setup-package-manager.js +0 -46
  384. package/vendor/theus-arsenal/scripts/skills-health.js +0 -143
  385. package/vendor/theus-arsenal/skills/a11y-frontend/SKILL.md +0 -444
  386. package/vendor/theus-arsenal/skills/accesibilidad/SKILL.md +0 -144
  387. package/vendor/theus-arsenal/skills/acelerador-throughput-datos/SKILL.md +0 -72
  388. package/vendor/theus-arsenal/skills/agente-scraper-datos/SKILL.md +0 -771
  389. package/vendor/theus-arsenal/skills/analisis-plataformas-competencia/SKILL.md +0 -137
  390. package/vendor/theus-arsenal/skills/api-x/SKILL.md +0 -233
  391. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/SKILL.md +0 -335
  392. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer-loop.sh +0 -374
  393. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer.md +0 -188
  394. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/session-guardian.sh +0 -153
  395. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/start-observer.sh +0 -253
  396. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/config.json +0 -8
  397. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/hooks/observe.sh +0 -594
  398. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/detect-project.sh +0 -325
  399. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/instinct-cli.py +0 -1918
  400. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/lib/arsenal-dir.sh +0 -23
  401. package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/test_parse_instinct.py +0 -1420
  402. package/vendor/theus-arsenal/skills/arquitectura-hexagonal/SKILL.md +0 -275
  403. package/vendor/theus-arsenal/skills/arquitectura-limpia-android/SKILL.md +0 -340
  404. package/vendor/theus-arsenal/skills/arquitectura-pipelines-recomendacion/SKILL.md +0 -112
  405. package/vendor/theus-arsenal/skills/atlas-datos-prediccion/SKILL.md +0 -53
  406. package/vendor/theus-arsenal/skills/auditoria-arquitectura-agentes/SKILL.md +0 -257
  407. package/vendor/theus-arsenal/skills/auditoria-produccion/SKILL.md +0 -180
  408. package/vendor/theus-arsenal/skills/auditoria-rutas-clic/SKILL.md +0 -243
  409. package/vendor/theus-arsenal/skills/autoevaluacion-agente/SKILL.md +0 -180
  410. package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/high-score-example.md +0 -89
  411. package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/low-score-example.md +0 -87
  412. package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/evaluation-criteria.md +0 -71
  413. package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/hook-integration.md +0 -90
  414. package/vendor/theus-arsenal/skills/autoevaluacion-agente/scripts/evaluate.py +0 -409
  415. package/vendor/theus-arsenal/skills/autoevaluacion-agente/templates/evaluation-report.md +0 -86
  416. package/vendor/theus-arsenal/skills/automatizacion-ssh-netmiko/SKILL.md +0 -176
  417. package/vendor/theus-arsenal/skills/base-datos-pubmed/SKILL.md +0 -174
  418. package/vendor/theus-arsenal/skills/base-datos-uspto/SKILL.md +0 -173
  419. package/vendor/theus-arsenal/skills/benchmark/SKILL.md +0 -92
  420. package/vendor/theus-arsenal/skills/bitacora-crecimiento/SKILL.md +0 -120
  421. package/vendor/theus-arsenal/skills/bucle-verificacion/SKILL.md +0 -125
  422. package/vendor/theus-arsenal/skills/buscar-primero/SKILL.md +0 -184
  423. package/vendor/theus-arsenal/skills/busqueda-exa/SKILL.md +0 -111
  424. package/vendor/theus-arsenal/skills/cache-por-hash/SKILL.md +0 -160
  425. package/vendor/theus-arsenal/skills/campana-marketing/SKILL.md +0 -118
  426. package/vendor/theus-arsenal/skills/capacidad-producto/SKILL.md +0 -138
  427. package/vendor/theus-arsenal/skills/cazador-vulnerabilidades/SKILL.md +0 -104
  428. package/vendor/theus-arsenal/skills/compactacion-estrategica/SKILL.md +0 -115
  429. package/vendor/theus-arsenal/skills/comparador-baskets-prediccion/SKILL.md +0 -69
  430. package/vendor/theus-arsenal/skills/compras-energia/SKILL.md +0 -206
  431. package/vendor/theus-arsenal/skills/concurrencia-swift-6-2/SKILL.md +0 -224
  432. package/vendor/theus-arsenal/skills/consejo/SKILL.md +0 -201
  433. package/vendor/theus-arsenal/skills/construccion-harness-agentes/SKILL.md +0 -78
  434. package/vendor/theus-arsenal/skills/constructor-conectores/SKILL.md +0 -119
  435. package/vendor/theus-arsenal/skills/constructor-equipos/SKILL.md +0 -169
  436. package/vendor/theus-arsenal/skills/consulta-documentacion/SKILL.md +0 -92
  437. package/vendor/theus-arsenal/skills/consultas-genomicas-gget/SKILL.md +0 -170
  438. package/vendor/theus-arsenal/skills/contacto-inversores/SKILL.md +0 -95
  439. package/vendor/theus-arsenal/skills/corrutinas-flows-kotlin/SKILL.md +0 -284
  440. package/vendor/theus-arsenal/skills/creacion-video-remotion/SKILL.md +0 -46
  441. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/3d.md +0 -86
  442. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/animations.md +0 -29
  443. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/charts-bar-chart.tsx +0 -173
  444. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-typewriter.tsx +0 -100
  445. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-word-highlight.tsx +0 -108
  446. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets.md +0 -78
  447. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/audio.md +0 -172
  448. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/calculate-metadata.md +0 -104
  449. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/can-decode.md +0 -75
  450. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/charts.md +0 -58
  451. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/compositions.md +0 -146
  452. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/display-captions.md +0 -126
  453. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/extract-frames.md +0 -229
  454. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/fonts.md +0 -152
  455. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-audio-duration.md +0 -58
  456. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-dimensions.md +0 -68
  457. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-duration.md +0 -58
  458. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/gifs.md +0 -138
  459. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/images.md +0 -130
  460. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/import-srt-captions.md +0 -67
  461. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/lottie.md +0 -67
  462. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-dom-nodes.md +0 -34
  463. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-text.md +0 -143
  464. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/sequencing.md +0 -106
  465. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/tailwind.md +0 -11
  466. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/text-animations.md +0 -20
  467. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/timing.md +0 -179
  468. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transcribe-captions.md +0 -19
  469. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transitions.md +0 -122
  470. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/trimming.md +0 -52
  471. package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/videos.md +0 -171
  472. package/vendor/theus-arsenal/skills/cumplimiento-aduanero/SKILL.md +0 -270
  473. package/vendor/theus-arsenal/skills/cumplimiento-hipaa/SKILL.md +0 -78
  474. package/vendor/theus-arsenal/skills/datos-sensibles-salud/SKILL.md +0 -149
  475. package/vendor/theus-arsenal/skills/decimales-tokens-evm/SKILL.md +0 -128
  476. package/vendor/theus-arsenal/skills/decisiones-arquitectura/SKILL.md +0 -206
  477. package/vendor/theus-arsenal/skills/demos-ui/SKILL.md +0 -469
  478. package/vendor/theus-arsenal/skills/desarrollo-por-intencion/SKILL.md +0 -369
  479. package/vendor/theus-arsenal/skills/descubrimiento-marca/SKILL.md +0 -149
  480. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/10_proposito-porque.md +0 -42
  481. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/20_posicionamiento.md +0 -47
  482. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/30_audiencia-nicho.md +0 -53
  483. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/40_personalidad-arquetipo.md +0 -61
  484. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/50_voz-tono.md +0 -62
  485. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/60_narrativa-historia.md +0 -54
  486. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/70_tension-fundador.md +0 -50
  487. package/vendor/theus-arsenal/skills/descubrimiento-marca/references/90_SINTESIS.md +0 -133
  488. package/vendor/theus-arsenal/skills/descubrimiento-plugins-laravel/SKILL.md +0 -230
  489. package/vendor/theus-arsenal/skills/destilado-reglas/SKILL.md +0 -263
  490. package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-rules.sh +0 -58
  491. package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-skills.sh +0 -129
  492. package/vendor/theus-arsenal/skills/diagnostico-bgp/SKILL.md +0 -170
  493. package/vendor/theus-arsenal/skills/direccion-estetica/SKILL.md +0 -220
  494. package/vendor/theus-arsenal/skills/direccion-estetica/references/taxonomia-generos.md +0 -89
  495. package/vendor/theus-arsenal/skills/diseno-api/SKILL.md +0 -522
  496. package/vendor/theus-arsenal/skills/diseno-liquid-glass/SKILL.md +0 -282
  497. package/vendor/theus-arsenal/skills/e2e-escritorio-windows/SKILL.md +0 -879
  498. package/vendor/theus-arsenal/skills/edicion-video/SKILL.md +0 -289
  499. package/vendor/theus-arsenal/skills/enfoque-producto/SKILL.md +0 -91
  500. package/vendor/theus-arsenal/skills/entornos-flox/SKILL.md +0 -493
  501. package/vendor/theus-arsenal/skills/estandares-codigo/SKILL.md +0 -548
  502. package/vendor/theus-arsenal/skills/estandares-cpp/SKILL.md +0 -727
  503. package/vendor/theus-arsenal/skills/estandares-java/SKILL.md +0 -389
  504. package/vendor/theus-arsenal/skills/estilo-legado/SKILL.md +0 -149
  505. package/vendor/theus-arsenal/skills/estructura-informes-competencia/SKILL.md +0 -95
  506. package/vendor/theus-arsenal/skills/evaluacion-academica/SKILL.md +0 -165
  507. package/vendor/theus-arsenal/skills/explorador-skills/SKILL.md +0 -129
  508. package/vendor/theus-arsenal/skills/flujo-git/SKILL.md +0 -714
  509. package/vendor/theus-arsenal/skills/flujo-ingenieria-ml/SKILL.md +0 -343
  510. package/vendor/theus-arsenal/skills/flujo-tdd/SKILL.md +0 -584
  511. package/vendor/theus-arsenal/skills/forja-personas/SKILL.md +0 -298
  512. package/vendor/theus-arsenal/skills/forja-personas/gacha.py +0 -224
  513. package/vendor/theus-arsenal/skills/forja-personas/gacha.sh +0 -5
  514. package/vendor/theus-arsenal/skills/forja-personas/references/avatar-style.md +0 -123
  515. package/vendor/theus-arsenal/skills/forja-personas/references/boundary-rules.md +0 -53
  516. package/vendor/theus-arsenal/skills/forja-personas/references/error-handling.md +0 -53
  517. package/vendor/theus-arsenal/skills/forja-personas/references/identity-tension.md +0 -48
  518. package/vendor/theus-arsenal/skills/forja-personas/references/naming-system.md +0 -39
  519. package/vendor/theus-arsenal/skills/forja-personas/references/output-template.md +0 -166
  520. package/vendor/theus-arsenal/skills/fundamentos-animacion/SKILL.md +0 -296
  521. package/vendor/theus-arsenal/skills/generacion-iconos-ios/SKILL.md +0 -156
  522. package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/generate_icons.swift +0 -258
  523. package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/iconify_gen.sh +0 -235
  524. package/vendor/theus-arsenal/skills/gestion-excepciones-logisticas/SKILL.md +0 -208
  525. package/vendor/theus-arsenal/skills/gestion-transportistas/SKILL.md +0 -204
  526. package/vendor/theus-arsenal/skills/guia-adopcion-ml/SKILL.md +0 -64
  527. package/vendor/theus-arsenal/skills/harness-agente-autonomo/SKILL.md +0 -252
  528. package/vendor/theus-arsenal/skills/harness-eval-clinico/SKILL.md +0 -212
  529. package/vendor/theus-arsenal/skills/harness-evaluacion/SKILL.md +0 -296
  530. package/vendor/theus-arsenal/skills/harness-generador-evaluador/SKILL.md +0 -254
  531. package/vendor/theus-arsenal/skills/homelab-diseno-red/SKILL.md +0 -120
  532. package/vendor/theus-arsenal/skills/homelab-dns-pihole/SKILL.md +0 -264
  533. package/vendor/theus-arsenal/skills/homelab-preparacion-red/SKILL.md +0 -144
  534. package/vendor/theus-arsenal/skills/homelab-segmentacion-vlan/SKILL.md +0 -307
  535. package/vendor/theus-arsenal/skills/homelab-vpn-wireguard/SKILL.md +0 -299
  536. package/vendor/theus-arsenal/skills/ingenieria-agentica/SKILL.md +0 -96
  537. package/vendor/theus-arsenal/skills/inspeccion-animaciones-blender/SKILL.md +0 -164
  538. package/vendor/theus-arsenal/skills/instaladores-python/SKILL.md +0 -820
  539. package/vendor/theus-arsenal/skills/integracion-jira/SKILL.md +0 -307
  540. package/vendor/theus-arsenal/skills/inteligencia-leads/SKILL.md +0 -312
  541. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/agente-enriquecimiento.md +0 -87
  542. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/mapeador-mutuos.md +0 -76
  543. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/puntuador-senales.md +0 -60
  544. package/vendor/theus-arsenal/skills/inteligencia-leads/agents/redactor-outreach.md +0 -102
  545. package/vendor/theus-arsenal/skills/inteligencia-mercados-prediccion/SKILL.md +0 -53
  546. package/vendor/theus-arsenal/skills/introspeccion-agente/SKILL.md +0 -150
  547. package/vendor/theus-arsenal/skills/inventario-skills/SKILL.md +0 -194
  548. package/vendor/theus-arsenal/skills/inventario-skills/scripts/quick-diff.sh +0 -87
  549. package/vendor/theus-arsenal/skills/inventario-skills/scripts/save-results.sh +0 -57
  550. package/vendor/theus-arsenal/skills/inventario-skills/scripts/scan.sh +0 -170
  551. package/vendor/theus-arsenal/skills/investigacion-mercado/SKILL.md +0 -87
  552. package/vendor/theus-arsenal/skills/investigacion-oraculos-prediccion/SKILL.md +0 -59
  553. package/vendor/theus-arsenal/skills/investigacion-profunda/SKILL.md +0 -161
  554. package/vendor/theus-arsenal/skills/keccak256-nodejs/SKILL.md +0 -100
  555. package/vendor/theus-arsenal/skills/libro-decisiones-recursivo/SKILL.md +0 -82
  556. package/vendor/theus-arsenal/skills/lienzo-planes/SKILL.md +0 -111
  557. package/vendor/theus-arsenal/skills/limpieza-configuracion/SKILL.md +0 -116
  558. package/vendor/theus-arsenal/skills/logistica-inversa-devoluciones/SKILL.md +0 -241
  559. package/vendor/theus-arsenal/skills/manejo-errores/SKILL.md +0 -375
  560. package/vendor/theus-arsenal/skills/materiales-inversores/SKILL.md +0 -101
  561. package/vendor/theus-arsenal/skills/memoria-por-proyecto/SKILL.md +0 -170
  562. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/forget.mjs +0 -44
  563. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/info.mjs +0 -24
  564. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/init.mjs +0 -152
  565. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/list.mjs +0 -40
  566. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/migrate.mjs +0 -214
  567. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/resume.mjs +0 -36
  568. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/save.mjs +0 -210
  569. package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/shared.mjs +0 -402
  570. package/vendor/theus-arsenal/skills/memoria-por-proyecto/hooks/session-start.mjs +0 -238
  571. package/vendor/theus-arsenal/skills/metodologia-benchmarks/SKILL.md +0 -131
  572. package/vendor/theus-arsenal/skills/migraciones-bd/SKILL.md +0 -430
  573. package/vendor/theus-arsenal/skills/modelos-fundacionales-en-dispositivo/SKILL.md +0 -244
  574. package/vendor/theus-arsenal/skills/modo-seguro/SKILL.md +0 -86
  575. package/vendor/theus-arsenal/skills/motion-avanzado/SKILL.md +0 -592
  576. package/vendor/theus-arsenal/skills/motor-de-contenido/SKILL.md +0 -136
  577. package/vendor/theus-arsenal/skills/no-conformidades-calidad/SKILL.md +0 -249
  578. package/vendor/theus-arsenal/skills/onboarding-codigo/SKILL.md +0 -244
  579. package/vendor/theus-arsenal/skills/operaciones-agentes-empresariales/SKILL.md +0 -51
  580. package/vendor/theus-arsenal/skills/operaciones-conocimiento/SKILL.md +0 -159
  581. package/vendor/theus-arsenal/skills/operaciones-correo/SKILL.md +0 -123
  582. package/vendor/theus-arsenal/skills/operaciones-facturacion/SKILL.md +0 -142
  583. package/vendor/theus-arsenal/skills/operaciones-flujo-proyectos/SKILL.md +0 -110
  584. package/vendor/theus-arsenal/skills/operaciones-github/SKILL.md +0 -143
  585. package/vendor/theus-arsenal/skills/operaciones-google-workspace/SKILL.md +0 -100
  586. package/vendor/theus-arsenal/skills/operaciones-investigacion/SKILL.md +0 -124
  587. package/vendor/theus-arsenal/skills/operaciones-mensajes/SKILL.md +0 -103
  588. package/vendor/theus-arsenal/skills/operaciones-terminal/SKILL.md +0 -110
  589. package/vendor/theus-arsenal/skills/optimizacion-benchmark/SKILL.md +0 -70
  590. package/vendor/theus-arsenal/skills/optimizacion-costos-llm/SKILL.md +0 -182
  591. package/vendor/theus-arsenal/skills/optimizador-conexiones/SKILL.md +0 -191
  592. package/vendor/theus-arsenal/skills/optimizador-paralelismo/SKILL.md +0 -75
  593. package/vendor/theus-arsenal/skills/optimizador-prompts/SKILL.md +0 -374
  594. package/vendor/theus-arsenal/skills/orq-agregar-funcionalidad/SKILL.md +0 -37
  595. package/vendor/theus-arsenal/skills/orq-cambiar-funcionalidad/SKILL.md +0 -36
  596. package/vendor/theus-arsenal/skills/orq-construir-mvp/SKILL.md +0 -38
  597. package/vendor/theus-arsenal/skills/orq-corregir-defecto/SKILL.md +0 -36
  598. package/vendor/theus-arsenal/skills/orq-pipeline/SKILL.md +0 -134
  599. package/vendor/theus-arsenal/skills/orq-refinar-codigo/SKILL.md +0 -49
  600. package/vendor/theus-arsenal/skills/orquestacion-equipos-agentes/SKILL.md +0 -117
  601. package/vendor/theus-arsenal/skills/pagos-agentes-x402/SKILL.md +0 -237
  602. package/vendor/theus-arsenal/skills/patrones-angular/SKILL.md +0 -153
  603. package/vendor/theus-arsenal/skills/patrones-angular/references/angular-animations.md +0 -160
  604. package/vendor/theus-arsenal/skills/patrones-angular/references/angular-aria.md +0 -410
  605. package/vendor/theus-arsenal/skills/patrones-angular/references/cli.md +0 -86
  606. package/vendor/theus-arsenal/skills/patrones-angular/references/component-harnesses.md +0 -59
  607. package/vendor/theus-arsenal/skills/patrones-angular/references/component-styling.md +0 -91
  608. package/vendor/theus-arsenal/skills/patrones-angular/references/components.md +0 -117
  609. package/vendor/theus-arsenal/skills/patrones-angular/references/creating-services.md +0 -97
  610. package/vendor/theus-arsenal/skills/patrones-angular/references/data-resolvers.md +0 -69
  611. package/vendor/theus-arsenal/skills/patrones-angular/references/define-routes.md +0 -67
  612. package/vendor/theus-arsenal/skills/patrones-angular/references/defining-providers.md +0 -72
  613. package/vendor/theus-arsenal/skills/patrones-angular/references/di-fundamentals.md +0 -120
  614. package/vendor/theus-arsenal/skills/patrones-angular/references/e2e-testing.md +0 -56
  615. package/vendor/theus-arsenal/skills/patrones-angular/references/effects.md +0 -83
  616. package/vendor/theus-arsenal/skills/patrones-angular/references/hierarchical-injectors.md +0 -43
  617. package/vendor/theus-arsenal/skills/patrones-angular/references/host-elements.md +0 -80
  618. package/vendor/theus-arsenal/skills/patrones-angular/references/injection-context.md +0 -63
  619. package/vendor/theus-arsenal/skills/patrones-angular/references/inputs.md +0 -101
  620. package/vendor/theus-arsenal/skills/patrones-angular/references/linked-signal.md +0 -59
  621. package/vendor/theus-arsenal/skills/patrones-angular/references/loading-strategies.md +0 -61
  622. package/vendor/theus-arsenal/skills/patrones-angular/references/mcp.md +0 -67
  623. package/vendor/theus-arsenal/skills/patrones-angular/references/navigate-to-routes.md +0 -69
  624. package/vendor/theus-arsenal/skills/patrones-angular/references/outputs.md +0 -86
  625. package/vendor/theus-arsenal/skills/patrones-angular/references/reactive-forms.md +0 -122
  626. package/vendor/theus-arsenal/skills/patrones-angular/references/rendering-strategies.md +0 -44
  627. package/vendor/theus-arsenal/skills/patrones-angular/references/resource.md +0 -77
  628. package/vendor/theus-arsenal/skills/patrones-angular/references/route-animations.md +0 -56
  629. package/vendor/theus-arsenal/skills/patrones-angular/references/route-guards.md +0 -52
  630. package/vendor/theus-arsenal/skills/patrones-angular/references/router-lifecycle.md +0 -45
  631. package/vendor/theus-arsenal/skills/patrones-angular/references/router-testing.md +0 -87
  632. package/vendor/theus-arsenal/skills/patrones-angular/references/show-routes-with-outlets.md +0 -68
  633. package/vendor/theus-arsenal/skills/patrones-angular/references/signal-forms.md +0 -795
  634. package/vendor/theus-arsenal/skills/patrones-angular/references/signals-overview.md +0 -94
  635. package/vendor/theus-arsenal/skills/patrones-angular/references/tailwind-css.md +0 -69
  636. package/vendor/theus-arsenal/skills/patrones-angular/references/template-driven-forms.md +0 -114
  637. package/vendor/theus-arsenal/skills/patrones-angular/references/testing-fundamentals.md +0 -65
  638. package/vendor/theus-arsenal/skills/patrones-animacion/SKILL.md +0 -432
  639. package/vendor/theus-arsenal/skills/patrones-backend/SKILL.md +0 -560
  640. package/vendor/theus-arsenal/skills/patrones-cdss-clinico/SKILL.md +0 -249
  641. package/vendor/theus-arsenal/skills/patrones-cisco-ios/SKILL.md +0 -148
  642. package/vendor/theus-arsenal/skills/patrones-clickhouse/SKILL.md +0 -443
  643. package/vendor/theus-arsenal/skills/patrones-compose-multiplatform/SKILL.md +0 -298
  644. package/vendor/theus-arsenal/skills/patrones-dart-flutter/SKILL.md +0 -566
  645. package/vendor/theus-arsenal/skills/patrones-despliegue/SKILL.md +0 -426
  646. package/vendor/theus-arsenal/skills/patrones-django/SKILL.md +0 -739
  647. package/vendor/theus-arsenal/skills/patrones-django-celery/SKILL.md +0 -456
  648. package/vendor/theus-arsenal/skills/patrones-docker/SKILL.md +0 -363
  649. package/vendor/theus-arsenal/skills/patrones-dotnet/SKILL.md +0 -321
  650. package/vendor/theus-arsenal/skills/patrones-emr-clinico/SKILL.md +0 -168
  651. package/vendor/theus-arsenal/skills/patrones-fastapi/SKILL.md +0 -513
  652. package/vendor/theus-arsenal/skills/patrones-frontend/SKILL.md +0 -655
  653. package/vendor/theus-arsenal/skills/patrones-go/SKILL.md +0 -674
  654. package/vendor/theus-arsenal/skills/patrones-jpa/SKILL.md +0 -156
  655. package/vendor/theus-arsenal/skills/patrones-kotlin/SKILL.md +0 -717
  656. package/vendor/theus-arsenal/skills/patrones-kotlin-exposed/SKILL.md +0 -724
  657. package/vendor/theus-arsenal/skills/patrones-kotlin-ktor/SKILL.md +0 -694
  658. package/vendor/theus-arsenal/skills/patrones-kubernetes/SKILL.md +0 -754
  659. package/vendor/theus-arsenal/skills/patrones-laravel/SKILL.md +0 -414
  660. package/vendor/theus-arsenal/skills/patrones-mcp/SKILL.md +0 -66
  661. package/vendor/theus-arsenal/skills/patrones-mysql/SKILL.md +0 -420
  662. package/vendor/theus-arsenal/skills/patrones-nestjs/SKILL.md +0 -229
  663. package/vendor/theus-arsenal/skills/patrones-nextjs/SKILL.md +0 -56
  664. package/vendor/theus-arsenal/skills/patrones-nuxt4/SKILL.md +0 -99
  665. package/vendor/theus-arsenal/skills/patrones-perl/SKILL.md +0 -508
  666. package/vendor/theus-arsenal/skills/patrones-postgres/SKILL.md +0 -141
  667. package/vendor/theus-arsenal/skills/patrones-prisma/SKILL.md +0 -399
  668. package/vendor/theus-arsenal/skills/patrones-python/SKILL.md +0 -750
  669. package/vendor/theus-arsenal/skills/patrones-pytorch/SKILL.md +0 -400
  670. package/vendor/theus-arsenal/skills/patrones-quarkus/SKILL.md +0 -729
  671. package/vendor/theus-arsenal/skills/patrones-react/SKILL.md +0 -339
  672. package/vendor/theus-arsenal/skills/patrones-react-native/SKILL.md +0 -325
  673. package/vendor/theus-arsenal/skills/patrones-redis/SKILL.md +0 -401
  674. package/vendor/theus-arsenal/skills/patrones-rust/SKILL.md +0 -504
  675. package/vendor/theus-arsenal/skills/patrones-springboot/SKILL.md +0 -314
  676. package/vendor/theus-arsenal/skills/patrones-swiftui/SKILL.md +0 -261
  677. package/vendor/theus-arsenal/skills/patrones-tinystruct/SKILL.md +0 -281
  678. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/architecture.md +0 -90
  679. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/data-handling.md +0 -60
  680. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/database.md +0 -99
  681. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/routing.md +0 -64
  682. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/system-usage.md +0 -97
  683. package/vendor/theus-arsenal/skills/patrones-tinystruct/references/testing.md +0 -72
  684. package/vendor/theus-arsenal/skills/patrones-vite/SKILL.md +0 -449
  685. package/vendor/theus-arsenal/skills/patrones-vue/SKILL.md +0 -469
  686. package/vendor/theus-arsenal/skills/persistencia-actores-swift/SKILL.md +0 -148
  687. package/vendor/theus-arsenal/skills/pipeline-opensource/SKILL.md +0 -254
  688. package/vendor/theus-arsenal/skills/planificacion-demanda-inventario/SKILL.md +0 -236
  689. package/vendor/theus-arsenal/skills/planificador-trades-prediccion/SKILL.md +0 -60
  690. package/vendor/theus-arsenal/skills/plano-tecnico/SKILL.md +0 -102
  691. package/vendor/theus-arsenal/skills/presentaciones-html/SKILL.md +0 -189
  692. package/vendor/theus-arsenal/skills/presentaciones-html/STYLE_PRESETS.md +0 -330
  693. package/vendor/theus-arsenal/skills/presentaciones-html/animation-patterns.md +0 -122
  694. package/vendor/theus-arsenal/skills/presentaciones-html/html-template.md +0 -419
  695. package/vendor/theus-arsenal/skills/presentaciones-html/scripts/export-pdf.sh +0 -418
  696. package/vendor/theus-arsenal/skills/presentaciones-html/scripts/extract-pptx.py +0 -96
  697. package/vendor/theus-arsenal/skills/presentaciones-html/viewport-base.css +0 -153
  698. package/vendor/theus-arsenal/skills/presupuesto-contexto/SKILL.md +0 -263
  699. package/vendor/theus-arsenal/skills/procesamiento-documentos-nutrient/SKILL.md +0 -168
  700. package/vendor/theus-arsenal/skills/programacion-produccion/SKILL.md +0 -220
  701. package/vendor/theus-arsenal/skills/publicacion-cruzada/SKILL.md +0 -117
  702. package/vendor/theus-arsenal/skills/publicador-social/SKILL.md +0 -121
  703. package/vendor/theus-arsenal/skills/puerta-de-entrega/SKILL.md +0 -164
  704. package/vendor/theus-arsenal/skills/puerta-de-entrega/hooks/puerta-entrega.py +0 -242
  705. package/vendor/theus-arsenal/skills/pulir-interfaces/SKILL.md +0 -126
  706. package/vendor/theus-arsenal/skills/qa-navegador/SKILL.md +0 -103
  707. package/vendor/theus-arsenal/skills/ranking-grafo-social/SKILL.md +0 -153
  708. package/vendor/theus-arsenal/skills/recuperacion-iterativa/SKILL.md +0 -236
  709. package/vendor/theus-arsenal/skills/redaccion-articulos/SKILL.md +0 -81
  710. package/vendor/theus-arsenal/skills/regex-vs-llm/SKILL.md +0 -221
  711. package/vendor/theus-arsenal/skills/regresion-codigo-ia/SKILL.md +0 -388
  712. package/vendor/theus-arsenal/skills/rendimiento-react/SKILL.md +0 -567
  713. package/vendor/theus-arsenal/skills/revision-codigo-flutter-dart/SKILL.md +0 -442
  714. package/vendor/theus-arsenal/skills/revision-literatura-cientifica/SKILL.md +0 -191
  715. package/vendor/theus-arsenal/skills/revision-multiperspectiva/SKILL.md +0 -302
  716. package/vendor/theus-arsenal/skills/revision-riesgo-prediccion/SKILL.md +0 -56
  717. package/vendor/theus-arsenal/skills/revision-seguridad/SKILL.md +0 -505
  718. package/vendor/theus-arsenal/skills/revision-seguridad/cloud-infrastructure-security.md +0 -361
  719. package/vendor/theus-arsenal/skills/runtime-bun/SKILL.md +0 -83
  720. package/vendor/theus-arsenal/skills/salud-codigo-codescene/SKILL.md +0 -163
  721. package/vendor/theus-arsenal/skills/salud-interfaces-red/SKILL.md +0 -143
  722. package/vendor/theus-arsenal/skills/seguimiento-costos/SKILL.md +0 -122
  723. package/vendor/theus-arsenal/skills/seguridad-agentes-trading/SKILL.md +0 -145
  724. package/vendor/theus-arsenal/skills/seguridad-amm-defi/SKILL.md +0 -166
  725. package/vendor/theus-arsenal/skills/seguridad-django/SKILL.md +0 -648
  726. package/vendor/theus-arsenal/skills/seguridad-laravel/SKILL.md +0 -945
  727. package/vendor/theus-arsenal/skills/seguridad-perl/SKILL.md +0 -507
  728. package/vendor/theus-arsenal/skills/seguridad-quarkus/SKILL.md +0 -474
  729. package/vendor/theus-arsenal/skills/seguridad-springboot/SKILL.md +0 -279
  730. package/vendor/theus-arsenal/skills/seo/SKILL.md +0 -166
  731. package/vendor/theus-arsenal/skills/sistema-diseno/SKILL.md +0 -81
  732. package/vendor/theus-arsenal/skills/sistema-operativo-agentico/SKILL.md +0 -375
  733. package/vendor/theus-arsenal/skills/sistemas-latencia-critica/SKILL.md +0 -65
  734. package/vendor/theus-arsenal/skills/tdd-django/SKILL.md +0 -735
  735. package/vendor/theus-arsenal/skills/tdd-laravel/SKILL.md +0 -673
  736. package/vendor/theus-arsenal/skills/tdd-quarkus/SKILL.md +0 -817
  737. package/vendor/theus-arsenal/skills/tdd-springboot/SKILL.md +0 -165
  738. package/vendor/theus-arsenal/skills/testing-cpp/SKILL.md +0 -328
  739. package/vendor/theus-arsenal/skills/testing-csharp/SKILL.md +0 -321
  740. package/vendor/theus-arsenal/skills/testing-e2e/SKILL.md +0 -325
  741. package/vendor/theus-arsenal/skills/testing-go/SKILL.md +0 -719
  742. package/vendor/theus-arsenal/skills/testing-kotlin/SKILL.md +0 -829
  743. package/vendor/theus-arsenal/skills/testing-perl/SKILL.md +0 -479
  744. package/vendor/theus-arsenal/skills/testing-protocolos-di-swift/SKILL.md +0 -196
  745. package/vendor/theus-arsenal/skills/testing-python/SKILL.md +0 -815
  746. package/vendor/theus-arsenal/skills/testing-react/SKILL.md +0 -421
  747. package/vendor/theus-arsenal/skills/testing-rust/SKILL.md +0 -506
  748. package/vendor/theus-arsenal/skills/tours-de-codigo/SKILL.md +0 -252
  749. package/vendor/theus-arsenal/skills/traduccion-documentos-visado/SKILL.md +0 -121
  750. package/vendor/theus-arsenal/skills/ui-a-vue/SKILL.md +0 -141
  751. package/vendor/theus-arsenal/skills/validacion-config-red/SKILL.md +0 -211
  752. package/vendor/theus-arsenal/skills/verificacion-django/SKILL.md +0 -470
  753. package/vendor/theus-arsenal/skills/verificacion-factual/SKILL.md +0 -142
  754. package/vendor/theus-arsenal/skills/verificacion-laravel/SKILL.md +0 -184
  755. package/vendor/theus-arsenal/skills/verificacion-quarkus/SKILL.md +0 -485
  756. package/vendor/theus-arsenal/skills/verificacion-springboot/SKILL.md +0 -237
  757. package/vendor/theus-arsenal/skills/videodb/SKILL.md +0 -374
  758. package/vendor/theus-arsenal/skills/videodb/reference/api-reference.md +0 -550
  759. package/vendor/theus-arsenal/skills/videodb/reference/capture-reference.md +0 -407
  760. package/vendor/theus-arsenal/skills/videodb/reference/capture.md +0 -101
  761. package/vendor/theus-arsenal/skills/videodb/reference/editor.md +0 -443
  762. package/vendor/theus-arsenal/skills/videodb/reference/generative.md +0 -331
  763. package/vendor/theus-arsenal/skills/videodb/reference/rtstream-reference.md +0 -564
  764. package/vendor/theus-arsenal/skills/videodb/reference/rtstream.md +0 -65
  765. package/vendor/theus-arsenal/skills/videodb/reference/search.md +0 -230
  766. package/vendor/theus-arsenal/skills/videodb/reference/streaming.md +0 -406
  767. package/vendor/theus-arsenal/skills/videodb/reference/use-cases.md +0 -118
  768. package/vendor/theus-arsenal/skills/videodb/scripts/ws_listener.py +0 -279
  769. package/vendor/theus-arsenal/skills/videos-manim/SKILL.md +0 -91
  770. package/vendor/theus-arsenal/skills/videos-manim/assets/network_graph_scene.py +0 -52
  771. package/vendor/theus-arsenal/skills/vigilancia-canary/SKILL.md +0 -108
  772. 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
+ }