Skip to content

send_input: drop the screen cross-check; cut tests_forged.rs to parser robustness - #73

Merged
chaodu-agent merged 1 commit into
mainfrom
simplify/d4-send-input
Nov 14, 2026
Merged

chaodu-agent merged 1 commit into
mainfrom
simplify/d4-send-input

Conversation

@chaodu-agent

Copy link
Copy Markdown
Collaborator

Summary

  • send_input no longer reads the screen. The check that refused a send when a pane reported idle or done while a permission dialog was on screen is removed, together with screen_shows_permission_dialog. Pane programs are inside the trust boundary (see docs/adr/trust-model.md), and status forgery is not a security boundary.
  • refuse_when_blocked and its tests are unchanged.
  • The extra scripted read_ok step is removed from the tool flows, and the tests of the removed check are deleted.
  • src/native/tests_forged.rs goes from 628 to about 80 lines and keeps the OSC 7501 parser robustness tests: split at every byte, huge or unterminated body with bounded memory, and alternate screen.
  • Docs already state that forgery is not a boundary; no doc claims a defense.
  • Reverts the substance of test: forged OSC 7501 status vs send guards; refuse send over a shown dialog #69 (issue Test that a forged OSC 7501 status cannot make send_input type into a blocked pane #62).

Diff

git diff --shortstat origin/main...HEAD: 3 files changed, 30 insertions(+), 733 deletions(-)

Test record

On both macmini and p1: cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test all pass (483 unit tests plus integration tests). tool_schema_snapshot is green and its snapshot is unchanged.

… to parser robustness

Pane programs are inside the trust boundary (docs/adr/trust-model.md), so a
status that says idle or done is no longer checked against the screen. Removes
the cross-check in send_text_within and screen_shows_permission_dialog, the
extra scripted read step in the tool flows, and the tests of the removed
check. tests_forged.rs keeps the OSC 7501 parser robustness tests (split,
huge, alternate screen). refuse_when_blocked is unchanged.

Verified on macmini and p1: cargo fmt --check, cargo clippy --all-targets
-- -D warnings, cargo test (483 unit tests plus integration tests incl.
tool_schema_snapshot, snapshot unchanged): all pass.
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Important

CHANGES REQUESTED ⚠️ - The simplification fits the trust model, but it reopens #62 without a recorded decision and deletes OSC 7501 parser tests that are not about forgery.

What This PR Does

send_input no longer reads the screen to cross-check an idle/done status against a visible permission dialog. The PR removes that check, screen_shows_permission_dialog, and the tests behind them. This reverts the substance of #69 (issue #62), on the grounds that docs/adr/trust-model.md makes status forgery a non-goal.

How It Works

  • src/tools.rs: the pre-guard block in send_text_within and the screen_shows_permission_dialog helper are deleted, together with their unit tests. The InputGuard { refuse_when_blocked: true, .. } path that follows is unchanged.
  • src/tool_flows.rs: the scripted read step disappears from the send_input flows (["get","get","read","send"] becomes ["get","get","send"]), and send_input_refuses_a_done_pane_whose_screen_shows_the_dialog is removed.
  • src/native/tests_forged.rs: goes from 628 to 77 lines and from 16 tests to 3 (split at every byte, huge field with bounded memory, alternate screen).

Findings

# Severity Finding Location
1 🟡 #62 is reopened with no recorded decision on genuine status/render races src/tools.rs:1018
2 🟡 Parser and pane-state tests that do not depend on the threat model are deleted src/native/tests_forged.rs
3 🟢 refuse_when_blocked and the guarded answer_dialog path are unchanged src/tools.rs:1019-1029, src/native/mod.rs:975-980
4 🟢 One fewer screen read per send, no dangling references, no dependency or CI changes -
Finding Details

🟡 F1: #62 is reopened with no recorded decision on genuine races

