codejury 0.9.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. {codejury-0.9.0 → codejury-0.10.0}/PKG-INFO +7 -2
  2. {codejury-0.9.0 → codejury-0.10.0}/README.md +6 -1
  3. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/verifier.py +13 -4
  4. {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/provenance.py +83 -38
  5. {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/taint.py +79 -66
  6. {codejury-0.9.0 → codejury-0.10.0}/codejury/assembly.py +15 -3
  7. {codejury-0.9.0 → codejury-0.10.0}/codejury/cli.py +2 -2
  8. {codejury-0.9.0 → codejury-0.10.0}/codejury/evaluation.py +14 -3
  9. codejury-0.10.0/codejury/orchestrators/adaptive.py +55 -0
  10. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/taint_gate.py +1 -1
  11. {codejury-0.9.0 → codejury-0.10.0}/codejury/reporting.py +2 -9
  12. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/PKG-INFO +7 -2
  13. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/SOURCES.txt +2 -0
  14. {codejury-0.9.0 → codejury-0.10.0}/pyproject.toml +1 -1
  15. codejury-0.10.0/tests/test_adaptive.py +71 -0
  16. {codejury-0.9.0 → codejury-0.10.0}/tests/test_assembly.py +20 -1
  17. {codejury-0.9.0 → codejury-0.10.0}/tests/test_evaluation.py +11 -0
  18. {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint.py +27 -1
  19. {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint_crossfile.py +10 -0
  20. {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint_gate.py +17 -0
  21. {codejury-0.9.0 → codejury-0.10.0}/LICENSE +0 -0
  22. {codejury-0.9.0 → codejury-0.10.0}/codejury/__init__.py +0 -0
  23. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/__init__.py +0 -0
  24. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/base.py +0 -0
  25. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/debate.py +0 -0
  26. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/mock.py +0 -0
  27. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/parsing.py +0 -0
  28. {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/refuter.py +0 -0
  29. {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/__init__.py +0 -0
  30. {codejury-0.9.0 → codejury-0.10.0}/codejury/baseline.py +0 -0
  31. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/authentication.yaml +0 -0
  32. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/authorization.yaml +0 -0
  33. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/business_logic.yaml +0 -0
  34. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/crypto.yaml +0 -0
  35. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/data_protection.yaml +0 -0
  36. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/dependency_config.yaml +0 -0
  37. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/error_logging.yaml +0 -0
  38. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/excessive_agency.yaml +0 -0
  39. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/input_validation.yaml +0 -0
  40. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
  41. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/output_encoding.yaml +0 -0
  42. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/prompt_injection.yaml +0 -0
  43. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/secrets.yaml +0 -0
  44. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/session.yaml +0 -0
  45. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
  46. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
  47. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
  48. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
  49. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
  50. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
  51. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
  52. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
  53. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
  54. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
  55. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_sha256_password.yaml +0 -0
  56. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
  57. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
  58. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authz_owner_safe.yaml +0 -0
  59. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
  60. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
  61. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
  62. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
  63. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
  64. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
  65. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
  66. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
  67. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
  68. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
  69. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
  70. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
  71. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
  72. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
  73. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
  74. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
  75. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
  76. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
  77. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
  78. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
  79. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
  80. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/literal_eval_safe.yaml +0 -0
  81. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_basename_safe.yaml +0 -0
  82. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_contained_safe.yaml +0 -0
  83. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
  84. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
  85. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
  86. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
  87. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
  88. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
  89. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
  90. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/secrets_env_safe.yaml +0 -0
  91. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
  92. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
  93. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
  94. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
  95. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
  96. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
  97. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
  98. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
  99. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
  100. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
  101. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
  102. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
  103. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
  104. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
  105. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
  106. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
  107. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
  108. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
  109. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
  110. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/suppressions.yaml +0 -0
  111. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/taint.yaml +0 -0
  112. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
  113. {codejury-0.9.0 → codejury-0.10.0}/codejury/data/tasks/quick_scan_single.yaml +0 -0
  114. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/__init__.py +0 -0
  115. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/artifact.py +0 -0
  116. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/capability.py +0 -0
  117. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/context.py +0 -0
  118. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/observation.py +0 -0
  119. {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/result.py +0 -0
  120. {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/__init__.py +0 -0
  121. {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/cache.py +0 -0
  122. {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/json_parse.py +0 -0
  123. {codejury-0.9.0 → codejury-0.10.0}/codejury/integrations/__init__.py +0 -0
  124. {codejury-0.9.0 → codejury-0.10.0}/codejury/integrations/github.py +0 -0
  125. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/__init__.py +0 -0
  126. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/base.py +0 -0
  127. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/challenge.py +0 -0
  128. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/debate.py +0 -0
  129. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/pipeline.py +0 -0
  130. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/reflexion.py +0 -0
  131. {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/single.py +0 -0
  132. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/__init__.py +0 -0
  133. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/anthropic.py +0 -0
  134. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/base.py +0 -0
  135. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/litellm.py +0 -0
  136. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/mock.py +0 -0
  137. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/openai.py +0 -0
  138. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/openai_format.py +0 -0
  139. {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/retry.py +0 -0
  140. {codejury-0.9.0 → codejury-0.10.0}/codejury/resources.py +0 -0
  141. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/__init__.py +0 -0
  142. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/base.py +0 -0
  143. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/callers.py +0 -0
  144. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/chunker.py +0 -0
  145. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/diff.py +0 -0
  146. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/function.py +0 -0
  147. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/mock.py +0 -0
  148. {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/repo.py +0 -0
  149. {codejury-0.9.0 → codejury-0.10.0}/codejury/suppression.py +0 -0
  150. {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/__init__.py +0 -0
  151. {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/base.py +0 -0
  152. {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/registry.py +0 -0
  153. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/dependency_links.txt +0 -0
  154. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/entry_points.txt +0 -0
  155. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/requires.txt +0 -0
  156. {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/top_level.txt +0 -0
  157. {codejury-0.9.0 → codejury-0.10.0}/setup.cfg +0 -0
  158. {codejury-0.9.0 → codejury-0.10.0}/tests/test_anthropic_provider.py +0 -0
  159. {codejury-0.9.0 → codejury-0.10.0}/tests/test_audit_pipeline.py +0 -0
  160. {codejury-0.9.0 → codejury-0.10.0}/tests/test_baseline.py +0 -0
  161. {codejury-0.9.0 → codejury-0.10.0}/tests/test_cache.py +0 -0
  162. {codejury-0.9.0 → codejury-0.10.0}/tests/test_callers.py +0 -0
  163. {codejury-0.9.0 → codejury-0.10.0}/tests/test_capability.py +0 -0
  164. {codejury-0.9.0 → codejury-0.10.0}/tests/test_challenge.py +0 -0
  165. {codejury-0.9.0 → codejury-0.10.0}/tests/test_cli_audit.py +0 -0
  166. {codejury-0.9.0 → codejury-0.10.0}/tests/test_context.py +0 -0
  167. {codejury-0.9.0 → codejury-0.10.0}/tests/test_debate_agents.py +0 -0
  168. {codejury-0.9.0 → codejury-0.10.0}/tests/test_debate_orchestrator.py +0 -0
  169. {codejury-0.9.0 → codejury-0.10.0}/tests/test_diff_source.py +0 -0
  170. {codejury-0.9.0 → codejury-0.10.0}/tests/test_function_source.py +0 -0
  171. {codejury-0.9.0 → codejury-0.10.0}/tests/test_integrations.py +0 -0
  172. {codejury-0.9.0 → codejury-0.10.0}/tests/test_json_parse.py +0 -0
  173. {codejury-0.9.0 → codejury-0.10.0}/tests/test_litellm_provider.py +0 -0
  174. {codejury-0.9.0 → codejury-0.10.0}/tests/test_openai_provider.py +0 -0
  175. {codejury-0.9.0 → codejury-0.10.0}/tests/test_orchestrator.py +0 -0
  176. {codejury-0.9.0 → codejury-0.10.0}/tests/test_pipeline_orchestrator.py +0 -0
  177. {codejury-0.9.0 → codejury-0.10.0}/tests/test_provenance.py +0 -0
  178. {codejury-0.9.0 → codejury-0.10.0}/tests/test_reflexion_orchestrator.py +0 -0
  179. {codejury-0.9.0 → codejury-0.10.0}/tests/test_repo_source.py +0 -0
  180. {codejury-0.9.0 → codejury-0.10.0}/tests/test_reporting.py +0 -0
  181. {codejury-0.9.0 → codejury-0.10.0}/tests/test_retry_provider.py +0 -0
  182. {codejury-0.9.0 → codejury-0.10.0}/tests/test_sarif.py +0 -0
  183. {codejury-0.9.0 → codejury-0.10.0}/tests/test_suppression.py +0 -0
  184. {codejury-0.9.0 → codejury-0.10.0}/tests/test_tasks.py +0 -0
  185. {codejury-0.9.0 → codejury-0.10.0}/tests/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: General-purpose Application Security AI audit framework -- five-layer architecture, capabilities as first-class data
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -83,7 +83,7 @@ git diff | codejury audit --provider anthropic
83
83
  | `codejury run <task>` | Run a named task preset (see [Tasks](#tasks)). |
84
84
  | `codejury eval` | Score the golden cases; report precision / recall / F1, overall and per capability. |
85
85
 
86
- Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint}`,
86
+ Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint,adaptive}`,
87
87
  `--provider {anthropic,openai,litellm}`, `--model`,
88
88
  `--format {text,markdown,json,sarif}`.
89
89
 
@@ -103,6 +103,11 @@ value reaching the sink is constant, sanitized, or trusted (using cross-file
103
103
  caller/callee context). It downgrades only on positive proof, so it removes false
104
104
  positives without dropping real findings.
105
105
 
106
+ `--orchestrator adaptive` keeps cost down: it runs the single verifier first and
107
+ escalates to a full debate only for artifacts worth it -- any VULNERABLE verdict
108
+ (verify a flag before reporting) or a low-confidence PARTIAL/UNKNOWN one. Clean,
109
+ confident files pay a single call; only the rest pay for debate.
110
+
106
111
  `--format sarif` emits a SARIF 2.1.0 log (validates against the official schema)
107
112
  for CI and security dashboards: each problem with a code location becomes a
108
113
  result carrying its capability (as the rule id), CWE, and a precise location.
@@ -53,7 +53,7 @@ git diff | codejury audit --provider anthropic
53
53
  | `codejury run <task>` | Run a named task preset (see [Tasks](#tasks)). |
54
54
  | `codejury eval` | Score the golden cases; report precision / recall / F1, overall and per capability. |
55
55
 
56
- Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint}`,
56
+ Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint,adaptive}`,
57
57
  `--provider {anthropic,openai,litellm}`, `--model`,
58
58
  `--format {text,markdown,json,sarif}`.
59
59
 
@@ -73,6 +73,11 @@ value reaching the sink is constant, sanitized, or trusted (using cross-file
73
73
  caller/callee context). It downgrades only on positive proof, so it removes false
74
74
  positives without dropping real findings.
75
75
 
76
+ `--orchestrator adaptive` keeps cost down: it runs the single verifier first and
77
+ escalates to a full debate only for artifacts worth it -- any VULNERABLE verdict
78
+ (verify a flag before reporting) or a low-confidence PARTIAL/UNKNOWN one. Clean,
79
+ confident files pay a single call; only the rest pay for debate.
80
+
76
81
  `--format sarif` emits a SARIF 2.1.0 log (validates against the official schema)
77
82
  for CI and security dashboards: each problem with a code location becomes a
78
83
  result carrying its capability (as the rule id), CWE, and a precise location.
@@ -93,16 +93,25 @@ def _build_prompt(path: str, content: str, cap: Capability, context: str = "") -
93
93
  )
94
94
 
95
95
 
96
- def _anti_pattern_cwes(cap: Capability) -> dict[str, str]:
97
- """Map anti_pattern id -> CWE, so a verdict can inherit the CWE it matched."""
96
+ _SEVERITY_RANK = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0}
97
+
98
+
99
+ def _anti_pattern_cwes(cap: Capability) -> dict[str, tuple[int, str]]:
100
+ """Map anti_pattern id -> (severity rank, CWE), so a verdict can inherit the CWE
101
+ of the most severe anti-pattern it matched (deterministic, not first-seen)."""
98
102
  return {
99
- p.id: p.cwe
103
+ p.id: (_SEVERITY_RANK.get(p.severity, 2), p.cwe)
100
104
  for sub in cap.sub_capabilities.values()
101
105
  for p in sub.anti_patterns
102
106
  if p.cwe
103
107
  }
104
108
 
105
109
 
110
+ def _resolve_cwe(matched_anti: list[str], cwe_by_id: dict[str, tuple[int, str]]) -> str:
111
+ matched = [cwe_by_id[a] for a in matched_anti if a in cwe_by_id]
112
+ return max(matched, key=lambda rank_cwe: rank_cwe[0])[1] if matched else ""
113
+
114
+
106
115
  def _parse_verdicts(text: str, cap: Capability) -> list[Verdict]:
107
116
  obj = extract_json_object(text)
108
117
  if not obj:
@@ -122,7 +131,7 @@ def _parse_verdicts(text: str, cap: Capability) -> list[Verdict]:
122
131
  reasoning=str(v.get("reasoning", "")),
123
132
  matched_correct=str_list(v.get("matched_correct")),
124
133
  matched_anti=matched_anti,
125
- cwe=next((cwe_by_id[a] for a in matched_anti if a in cwe_by_id), ""),
134
+ cwe=_resolve_cwe(matched_anti, cwe_by_id),
126
135
  evidence=to_evidence(v.get("evidence")),
127
136
  confidence=to_float(v.get("confidence"), 0.5),
128
137
  )
@@ -72,7 +72,40 @@ def find_calls(scope: ast.AST, callee: str) -> list[ast.Call]:
72
72
 
73
73
  def trace_value(func: ast.FunctionDef | ast.AsyncFunctionDef, expr: ast.AST) -> Origin:
74
74
  """Trace where ``expr`` (an expression inside ``func``) gets its value from."""
75
- return _classify(expr, _params(func), _assignments(func), frozenset())
75
+ params = _params(func)
76
+
77
+ def leaf(node: ast.AST, recurse) -> Origin:
78
+ if isinstance(node, ast.Call):
79
+ # the return value's provenance depends on the callee's semantics, which
80
+ # P1-03 decides with the vocabulary; here we just name the callee.
81
+ name = _call_name(node)
82
+ return Origin(calls=frozenset({name})) if name else _UNKNOWN
83
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
84
+ dotted = _dotted(node)
85
+ origin = Origin(attrs=frozenset({dotted})) if dotted else _UNKNOWN
86
+ if _root_name(node) in params: # e.g. request.args[...] where `request` is a parameter
87
+ origin = origin.merge(Origin(params=frozenset({_root_name(node)})))
88
+ return origin
89
+ return _UNKNOWN
90
+
91
+ return reduce_value(
92
+ expr,
93
+ params=params,
94
+ assigns=_assignments(func),
95
+ combine=_merge_origins,
96
+ leaf=leaf,
97
+ on_param=lambda n: Origin(params=frozenset({n})),
98
+ on_global=lambda n: Origin(globals_=frozenset({n})),
99
+ on_constant=lambda: _LITERAL,
100
+ on_cycle=Origin, # assignment cycle -- no new information
101
+ )
102
+
103
+
104
+ def _merge_origins(origins: list[Origin]) -> Origin:
105
+ out = Origin()
106
+ for o in origins:
107
+ out = out.merge(o)
108
+ return out
76
109
 
77
110
 
78
111
  def parameters(func: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
@@ -100,51 +133,63 @@ def access_root(node: ast.AST) -> str | None:
100
133
  return _root_name(node)
101
134
 
102
135
 
103
- def _classify(expr: ast.AST, params: set[str], assigns: dict[str, list[ast.AST]], seen: frozenset[str]) -> Origin:
136
+ def reduce_value(
137
+ expr: ast.AST,
138
+ *,
139
+ params: set[str],
140
+ assigns: dict[str, list[ast.AST]],
141
+ combine,
142
+ leaf,
143
+ on_param,
144
+ on_global,
145
+ on_constant,
146
+ on_cycle,
147
+ seen: frozenset[str] = frozenset(),
148
+ ):
149
+ """Walk a value expression, folding it to a single result of the caller's type.
150
+
151
+ This is the structural recursion shared by provenance (P1-01) and taint
152
+ classification (P1-03): the composite forms that pass a value through (binops,
153
+ f-strings, conditionals, collections) and the local-name dispatch (parameter /
154
+ assignment chain / assignment cycle / free global) are handled here once. The
155
+ caller supplies the policy:
156
+
157
+ - ``combine(list)`` -> fold component results (also defines the empty result);
158
+ - ``on_constant() / on_param(name) / on_global(name) / on_cycle()`` -> leaf
159
+ results for a literal, a parameter, a free name, and an assignment cycle;
160
+ - ``leaf(node, recurse)`` -> everything else (calls, attribute/subscript
161
+ access, unmodelled nodes); ``recurse`` re-enters the walk for sub-expressions
162
+ (e.g. a taint propagator recursing into its arguments).
163
+ """
164
+
165
+ def recurse(e: ast.AST, _seen: frozenset[str] | None = None):
166
+ return reduce_value(
167
+ e, params=params, assigns=assigns, combine=combine, leaf=leaf,
168
+ on_param=on_param, on_global=on_global, on_constant=on_constant,
169
+ on_cycle=on_cycle, seen=seen if _seen is None else _seen,
170
+ )
171
+
104
172
  if isinstance(expr, ast.Constant):
105
- return _LITERAL
173
+ return on_constant()
106
174
  if isinstance(expr, ast.JoinedStr): # f-string: literal parts + interpolated exprs
107
- origin = _LITERAL
108
- for value in expr.values:
109
- if isinstance(value, ast.FormattedValue):
110
- origin = origin.merge(_classify(value.value, params, assigns, seen))
111
- return origin
175
+ return combine([on_constant()] + [recurse(v.value) for v in expr.values if isinstance(v, ast.FormattedValue)])
112
176
  if isinstance(expr, ast.BinOp):
113
- return _classify(expr.left, params, assigns, seen).merge(_classify(expr.right, params, assigns, seen))
114
- if isinstance(expr, (ast.BoolOp,)):
115
- return _merge_all(expr.values, params, assigns, seen)
116
- if isinstance(expr, ast.IfExp): # value is one branch or the other; the test does not flow in
117
- return _classify(expr.body, params, assigns, seen).merge(_classify(expr.orelse, params, assigns, seen))
177
+ return combine([recurse(expr.left), recurse(expr.right)])
178
+ if isinstance(expr, ast.BoolOp):
179
+ return combine([recurse(v) for v in expr.values])
180
+ if isinstance(expr, ast.IfExp): # one branch or the other; the test does not flow in
181
+ return combine([recurse(expr.body), recurse(expr.orelse)])
118
182
  if isinstance(expr, (ast.List, ast.Tuple, ast.Set)):
119
- return _merge_all(expr.elts, params, assigns, seen)
120
- if isinstance(expr, ast.Call):
121
- # the return value's taint depends on the callee's semantics, which P1-03
122
- # decides with the sanitizer/propagator catalog; here we just name the callee.
123
- name = _call_name(expr)
124
- return Origin(calls=frozenset({name})) if name else _UNKNOWN
125
- if isinstance(expr, (ast.Attribute, ast.Subscript)):
126
- dotted = _dotted(expr)
127
- origin = Origin(attrs=frozenset({dotted})) if dotted else _UNKNOWN
128
- root = _root_name(expr)
129
- if root in params: # e.g. request.args[...] where `request` is a parameter
130
- origin = origin.merge(Origin(params=frozenset({root})))
131
- return origin
183
+ return combine([recurse(e) for e in expr.elts])
132
184
  if isinstance(expr, ast.Name):
133
185
  if expr.id in seen: # assignment cycle -- stop
134
- return Origin()
186
+ return on_cycle()
135
187
  if expr.id in params:
136
- return Origin(params=frozenset({expr.id}))
188
+ return on_param(expr.id)
137
189
  if expr.id in assigns:
138
- return _merge_all(assigns[expr.id], params, assigns, seen | {expr.id})
139
- return Origin(globals_=frozenset({expr.id})) # module global, import, or builtin
140
- return _UNKNOWN
141
-
142
-
143
- def _merge_all(exprs: list[ast.AST], params, assigns, seen) -> Origin:
144
- origin = Origin()
145
- for e in exprs:
146
- origin = origin.merge(_classify(e, params, assigns, seen))
147
- return origin
190
+ return combine([recurse(r, seen | {expr.id}) for r in assigns[expr.id]])
191
+ return on_global(expr.id) # module global, import, or builtin
192
+ return leaf(expr, recurse)
148
193
 
149
194
 
150
195
  def _params(func: ast.AST) -> set[str]:
@@ -21,6 +21,7 @@ PARAM, for the cross-file caller hop (next) to resolve.
21
21
  from __future__ import annotations
22
22
 
23
23
  import ast
24
+ import functools
24
25
  from dataclasses import dataclass
25
26
  from enum import Enum
26
27
  from pathlib import Path
@@ -34,6 +35,7 @@ from codejury.analysis.provenance import (
34
35
  callee,
35
36
  find_calls,
36
37
  parameters,
38
+ reduce_value,
37
39
  )
38
40
  from codejury.resources import TAINT_FILE
39
41
 
@@ -107,59 +109,55 @@ def taint_of(
107
109
 
108
110
  ``resolve_param`` is an optional ``(name) -> Taint`` callback; when given, a
109
111
  value that reaches a parameter is resolved through it (the cross-file caller
110
- hop, P1-03b) instead of returning PARAM.
112
+ hop, P1-03b) instead of returning PARAM. Shares the structural recursion with
113
+ provenance via ``reduce_value``; this is the vocabulary-aware policy.
111
114
  """
112
- return _walk(func, expr, vocab, assignments(func), parameters(func), frozenset(), resolve_param)
113
-
114
-
115
- def _walk(func, expr, vocab, assigns, params, seen, resolve) -> Taint:
116
- def w(e):
117
- return _walk(func, e, vocab, assigns, params, seen, resolve)
118
-
119
- if isinstance(expr, ast.Constant):
120
- return Taint.CONSTANT
121
- if isinstance(expr, ast.JoinedStr):
122
- return _combine([Taint.CONSTANT] + [w(v.value) for v in expr.values if isinstance(v, ast.FormattedValue)])
123
- if isinstance(expr, ast.BinOp):
124
- return _combine([w(expr.left), w(expr.right)])
125
- if isinstance(expr, ast.BoolOp):
126
- return _combine([w(v) for v in expr.values])
127
- if isinstance(expr, ast.IfExp):
128
- return _combine([w(expr.body), w(expr.orelse)])
129
- if isinstance(expr, (ast.List, ast.Tuple, ast.Set)):
130
- return _combine([w(e) for e in expr.elts] or [Taint.CONSTANT])
131
- if isinstance(expr, ast.Call):
132
- if _callee_in(expr, vocab.sanitizers):
133
- return Taint.SANITIZED # a sanitizer cleans its result regardless of input
134
- if _callee_in(expr, vocab.sources):
135
- return Taint.EXTERNAL # e.g. input()
136
- if _callee_in(expr, vocab.propagators) or _callee_in(expr, vocab.safe_sinks):
137
- return _combine([w(a) for a in expr.args] or [Taint.CONSTANT])
138
- return Taint.UNKNOWN # unknown call -- a cross-file hop may resolve it later
139
- if isinstance(expr, (ast.Attribute, ast.Subscript)):
140
- path = access_path(expr)
141
- if path and _access_in(path, vocab.sources):
142
- return Taint.EXTERNAL
143
- if path and _access_in(path, vocab.trusted):
144
- return Taint.TRUSTED
145
- root = access_root(expr)
146
- if root in params: # attribute of a parameter -- resolve at call site
147
- return resolve(root) if resolve else Taint.PARAM
148
- return Taint.UNKNOWN # unknown object attribute (e.g. self.x): not provably safe
149
- if isinstance(expr, ast.Name):
150
- if expr.id in seen:
151
- return Taint.CONSTANT # assignment cycle: no new information
152
- if expr.id in params:
153
- return resolve(expr.id) if resolve else Taint.PARAM
154
- if expr.id in assigns:
155
- return _combine([_walk(func, r, vocab, assigns, params, seen | {expr.id}, resolve)
156
- for r in assigns[expr.id]])
157
- return Taint.TRUSTED # free module global / builtin -- conventionally a constant
158
- return Taint.UNKNOWN
115
+ params = parameters(func)
116
+
117
+ def on_param(name: str) -> Taint:
118
+ return resolve_param(name) if resolve_param else Taint.PARAM
119
+
120
+ def leaf(node: ast.AST, recurse) -> Taint:
121
+ if isinstance(node, ast.Call):
122
+ if _callee_in(node, vocab.sanitizers):
123
+ return Taint.SANITIZED # a sanitizer cleans its result regardless of input
124
+ if _callee_in(node, vocab.sources):
125
+ return Taint.EXTERNAL # e.g. input()
126
+ # a method ON a source/trusted object, e.g. request.args.get("id")
127
+ func_path = access_path(node.func)
128
+ if func_path and _access_in(func_path, vocab.sources):
129
+ return Taint.EXTERNAL
130
+ if func_path and _access_in(func_path, vocab.trusted):
131
+ return Taint.TRUSTED
132
+ if _callee_in(node, vocab.propagators) or _callee_in(node, vocab.safe_sinks):
133
+ return _combine([recurse(a) for a in node.args])
134
+ return Taint.UNKNOWN # unknown call -- a cross-file hop may resolve it later
135
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
136
+ path = access_path(node)
137
+ if path and _access_in(path, vocab.sources):
138
+ return Taint.EXTERNAL
139
+ if path and _access_in(path, vocab.trusted):
140
+ return Taint.TRUSTED
141
+ if access_root(node) in params: # attribute of a parameter -- resolve at the call site
142
+ return on_param(access_root(node))
143
+ return Taint.UNKNOWN # unknown object attribute (e.g. self.x): not provably safe
144
+ return Taint.UNKNOWN
145
+
146
+ return reduce_value(
147
+ expr,
148
+ params=params,
149
+ assigns=assignments(func),
150
+ combine=_combine,
151
+ leaf=leaf,
152
+ on_param=on_param,
153
+ on_global=lambda n: Taint.TRUSTED, # free module global / builtin -- conventionally a constant
154
+ on_constant=lambda: Taint.CONSTANT,
155
+ on_cycle=lambda: Taint.CONSTANT, # assignment cycle: no new information
156
+ )
159
157
 
160
158
 
161
159
  def _combine(taints: list[Taint]) -> Taint:
162
- return max(taints, key=lambda t: _RANK[t])
160
+ return max(taints, key=lambda t: _RANK[t]) if taints else Taint.CONSTANT
163
161
 
164
162
 
165
163
  def _callee_in(call: ast.Call, names: tuple[str, ...]) -> bool:
@@ -197,10 +195,11 @@ def taint_in_repo(
197
195
 
198
196
 
199
197
  def _caller_resolver(func, files, vocab):
198
+ sites = _call_sites(func.name, files) # computed once, reused across parameters
200
199
  def resolve(param_name: str) -> Taint:
201
200
  index = _param_index(func, param_name)
202
201
  results = []
203
- for scope, call in _call_sites(func.name, files):
202
+ for scope, call in sites:
204
203
  arg = _arg_for_param(call, index, param_name)
205
204
  # one hop only: classify the caller's argument without recursing further
206
205
  results.append(taint_of(scope, arg, vocab) if arg is not None else Taint.UNKNOWN)
@@ -210,6 +209,8 @@ def _caller_resolver(func, files, vocab):
210
209
 
211
210
  def _param_index(func, name: str) -> int | None:
212
211
  positional = [*func.args.posonlyargs, *func.args.args]
212
+ if positional and positional[0].arg in ("self", "cls"):
213
+ positional = positional[1:] # bound-method call sites omit the receiver
213
214
  for i, arg in enumerate(positional):
214
215
  if arg.arg == name:
215
216
  return i
@@ -225,12 +226,21 @@ def _arg_for_param(call: ast.Call, index: int | None, name: str) -> ast.AST | No
225
226
  return None
226
227
 
227
228
 
229
+ @functools.lru_cache(maxsize=256)
230
+ def _parse(source: str) -> ast.Module | None:
231
+ """Parse a source string once and cache it (the same files are walked repeatedly
232
+ during cross-file resolution); None if it does not parse."""
233
+ try:
234
+ return ast.parse(source)
235
+ except SyntaxError:
236
+ return None
237
+
238
+
228
239
  def _call_sites(name: str, files: dict[str, str]) -> list[tuple[ast.AST, ast.Call]]:
229
240
  sites = []
230
241
  for source in files.values():
231
- try:
232
- tree = ast.parse(source)
233
- except SyntaxError:
242
+ tree = _parse(source)
243
+ if tree is None:
234
244
  continue
235
245
  funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
236
246
  for call in find_calls(tree, name):
@@ -250,24 +260,27 @@ def worst_sink_taint(content: str, files: dict[str, str], vocab: TaintVocab) ->
250
260
 
251
261
  A "potential sink" is any call that is not a safe sink, sanitizer, or
252
262
  propagator (those are not where injection happens). Each such call's argument
253
- taint is classified with the cross-file resolver, and the worst is returned.
254
- ``Taint.CONSTANT`` when there is no sink to worry about; ``None`` when the
255
- code does not parse (the caller should then not act).
256
-
257
- Used by the taint gate to downgrade an input_validation finding only when the
258
- whole artifact is provably clean -- so a single tainted sink keeps every
259
- finding (recall preserved).
263
+ taint is classified with the cross-file resolver. A safe sink that consumes
264
+ tainted data (e.g. ``json.loads(request.data)``) contributes SANITIZED -- the
265
+ data was handled safely. The worst contribution is returned.
266
+
267
+ ``Taint.UNKNOWN`` when no inspectable sink is found (the artifact may still be
268
+ unsafe via a return value or an implicit sink, so it is NOT assumed clean);
269
+ ``None`` when the code does not parse. The taint gate downgrades a finding only
270
+ on a SAFE result, so an unproven artifact keeps its findings (recall preserved).
260
271
  """
261
- try:
262
- tree = ast.parse(content)
263
- except SyntaxError:
272
+ tree = _parse(content)
273
+ if tree is None:
264
274
  return None
265
275
  funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
266
276
  taints: list[Taint] = []
267
277
  for call in [n for n in ast.walk(tree) if isinstance(n, ast.Call)]:
268
- if is_safe_sink(call, vocab) or _callee_in(call, vocab.sanitizers) or _callee_in(call, vocab.propagators):
269
- continue # not a place an injection lands
278
+ if is_safe_sink(call, vocab):
279
+ taints.append(Taint.SANITIZED) # tainted data consumed by a safe parser
280
+ continue
281
+ if _callee_in(call, vocab.sanitizers) or _callee_in(call, vocab.propagators):
282
+ continue # not a sink itself; counted via the enclosing sink's argument
270
283
  scope = _enclosing_scope(funcs, call) or tree
271
284
  for arg in (*call.args, *(kw.value for kw in call.keywords)):
272
285
  taints.append(taint_in_repo(scope, arg, vocab, files))
273
- return _combine(taints) if taints else Taint.CONSTANT
286
+ return _combine(taints) if taints else Taint.UNKNOWN
@@ -18,6 +18,7 @@ from codejury.domain.context import AnalysisContext
18
18
  from codejury.domain.result import AnalysisResult
19
19
  from codejury.infrastructure.cache import VerdictCache, verdict_key
20
20
  from codejury.orchestrators.base import Orchestrator
21
+ from codejury.orchestrators.adaptive import AdaptiveOrchestrator
21
22
  from codejury.orchestrators.challenge import ChallengeOrchestrator
22
23
  from codejury.orchestrators.debate import DebateOrchestrator
23
24
  from codejury.orchestrators.pipeline import PipelineOrchestrator
@@ -31,7 +32,7 @@ from codejury.providers.openai import OpenAIProvider
31
32
  from codejury.providers.retry import RetryProvider
32
33
  from codejury.sources.base import Source
33
34
 
34
- STRATEGIES = ("single", "pipeline", "debate", "reflexion", "challenge", "taint")
35
+ STRATEGIES = ("single", "pipeline", "debate", "reflexion", "challenge", "taint", "adaptive")
35
36
  PROVIDERS = ("anthropic", "openai", "litellm")
36
37
  DEFAULT_MODEL = os.environ.get("CODEJURY_MODEL", "claude-sonnet-4-6")
37
38
  DEFAULT_API_BASE = os.environ.get("CODEJURY_API_BASE")
@@ -59,6 +60,11 @@ def build_orchestration(
59
60
  roles = (FinderAgent, ChallengerAgent, JudgeAgent)
60
61
  agents = {cls.role: cls(provider=provider, model=model, max_tokens=max_tokens) for cls in roles}
61
62
  return agents, DebateOrchestrator()
63
+ if strategy == "adaptive":
64
+ roles = (FinderAgent, ChallengerAgent, JudgeAgent)
65
+ agents = {cls.role: cls(provider=provider, model=model, max_tokens=max_tokens) for cls in roles}
66
+ agents["verifier"] = VerifierAgent(provider=provider, model=model, max_tokens=max_tokens)
67
+ return agents, AdaptiveOrchestrator()
62
68
  if strategy == "reflexion":
63
69
  agents = {
64
70
  "actor": FinderAgent(provider=provider, model=model, max_tokens=max_tokens),
@@ -79,9 +85,15 @@ def build_orchestration(
79
85
  return verifier, SingleOrchestrator()
80
86
 
81
87
 
82
- def orchestration_descriptor(strategy: str, model: str, max_tokens: int) -> str:
88
+ def provider_tag(provider: Provider) -> str:
89
+ """A stable short name for a provider (unwrapping RetryProvider) for cache keys,
90
+ so two providers that accept the same model string do not share cached verdicts."""
91
+ return type(getattr(provider, "_inner", provider)).__name__
92
+
93
+
94
+ def orchestration_descriptor(provider: Provider, strategy: str, model: str, max_tokens: int) -> str:
83
95
  """The non-code, non-capability inputs that affect a verdict, as a cache tag."""
84
- return f"{strategy}|{model}|{max_tokens}"
96
+ return f"{provider_tag(provider)}|{strategy}|{model}|{max_tokens}"
85
97
 
86
98
 
87
99
  def run_over_artifacts(
@@ -79,7 +79,7 @@ def audit(
79
79
  agents, orchestrator = build_orchestration(strategy, provider=provider, model=model, max_tokens=max_tokens)
80
80
  return run_over_source(
81
81
  DiffSource(diff_text), capabilities, agents, orchestrator,
82
- cache=cache, orchestration=orchestration_descriptor(strategy, model, max_tokens),
82
+ cache=cache, orchestration=orchestration_descriptor(provider, strategy, model, max_tokens),
83
83
  )
84
84
 
85
85
 
@@ -114,7 +114,7 @@ def scan(
114
114
  agents, orchestrator = build_orchestration(strategy, provider=provider, model=model, max_tokens=max_tokens)
115
115
  return run_over_artifacts(
116
116
  artifacts, capabilities, agents, orchestrator,
117
- cache=cache, orchestration=orchestration_descriptor(strategy, model, max_tokens),
117
+ cache=cache, orchestration=orchestration_descriptor(provider, strategy, model, max_tokens),
118
118
  )
119
119
 
120
120
 
@@ -159,8 +159,19 @@ def evaluate(
159
159
  capabilities=[capability],
160
160
  )
161
161
  result = orchestrator.run(agents, ctx)
162
- if result.error: # e.g. a provider auth failure -- surface it, don't score blanks
163
- raise RuntimeError(result.error)
164
- predicted = any(getattr(o, "status", None) == "VULNERABLE" for o in result.observations)
162
+ if result.error: # e.g. a provider auth failure -- surface it (with the case), don't score blanks
163
+ raise RuntimeError(f"case {case.name!r}: {result.error}")
164
+ predicted = _predicted_vulnerable(result.observations)
165
165
  report.record(case.capability, actual=case.vulnerable, predicted=predicted)
166
166
  return report
167
+
168
+
169
+ def _predicted_vulnerable(observations: list) -> bool:
170
+ """Did the run flag a problem? A Finding (debate/reflexion) or a VULNERABLE
171
+ Verdict (single/pipeline/taint) counts; SECURE verdicts and dismissed
172
+ concessions do not. (A bare Finding has no ``status``, so checking only
173
+ status would score debate/reflexion as zero recall.)"""
174
+ return any(
175
+ o.kind == "finding" or (o.kind == "verdict" and getattr(o, "status", None) == "VULNERABLE")
176
+ for o in observations
177
+ )
@@ -0,0 +1,55 @@
1
+ """AdaptiveOrchestrator -- cheap triage, deep debate only where it pays off.
2
+
3
+ Most files are clean, and debate is several model calls; running it everywhere
4
+ wastes them. So this runs the single verifier first, then escalates to a full
5
+ Finder -> Challenger -> Judge debate only when the cheap pass found something
6
+ worth a second opinion:
7
+
8
+ - **high-risk**: any VULNERABLE verdict -- a flagged vulnerability is verified by
9
+ the adversarial pass before it is reported / gates CI;
10
+ - **disputed**: a PARTIAL or UNKNOWN verdict the verifier is unsure about
11
+ (confidence below the threshold).
12
+
13
+ A clean artifact (only confident SECURE / NOT_PRESENT verdicts) pays one verifier
14
+ call and stops. An escalated artifact is handed to the debate, whose deeper
15
+ ruling supersedes the triage verdicts for that artifact.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from codejury.agents.base import Agent
21
+ from codejury.domain.context import AnalysisContext
22
+ from codejury.domain.observation import Verdict
23
+ from codejury.domain.result import AnalysisResult
24
+ from codejury.orchestrators.base import Orchestrator
25
+ from codejury.orchestrators.debate import DebateOrchestrator
26
+
27
+ _DISPUTABLE = ("PARTIAL", "UNKNOWN")
28
+
29
+
30
+ class AdaptiveOrchestrator(Orchestrator):
31
+ def __init__(self, *, confidence_threshold: float = 0.7, debate: DebateOrchestrator | None = None) -> None:
32
+ self._threshold = confidence_threshold
33
+ self._debate = debate if debate is not None else DebateOrchestrator()
34
+
35
+ def run(self, agents: dict[str, Agent], context: AnalysisContext) -> AnalysisResult:
36
+ if "verifier" not in agents:
37
+ return AnalysisResult(error="adaptive requires agents: verifier")
38
+ try:
39
+ verdicts = agents["verifier"].run(context)
40
+ except Exception as exc:
41
+ return AnalysisResult(error=f"agent 'verifier' failed: {exc}")
42
+
43
+ if not self._needs_escalation(verdicts):
44
+ return AnalysisResult(observations=verdicts) # cheap path: confident, low-risk
45
+ return self._debate.run(agents, context) # deep path supersedes the triage
46
+
47
+ def _needs_escalation(self, verdicts: list) -> bool:
48
+ for v in verdicts:
49
+ if not isinstance(v, Verdict):
50
+ continue
51
+ if v.status == "VULNERABLE": # high-risk: verify a flagged vuln adversarially
52
+ return True
53
+ if v.status in _DISPUTABLE and v.confidence < self._threshold: # disputed
54
+ return True
55
+ return False
@@ -58,7 +58,7 @@ class TaintGateOrchestrator(Orchestrator):
58
58
  for v in verdicts:
59
59
  if (
60
60
  isinstance(v, Verdict)
61
- and v.status in ("VULNERABLE", "PARTIAL")
61
+ and v.status == "VULNERABLE" # leave PARTIAL ("incomplete validation") signal intact
62
62
  and v.capability.split(".")[0] in self._taint_capabilities
63
63
  ):
64
64
  observations.append(
@@ -9,8 +9,8 @@ dismissed.
9
9
  from __future__ import annotations
10
10
 
11
11
  import json
12
- from importlib.metadata import PackageNotFoundError, version
13
12
 
13
+ from codejury import __version__ as _tool_version
14
14
  from codejury.domain.observation import Observation, observation_from_dict
15
15
  from codejury.domain.result import AnalysisResult
16
16
 
@@ -24,13 +24,6 @@ _PROBLEM_STATUSES = ("VULNERABLE", "PARTIAL")
24
24
  _SARIF_LEVEL = {"CRITICAL": "error", "HIGH": "error", "MEDIUM": "warning", "LOW": "note", "INFO": "note"}
25
25
 
26
26
 
27
- def _tool_version() -> str:
28
- try:
29
- return version("codejury")
30
- except PackageNotFoundError:
31
- return "0"
32
-
33
-
34
27
  def to_json(results: Results) -> str:
35
28
  payload = {
36
29
  "files": [
@@ -183,7 +176,7 @@ def to_sarif(results: Results) -> str:
183
176
  "driver": {
184
177
  "name": "codejury",
185
178
  "informationUri": "https://github.com/aiseclabs/codejury",
186
- "version": _tool_version(),
179
+ "version": _tool_version,
187
180
  "rules": rules,
188
181
  }
189
182
  },