codejury 0.9.1__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.1 → codejury-0.10.0}/PKG-INFO +7 -2
  2. {codejury-0.9.1 → codejury-0.10.0}/README.md +6 -1
  3. {codejury-0.9.1 → codejury-0.10.0}/codejury/analysis/provenance.py +83 -38
  4. {codejury-0.9.1 → codejury-0.10.0}/codejury/analysis/taint.py +46 -55
  5. {codejury-0.9.1 → codejury-0.10.0}/codejury/assembly.py +7 -1
  6. codejury-0.10.0/codejury/orchestrators/adaptive.py +55 -0
  7. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/PKG-INFO +7 -2
  8. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/SOURCES.txt +2 -0
  9. {codejury-0.9.1 → codejury-0.10.0}/pyproject.toml +1 -1
  10. codejury-0.10.0/tests/test_adaptive.py +71 -0
  11. {codejury-0.9.1 → codejury-0.10.0}/LICENSE +0 -0
  12. {codejury-0.9.1 → codejury-0.10.0}/codejury/__init__.py +0 -0
  13. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/__init__.py +0 -0
  14. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/base.py +0 -0
  15. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/debate.py +0 -0
  16. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/mock.py +0 -0
  17. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/parsing.py +0 -0
  18. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/refuter.py +0 -0
  19. {codejury-0.9.1 → codejury-0.10.0}/codejury/agents/verifier.py +0 -0
  20. {codejury-0.9.1 → codejury-0.10.0}/codejury/analysis/__init__.py +0 -0
  21. {codejury-0.9.1 → codejury-0.10.0}/codejury/baseline.py +0 -0
  22. {codejury-0.9.1 → codejury-0.10.0}/codejury/cli.py +0 -0
  23. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/authentication.yaml +0 -0
  24. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/authorization.yaml +0 -0
  25. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/business_logic.yaml +0 -0
  26. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/crypto.yaml +0 -0
  27. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/data_protection.yaml +0 -0
  28. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/dependency_config.yaml +0 -0
  29. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/error_logging.yaml +0 -0
  30. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/excessive_agency.yaml +0 -0
  31. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/input_validation.yaml +0 -0
  32. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
  33. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/output_encoding.yaml +0 -0
  34. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/prompt_injection.yaml +0 -0
  35. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/secrets.yaml +0 -0
  36. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/capabilities/session.yaml +0 -0
  37. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
  38. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
  39. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
  40. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
  41. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
  42. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
  43. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
  44. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
  45. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
  46. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
  47. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_sha256_password.yaml +0 -0
  48. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
  49. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
  50. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/authz_owner_safe.yaml +0 -0
  51. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
  52. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
  53. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
  54. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
  55. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
  56. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
  57. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
  58. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
  59. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
  60. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
  61. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
  62. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
  63. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
  64. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
  65. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
  66. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
  67. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
  68. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
  69. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
  70. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
  71. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
  72. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/literal_eval_safe.yaml +0 -0
  73. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/path_basename_safe.yaml +0 -0
  74. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/path_contained_safe.yaml +0 -0
  75. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
  76. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
  77. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
  78. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
  79. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
  80. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
  81. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
  82. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/secrets_env_safe.yaml +0 -0
  83. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
  84. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
  85. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
  86. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
  87. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
  88. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
  89. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
  90. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
  91. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
  92. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
  93. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
  94. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
  95. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
  96. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
  97. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
  98. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
  99. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
  100. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
  101. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
  102. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/suppressions.yaml +0 -0
  103. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/taint.yaml +0 -0
  104. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
  105. {codejury-0.9.1 → codejury-0.10.0}/codejury/data/tasks/quick_scan_single.yaml +0 -0
  106. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/__init__.py +0 -0
  107. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/artifact.py +0 -0
  108. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/capability.py +0 -0
  109. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/context.py +0 -0
  110. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/observation.py +0 -0
  111. {codejury-0.9.1 → codejury-0.10.0}/codejury/domain/result.py +0 -0
  112. {codejury-0.9.1 → codejury-0.10.0}/codejury/evaluation.py +0 -0
  113. {codejury-0.9.1 → codejury-0.10.0}/codejury/infrastructure/__init__.py +0 -0
  114. {codejury-0.9.1 → codejury-0.10.0}/codejury/infrastructure/cache.py +0 -0
  115. {codejury-0.9.1 → codejury-0.10.0}/codejury/infrastructure/json_parse.py +0 -0
  116. {codejury-0.9.1 → codejury-0.10.0}/codejury/integrations/__init__.py +0 -0
  117. {codejury-0.9.1 → codejury-0.10.0}/codejury/integrations/github.py +0 -0
  118. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/__init__.py +0 -0
  119. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/base.py +0 -0
  120. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/challenge.py +0 -0
  121. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/debate.py +0 -0
  122. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/pipeline.py +0 -0
  123. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/reflexion.py +0 -0
  124. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/single.py +0 -0
  125. {codejury-0.9.1 → codejury-0.10.0}/codejury/orchestrators/taint_gate.py +0 -0
  126. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/__init__.py +0 -0
  127. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/anthropic.py +0 -0
  128. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/base.py +0 -0
  129. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/litellm.py +0 -0
  130. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/mock.py +0 -0
  131. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/openai.py +0 -0
  132. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/openai_format.py +0 -0
  133. {codejury-0.9.1 → codejury-0.10.0}/codejury/providers/retry.py +0 -0
  134. {codejury-0.9.1 → codejury-0.10.0}/codejury/reporting.py +0 -0
  135. {codejury-0.9.1 → codejury-0.10.0}/codejury/resources.py +0 -0
  136. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/__init__.py +0 -0
  137. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/base.py +0 -0
  138. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/callers.py +0 -0
  139. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/chunker.py +0 -0
  140. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/diff.py +0 -0
  141. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/function.py +0 -0
  142. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/mock.py +0 -0
  143. {codejury-0.9.1 → codejury-0.10.0}/codejury/sources/repo.py +0 -0
  144. {codejury-0.9.1 → codejury-0.10.0}/codejury/suppression.py +0 -0
  145. {codejury-0.9.1 → codejury-0.10.0}/codejury/tasks/__init__.py +0 -0
  146. {codejury-0.9.1 → codejury-0.10.0}/codejury/tasks/base.py +0 -0
  147. {codejury-0.9.1 → codejury-0.10.0}/codejury/tasks/registry.py +0 -0
  148. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/dependency_links.txt +0 -0
  149. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/entry_points.txt +0 -0
  150. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/requires.txt +0 -0
  151. {codejury-0.9.1 → codejury-0.10.0}/codejury.egg-info/top_level.txt +0 -0
  152. {codejury-0.9.1 → codejury-0.10.0}/setup.cfg +0 -0
  153. {codejury-0.9.1 → codejury-0.10.0}/tests/test_anthropic_provider.py +0 -0
  154. {codejury-0.9.1 → codejury-0.10.0}/tests/test_assembly.py +0 -0
  155. {codejury-0.9.1 → codejury-0.10.0}/tests/test_audit_pipeline.py +0 -0
  156. {codejury-0.9.1 → codejury-0.10.0}/tests/test_baseline.py +0 -0
  157. {codejury-0.9.1 → codejury-0.10.0}/tests/test_cache.py +0 -0
  158. {codejury-0.9.1 → codejury-0.10.0}/tests/test_callers.py +0 -0
  159. {codejury-0.9.1 → codejury-0.10.0}/tests/test_capability.py +0 -0
  160. {codejury-0.9.1 → codejury-0.10.0}/tests/test_challenge.py +0 -0
  161. {codejury-0.9.1 → codejury-0.10.0}/tests/test_cli_audit.py +0 -0
  162. {codejury-0.9.1 → codejury-0.10.0}/tests/test_context.py +0 -0
  163. {codejury-0.9.1 → codejury-0.10.0}/tests/test_debate_agents.py +0 -0
  164. {codejury-0.9.1 → codejury-0.10.0}/tests/test_debate_orchestrator.py +0 -0
  165. {codejury-0.9.1 → codejury-0.10.0}/tests/test_diff_source.py +0 -0
  166. {codejury-0.9.1 → codejury-0.10.0}/tests/test_evaluation.py +0 -0
  167. {codejury-0.9.1 → codejury-0.10.0}/tests/test_function_source.py +0 -0
  168. {codejury-0.9.1 → codejury-0.10.0}/tests/test_integrations.py +0 -0
  169. {codejury-0.9.1 → codejury-0.10.0}/tests/test_json_parse.py +0 -0
  170. {codejury-0.9.1 → codejury-0.10.0}/tests/test_litellm_provider.py +0 -0
  171. {codejury-0.9.1 → codejury-0.10.0}/tests/test_openai_provider.py +0 -0
  172. {codejury-0.9.1 → codejury-0.10.0}/tests/test_orchestrator.py +0 -0
  173. {codejury-0.9.1 → codejury-0.10.0}/tests/test_pipeline_orchestrator.py +0 -0
  174. {codejury-0.9.1 → codejury-0.10.0}/tests/test_provenance.py +0 -0
  175. {codejury-0.9.1 → codejury-0.10.0}/tests/test_reflexion_orchestrator.py +0 -0
  176. {codejury-0.9.1 → codejury-0.10.0}/tests/test_repo_source.py +0 -0
  177. {codejury-0.9.1 → codejury-0.10.0}/tests/test_reporting.py +0 -0
  178. {codejury-0.9.1 → codejury-0.10.0}/tests/test_retry_provider.py +0 -0
  179. {codejury-0.9.1 → codejury-0.10.0}/tests/test_sarif.py +0 -0
  180. {codejury-0.9.1 → codejury-0.10.0}/tests/test_suppression.py +0 -0
  181. {codejury-0.9.1 → codejury-0.10.0}/tests/test_taint.py +0 -0
  182. {codejury-0.9.1 → codejury-0.10.0}/tests/test_taint_crossfile.py +0 -0
  183. {codejury-0.9.1 → codejury-0.10.0}/tests/test_taint_gate.py +0 -0
  184. {codejury-0.9.1 → codejury-0.10.0}/tests/test_tasks.py +0 -0
  185. {codejury-0.9.1 → 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.1
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.
@@ -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]:
@@ -35,6 +35,7 @@ from codejury.analysis.provenance import (
35
35
  callee,
36
36
  find_calls,
37
37
  parameters,
38
+ reduce_value,
38
39
  )
