Skip to content

(remote): send a prompt to an unattached remote session (#219) - #397

Merged
devsuitup merged 5 commits into
mainfrom
feat/219-remote-send-prompt
Oct 2, 2026
Merged

devsuitup merged 5 commits into
mainfrom
feat/219-remote-send-prompt

Conversation

@devsuitup

Copy link
Copy Markdown
Owner

First part of #219: send a prompt to a live remote session that is not attached in a terminal, over the CLI's own messaging socket. The issue stays open for the follow-ups listed below.

What changed

  • remote-send.js (new): builds the NDJSON line {"type":"user","message":{"role":"user","content":...},"msgV":1,"session_id":...} (one trailing \n, embedded newlines stay escaped, session_id included so a descriptor whose pid was reused never reaches another session), builds the remote command, and maps its result. The remote command holds only fixed text, the integer pid and the single-quoted socket path: pid check, [ -S path ], then ncat --send-only -U or nc -N -U. Exit 7 = the pid is no longer a claude process, 8 = socket gone, 127 = no ncat/nc.
  • remote-attach.js: defaultRunRemoteCommand takes an input option. stdin becomes a pipe, -n is dropped, the text is written and stdin closed. Same spawn site as before, so remote-ssh-spawn-sites.test.js is untouched. A timeout reports timedOut: true, which the adapter turns into a failure ("no confirmation that the line was written"), never a success.
  • IPC remote-send-prompt {alias, sessionId, text} (main.js, preload.js), modelled on remote-stop-session. The descriptor, and with it the socket path, is looked up main-side; the renderer never supplies a path. A session attached in a terminal is refused.
  • UI: a "Send a prompt…" button on remote rows, shown by CSS only for a live, unattached row (same gating as Stop), opening a small dialog in public/dialogs.js. The dialog says "Sent", never "delivered", and keeps the text when a send fails.
  • Refusals before any ssh: no socket in the descriptor, a Windows named pipe (the channel needs the key file, which Switchboard does not read), a path that is not ^/[A-Za-z0-9._/-]+\.sock$ / has .. / exceeds 107 bytes, a line over 1 MiB of UTF-8, the same text to the same session within 30 s (injected clock; armed only by a successful send).
  • Docs: docs/remote-hosts.md, .ai/contexts/session-cache.md; CHANGELOG entry.

The prompt text never reaches a command line, local or remote. ssh is spawned with an argv array, no shell. .key files and the 8192-byte descriptor cap are untouched.

Tests

  • test/remote-send.test.js: the line, path validation, the command, refusals without spawn, byte cap (boundary inclusive, multibyte), exit-code messages, timeout, 30 s dedupe, IPC contract, preload/main wiring. A real sh runs the delivery segment with a fake nc/ncat on PATH and checks the exact stdin and argv; where /proc/<pid>/cmdline exists it also runs the full command against a live claude-named process and a real unix socket (skipped on Windows).
  • test/remote-run-input.test.js: -n absent with input, stdin piped, exact write then close, timeout, stdin error.
  • test/dom-sidebar-remote-send.test.js, test/dom-send-prompt-dialog.test.js: button on remote rows only, click does not open the row, CSS gating, dialog behaviour.
  • remote-ssh-spawn-sites.test.js and remote-stop.test.js unchanged and green.
  • Mutations (each turned a test red, then reverted): text appended to the command line; trailing newline dropped / doubled / embedded newline unescaped; session_id dropped; -n kept with input; stdin ignored / not closed; timedOut flag dropped; path regex loosened; .. allowed; 107 to 108 bytes; length cap removed; buildSendCommand without validation; Windows pipe not special-cased; each of exit 7, 8, 127, timeout and non-zero mapped to success; cap measured on characters / exclusive / excluding the newline; dedupe window 29 s / never armed / armed on failure / ignoring session / ignoring host / real clock; nc -N and --send-only dropped; ncat never preferred; exec dropped; payload-supplied socket path honoured; attached check removed; text type unchecked; preload leaking an extra field; pid guard removed; button on local rows; click bubbling; CSS showing it on attached rows; dialog saying "Delivered", clearing text on failure, sending empty or trimmed text, rendering the error as HTML.

Not verified (needs a real host)

  • That the prompt is actually delivered (the session's statusUpdatedAt moves).
  • Which nc variant the host has, and that it exits on end of input.
  • That Windows OpenSSH passes the stdin EOF through.
  • The full command against a real /proc was not run on the machine this was written on.

Out of scope

Inbox / notify_when_idle / replies; Windows hosts; trigger files targeting remote ids; the tier ladder (#218); launch and enrolment (#222); the attention state (#394).

Refs #219

A live remote session that is not open in a terminal can now be given a
prompt from its row. The text is written as one NDJSON line to the
session's messaging socket through a single ssh, on stdin only, so it
never reaches a command line. The socket path comes from the descriptor
on the main side; the renderer sends only alias, session id and text.

Refs #219
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 91011d5 (adversarial review in progress).

@devsuitup

Copy link
Copy Markdown
Owner Author

Adversarial review at 91011d5: no injection path, no wrong-target path; changes needed.

  • Blocker (CI): test (22, ubuntu-latest) patch coverage 73% < 80% — public/dialogs.js 452-516 counted at 3%. test/dom-send-prompt-dialog.test.js evaluates the file with a bare basename as filename, so coverage does not attribute it (dom-setup.js passes the absolute path).
  • nc variants: busybox / netcat-traditional reject -N/-U with exit 1, so the user gets a raw usage error instead of the "install netcat-openbsd or ncat" message. Capability will be probed in the remote command.
  • Timeout: the line may already be on the socket when nc fails to exit; the dedupe is not armed then, so a retry after 30 s can deliver twice. Will arm on timeout and say "may have been sent".
  • Dedupe race: the key is set only after success; two concurrent sends of the same text both pass.

Checked and holding: the prompt travels on stdin only; the remote command holds fixed text, an integer pid and the validated, single-quoted path; the alias cannot start with - (declared-alias pattern); the socket path comes only from the main-side descriptor; session_id is JSON-encoded from the descriptor; timeout is a failure, never "sent"; 1 MiB measured on bytes; attached sessions refused; preload forwards exactly {alias, sessionId, text}; status via textContent. Both windows-2022 jobs pass.

The remote command now runs ncat only if it has --send-only and nc only
if its usage line lists -N and -U, else exits 127, so busybox and
netcat-traditional get the install hint instead of a usage error. The
30 s dedupe key is reserved before the spawn, kept on a timeout (the
line may already be on the socket) and released on a definite failure.
Also: bound the text length before encoding, scope the dialog's key
handler to the dialog, and attribute dialogs.js to its real path in the
coverage test.

Refs #219
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 59a4a38 (adversarial review in progress).

The remote command is now one single-quoted argument of sh -c, so a
login shell such as fish never parses it. The nc usage pattern is the
intended literal and is pinned through a real grep. The dialog overlay
is focusable and clicks inside return focus to the textarea so Escape
keeps working.

Refs #219
@devsuitup

Copy link
Copy Markdown
Owner Author

Re-review at 047a624: the re-review items are closed — the send script now runs under sh -c with the whole script single-quoted (no dependence on the login shell), the nc usage pattern is a String.raw with a literal \[ and is pinned by a test through real grep -E, the no-session-id dialog path is covered, focus stays in the dialog so Escape/Ctrl+Enter work after clicks. Known and documented: the tmux probe/stop commands in remote-attach.js/remote-stop.js still rely on a POSIX login shell (pre-existing). Ready to merge once CI (incl. the patch-coverage gate) is green on 047a624.

@devsuitup
devsuitup merged commit 1d6361d into main Oct 2, 2026
10 checks passed
@devsuitup
devsuitup deleted the feat/219-remote-send-prompt branch October 2, 2026 09:58
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