diff --git a/hooks/enforce-tentacle.py b/hooks/enforce-tentacle.py index 4c432538..b78ed54f 100644 --- a/hooks/enforce-tentacle.py +++ b/hooks/enforce-tentacle.py @@ -219,8 +219,14 @@ def main(): # FP-1: session-state files (e.g. /research outputs) are not project source; # skip threshold check so create/edit to these paths is never blocked. + # Note: Create tool sends path as filePath in toolInput/input, not "path" in toolArgs. if tool_name in ("edit", "create"): - file_path = (data.get("toolArgs") or {}).get("path", "") + file_path = ( + (data.get("toolArgs") or {}).get("path", "") + or (data.get("toolArgs") or {}).get("filePath", "") + or (data.get("toolInput") or {}).get("filePath", "") + or (data.get("input") or {}).get("filePath", "") + ) _ss_abs = str(Path.home() / ".copilot" / "session-state") if file_path and (file_path.startswith(_ss_abs) or ".copilot/session-state" in file_path): return diff --git a/hooks/rules/tentacle.py b/hooks/rules/tentacle.py index e6f41ace..e32ca5ee 100644 --- a/hooks/rules/tentacle.py +++ b/hooks/rules/tentacle.py @@ -204,8 +204,13 @@ def evaluate(self, event, data): # FP-1: session-state files (e.g. /research outputs) are not project source; # skip threshold check so create/edit to these paths is never blocked. + # Note: Create tool sends path as filePath in toolInput/input, not "path" in toolArgs. if tool_name in ("edit", "create"): - file_path = tool_args.get("path", "") + file_path = ( + tool_args.get("path", "") + or tool_args.get("filePath", "") + or (data.get("input") or {}).get("filePath", "") + ) if file_path and is_session_path(file_path): return None diff --git a/tests/test_hooks.py b/tests/test_hooks.py index b3278520..388e86a1 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -3420,7 +3420,7 @@ def _el_run_edit18(path): f"Expected None, got: {_r19_create!r}", ) - # FP-1: edit to session-state path must be allowed + # FP-1: edit to session-state path (toolArgs.path) must be allowed _r19_edit = _ter19.evaluate( "preToolUse", { @@ -3434,6 +3434,34 @@ def _el_run_edit18(path): f"Expected None, got: {_r19_edit!r}", ) + # FP-1: create to session-state via toolInput.filePath must be allowed + _r19_create_input = _ter19.evaluate( + "preToolUse", + { + "toolName": "create", + "toolInput": {"filePath": _ss_create_path19}, + }, + ) + test( + "FP-1: TentacleEnforceRule create to session-state via toolInput.filePath → None", + _r19_create_input is None, + f"Expected None, got: {_r19_create_input!r}", + ) + + # FP-1: create to session-state via input.filePath must be allowed + _r19_create_input2 = _ter19.evaluate( + "preToolUse", + { + "toolName": "create", + "input": {"filePath": _ss_create_path19}, + }, + ) + test( + "FP-1: TentacleEnforceRule create to session-state via input.filePath → None", + _r19_create_input2 is None, + f"Expected None, got: {_r19_create_input2!r}", + ) + # Non-session edit at threshold must still deny _r19_deny = _ter19.evaluate( "preToolUse", @@ -3586,6 +3614,46 @@ def _run_standalone19e(tool_name, path): f"Expected deny output, got: {_out19e_deny!r}", ) + # FP-1 parity: standalone create to session-state via toolInput.filePath + def _run_standalone19e_raw(payload): + import sys as _sys + + _old_in, _old_out = _sys.stdin, _sys.stdout + _sys.stdin = _io19e.StringIO(payload) + _sys.stdout = _io19e.StringIO() + try: + _mod19e.main() + return _sys.stdout.getvalue().strip() + finally: + _sys.stdin, _sys.stdout = _old_in, _old_out + + _out19e_toolInput = _run_standalone19e_raw( + json.dumps({"toolName": "create", "toolInput": {"filePath": _ss_create19e}}) + ) + test( + "FP-1 parity: standalone create to session-state via toolInput.filePath → no output", + _out19e_toolInput == "", + f"Expected no output (allow), got: {_out19e_toolInput!r}", + ) + + _out19e_input = _run_standalone19e_raw( + json.dumps({"toolName": "create", "input": {"filePath": _ss_create19e}}) + ) + test( + "FP-1 parity: standalone create to session-state via input.filePath → no output", + _out19e_input == "", + f"Expected no output (allow), got: {_out19e_input!r}", + ) + + _out19e_toolArgs_filePath = _run_standalone19e_raw( + json.dumps({"toolName": "create", "toolArgs": {"filePath": _ss_create19e}}) + ) + test( + "FP-1 parity: standalone create to session-state via toolArgs.filePath → no output", + _out19e_toolArgs_filePath == "", + f"Expected no output (allow), got: {_out19e_toolArgs_filePath!r}", + ) + test("Section 19e standalone parity tests ran without exception", True) except Exception as _e19e: test("Section 19e standalone parity tests ran without exception", False, str(_e19e))