Skip to content

Pass CHOCK_CAN_ASK to guards from the vendored dispatcher (C1b) - #255

Merged
jothimani-rajendran merged 2 commits into
mainfrom
c1b-can-ask-env
Oct 11, 2026
Merged

jothimani-rajendran merged 2 commits into
mainfrom
c1b-can-ask-env

Conversation

@jothimani-rajendran

@jothimani-rajendran jothimani-rajendran commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

Plan item C1b (org-plan plan/one-oh-gaps.md at a8d101f, plus the orchestrator's review answer). It blocks C1 (chock-catalog#254). A guard can't tell whether its ask (exit 3) will show as a prompt or turn into a block, because the guard never learns which client is running it. This PR has the vendored dispatcher tell it, through the CHOCK_CAN_ASK environment variable.

  • guard_runner: adds CAN_ASK_ENV = "CHOCK_CAN_ASK". run_guard_detailed() and evaluate() take a keyword-only can_ask: bool | None = None. When it isn't None, the guard's environment gets "1" or "0". interpreter_env() drops any inherited value, so the variable is unset outside the dispatcher (eval replay, direct calls). The caller's environment can never set it.
  • runtime_bundle.pre_tool_can_ask(agent) builds a map from each wire event the agent maps to pre_tool to its honours_escalate value. The map is rendered into the dispatcher as a literal (_CHOCK_CAN_ASK), because the vscode_copilot bundle has no VENDOR dict to read at runtime.
  • In dispatch.py.tmpl, _judge() passes can_ask=_chock_can_ask(event.raw). That function looks up the payload's own event name in the map. The name comes from the vendor's event_key plus the shared keys. The cursor bundle uses cursor_wire(raw), the same inference its parse and respond use. An event the runtime can't name, or one missing from the map, gives False.
  • spec/script-backed-gates.md documents CHOCK_CAN_ASK: its values, when it is unset, and that a guard must treat unset as "can ask".
event CHOCK_CAN_ASK
claude_code PreToolUse, vscode_copilot PreToolUse, gemini_cli BeforeTool, cursor beforeShellExecution / beforeMCPExecution (also an unnamed Cursor shell payload) 1
cursor preToolUse, beforeReadFile, beforeTabFileRead; codex_cli, devin, grok, tabnine, windsurf pre-tool; vscode_copilot preToolUse (vendor data has no value); any unknown or unnamed event 0

Measured (the rendered cursor bundle, a beforeShellExecution payload, a guard that prints confirm first and exits 3):

stdout: {"permission": "ask", "user_message": "chock policy g asks before this runs: confirm first", "agent_message": "chock policy g asks before this runs: confirm first"}
exit code: 0

So Cursor does show the ask at beforeShellExecution, and CHOCK_CAN_ASK=1 there is correct.

Definition of done

  • chock check → All checks passed (locally)
  • chock sync --repo . --check clean
  • pytest -q green locally (3504 passed, 83 skipped, Python 3.13); tests/test_guard_can_ask.py has 30 tests: per-vendor value through each rendered bundle; Cursor beforeShellExecution 1, preToolUse 0, unnamed shell payload 1; an unknown event 0; unset in eval replay and direct calls; ask, block and clean verdicts unchanged
  • changelog.d/guard-can-ask-env.md
  • ruff check . and ruff format --check . clean
  • matrix, registry, acceptance: not affected

Claims

  • No surface claims more enforcement than before. The change is opt-in: no guard reads the variable yet, and verdicts are identical with can_ask set to True, False or None.

🤖 Generated with Claude Code

jothimani-rajendran and others added 2 commits October 11, 2026 14:46
A guard cannot tell whether its ask (exit 3) becomes a prompt or a
block, because no client identity reaches it. The C1 guard needs that
to let a credential switch through on clients that cannot prompt.

- guard_runner: CAN_ASK_ENV = "CHOCK_CAN_ASK". run_guard_detailed() and
  evaluate() take keyword-only can_ask: bool | None = None; when not
  None the guard's env gets "1" or "0". interpreter_env() drops any
  inherited value, so it is unset outside the dispatcher (eval replay,
  direct calls).
- dispatch.py.tmpl: _judge() passes can_ask, resolved at render time by
  runtime_bundle.pre_tool_can_ask(): honours_escalate of the vendor's
  native pre_tool gate; where several events map to pre_tool (cursor,
  devin), the one wire_events names. Render time, not VENDOR at runtime,
  because the vscode_copilot bundle has no VENDOR dict.
- Values: 1 claude_code, vscode_copilot, gemini_cli; 0 cursor,
  codex_cli, devin, grok, tabnine, windsurf, antigravity.
- tests/test_guard_can_ask.py: per-vendor value through each rendered
  bundle, unset in eval replay and direct calls, verdicts unchanged for
  ask/block/clean guards. test_pretooluse_logging stubs accept **kwargs.
- Runtime goldens and .chock/bin regenerated (chock sync --check clean).

Opt-in: no guard reads the variable yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Review answer: Cursor wires command guards at beforeShellExecution, which
honours ask, so a per-agent constant of 0 was wrong there.

- runtime_bundle.pre_tool_can_ask(agent) now returns {wire event:
  honours_escalate} for every event the agent maps to pre_tool, rendered
  as a literal (_CHOCK_CAN_ASK) so the vscode_copilot bundle still needs
  no runtime VENDOR.
- dispatch.py.tmpl: _chock_can_ask(raw) looks the payload's own event
  name up in that map: the vendor's event_key plus the shared keys, or
  cursor_wire(raw) in the cursor bundle (same inference its parse and
  respond use). An unknown or unnamed event is False.
- Cursor: beforeShellExecution and an unnamed shell payload -> 1,
  preToolUse -> 0. Other vendors unchanged.
- spec/script-backed-gates.md documents CHOCK_CAN_ASK: values, when it
  is unset, and that a guard treats unset as "can ask".
- Goldens and .chock/bin regenerated (chock sync --check clean).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review October 11, 2026 15:13
@jothimani-rajendran
jothimani-rajendran merged commit a2dd3de into main Oct 11, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant