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
25 changes: 21 additions & 4 deletions .chock/bin/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,8 @@ def allowed_seconds(expired):

ARGV_FALLBACK_ENV = 'CHOCK_ARGV_FALLBACK'

CAN_ASK_ENV = 'CHOCK_CAN_ASK'

_POSIX_BASH = ('bash', '/bin/bash', '/usr/bin/bash')

_GIT_FOR_WINDOWS = ('C:\\Program Files\\Git', 'C:\\Program Files (x86)\\Git')
Expand Down Expand Up @@ -804,6 +806,7 @@ def bash_candidates() -> list[str]:
def interpreter_env(interpreter: str) -> dict[str, str]:
"""The environment a guard runs in: on Windows, Git's usr/bin ahead on PATH for sed and grep."""
env = dict(_chock_os.environ)
env.pop(CAN_ASK_ENV, None)
if _chock_os.name != _WINDOWS:
return env
home = _chock_Path(interpreter).parent
Expand Down Expand Up @@ -872,7 +875,7 @@ def _no_interpreter(guard: _chock_Path, deadline: float) -> tuple[str, str]:
print(f'chock: {reason}', file=sys.stderr)
return (GUARD_ERRORED, reason)

def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[str, str]:
def run_guard_detailed(guard: _chock_Path, command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str]:
"""`run_guard`'s verdict plus the guard's own first line, which an ask carries to the user."""
args, fallback = split_command(command)
if not args:
Expand All @@ -885,6 +888,7 @@ def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[
env = {**interpreter_env(interpreter), 'CHOCK_RAW_COMMAND': command, 'CHOCK_TOOL': normalize_tool(tool)}
if fallback:
env[ARGV_FALLBACK_ENV] = '1'
env.update({} if can_ask is None else {CAN_ASK_ENV: '1' if can_ask else '0'})
proc = _chock_subprocess.run([interpreter, str(guard), *args], capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=time_left(deadline, [interpreter, str(guard)]), check=False)
except _chock_subprocess.TimeoutExpired:
print(f'chock: guard timed out after {ENGINE_BUDGET_SECONDS}s, not checked', file=sys.stderr)
Expand Down Expand Up @@ -941,14 +945,14 @@ def log_outcome(guard: _chock_Path, tool: str, *, verdict: str) -> None:
except Exception:
return

def evaluate(argv: list[str], command: str, tool: str='') -> tuple[str, str] | None:
def evaluate(argv: list[str], command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str] | None:
"""Run the guard named on `argv` (`--guard <path>`) against `command`."""
guard = guard_path_from_argv(argv)
if guard is None:
return None
if not guard.exists():
return (VERDICT_DENY, f"chock guard {guard} is missing, so this command cannot be checked. Run `chock sync --repo .` to reinstall the policy's guards.")
verdict, message = run_guard_detailed(guard, command, tool)
verdict, message = run_guard_detailed(guard, command, tool, can_ask=can_ask)
logged = {GUARD_BLOCKED: 'block', GUARD_ASKED: 'ask', GUARD_CLEAN: 'allow'}
if verdict in logged:
log_outcome(guard, tool, verdict=logged[verdict])
Expand Down Expand Up @@ -2273,6 +2277,19 @@ def _chock_unreadable():
decision = Decision.escalate(reason) if action == "ask" else Decision.deny(reason)
text, code = respond(degrade(decision, event), event)
return (text, code) if _chock_refuses(text, code) else ("", 2)
#: Per wire event mapped to pre_tool: whether the client shows a guard's ask as a prompt.
_CHOCK_CAN_ASK = {'PreToolUse': True}
_CHOCK_EVENT_KEYS = ('hook_event_name', 'hookEventName', 'agent_action_name')


def _chock_wire_name(raw):
names = (raw.get(key) for key in _CHOCK_EVENT_KEYS) if isinstance(raw, dict) else ()
return next((name for name in names if isinstance(name, str)), None)


def _chock_can_ask(raw):
# An event this bundle cannot name, or one the vendor data does not list, cannot prompt.
return _CHOCK_CAN_ASK.get(_chock_wire_name(raw), False)


def _spoken(verdict):
Expand All @@ -2284,7 +2301,7 @@ def _spoken(verdict):

def _judge(event):
if event.event == "pre_tool" and event.command:
verdict = evaluate(sys.argv[1:], event.command, event.tool or "")
verdict = evaluate(sys.argv[1:], event.command, event.tool or "", can_ask=_chock_can_ask(event.raw))
if verdict is not None:
outcome, reason = verdict
return Decision.escalate(reason) if outcome == ESCALATE else Decision.deny(reason)
Expand Down
25 changes: 21 additions & 4 deletions .chock/bin/codex_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,8 @@ def allowed_seconds(expired):

ARGV_FALLBACK_ENV = 'CHOCK_ARGV_FALLBACK'

CAN_ASK_ENV = 'CHOCK_CAN_ASK'

_POSIX_BASH = ('bash', '/bin/bash', '/usr/bin/bash')

_GIT_FOR_WINDOWS = ('C:\\Program Files\\Git', 'C:\\Program Files (x86)\\Git')
Expand Down Expand Up @@ -801,6 +803,7 @@ def bash_candidates() -> list[str]:
def interpreter_env(interpreter: str) -> dict[str, str]:
"""The environment a guard runs in: on Windows, Git's usr/bin ahead on PATH for sed and grep."""
env = dict(_chock_os.environ)
env.pop(CAN_ASK_ENV, None)
if _chock_os.name != _WINDOWS:
return env
home = _chock_Path(interpreter).parent
Expand Down Expand Up @@ -869,7 +872,7 @@ def _no_interpreter(guard: _chock_Path, deadline: float) -> tuple[str, str]:
print(f'chock: {reason}', file=sys.stderr)
return (GUARD_ERRORED, reason)

def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[str, str]:
def run_guard_detailed(guard: _chock_Path, command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str]:
"""`run_guard`'s verdict plus the guard's own first line, which an ask carries to the user."""
args, fallback = split_command(command)
if not args:
Expand All @@ -882,6 +885,7 @@ def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[
env = {**interpreter_env(interpreter), 'CHOCK_RAW_COMMAND': command, 'CHOCK_TOOL': normalize_tool(tool)}
if fallback:
env[ARGV_FALLBACK_ENV] = '1'
env.update({} if can_ask is None else {CAN_ASK_ENV: '1' if can_ask else '0'})
proc = _chock_subprocess.run([interpreter, str(guard), *args], capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=time_left(deadline, [interpreter, str(guard)]), check=False)
except _chock_subprocess.TimeoutExpired:
print(f'chock: guard timed out after {ENGINE_BUDGET_SECONDS}s, not checked', file=sys.stderr)
Expand Down Expand Up @@ -938,14 +942,14 @@ def log_outcome(guard: _chock_Path, tool: str, *, verdict: str) -> None:
except Exception:
return

def evaluate(argv: list[str], command: str, tool: str='') -> tuple[str, str] | None:
def evaluate(argv: list[str], command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str] | None:
"""Run the guard named on `argv` (`--guard <path>`) against `command`."""
guard = guard_path_from_argv(argv)
if guard is None:
return None
if not guard.exists():
return (VERDICT_DENY, f"chock guard {guard} is missing, so this command cannot be checked. Run `chock sync --repo .` to reinstall the policy's guards.")
verdict, message = run_guard_detailed(guard, command, tool)
verdict, message = run_guard_detailed(guard, command, tool, can_ask=can_ask)
logged = {GUARD_BLOCKED: 'block', GUARD_ASKED: 'ask', GUARD_CLEAN: 'allow'}
if verdict in logged:
log_outcome(guard, tool, verdict=logged[verdict])
Expand Down Expand Up @@ -2204,6 +2208,19 @@ def _chock_unreadable():
decision = Decision.escalate(reason) if action == "ask" else Decision.deny(reason)
text, code = respond(degrade(decision, event), event)
return (text, code) if _chock_refuses(text, code) else ("", 2)
#: Per wire event mapped to pre_tool: whether the client shows a guard's ask as a prompt.
_CHOCK_CAN_ASK = {'PreToolUse': False}
_CHOCK_EVENT_KEYS = ('hook_event_name', 'hookEventName', 'agent_action_name')


def _chock_wire_name(raw):
names = (raw.get(key) for key in _CHOCK_EVENT_KEYS) if isinstance(raw, dict) else ()
return next((name for name in names if isinstance(name, str)), None)


def _chock_can_ask(raw):
# An event this bundle cannot name, or one the vendor data does not list, cannot prompt.
return _CHOCK_CAN_ASK.get(_chock_wire_name(raw), False)


def _spoken(verdict):
Expand All @@ -2215,7 +2232,7 @@ def _spoken(verdict):

def _judge(event):
if event.event == "pre_tool" and event.command:
verdict = evaluate(sys.argv[1:], event.command, event.tool or "")
verdict = evaluate(sys.argv[1:], event.command, event.tool or "", can_ask=_chock_can_ask(event.raw))
if verdict is not None:
outcome, reason = verdict
return Decision.escalate(reason) if outcome == ESCALATE else Decision.deny(reason)
Expand Down
25 changes: 21 additions & 4 deletions .chock/bin/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,8 @@ def allowed_seconds(expired):

ARGV_FALLBACK_ENV = 'CHOCK_ARGV_FALLBACK'

CAN_ASK_ENV = 'CHOCK_CAN_ASK'

_POSIX_BASH = ('bash', '/bin/bash', '/usr/bin/bash')

_GIT_FOR_WINDOWS = ('C:\\Program Files\\Git', 'C:\\Program Files (x86)\\Git')
Expand Down Expand Up @@ -719,6 +721,7 @@ def bash_candidates() -> list[str]:
def interpreter_env(interpreter: str) -> dict[str, str]:
"""The environment a guard runs in: on Windows, Git's usr/bin ahead on PATH for sed and grep."""
env = dict(_chock_os.environ)
env.pop(CAN_ASK_ENV, None)
if _chock_os.name != _WINDOWS:
return env
home = _chock_Path(interpreter).parent
Expand Down Expand Up @@ -787,7 +790,7 @@ def _no_interpreter(guard: _chock_Path, deadline: float) -> tuple[str, str]:
print(f'chock: {reason}', file=sys.stderr)
return (GUARD_ERRORED, reason)

def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[str, str]:
def run_guard_detailed(guard: _chock_Path, command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str]:
"""`run_guard`'s verdict plus the guard's own first line, which an ask carries to the user."""
args, fallback = split_command(command)
if not args:
Expand All @@ -800,6 +803,7 @@ def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[
env = {**interpreter_env(interpreter), 'CHOCK_RAW_COMMAND': command, 'CHOCK_TOOL': normalize_tool(tool)}
if fallback:
env[ARGV_FALLBACK_ENV] = '1'
env.update({} if can_ask is None else {CAN_ASK_ENV: '1' if can_ask else '0'})
proc = _chock_subprocess.run([interpreter, str(guard), *args], capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=time_left(deadline, [interpreter, str(guard)]), check=False)
except _chock_subprocess.TimeoutExpired:
print(f'chock: guard timed out after {ENGINE_BUDGET_SECONDS}s, not checked', file=sys.stderr)
Expand Down Expand Up @@ -856,14 +860,14 @@ def log_outcome(guard: _chock_Path, tool: str, *, verdict: str) -> None:
except Exception:
return

def evaluate(argv: list[str], command: str, tool: str='') -> tuple[str, str] | None:
def evaluate(argv: list[str], command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str] | None:
"""Run the guard named on `argv` (`--guard <path>`) against `command`."""
guard = guard_path_from_argv(argv)
if guard is None:
return None
if not guard.exists():
return (VERDICT_DENY, f"chock guard {guard} is missing, so this command cannot be checked. Run `chock sync --repo .` to reinstall the policy's guards.")
verdict, message = run_guard_detailed(guard, command, tool)
verdict, message = run_guard_detailed(guard, command, tool, can_ask=can_ask)
logged = {GUARD_BLOCKED: 'block', GUARD_ASKED: 'ask', GUARD_CLEAN: 'allow'}
if verdict in logged:
log_outcome(guard, tool, verdict=logged[verdict])
Expand Down Expand Up @@ -2122,6 +2126,19 @@ def _chock_unreadable():
decision = Decision.escalate(reason) if action == "ask" else Decision.deny(reason)
text, code = respond(degrade(decision, event), event)
return (text, code) if _chock_refuses(text, code) else ("", 2)
#: Per wire event mapped to pre_tool: whether the client shows a guard's ask as a prompt.
_CHOCK_CAN_ASK = {'beforeMCPExecution': True, 'beforeReadFile': False, 'beforeShellExecution': True, 'beforeTabFileRead': False, 'preToolUse': False}
_CHOCK_EVENT_KEYS = ('hook_event_name', 'hookEventName', 'agent_action_name')


def _chock_wire_name(raw):
names = (raw.get(key) for key in _CHOCK_EVENT_KEYS) if isinstance(raw, dict) else ()
return next((name for name in names if isinstance(name, str)), None)


def _chock_can_ask(raw):
# An event this bundle cannot name, or one the vendor data does not list, cannot prompt.
return _CHOCK_CAN_ASK.get(cursor_wire(raw), False)


def _spoken(verdict):
Expand All @@ -2133,7 +2150,7 @@ def _spoken(verdict):

def _judge(event):
if event.event == "pre_tool" and event.command:
verdict = evaluate(sys.argv[1:], event.command, event.tool or "")
verdict = evaluate(sys.argv[1:], event.command, event.tool or "", can_ask=_chock_can_ask(event.raw))
if verdict is not None:
outcome, reason = verdict
return Decision.escalate(reason) if outcome == ESCALATE else Decision.deny(reason)
Expand Down
25 changes: 21 additions & 4 deletions .chock/bin/devin.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,8 @@ def allowed_seconds(expired):

ARGV_FALLBACK_ENV = 'CHOCK_ARGV_FALLBACK'

CAN_ASK_ENV = 'CHOCK_CAN_ASK'

_POSIX_BASH = ('bash', '/bin/bash', '/usr/bin/bash')

_GIT_FOR_WINDOWS = ('C:\\Program Files\\Git', 'C:\\Program Files (x86)\\Git')
Expand Down Expand Up @@ -773,6 +775,7 @@ def bash_candidates() -> list[str]:
def interpreter_env(interpreter: str) -> dict[str, str]:
"""The environment a guard runs in: on Windows, Git's usr/bin ahead on PATH for sed and grep."""
env = dict(_chock_os.environ)
env.pop(CAN_ASK_ENV, None)
if _chock_os.name != _WINDOWS:
return env
home = _chock_Path(interpreter).parent
Expand Down Expand Up @@ -841,7 +844,7 @@ def _no_interpreter(guard: _chock_Path, deadline: float) -> tuple[str, str]:
print(f'chock: {reason}', file=sys.stderr)
return (GUARD_ERRORED, reason)

def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[str, str]:
def run_guard_detailed(guard: _chock_Path, command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str]:
"""`run_guard`'s verdict plus the guard's own first line, which an ask carries to the user."""
args, fallback = split_command(command)
if not args:
Expand All @@ -854,6 +857,7 @@ def run_guard_detailed(guard: _chock_Path, command: str, tool: str='') -> tuple[
env = {**interpreter_env(interpreter), 'CHOCK_RAW_COMMAND': command, 'CHOCK_TOOL': normalize_tool(tool)}
if fallback:
env[ARGV_FALLBACK_ENV] = '1'
env.update({} if can_ask is None else {CAN_ASK_ENV: '1' if can_ask else '0'})
proc = _chock_subprocess.run([interpreter, str(guard), *args], capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=time_left(deadline, [interpreter, str(guard)]), check=False)
except _chock_subprocess.TimeoutExpired:
print(f'chock: guard timed out after {ENGINE_BUDGET_SECONDS}s, not checked', file=sys.stderr)
Expand Down Expand Up @@ -910,14 +914,14 @@ def log_outcome(guard: _chock_Path, tool: str, *, verdict: str) -> None:
except Exception:
return

def evaluate(argv: list[str], command: str, tool: str='') -> tuple[str, str] | None:
def evaluate(argv: list[str], command: str, tool: str='', *, can_ask: bool | None=None) -> tuple[str, str] | None:
"""Run the guard named on `argv` (`--guard <path>`) against `command`."""
guard = guard_path_from_argv(argv)
if guard is None:
return None
if not guard.exists():
return (VERDICT_DENY, f"chock guard {guard} is missing, so this command cannot be checked. Run `chock sync --repo .` to reinstall the policy's guards.")
verdict, message = run_guard_detailed(guard, command, tool)
verdict, message = run_guard_detailed(guard, command, tool, can_ask=can_ask)
logged = {GUARD_BLOCKED: 'block', GUARD_ASKED: 'ask', GUARD_CLEAN: 'allow'}
if verdict in logged:
log_outcome(guard, tool, verdict=logged[verdict])
Expand Down Expand Up @@ -2176,6 +2180,19 @@ def _chock_unreadable():
decision = Decision.escalate(reason) if action == "ask" else Decision.deny(reason)
text, code = respond(degrade(decision, event), event)
return (text, code) if _chock_refuses(text, code) else ("", 2)
#: Per wire event mapped to pre_tool: whether the client shows a guard's ask as a prompt.
_CHOCK_CAN_ASK = {'PermissionRequest': False, 'PreToolUse': False}
_CHOCK_EVENT_KEYS = ('hook_event_name', 'hookEventName', 'agent_action_name')


def _chock_wire_name(raw):
names = (raw.get(key) for key in _CHOCK_EVENT_KEYS) if isinstance(raw, dict) else ()
return next((name for name in names if isinstance(name, str)), None)


def _chock_can_ask(raw):
# An event this bundle cannot name, or one the vendor data does not list, cannot prompt.
return _CHOCK_CAN_ASK.get(_chock_wire_name(raw), False)


def _spoken(verdict):
Expand All @@ -2187,7 +2204,7 @@ def _spoken(verdict):

def _judge(event):
if event.event == "pre_tool" and event.command:
verdict = evaluate(sys.argv[1:], event.command, event.tool or "")
verdict = evaluate(sys.argv[1:], event.command, event.tool or "", can_ask=_chock_can_ask(event.raw))
if verdict is not None:
outcome, reason = verdict
return Decision.escalate(reason) if outcome == ESCALATE else Decision.deny(reason)
Expand Down
Loading
Loading