claude-dev-env 1.60.0 → 1.61.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.
- package/CLAUDE.md +4 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_enforcer.py +8 -0
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
- package/hooks/blocking/config/verified_commit_constants.py +14 -2
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
- package/hooks/blocking/test_destructive_command_blocker.py +213 -0
- package/hooks/blocking/test_verification_verdict_store.py +212 -0
- package/hooks/blocking/test_verified_commit_gate.py +127 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +74 -95
- package/hooks/blocking/verification_verdict_store.py +240 -0
- package/hooks/blocking/verified_commit_gate.py +20 -8
- package/hooks/blocking/verifier_verdict_minter.py +46 -124
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/orphan-css-class.md +23 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +202 -13
- package/skills/autoconverge/workflow/converge.mjs +392 -51
- package/skills/autoconverge/workflow/test_render_report.py +30 -0
|
@@ -247,6 +247,10 @@ def test_rm_rf_asks_when_any_target_is_non_ephemeral() -> None:
|
|
|
247
247
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
def test_rm_rf_asks_when_target_has_nested_temp_segment_not_at_root() -> None:
|
|
251
|
+
_assert_hook_asks("rm -rf /home/victim/temp/secret")
|
|
252
|
+
|
|
253
|
+
|
|
250
254
|
def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_target() -> None:
|
|
251
255
|
payload = _make_bash_payload("rm -rf -- /tmp/scratch -non_ephemeral")
|
|
252
256
|
|
|
@@ -404,6 +408,26 @@ def test_rm_rf_asks_when_tool_input_cwd_is_ephemeral_but_rm_target_is_absolute_n
|
|
|
404
408
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
405
409
|
|
|
406
410
|
|
|
411
|
+
def test_rm_rf_asks_when_subshell_cd_changes_dir_before_relative_rm() -> None:
|
|
412
|
+
_assert_hook_asks('cd "/tmp/scratch" && (cd /; rm -rf etc)')
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_rm_rf_asks_when_second_top_level_cd_changes_dir_before_relative_rm() -> None:
|
|
416
|
+
_assert_hook_asks('cd "/tmp/scratch" && cd / && rm -rf etc')
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_rm_rf_asks_when_pushd_changes_dir_before_relative_rm() -> None:
|
|
420
|
+
_assert_hook_asks('cd "/tmp/scratch" && pushd / && rm -rf etc')
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_rm_rf_allowed_when_subshell_cd_present_but_rm_target_is_absolute_ephemeral() -> None:
|
|
424
|
+
_assert_hook_allows('cd "/tmp/scratch" && (cd /; rm -rf /tmp/scratch/keep)')
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_target_has_nested_tmp_segment_not_at_root() -> None:
|
|
428
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /home/victim/tmp/secret')
|
|
429
|
+
|
|
430
|
+
|
|
407
431
|
def test_git_push_force_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
408
432
|
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && git push --force')
|
|
409
433
|
|
|
@@ -1038,6 +1062,14 @@ def test_compound_rm_allowed_when_two_absolute_ephemeral_targets_then_echo() ->
|
|
|
1038
1062
|
_assert_hook_allows("rm -rf /tmp/pr136 /tmp/difftest && echo 'cleaned'")
|
|
1039
1063
|
|
|
1040
1064
|
|
|
1065
|
+
def test_compound_rm_allowed_when_subshell_paren_glued_rm_targets_absolute_ephemeral() -> None:
|
|
1066
|
+
_assert_hook_allows("rm -rf /tmp/a && (rm -rf /tmp/b)")
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def test_compound_rm_asks_when_subshell_paren_glued_rm_targets_non_ephemeral() -> None:
|
|
1070
|
+
_assert_hook_asks("rm -rf /tmp/a && (rm -rf /etc)")
|
|
1071
|
+
|
|
1072
|
+
|
|
1041
1073
|
def test_compound_rm_allowed_when_followed_by_gh_pipeline_and_echo() -> None:
|
|
1042
1074
|
_assert_hook_allows('rm -rf /tmp/reply && gh pr checks 19 2>&1 | head -5 && echo "x"')
|
|
1043
1075
|
|
|
@@ -1142,6 +1174,14 @@ def test_compound_rm_asks_when_second_rm_target_glues_redirect_to_non_ephemeral_
|
|
|
1142
1174
|
_assert_hook_asks("rm -rf /tmp/a /tmp/b>/etc/hosts")
|
|
1143
1175
|
|
|
1144
1176
|
|
|
1177
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_rm_segment_redirects_to_non_ephemeral_file() -> None:
|
|
1178
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /tmp/x>/etc/passwd')
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_relative_rm_target_redirects_to_non_ephemeral_file() -> None:
|
|
1182
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf build>/etc/passwd')
|
|
1183
|
+
|
|
1184
|
+
|
|
1145
1185
|
def test_compound_rm_asks_when_git_config_sets_value_after_ephemeral_rm() -> None:
|
|
1146
1186
|
_assert_hook_asks("rm -rf /tmp/x && git config --global user.name evil")
|
|
1147
1187
|
|
|
@@ -1517,6 +1557,26 @@ def test_subshell_grouped_rm_asks_when_benign_command_precedes_grouped_rm() -> N
|
|
|
1517
1557
|
_assert_hook_asks("echo hi; (rm -rf /etc)")
|
|
1518
1558
|
|
|
1519
1559
|
|
|
1560
|
+
def test_string_execution_asks_when_subshell_paren_glued_to_bash_dash_c() -> None:
|
|
1561
|
+
_assert_hook_asks('cd "/tmp/scratch" && (bash -c \'rm -rf /etc\')')
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
def test_string_execution_asks_when_subshell_paren_glued_to_timeout_wrapping_bash() -> None:
|
|
1565
|
+
_assert_hook_asks('cd "/tmp/scratch" && (timeout 5 bash -c \'rm -rf /etc\')')
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_subshell_paren_glued_to_rm_targets_etc() -> None:
|
|
1569
|
+
_assert_hook_asks('cd "/tmp/scratch" && (rm -rf /etc)')
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_brace_glued_to_rm_targets_etc() -> None:
|
|
1573
|
+
_assert_hook_asks('cd "/tmp/scratch" && {rm -rf /etc;}')
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def test_rm_rf_allowed_when_cd_ephemeral_and_subshell_paren_wraps_relative_ephemeral_target() -> None:
|
|
1577
|
+
_assert_hook_allows('cd "/tmp/scratch" && (rm -rf build)')
|
|
1578
|
+
|
|
1579
|
+
|
|
1520
1580
|
# --- convergence branch exemption unit tests ---
|
|
1521
1581
|
|
|
1522
1582
|
import importlib.util
|
|
@@ -1773,3 +1833,156 @@ def test_launcher_execution_allows_when_timeout_infinity_wraps_ephemeral_rm() ->
|
|
|
1773
1833
|
|
|
1774
1834
|
def test_launcher_execution_allows_when_timeout_seconds_wraps_ephemeral_rm() -> None:
|
|
1775
1835
|
_assert_hook_allows("timeout 5 rm -rf /tmp/scratch")
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def test_rm_rf_allowed_when_cd_worktree_then_temp_env_var_rm_then_mkdir_tar_compound() -> None:
|
|
1839
|
+
_assert_hook_allows(
|
|
1840
|
+
'cd "/Users/dev/proj/.git/worktrees/spindle" '
|
|
1841
|
+
'&& rm -rf "$TEMP/pr621_check" '
|
|
1842
|
+
'&& mkdir -p "$TEMP/pr621_check" '
|
|
1843
|
+
"&& git archive HEAD packages | tar -x -C \"$TEMP/pr621_check\" "
|
|
1844
|
+
'&& ls "$TEMP/pr621_check/packages" | head -40'
|
|
1845
|
+
)
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def test_rm_rf_allowed_when_cd_worktree_then_find_exec_rm_then_pytest_compound() -> None:
|
|
1849
|
+
_assert_hook_allows(
|
|
1850
|
+
'cd "/Users/dev/proj/worktrees/os-update-system" '
|
|
1851
|
+
'&& find shared_utils/samsung_utils -name "__pycache__" -type d '
|
|
1852
|
+
"-exec rm -rf {} + 2>/dev/null"
|
|
1853
|
+
'; PYTHONPATH="/Users/dev/proj/worktrees/os-update-system" '
|
|
1854
|
+
'C:/Python313/python.exe -m pytest "tests/" -p no:cacheprovider -q 2>&1 | tail -15'
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
def test_rm_rf_allowed_when_cd_ephemeral_and_sibling_mkdir_has_dash_p_flag() -> None:
|
|
1859
|
+
_assert_hook_allows('cd "/tmp/scratch" && rm -rf build && mkdir -p out')
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
def test_rm_rf_allowed_when_cd_ephemeral_and_rm_target_uses_temp_env_var() -> None:
|
|
1863
|
+
_assert_hook_allows('cd "/tmp/scratch" && rm -rf "$TEMP/build"')
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_bash_dash_c_executes_rm_on_non_ephemeral() -> None:
|
|
1867
|
+
_assert_hook_asks("cd \"/tmp/scratch\" && rm -rf build && bash -c 'rm -rf /etc'")
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_rm_target_uses_non_temp_env_var() -> None:
|
|
1871
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf "$HOME/important"')
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
def test_rm_rf_asks_when_cd_ephemeral_and_second_rm_segment_targets_non_ephemeral() -> None:
|
|
1875
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf build && rm -rf /etc/passwd')
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_bin_rm_targets_non_ephemeral() -> None:
|
|
1879
|
+
_assert_hook_asks('cd "/tmp/scratch" && /bin/rm -rf /etc')
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_target_is_command_substitution() -> None:
|
|
1883
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf $(somecmd)')
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_target_is_brace_expansion_escaping_namespace() -> None:
|
|
1887
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf {build,/etc}')
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_temp_var_splices_after_absolute_literal_prefix() -> None:
|
|
1891
|
+
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /data$TMP/x')
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_rm_search_root_escapes_namespace() -> None:
|
|
1895
|
+
_assert_hook_asks('cd "/tmp/scratch" && find /etc -name x -exec rm -rf {} +')
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_subshell_find_exec_rm_search_root_escapes() -> None:
|
|
1899
|
+
_assert_hook_asks('cd "/tmp/scratch" && (find /etc -exec rm -rf {} +)')
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
def test_rm_rf_asks_when_find_exec_rm_safe_but_sibling_standalone_rm_targets_non_ephemeral() -> None:
|
|
1903
|
+
_assert_hook_asks(
|
|
1904
|
+
'cd "/tmp/scratch" && find . -name x -exec rm -rf {} + ; rm -rf /etc/passwd'
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_rm_redirects_to_non_ephemeral_file() -> None:
|
|
1909
|
+
_assert_hook_asks('cd "/tmp/scratch" && find /tmp/scratch -exec rm -rf {} + >/etc/passwd')
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def test_rm_rf_allowed_when_cd_ephemeral_and_relative_build_target() -> None:
|
|
1913
|
+
_assert_hook_allows('cd "/tmp/scratch" && rm -rf build')
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
def test_rm_rf_allowed_when_cd_ephemeral_and_find_exec_rm_search_root_is_dot() -> None:
|
|
1917
|
+
_assert_hook_allows('cd "/tmp/scratch" && find . -name x -exec rm -rf {} +')
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_bash_dash_c_deletes_non_ephemeral() -> None:
|
|
1921
|
+
_assert_hook_asks("cd \"/tmp/scratch\" && find . -exec bash -c 'rm -rf /etc' \\;")
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_sh_dash_c_deletes_non_ephemeral() -> None:
|
|
1925
|
+
_assert_hook_asks("cd \"/tmp/scratch\" && find . -exec sh -c 'rm -rf /etc' \\;")
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_execdir_bash_dash_c_deletes_non_ephemeral() -> None:
|
|
1929
|
+
_assert_hook_asks("cd \"/tmp/scratch\" && find . -execdir bash -c 'rm -rf /etc' \\;")
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_python_dash_c_deletes_non_ephemeral() -> None:
|
|
1933
|
+
_assert_hook_asks(
|
|
1934
|
+
"cd \"/tmp/scratch\" && find . -exec python -c 'import os; os.system(\"rm -rf /etc\")' \\;"
|
|
1935
|
+
)
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
# H1: find global option before the search root must not defeat the escape check
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
def test_rm_rf_asks_when_find_dash_l_global_option_precedes_non_ephemeral_search_root() -> None:
|
|
1942
|
+
_assert_hook_asks('cd "/tmp/scratch" && find -L /etc -name x -exec rm -rf {} +')
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
def test_rm_rf_asks_when_find_dash_p_global_option_precedes_non_ephemeral_execdir_root() -> None:
|
|
1946
|
+
_assert_hook_asks('cd "/tmp/scratch" && find -P /etc -execdir rm -rf {} +')
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def test_rm_rf_asks_when_find_optimization_level_option_precedes_non_ephemeral_search_root() -> None:
|
|
1950
|
+
_assert_hook_asks('cd "/tmp/scratch" && find -O3 /etc -exec rm -rf {} +')
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def test_rm_rf_asks_when_standalone_find_optimization_option_precedes_non_ephemeral_search_root() -> None:
|
|
1954
|
+
_assert_hook_asks('cd "/tmp/scratch" && find -O /etc -exec rm -rf {} +')
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def test_rm_rf_asks_when_find_debug_option_value_precedes_non_ephemeral_search_root() -> None:
|
|
1958
|
+
_assert_hook_asks('cd "/tmp/scratch" && find -D tree /etc -exec rm -rf {} +')
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
def test_rm_rf_allowed_when_find_global_option_precedes_ephemeral_dot_search_root() -> None:
|
|
1962
|
+
_assert_hook_allows('cd "/tmp/scratch" && find -L . -name x -exec rm -rf {} +')
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
# H2: multi -exec with a \\; terminator must not sever the destructive action from detection
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
def test_rm_rf_asks_when_multi_exec_second_action_runs_bash_dash_c_deleting_non_ephemeral() -> None:
|
|
1969
|
+
_assert_hook_asks(
|
|
1970
|
+
"cd \"/tmp/scratch\" && find . -exec touch {} \\; -exec bash -c 'rm -rf /etc' \\;"
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def test_rm_rf_asks_when_multi_exec_second_action_runs_sh_dash_c_deleting_non_ephemeral() -> None:
|
|
1975
|
+
_assert_hook_asks(
|
|
1976
|
+
"cd \"/tmp/scratch\" && find . -exec echo {} \\; -exec sh -c 'rm -rf /etc' \\;"
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def test_rm_rf_allowed_when_multi_exec_both_actions_target_only_ephemeral_paths() -> None:
|
|
1981
|
+
_assert_hook_allows("cd \"/tmp/scratch\" && find . -exec echo {} \\; -exec rm -rf {} \\;")
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
# H3: parallel forwarding an interpreter that deletes a non-ephemeral path must ask
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def test_rm_rf_asks_when_parallel_forwards_bash_dash_c_deleting_non_ephemeral() -> None:
|
|
1988
|
+
_assert_hook_asks("cd \"/tmp/scratch\" && parallel bash -c 'rm -rf /etc' ::: x")
|
|
@@ -6,6 +6,7 @@ path the verified_commit_gate hook runs.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import importlib.util
|
|
9
|
+
import json
|
|
9
10
|
import pathlib
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
@@ -25,6 +26,8 @@ store_spec.loader.exec_module(store_module)
|
|
|
25
26
|
is_verification_exempt_diff = store_module.is_verification_exempt_diff
|
|
26
27
|
resolve_merge_base = store_module.resolve_merge_base
|
|
27
28
|
branch_surface_manifest = store_module.branch_surface_manifest
|
|
29
|
+
manifest_sha256 = store_module.manifest_sha256
|
|
30
|
+
workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
|
|
28
31
|
|
|
29
32
|
constants_spec = importlib.util.spec_from_file_location(
|
|
30
33
|
"verified_commit_constants",
|
|
@@ -276,3 +279,212 @@ def test_production_change_is_gated_on_nonstandard_default_branch(
|
|
|
276
279
|
encoding="utf-8",
|
|
277
280
|
)
|
|
278
281
|
assert _exemption_for(work_dir) is False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
MATCHING_MANIFEST_SHA256 = "a" * 64
|
|
285
|
+
OTHER_MANIFEST_SHA256 = "b" * 64
|
|
286
|
+
VERIFIER_AGENT_TYPE = "code-verifier"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _verdict_transcript_text(is_all_pass: bool, bound_manifest_sha256: str) -> str:
|
|
290
|
+
verdict_record = {
|
|
291
|
+
"all_pass": is_all_pass,
|
|
292
|
+
"findings": [],
|
|
293
|
+
"manifest_sha256": bound_manifest_sha256,
|
|
294
|
+
}
|
|
295
|
+
assistant_text = (
|
|
296
|
+
"Verification complete.\n\n```verdict\n"
|
|
297
|
+
+ json.dumps(verdict_record)
|
|
298
|
+
+ "\n```\n"
|
|
299
|
+
)
|
|
300
|
+
assistant_entry = {
|
|
301
|
+
"type": "assistant",
|
|
302
|
+
"message": {"content": [{"type": "text", "text": assistant_text}]},
|
|
303
|
+
}
|
|
304
|
+
return json.dumps(assistant_entry) + "\n"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _write_agent_transcript(
|
|
308
|
+
subagents_dir: pathlib.Path,
|
|
309
|
+
agent_id: str,
|
|
310
|
+
agent_type: str,
|
|
311
|
+
transcript_text: str,
|
|
312
|
+
should_write_sidecar: bool,
|
|
313
|
+
) -> None:
|
|
314
|
+
workflow_dir = subagents_dir / "workflows" / "wf_x"
|
|
315
|
+
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
(workflow_dir / f"agent-{agent_id}.jsonl").write_text(
|
|
317
|
+
transcript_text, encoding="utf-8"
|
|
318
|
+
)
|
|
319
|
+
if should_write_sidecar:
|
|
320
|
+
(workflow_dir / f"agent-{agent_id}.meta.json").write_text(
|
|
321
|
+
json.dumps({"agentType": agent_type}), encoding="utf-8"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _session_transcript_path(tmp_path: pathlib.Path, session_id: str) -> pathlib.Path:
|
|
326
|
+
session_root = tmp_path / "projects" / "demo"
|
|
327
|
+
session_root.mkdir(parents=True)
|
|
328
|
+
transcript_path = session_root / f"{session_id}.jsonl"
|
|
329
|
+
transcript_path.write_text("", encoding="utf-8")
|
|
330
|
+
return transcript_path
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_workflow_verdict_covers_surface_true_for_matching_passing_verifier(
|
|
334
|
+
tmp_path: pathlib.Path,
|
|
335
|
+
) -> None:
|
|
336
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
337
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
338
|
+
_write_agent_transcript(
|
|
339
|
+
subagents_dir,
|
|
340
|
+
"01",
|
|
341
|
+
VERIFIER_AGENT_TYPE,
|
|
342
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
343
|
+
should_write_sidecar=True,
|
|
344
|
+
)
|
|
345
|
+
assert (
|
|
346
|
+
workflow_verdict_covers_surface(
|
|
347
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
348
|
+
)
|
|
349
|
+
is True
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_workflow_verdict_covers_surface_false_for_nonmatching_hash(
|
|
354
|
+
tmp_path: pathlib.Path,
|
|
355
|
+
) -> None:
|
|
356
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
357
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
358
|
+
_write_agent_transcript(
|
|
359
|
+
subagents_dir,
|
|
360
|
+
"01",
|
|
361
|
+
VERIFIER_AGENT_TYPE,
|
|
362
|
+
_verdict_transcript_text(True, OTHER_MANIFEST_SHA256),
|
|
363
|
+
should_write_sidecar=True,
|
|
364
|
+
)
|
|
365
|
+
assert (
|
|
366
|
+
workflow_verdict_covers_surface(
|
|
367
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
368
|
+
)
|
|
369
|
+
is False
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_workflow_verdict_covers_surface_false_for_all_pass_false(
|
|
374
|
+
tmp_path: pathlib.Path,
|
|
375
|
+
) -> None:
|
|
376
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
377
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
378
|
+
_write_agent_transcript(
|
|
379
|
+
subagents_dir,
|
|
380
|
+
"01",
|
|
381
|
+
VERIFIER_AGENT_TYPE,
|
|
382
|
+
_verdict_transcript_text(False, MATCHING_MANIFEST_SHA256),
|
|
383
|
+
should_write_sidecar=True,
|
|
384
|
+
)
|
|
385
|
+
assert (
|
|
386
|
+
workflow_verdict_covers_surface(
|
|
387
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
388
|
+
)
|
|
389
|
+
is False
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
|
|
394
|
+
tmp_path: pathlib.Path,
|
|
395
|
+
) -> None:
|
|
396
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
397
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
398
|
+
_write_agent_transcript(
|
|
399
|
+
subagents_dir,
|
|
400
|
+
"01",
|
|
401
|
+
"clean-coder",
|
|
402
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
403
|
+
should_write_sidecar=True,
|
|
404
|
+
)
|
|
405
|
+
assert (
|
|
406
|
+
workflow_verdict_covers_surface(
|
|
407
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
408
|
+
)
|
|
409
|
+
is False
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_workflow_verdict_covers_surface_false_for_missing_sidecar(
|
|
414
|
+
tmp_path: pathlib.Path,
|
|
415
|
+
) -> None:
|
|
416
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
417
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
418
|
+
_write_agent_transcript(
|
|
419
|
+
subagents_dir,
|
|
420
|
+
"01",
|
|
421
|
+
VERIFIER_AGENT_TYPE,
|
|
422
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
423
|
+
should_write_sidecar=False,
|
|
424
|
+
)
|
|
425
|
+
assert (
|
|
426
|
+
workflow_verdict_covers_surface(
|
|
427
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
428
|
+
)
|
|
429
|
+
is False
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
|
|
434
|
+
tmp_path: pathlib.Path,
|
|
435
|
+
) -> None:
|
|
436
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
437
|
+
assert (
|
|
438
|
+
workflow_verdict_covers_surface(
|
|
439
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
440
|
+
)
|
|
441
|
+
is False
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_workflow_verdict_covers_surface_true_when_transcript_is_under_subagents(
|
|
446
|
+
tmp_path: pathlib.Path,
|
|
447
|
+
) -> None:
|
|
448
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
449
|
+
_write_agent_transcript(
|
|
450
|
+
subagents_dir,
|
|
451
|
+
"01",
|
|
452
|
+
VERIFIER_AGENT_TYPE,
|
|
453
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
454
|
+
should_write_sidecar=True,
|
|
455
|
+
)
|
|
456
|
+
caller_transcript_path = (
|
|
457
|
+
subagents_dir / "workflows" / "wf_x" / "agent-00.jsonl"
|
|
458
|
+
)
|
|
459
|
+
caller_transcript_path.write_text("", encoding="utf-8")
|
|
460
|
+
assert (
|
|
461
|
+
workflow_verdict_covers_surface(
|
|
462
|
+
str(caller_transcript_path), MATCHING_MANIFEST_SHA256
|
|
463
|
+
)
|
|
464
|
+
is True
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> None:
|
|
469
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
470
|
+
(work_dir / "src" / "app.py").write_text(
|
|
471
|
+
"def add(left: int, right: int) -> int:\n return left - right\n",
|
|
472
|
+
encoding="utf-8",
|
|
473
|
+
)
|
|
474
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
475
|
+
assert merge_base_sha is not None
|
|
476
|
+
surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
|
|
477
|
+
assert surface_manifest_text is not None
|
|
478
|
+
expected_hash = manifest_sha256(surface_manifest_text)
|
|
479
|
+
completed_process = subprocess.run(
|
|
480
|
+
[
|
|
481
|
+
sys.executable,
|
|
482
|
+
str(_HOOK_DIR / "verification_verdict_store.py"),
|
|
483
|
+
"--manifest-hash",
|
|
484
|
+
str(work_dir),
|
|
485
|
+
],
|
|
486
|
+
check=True,
|
|
487
|
+
capture_output=True,
|
|
488
|
+
text=True,
|
|
489
|
+
)
|
|
490
|
+
assert completed_process.stdout.strip() == expected_hash
|
|
@@ -6,8 +6,10 @@ decide what to gate.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import importlib.util
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
import pathlib
|
|
12
|
+
import subprocess
|
|
11
13
|
import sys
|
|
12
14
|
|
|
13
15
|
import pytest
|
|
@@ -25,6 +27,21 @@ assert gate_spec.loader is not None
|
|
|
25
27
|
gate_module = importlib.util.module_from_spec(gate_spec)
|
|
26
28
|
gate_spec.loader.exec_module(gate_module)
|
|
27
29
|
gated_repo_directories = gate_module.gated_repo_directories
|
|
30
|
+
deny_reason_for_directory = gate_module.deny_reason_for_directory
|
|
31
|
+
|
|
32
|
+
store_spec = importlib.util.spec_from_file_location(
|
|
33
|
+
"verification_verdict_store",
|
|
34
|
+
_HOOK_DIR / "verification_verdict_store.py",
|
|
35
|
+
)
|
|
36
|
+
assert store_spec is not None
|
|
37
|
+
assert store_spec.loader is not None
|
|
38
|
+
store_module = importlib.util.module_from_spec(store_spec)
|
|
39
|
+
store_spec.loader.exec_module(store_module)
|
|
40
|
+
resolve_merge_base = store_module.resolve_merge_base
|
|
41
|
+
branch_surface_manifest = store_module.branch_surface_manifest
|
|
42
|
+
manifest_sha256 = store_module.manifest_sha256
|
|
43
|
+
|
|
44
|
+
PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
28
45
|
|
|
29
46
|
|
|
30
47
|
def test_plain_git_commit_is_gated() -> None:
|
|
@@ -366,3 +383,113 @@ def test_git_verb_inside_gh_comment_body_is_not_gated() -> None:
|
|
|
366
383
|
assert gated_repo_directories(
|
|
367
384
|
'gh pr comment -b "please git commit your work"', "/d"
|
|
368
385
|
) == []
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
|
|
389
|
+
subprocess.run(
|
|
390
|
+
["git", "-C", str(repo_dir), *git_arguments],
|
|
391
|
+
check=True,
|
|
392
|
+
capture_output=True,
|
|
393
|
+
text=True,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _make_gated_repo(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
398
|
+
origin_dir = tmp_path / "origin.git"
|
|
399
|
+
work_dir = tmp_path / "work"
|
|
400
|
+
work_dir.mkdir()
|
|
401
|
+
subprocess.run(
|
|
402
|
+
["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
|
|
403
|
+
check=True,
|
|
404
|
+
capture_output=True,
|
|
405
|
+
text=True,
|
|
406
|
+
)
|
|
407
|
+
_run_git(work_dir, "init", "--initial-branch=main")
|
|
408
|
+
_run_git(work_dir, "config", "user.email", "tests@example.com")
|
|
409
|
+
_run_git(work_dir, "config", "user.name", "Gate Tests")
|
|
410
|
+
(work_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
411
|
+
_run_git(work_dir, "add", "-A")
|
|
412
|
+
_run_git(work_dir, "commit", "-m", "base")
|
|
413
|
+
_run_git(work_dir, "remote", "add", "origin", str(origin_dir))
|
|
414
|
+
_run_git(work_dir, "push", "-u", "origin", "main")
|
|
415
|
+
(work_dir / "app.py").write_text(
|
|
416
|
+
"def add(left: int, right: int) -> int:\n return left - right\n",
|
|
417
|
+
encoding="utf-8",
|
|
418
|
+
)
|
|
419
|
+
return work_dir
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _live_surface_hash(work_dir: pathlib.Path) -> str:
|
|
423
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
424
|
+
assert merge_base_sha is not None
|
|
425
|
+
surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
|
|
426
|
+
assert surface_manifest_text is not None
|
|
427
|
+
return manifest_sha256(surface_manifest_text)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _write_workflow_verdict(
|
|
431
|
+
transcript_path: pathlib.Path, bound_manifest_sha256: str
|
|
432
|
+
) -> None:
|
|
433
|
+
subagents_dir = transcript_path.with_suffix("") / "subagents"
|
|
434
|
+
workflow_dir = subagents_dir / "workflows" / "wf_x"
|
|
435
|
+
workflow_dir.mkdir(parents=True)
|
|
436
|
+
verdict_record = {
|
|
437
|
+
"all_pass": True,
|
|
438
|
+
"findings": [],
|
|
439
|
+
"manifest_sha256": bound_manifest_sha256,
|
|
440
|
+
}
|
|
441
|
+
assistant_text = (
|
|
442
|
+
"Verification complete.\n\n```verdict\n"
|
|
443
|
+
+ json.dumps(verdict_record)
|
|
444
|
+
+ "\n```\n"
|
|
445
|
+
)
|
|
446
|
+
assistant_entry = {
|
|
447
|
+
"type": "assistant",
|
|
448
|
+
"message": {"content": [{"type": "text", "text": assistant_text}]},
|
|
449
|
+
}
|
|
450
|
+
(workflow_dir / "agent-01.jsonl").write_text(
|
|
451
|
+
json.dumps(assistant_entry) + "\n", encoding="utf-8"
|
|
452
|
+
)
|
|
453
|
+
(workflow_dir / "agent-01.meta.json").write_text(
|
|
454
|
+
json.dumps({"agentType": "code-verifier"}), encoding="utf-8"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
|
|
459
|
+
home_text = str(fake_home)
|
|
460
|
+
monkeypatch.setenv("HOME", home_text)
|
|
461
|
+
monkeypatch.setenv("USERPROFILE", home_text)
|
|
462
|
+
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
463
|
+
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_workflow_verdict_allows_commit_without_a_minted_verdict_file(
|
|
467
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
468
|
+
) -> None:
|
|
469
|
+
fake_home = tmp_path / "home"
|
|
470
|
+
fake_home.mkdir()
|
|
471
|
+
_isolate_home(monkeypatch, fake_home)
|
|
472
|
+
work_dir = _make_gated_repo(tmp_path)
|
|
473
|
+
live_surface_hash = _live_surface_hash(work_dir)
|
|
474
|
+
transcript_path = tmp_path / "projects" / "demo" / "sess1.jsonl"
|
|
475
|
+
transcript_path.parent.mkdir(parents=True)
|
|
476
|
+
transcript_path.write_text("", encoding="utf-8")
|
|
477
|
+
_write_workflow_verdict(transcript_path, live_surface_hash)
|
|
478
|
+
assert (
|
|
479
|
+
deny_reason_for_directory(str(work_dir), str(transcript_path)) is None
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def test_no_verdict_of_either_kind_denies_the_commit(
|
|
484
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
485
|
+
) -> None:
|
|
486
|
+
fake_home = tmp_path / "home"
|
|
487
|
+
fake_home.mkdir()
|
|
488
|
+
_isolate_home(monkeypatch, fake_home)
|
|
489
|
+
work_dir = _make_gated_repo(tmp_path)
|
|
490
|
+
transcript_path = tmp_path / "projects" / "demo" / "sess1.jsonl"
|
|
491
|
+
transcript_path.parent.mkdir(parents=True)
|
|
492
|
+
transcript_path.write_text("", encoding="utf-8")
|
|
493
|
+
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
494
|
+
assert deny_reason is not None
|
|
495
|
+
assert "VERIFIED_COMMIT_GATE" in deny_reason
|