After this PR, a pane whose status is idle or done while a permission dialog is still drawn reaches backend.send, so keystrokes can land in the dialog. The ADR removes the forgery case from the threat model ("Status forgery ... Shepherd does not treat that as a security boundary"). It does not cover the case where a genuine agent publishes idle/done before the dialog is cleared or rendered, for example a stale render after an answer or status emitted ahead of the redraw. Status publication and screen rendering are not atomic.

The ADR's own decision test (keep a mitigation if it stops "a real field bug ... backed by a measurement") supports removal as long as no such race has been measured. We could not find a measurement either way. The #69 tests forged the OSC output and do not show a genuine-agent race.

Requested change (any one is enough):

  • State in the PR body, or in the ADR, that Test that a forged OSC 7501 status cannot make send_input type into a blocked pane #62 is superseded and that send_input treats pane status as authoritative even if the screen still shows a dialog, with genuine races out of scope until measured; or
  • Keep a narrower mitigation, such as a screen check only on a blocked -> idle/done transition, or a structured permission event where the backend provides one.

Optional: add a neutral test that pins the new behavior (status idle with a dialog on screen sends), so the trade-off is recorded in code rather than only implied by the deletion.

🟡 F2: Deleted tests cover parser behavior, not only forgery

Most of the removed tests check the OSC 7501 parser and pane-state accounting, which still apply under the trust model. Removed with no replacement:

  • Emulator level: an_unterminated_or_aborted_status_is_not_applied (CAN/SUB abort, C1 ST), a_status_ended_by_another_escape_sequence_still_counts, extra_and_repeated_fields_follow_the_parser_rules (first state wins, unknown/non-ASCII fields), a_status_smuggled_in_a_title_or_a_string_sequence (title/DCS/APC/PM/SOS), and the embedded-in-output test that checks the status is stripped from screen text.
  • Pane level (real PTY rig): a_forged_status_split_across_reads_applies_only_when_complete, a_discarded_or_aborted_forgery_leaves_the_status_alone, a_flood_of_flips_is_bounded_in_the_log_but_not_in_the_turn, and a_forged_blocked_refuses_send_input_but_the_operator_can_still_act.

Elsewhere in the tree, only term.rs::an_unterminated_osc_is_bounded_and_discarded overlaps, and only partly. The field rules and the title/string-sequence handling are tested nowhere else after this PR, so a parser regression there would pass CI.

Requested change: restore these tests under neutral names in a parser-focused file. Suggested names: unterminated_or_aborted_status_is_not_applied, status_ends_at_escape_sequence_boundary, status_fields_follow_parser_rules, status_is_detected_only_in_valid_escape_contexts, status_is_removed_from_rendered_text, status_split_across_reads_applies_only_when_complete, discarded_status_does_not_change_state, rapid_status_flips_bound_log_and_increment_turn. The tests whose only purpose is to defend against a hostile pane (idle/done over a visible dialog) can stay deleted, consistent with the ADR.

🟢 F3: Blocked-state guard intact

send_text_within still builds InputGuard { refuse_when_blocked: true, .. }, and native check_guard still refuses State::Blocked. The guard call sites match on both sides of the diff. The diff in tool_flows.rs only removes the read step and re-indexes the log.

🟢 F4: Smaller hot path, clean removal

Every send_input on an idle/done pane with a known dialect no longer reads and parses the screen. No references to screen_shows_permission_dialog remain, no dependency, workflow or CI files changed, and git diff --check is clean.

Baseline Check
What's Good (🟢)

5. Three Reasons We Might Not Need This PR

  1. The check is cheap insurance - One screen read per send on a known dialect is a small cost, and it fails closed in the one place where a wrong keystroke can approve a dialog.
  2. It covers a correctness case, not only forgery - A genuine status/render race would type into a dialog in the same way, and the trust model does not address that case.
  3. The test cut is broader than the policy change - Dropping the guard needs only the hostile-pane tests removed. The parser tests could stay with no cost to the simplification.

@chaodu-agent
chaodu-agent merged commit c01bb31 into main Nov 14, 2026
4 of 5 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