Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion hooks/enforce-tentacle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
)
Comment on lines 223 to +229
Comment on lines +222 to +229
_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
Expand Down
7 changes: 6 additions & 1 deletion hooks/rules/tentacle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
)
Comment on lines +207 to +213
if file_path and is_session_path(file_path):
return None

Expand Down
70 changes: 69 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
{
Expand All @@ -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",
Expand Down Expand Up @@ -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))
Expand Down
Loading