feat(commands): neutral send_keys action to replay a key/text step list to a pane#5
feat(commands): neutral send_keys action to replay a key/text step list to a pane#5Micaxes wants to merge 1 commit into
Conversation
…ist to a pane) Surfaces the existing pane.send_keys / pane.send_text socket primitives as a connector-facing command so a connector can answer a TUI prompt (digit+enter, arrow toggles, or navigate + type a write-in) without ever touching herdr itself. The connector builds the sequence; tendwire relays it verbatim. - commands.py: `send_keys` in ALLOWED_ACTIONS; carries `params.steps`, an ordered list where each step has exactly one of `keys` (whitelisted neutral key tokens) or `text` (validated like instruction.text). New validate_pane_steps(). - actions.py: dry-run preview via _send_keys_result (real delivery is the socket path); non-dry-run on the pure path reports backend_unsupported. - command_submission.py: route non-dry-run send_keys through the mutating path (receipts + idempotency) to a new _socket_send_keys_envelope that resolves the neutral target to a pane and replays the steps via _submit_private_pane_keys. Target stays neutral (worker_id/fingerprint/space/name); key tokens carry no routing/identity data. +11 tests; 364 command/action/submission tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Cross-referencing from the connector half — I just reviewed herdres#150 (changes requested), and one finding really belongs to this PR's contract, so raising it here before the interface freezes: Raw
Suggestion: since Tendwire already carries the decision in |
plotarmordev
left a comment
There was a problem hiding this comment.
Thank you for the contribution. I reviewed this against current main, including the now-merged semantic remote-decision pair (Tendwire #6 / Herdres #153). I do not think a generic connector-facing send_keys action should be added.\n\nRelease blockers:\n\n1. The action delegates backend calibration to an untrusted connector and exposes a low-level sequence of text, navigation, Enter, Escape, and ctrl+letter operations. That is materially broader than the neutral API boundary: a stale or buggy connector can mutate whatever TUI state currently owns the pane. Current main instead accepts a semantic answer_decision selection and keeps private key calibration in Tendwire.\n2. The request is not bound to the exact current pending revision, private binding fingerprint, and observed pane target. A prompt can change between connector observation and replay, allowing the sequence to answer or operate on a different screen.\n3. Multi-step replay is not transactionally knowable. If an early step applies and a later Herdr call returns an error, _socket_send_keys_envelope returns backend_failed rather than terminal uncertainty even though the pane was partially mutated. The stored receipt can therefore claim a definitive failure for an externally changed pane.\n4. The branch predates the current command envelope, receipt authority, pre-send classification, answer_pending, and answer_decision work. Applying it to current main conflicts in command_submission.py, core/actions.py, core/commands.py, and tests/test_commands.py; merging it would require a redesign rather than a conflict-only refresh.\n\nThe original use case is now covered by answer_decision: connectors send option references or write-in text, Tendwire revalidates the exact durable decision/source and owns the calibrated private pane operations. If another TUI interaction is needed later, it should be added as another semantic, revision-bound action rather than a public raw-key relay.
|
Closing as superseded by the narrower semantic answer_decision contract in #6 and its paired Herdres implementation. The detailed review above records the safety and current-main integration reasons; no contributor branch history was modified. |
Adds a connector-facing
send_keyscommand that surfaces tendwire's existingpane.send_keys/pane.send_textsocket primitives, so a connector can answer a blocked TUI prompt (digit+enter, arrow toggles, or navigate + type a write-in) without ever touching herdr itself. The connector builds the sequence; tendwire relays it verbatim over the authoritative socket path.Why
A connector that surfaces an agent's AskUserQuestion / ExitPlanMode prompt (e.g. to chat/Telegram) needs a neutral way to send the chosen answer back to the pane. Today the only mutating action is
send_instruction, whose payload is a single line oftext(Enter-terminated, escape/control sequences stripped) — it can't press a digit, toggle checkboxes with arrows, or navigate to a write-in field.send_keysfills that gap using primitives the socket backend already exposes.Shape
commands.py:send_keysadded toALLOWED_ACTIONS. It carriesparams.steps— an ordered list where each step has exactly one ofkeys(a non-empty list of whitelisted neutral key tokens:enter/tab/space/backspace/escape/arrows/home/end/delete/pageup/pagedown/a digit/ctrl+<letter>) ortext(validated identically toinstruction.text, so a write-in can't smuggle escape/control sequences). Newvalidate_pane_steps().actions.py:_send_keys_resultgives a dry-run preview (resolve target →dry_run); the real replay is socket-only, so a non-dry-run on the pure path reportsbackend_unsupported.command_submission.py: non-dry-runsend_keysflows through the existing mutating path (receipts +request_ididempotency) to a new_socket_send_keys_envelopethat resolves the neutral target to a pane and replays the steps via_submit_private_pane_keys(a small inter-step delay lets the TUI repaint).Neutrality
The target stays neutral (
worker_id/worker_fingerprint/space_id/name);pane_id/terminal_id/session_idremain forbidden request fields. Key tokens are a fixed whitelist carrying no routing/identity data.Tests
+11 tests (valid single/multi/write-in sequences, target/
request_idrequirements, rejection of empty/unknown/escape/both-keys-and-text steps, dry-run).364command/action/submission tests pass. The pre-existingtest_herdr_socket/test_daemon/test_herdr_eventsfailures in a socket-less environment are unrelated to this change (identical with and without it).🤖 Generated with Claude Code