39
40
  from codejury.resources import TAINT_FILE
40
41
 
@@ -108,65 +109,55 @@ def taint_of(
108
109
 
109
110
  ``resolve_param`` is an optional ``(name) -> Taint`` callback; when given, a
110
111
  value that reaches a parameter is resolved through it (the cross-file caller
111
- 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.
112
114
  """
113
- return _walk(func, expr, vocab, assignments(func), parameters(func), frozenset(), resolve_param)
114
-
115
-
116
- def _walk(func, expr, vocab, assigns, params, seen, resolve) -> Taint:
117
- def w(e):
118
- return _walk(func, e, vocab, assigns, params, seen, resolve)
119
-
120
- if isinstance(expr, ast.Constant):
121
- return Taint.CONSTANT
122
- if isinstance(expr, ast.JoinedStr):
123
- return _combine([Taint.CONSTANT] + [w(v.value) for v in expr.values if isinstance(v, ast.FormattedValue)])
124
- if isinstance(expr, ast.BinOp):
125
- return _combine([w(expr.left), w(expr.right)])
126
- if isinstance(expr, ast.BoolOp):
127
- return _combine([w(v) for v in expr.values])
128
- if isinstance(expr, ast.IfExp):
129
- return _combine([w(expr.body), w(expr.orelse)])
130
- if isinstance(expr, (ast.List, ast.Tuple, ast.Set)):
131
- return _combine([w(e) for e in expr.elts] or [Taint.CONSTANT])
132
- if isinstance(expr, ast.Call):
133
- if _callee_in(expr, vocab.sanitizers):
134
- return Taint.SANITIZED # a sanitizer cleans its result regardless of input
135
- if _callee_in(expr, vocab.sources):
136
- return Taint.EXTERNAL # e.g. input()
137
- # a method ON a source/trusted object, e.g. request.args.get("id") or os.environ.get("X")
138
- func_path = access_path(expr.func)
139
- if func_path and _access_in(func_path, vocab.sources):
140
- return Taint.EXTERNAL
141
- if func_path and _access_in(func_path, vocab.trusted):
142
- return Taint.TRUSTED
143
- if _callee_in(expr, vocab.propagators) or _callee_in(expr, vocab.safe_sinks):
144
- return _combine([w(a) for a in expr.args] or [Taint.CONSTANT])
145
- return Taint.UNKNOWN # unknown call -- a cross-file hop may resolve it later
146
- if isinstance(expr, (ast.Attribute, ast.Subscript)):
147
- path = access_path(expr)
148
- if path and _access_in(path, vocab.sources):
149
- return Taint.EXTERNAL
150
- if path and _access_in(path, vocab.trusted):
151
- return Taint.TRUSTED
152
- root = access_root(expr)
153
- if root in params: # attribute of a parameter -- resolve at call site
154
- return resolve(root) if resolve else Taint.PARAM
155
- return Taint.UNKNOWN # unknown object attribute (e.g. self.x): not provably safe
156
- if isinstance(expr, ast.Name):
157
- if expr.id in seen:
158
- return Taint.CONSTANT # assignment cycle: no new information
159
- if expr.id in params:
160
- return resolve(expr.id) if resolve else Taint.PARAM
161
- if expr.id in assigns:
162
- return _combine([_walk(func, r, vocab, assigns, params, seen | {expr.id}, resolve)
163
- for r in assigns[expr.id]])
164
- return Taint.TRUSTED # free module global / builtin -- conventionally a constant
165
- 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
+ )
166
157
 
167
158
 
168
159
  def _combine(taints: list[Taint]) -> Taint:
169
- return max(taints, key=lambda t: _RANK[t])
160
+ return max(taints, key=lambda t: _RANK[t]) if taints else Taint.CONSTANT
170
161
 
171
162
 
172
163
  def _callee_in(call: ast.Call, names: tuple[str, ...]) -> bool:
@@ -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),
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.9.1
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.
@@ -120,6 +120,7 @@ codejury/infrastructure/json_parse.py
120
120
  codejury/integrations/__init__.py
121
121
  codejury/integrations/github.py
122
122
  codejury/orchestrators/__init__.py
123
+ codejury/orchestrators/adaptive.py
123
124
  codejury/orchestrators/base.py
124
125
  codejury/orchestrators/challenge.py
125
126
  codejury/orchestrators/debate.py
@@ -146,6 +147,7 @@ codejury/sources/repo.py
146
147
  codejury/tasks/__init__.py
147
148
  codejury/tasks/base.py
148
149
  codejury/tasks/registry.py
150
+ tests/test_adaptive.py
149
151
  tests/test_anthropic_provider.py
150
152
  tests/test_assembly.py
151
153
  tests/test_audit_pipeline.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  description = "General-purpose Application Security AI audit framework -- five-layer architecture, capabilities as first-class data"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,71 @@
1
+ import json
2
+
3
+ from codejury.assembly import build_orchestration
4
+ from codejury.domain.artifact import CodeArtifact
5
+ from codejury.domain.capability import Capability
6
+ from codejury.domain.context import AnalysisContext
7
+ from codejury.orchestrators.adaptive import AdaptiveOrchestrator
8
+ from codejury.providers.mock import MockProvider
9
+
10
+
11
+ def _verdict(status, confidence=0.9):
12
+ return json.dumps({"verdicts": [{"sub_capability": "x", "status": status, "confidence": confidence}]})
13
+
14
+
15
+ # A finder/judge JSON so an escalated debate produces a recognizable Finding.
16
+ _FINDING = json.dumps({"findings": [{"title": "deep issue", "severity": "HIGH"}]})
17
+
18
+
19
+ def _ctx():
20
+ return AnalysisContext(
21
+ artifact=CodeArtifact(kind="file", path="a.py", content="code"),
22
+ capabilities=[Capability(id="input_validation", name="input_validation")],
23
+ )
24
+
25
+
26
+ def _build(provider):
27
+ return build_orchestration("adaptive", provider=provider, model="m", max_tokens=8)
28
+
29
+
30
+ def test_clean_artifact_does_not_escalate():
31
+ # confident SECURE -> cheap path: the verifier's verdict is returned as-is,
32
+ # and the debate agents are never queried.
33
+ provider = MockProvider(default=_verdict("SECURE"))
34
+ agents, orch = _build(provider)
35
+ result = orch.run(agents, _ctx())
36
+ assert [v.status for v in result.observations if v.kind == "verdict"] == ["SECURE"]
37
+ assert len(provider.calls) == 1 # only the verifier ran
38
+
39
+
40
+ def test_vulnerable_verdict_escalates_to_debate():
41
+ # high-risk: a VULNERABLE verdict triggers the debate (more than one model call).
42
+ provider = MockProvider(responses=[_verdict("VULNERABLE")], default=_FINDING)
43
+ agents, orch = _build(provider)
44
+ orch.run(agents, _ctx())
45
+ assert len(provider.calls) > 1 # verifier + debate rounds
46
+
47
+
48
+ def test_low_confidence_partial_escalates():
49
+ provider = MockProvider(responses=[_verdict("PARTIAL", confidence=0.3)], default=_FINDING)
50
+ agents, orch = _build(provider)
51
+ orch.run(agents, _ctx())
52
+ assert len(provider.calls) > 1
53
+
54
+
55
+ def test_confident_partial_does_not_escalate():
56
+ provider = MockProvider(default=_verdict("PARTIAL", confidence=0.95))
57
+ agents, orch = _build(provider)
58
+ orch.run(agents, _ctx())
59
+ assert len(provider.calls) == 1 # confident enough -> no debate
60
+
61
+
62
+ def test_unknown_low_confidence_escalates():
63
+ provider = MockProvider(responses=[_verdict("UNKNOWN", confidence=0.5)], default=_FINDING)
64
+ agents, orch = _build(provider)
65
+ orch.run(agents, _ctx())
66
+ assert len(provider.calls) > 1
67
+
68
+
69
+ def test_missing_verifier_errors():
70
+ result = AdaptiveOrchestrator().run({}, _ctx())
71
+ assert result.error and "verifier" in result.error
File without changes
File without changes
File without changes
File without changes