← Về thư mục
scripts/test_ars_write_scope_guard.py
- payload · function · L74-L86 — def payload(tool_name, tool_input, cwd, agent_type=None, agent_id=None)
- symlink_or_skip · function · L89-L98 — def symlink_or_skip(testcase, target, link_name)
- WriteScopeGuardTest · class · L101-L318 — class WriteScopeGuardTest(unittest.TestCase)
- setUp · method · L102-L107 — def setUp(self)
- tearDown · method · L109-L110 — def tearDown(self)
- decide · method · L112-L113 — def decide(self, p)
- test_in_scope_write_allowed · method · L117-L121 — def test_in_scope_write_allowed(self)
- test_out_of_scope_write_denied · method · L123-L130 — def test_out_of_scope_write_denied(self): # bibliography_agent (phase 2) writing into phase3 dir = the #133 shape.
- test_in_scope_edit_allowed · method · L132-L137 — def test_in_scope_edit_allowed(self)
- test_out_of_scope_multiedit_denied_via_top_level_file_path · method · L139-L148 — def test_out_of_scope_multiedit_denied_via_top_level_file_path(self): # MultiEdit carries a SINGLE top-level file_path + edits[]; the path check # must use that top-level file_path, not a non-existent per-edit path array.
- test_in_scope_multiedit_allowed · method · L150-L155 — def test_in_scope_multiedit_allowed(self)
- test_bash_denied_wholesale_for_bucket_a · method · L160-L173 — def test_bash_denied_wholesale_for_bucket_a(self): # EVERY Bash command is denied for a Bucket A agent regardless of content — the only # policy that reaches zero fail-open by construction (neither "writes a file" nor "is # read-only" is decidable from a command string). One representative per class here # (+ the deny flag); the exhaustive historical-bypass list lives in # BashBypassRegressionTest.test_all_historical_bash_bypasses_denied.
- test_bash_deny_reason_routes_to_structured_and_search_tools · method · L175-L180 — def test_bash_deny_reason_routes_to_structured_and_search_tools(self): # The deny reason must tell the agent what to use instead (Grep/Glob + structured).
- test_bash_allowed_for_non_bucket_a · method · L182-L189 — def test_bash_allowed_for_non_bucket_a(self): # A non-Bucket-A agent / main session is unconstrained — ANY Bash passes (the guard # only fences the 23 single-phase agents). Covers a named non-bucket-A agent and the # absent-agent_type main session.
- test_infra_hooks_json_denied_for_every_agent · method · L196-L203 — def test_infra_hooks_json_denied_for_every_agent(self)
- test_infra_manifest_denied_even_with_broad_glob · method · L205-L211 — def test_infra_manifest_denied_even_with_broad_glob(self): # broad_test_agent has glob ["**"] which would otherwise permit everything; # Step 2 self-protection overrides any later allow.
- test_infra_hook_script_denied · method · L213-L218 — def test_infra_hook_script_denied(self): # The hook script itself (lives in scripts/) is part of the enforcement surface.
- test_infra_agent_frontmatter_denied · method · L220-L228 — def test_infra_agent_frontmatter_denied(self): # Agent definition files carry the agent_type==name binding; writing them would # let an agent rename itself out of the manifest and fail the guard open.
- test_traversal_bypass_to_infra_denied · method · L232-L241 — def test_traversal_bypass_to_infra_denied(self): # A Write whose raw file_path uses ../ to escape a phase dir back into hooks/. # Normalization (Step 1) must canonicalize it to hooks/hooks.json BEFORE Step 2, # so self-protection catches it. Proves normalize-first ordering.
- test_traversal_escaping_workspace_denied · method · L243-L249 — def test_traversal_escaping_workspace_denied(self): # ../ escaping the workspace root entirely must be denied FOR A BUCKET A AGENT # (commonpath guard). Bucket A stays fenced to its workspace regardless of actor.
- test_main_session_escape_allowed · method · L251-L261 — def test_main_session_escape_allowed(self): # #302: a MAIN-SESSION write whose target resolves outside the workspace root # (e.g. a sibling git worktree) is ALLOWED — the main session is unconstrained by # Slice 1 (§3.3). The escape deny is a Bucket A fence, not a main-session fence.
- test_non_bucket_a_agent_escape_allowed · method · L263-L271 — def test_non_bucket_a_agent_escape_allowed(self): # A Bucket B/C/D agent (agent_type present but not a manifest key) is also # unconstrained by Slice 1 — an escaping write is allowed, no advisory (it HAS a type).
- test_absent_agent_type_allowed · method · L275-L284 — def test_absent_agent_type_allowed(self): # Main session (no agent_type) is unconstrained by Slice 1.
- test_absent_agent_type_still_denied_for_infra · method · L286-L294 — def test_absent_agent_type_still_denied_for_infra(self): # Even the main session may not clobber the PLUGIN's own infra. Here the 3-arg # fallback puts plugin_root == workspace_root, so hooks/hooks.json IS a plugin infra # file and is denied regardless of actor (the #448 carve-out only spares a SEPARATE # user project's same-named file — see PluginRootInfraScopeTest).
- test_non_bucket_a_agent_type_allowed · method · L296-L301 — def test_non_bucket_a_agent_type_allowed(self): # An agent_type not in the manifest (e.g. a Bucket B/C/D agent) is unconstrained.
- test_absolute_path_under_workspace_matches_relative_glob · method · L305-L311 — def test_absolute_path_under_workspace_matches_relative_glob(self): # A Write with an ABSOLUTE file_path under the workspace must resolve and match # its relative glob (glob is workspace-root-anchored, not absolute, not cwd-rel).
- test_relative_path_resolved_against_cwd · method · L313-L318 — def test_relative_path_resolved_against_cwd(self): # A relative file_path resolves against cwd (= workspace here) then matches glob.
- GlobMatchBoundaryTest · class · L321-L364 — class GlobMatchBoundaryTest(unittest.TestCase)
- test_positive_matches · method · L347-L349 — def test_positive_matches(self)
- test_negative_non_matches · method · L351-L353 — def test_negative_non_matches(self)
- test_posix_literal_backslash_in_filename_not_split_into_segments · method · L356-L364 — def test_posix_literal_backslash_in_filename_not_split_into_segments(self): # On POSIX,
\ is a LEGAL filename character, NOT a separator. A root-level file # literally named phase2_x\notes.md is ONE segment and must NOT be rewritten into # two segments and matched against phase2_*/** — that would be a fence-escape # false-allow. (#330 PR #450's unconditional replace("\\","/") had this bug; the # rewrite is now gated on os.sep == "\". This test pins the POSIX no-op.)
- DenyJSONShapeTest · class · L367-L385 — class DenyJSONShapeTest(unittest.TestCase)
- test_render_deny_decision_json · method · L370-L376 — def test_render_deny_decision_json(self)
- test_render_allow_is_passthrough_not_grant · method · L378-L385 — def test_render_allow_is_passthrough_not_grant(self): # Non-deny must be PASS-THROUGH (no permissionDecision key), NOT an explicit # "allow" grant — an "allow" would skip every other permission rule (review # finding). Assert the key is absent entirely.
- NormalizationRegressionTest · class · L388-L459 — class NormalizationRegressionTest(unittest.TestCase)
- setUp · method · L391-L395 — def setUp(self)
- tearDown · method · L397-L398 — def tearDown(self)
- decide · method · L400-L401 — def decide(self, p)
- test_bash_bypass_shapes_that_broke_the_old_parser_still_deny · method · L403-L416 — def test_bash_bypass_shapes_that_broke_the_old_parser_still_deny(self): # The shapes that defeated the old literal-target parser (quote-blind redirection, # quoted
; in cp, no-whitespace redirection, cp -t DEST flag order) are now ALL # denied trivially by the all-Bash-deny policy — no parsing required. These stay as # regression cases so a future re-introduction of literal parsing can't reopen them.
- test_root_level_phase_named_file_denied · method · L418-L424 — def test_root_level_phase_named_file_denied(self): # FALSE-OPEN P0: a phase2 agent writing a ROOT file named
phase2_x.md must be # denied — it is not inside the phase dir (dir/** = descendants only).
- test_plugin_json_infra_protected · method · L426-L432 — def test_plugin_json_infra_protected(self)
- test_symlink_traversal_to_infra_denied · method · L434-L445 — def test_symlink_traversal_to_infra_denied(self): # A symlinked dir +
../ must resolve through the symlink (realpath), not be # lexically collapsed by abspath. Build: ws/link -> ws/phase2_investigation, then # write link/../hooks/hooks.json. realpath resolves link first => ws/hooks/hooks.json.
- test_symlink_escaping_workspace_denied · method · L447-L459 — def test_symlink_escaping_workspace_denied(self): # A symlink pointing OUTSIDE the workspace must be detected as an escape, not # approved on its lexical in-workspace name.
- BashBypassRegressionTest · class · L462-L531 — class BashBypassRegressionTest(unittest.TestCase)
- setUp · method · L474-L478 — def setUp(self)
- tearDown · method · L480-L481 — def tearDown(self)
- decide · method · L483-L484 — def decide(self, p)
- _bash · method · L486-L488 — def _bash(self, cmd)
- test_all_historical_bash_bypasses_denied · method · L490-L513 — def test_all_historical_bash_bypasses_denied(self): # Every shape that defeated an earlier denylist OR allowlist attempt. Under the final # all-Bash-deny policy they all deny trivially — no parsing, no allowlist membership, # no flag guard. (Labels name the bypass class so the coverage intent survives.)
- test_deep_path_glob_match_does_not_crash · method · L517-L531 — def test_deep_path_glob_match_does_not_crash(self): # ~1100 path segments raised RecursionError (limit 1000) in the recursive matcher, # crashing the hook (uncaught in main()) — fail-open or wedge depending on the CC # contract; either way unacceptable. The matcher must handle arbitrary depth without # blowing the stack (caught in review).
- MalformedPayloadTest · class · L534-L552 — class MalformedPayloadTest(unittest.TestCase)
- decide · method · L537-L538 — def decide(self, p)
- test_list_payload_does_not_crash · method · L540-L545 — def test_list_payload_does_not_crash(self): # evaluate_decision is the pure core; main() guards non-dict payloads. Here we # assert tool_input of the wrong type does not crash the core.
- test_null_tool_input_does_not_crash · method · L547-L552 — def test_null_tool_input_does_not_crash(self)
- PluginRootInfraScopeTest · class · L555-L752 — class PluginRootInfraScopeTest(unittest.TestCase)
- setUp · method · L562-L571 — def setUp(self): # Two DISTINCT roots: a user project and a separate plugin install dir.
- tearDown · method · L573-L575 — def tearDown(self)
- decide_in_project · method · L577-L579 — def decide_in_project(self, p): # workspace = user project; plugin_root = the separate ARS install.
- test_user_project_root_claude_md_allowed_main_session · method · L583-L587 — def test_user_project_root_claude_md_allowed_main_session(self): # This is exactly #448. Pre-fix this was DENY; post-fix it must be ALLOW.
- test_user_project_dotclaude_claude_md_allowed_main_session · method · L589-L592 — def test_user_project_dotclaude_claude_md_allowed_main_session(self)
- test_user_project_shared_agents_md_allowed · method · L594-L598 — def test_user_project_shared_agents_md_allowed(self): #
shared/agents/*.md is an ARS infra glob but ALSO a plausible user-project path.
- test_user_project_hooks_json_allowed · method · L600-L603 — def test_user_project_hooks_json_allowed(self)
- test_user_project_hooks_sh_allowed · method · L605-L608 — def test_user_project_hooks_sh_allowed(self)
- test_plugin_own_claude_md_allowed · method · L612-L619 — def test_plugin_own_claude_md_allowed(self): # #459: CLAUDE.md is no longer infra-protected, even the plugin's OWN copy. It is a # doc, not a load-bearing enforcement file — editing it cannot fail the guard open, # so it must NOT be denied. (The load-bearing files below stay protected; see # test_plugin_own_guard_script_denied / _hooks_json_denied / _shared_agents_md_denied.)
- test_plugin_own_dotclaude_claude_md_allowed · method · L621-L626 — def test_plugin_own_dotclaude_claude_md_allowed(self): # Same for .claude/CLAUDE.md inside the plugin root (#459).
- test_plugin_own_guard_script_denied · method · L628-L632 — def test_plugin_own_guard_script_denied(self)
- test_plugin_own_hooks_json_denied · method · L634-L637 — def test_plugin_own_hooks_json_denied(self)
- test_plugin_own_shared_agents_md_denied · method · L639-L642 — def test_plugin_own_shared_agents_md_denied(self)
- test_plugin_own_toplevel_agents_md_denied · method · L644-L651 — def test_plugin_own_toplevel_agents_md_denied(self): # The #413-materialized top-level agents/*.md carry the agent_type==name binding too; # #448 added
agents/*.md to the infra globs. A user-project agents/foo.md is still # allowed (covered by no deny test -> it falls through to Step 3 allow); only the # PLUGIN's own copy is protected.
- test_user_project_toplevel_agents_md_allowed · method · L653-L659 — def test_user_project_toplevel_agents_md_allowed(self): # The mirror: a user's OWN agents/foo.md (inside their project, outside plugin_root) # must remain writable — the #448 carve-out applies to agents/*.md too.
- test_fallback_none_plugin_root_claude_md_allowed · method · L668-L674 — def test_fallback_none_plugin_root_claude_md_allowed(self): # #459: even under the collapsed fallback, the user's own CLAUDE.md is writable — # because CLAUDE.md is no longer infra, not because we detected the layout.
- test_fallback_none_plugin_root_still_protects_load_bearing_infra · method · L676-L686 — def test_fallback_none_plugin_root_still_protects_load_bearing_infra(self): # The protection that MUST survive #459: under the collapsed fallback, the load-bearing # enforcement files (here the guard script) are still denied. Dropping CLAUDE.md did not # weaken self-protection of the files that can actually fail the guard open.
- test_bucket_a_phase_scope_unaffected_allow · method · L690-L694 — def test_bucket_a_phase_scope_unaffected_allow(self)
- test_bucket_a_cannot_reach_user_claude_md · method · L696-L704 — def test_bucket_a_cannot_reach_user_claude_md(self): # A Bucket A agent still can't write the user's root CLAUDE.md — not because of infra # protection now, but because it's outside the agent's phase glob (Step 4). The guard # is unchanged for agents; only the main session gained access.
- test_dual_root_traversal_from_phase_into_plugin_infra_denied · method · L709-L718 — def test_dual_root_traversal_from_phase_into_plugin_infra_denied(self): # A relative target that escapes the phase dir and
..-walks all the way into the # plugin tree's hooks/hooks.json. realpath in _infra_protected_target must resolve it # and deny — the infra check runs BEFORE the workspace-escape branch.
- test_dual_root_symlink_into_plugin_infra_denied · method · L720-L730 — def test_dual_root_symlink_into_plugin_infra_denied(self): # A symlink inside the user workspace pointing at a plugin infra file. realpath # resolves the link to the plugin tree -> infra-protected -> deny.
- test_dual_root_bucket_a_escape_toward_plugin_infra_denied · method · L732-L739 — def test_dual_root_bucket_a_escape_toward_plugin_infra_denied(self): # Bucket A agent trying to reach the plugin's own guard script. It is denied — the # infra check fires first; even if it didn't, the escape fence (#302) would catch it.
- test_dual_root_bucket_a_sibling_worktree_escape_still_allowed_for_main · method · L741-L752 — def test_dual_root_bucket_a_sibling_worktree_escape_still_allowed_for_main(self): # Sanity: a NON-infra escape (a sibling worktree outside both roots) is still allowed # for the main session — the infra reorder didn't over-deny genuine escapes.
- MainPluginRootComputationTest · class · L755-L786 — class MainPluginRootComputationTest(unittest.TestCase)
- test_plugin_root_from_env · method · L759-L772 — def test_plugin_root_from_env(self)
- test_plugin_own_file_denied_via_env · method · L774-L786 — def test_plugin_own_file_denied_via_env(self)