Files
Linux_post_install/AgentsReport/builder/2026-08-27_paste-fix.md
T
Your Name 06a05f0567
gates / consistency-and-conventions (push) Successful in 1m24s
ai continue ...
2026-09-04 11:09:21 -04:00

8.0 KiB
Raw Blame History

Builder Report — Paste-Safe Multi-Line Input in pos ai alias Insert Prompt

Date: 2026-08-27 Status: COMPLETE — root cause established, fix implemented, verified, committed, pushed


TL;DR

  • Root cause: menu_ask_value used plain line-oriented read -rp in canonical mode. A multi-line Ctrl+V paste floods the tty queue; read consumes only the first line and the rest stay queued — later prompts eat them, and after the script exits the interactive shell executes whatever remains (user-verified: $(whoami), ; ls, echo test, sudo apt update ran). Single-line paste was unaffected.
  • File changes: lib/menu-lib.sh 169→362 (new menu_read_value raw-mode bracketed-paste reader + menu_redraw; menu_ask_value now delegates to it); bin/pos-ai-alias 712→760 (prompt encode/decode for the ENV file, newline-safe truncate, edit wizard keeps the full original prompt).
  • Verification: pty harnesses — multiline + single-line paste captured verbatim with nothing executed, clean exit; Ctrl-D and Ctrl-C cancel cleanly (terminal restored); full create→list→show→edit E2E; encode/decode round-trips byte-exact.
  • Gates: bash -n ×2 · make gen idempotent · make check OK · make lint 0 FAIL / 0 WARN.
  • Commit: 4306a53 — pushed to main (300b742..4306a53).

[DONE]


1. Root Cause

lib/menu-lib.sh menu_ask_value ended in:

IFS= read -rp "$pr: " val

bash -p read is line-oriented and canonical: it reads until the first newline and returns. A bracketed multi-line paste delivers line one\nline two\n… into the tty input queue as a burst; the read takes line one, and every later line stays queued. Those leftovers are then consumed by the next prompt or — once the script exits and the interactive shell reads keyboard input again — executed as shell commands. That is the paste bug: user-pasted text (including $(whoami), ; ls, sudo apt update in the report which triggered real effects) was treated as live input.

Why not read -erp (readline)?

Tested on a real pty with bracketed paste enabled. Readline consumes the paste atomically (nothing executes, clean exit) — but on accept it returns only the first line; readline buffers a single line. That violates the core requirement that multi-line prompts paste correctly. A custom raw-mode reader is required.

Why not a plain read -N 1 byte loop?

bash read self-interrupts: with stty -isig verified on (diag: -isig -icanon -echo min 1 time 0), od -An -tu1 -N1 reads byte 3 (ETX) fine from the tty, and read -N 1 reads 0x03 fine from a pipe — but bash + tty + 0x03 dies by SIGINT (raw waitpid status 2) in both direct and command-substitution contexts, even with no trap and ISIG off. The reader therefore uses dd bs=4096 count=1 | od -An -tx1 | tr -d ' \n', which the pty tests show reads any byte (incl. 0x03/0x04) as plain data.

[DONE]


2. Fix

lib/menu-lib.shmenu_read_value() (new)

  • stty -icanon -echo -isig min 1 time 0 raw mode (isig off so Ctrl-C/Z/\ arrive as bytes); stty -g snapshot restored on every exit path; trap 'restore; trap - INT TERM; return 1' INT TERM.
  • Enables bracketed paste (\e[?2004h) on entry, disables (\e[?2004l) on exit.
  • Byte input: chunked dd|od|tr reader (one fork per input burst — pastes cost O(chunks), not O(per-byte forks)); bytes delivered as 2-hex-digit strings to a nameref; state (chunk/offset) persists because the reader runs in-place, never inside a $( ) subshell (an early implementation that returned bytes via command substitution looped forever — each subshell's offset increment was lost).
  • Behavior:
    • \e[200~\e[201~ → everything between inserted literally; newline/CR are data (echoed for display; CR renders matching CRLF pastes).
    • Enter outside a paste → submit. Backspace/DEL, Left/Right, Home/End, Delete, Ctrl-U. Up/Down ignored (no history).
    • Ctrl-D on empty → cancel; Ctrl-C/Ctrl-Z/Ctrl-\ → cancel. Cancel returns rc 1 → callers print CANCELLED and continue.
    • Fast append path (no redraw) when the cursor is at end-of-value — pastes render with 0 redraws.
    • Non-tty stdin or stty failure → falls back to plain read (fail-closed, no paste protection possible).
  • menu_ask_value now calls menu_read_value via command substitution (display → stderr, value → stdout, rc 1 = cancel/EOF).
  • Function index comment updated.

bin/pos-ai-alias — prompt persistence + display

  • _alias_prompt_encode\\\, newline → \n (escaped, so the ENV record stays single-line). Uses literal [ "$c" = '\' ] comparisons: bash case patterns do not match a single literal backslash (verified empirically).
  • _alias_prompt_decode — order-safe: \n first, then \\.
  • _alias_load now decodes the prompt field; _alias_save encodes it (previously the raw multi-line value was written straight into the |-delimited ENV record — corruption + line-splitting on round-trip).
  • _alias_prompt_truncate — now replaces newlines with \n for display and takes an optional max-length argument (create confirmation: 50; edit wizard: 80; edit summary: 40).
  • Edit wizard: the prompt default shown is the truncated render, but pressing Enter restores the full original prompt ([ "$tmp" = "$default_display" ] && tmp="$default_prompt") — fixes a pre-existing bug where Edit+Enter silently truncated >80-char prompts; empty-original Enter keeps empty and continues instead of aborting the wizard.

[DONE]


3. Tests Performed

All harnesses under /tmp (not committed).

Test Result
bash -n lib/menu-lib.sh bin/pos-ai-alias
Bracketed multiline paste through menu_ask_value (pty) — full capture
Nothing executed from the pasted content (incl. $(whoami), ; ls, sudo apt update)
Clean exit + terminal restored (\e[?2004l, sane termios)
Single-line paste
Ctrl-D on empty → CANCELLED → DONE, status 0
Ctrl-C → CANCELLED → DONE, status 0 (terminal not left in raw mode)
Encode/decode round-trips: backslash, literal \n text, mixed, empty
Full E2E: create (multiline paste with C:\temp\note + shell fragments) → ENV single-line record (\n/\\ escaped) → wrapper installed, executable, bash -n clean → wrapper passes whole prompt as ONE --system arg ($'…') → list table newline-safe → show decodes full prompt → edit Enter-keeps-full, "No changes — nothing to save."
Gates: make gen idempotent · make check OK · make lint 0 FAIL / 0 WARN

Harness files: pty_menulib_test.py, pty_cancel_test.py, pty_e2e_alias.py, roundtrip_test.sh, plus early experiments pty_paste_test.py/pty_paste_test2.py.

[DONE]


4. Notes / Remaining Risks

  • The reader is byte-oriented; multi-byte UTF-8 is assembled byte-by-byte (correct round-trip; cursor arithmetic counts bytes, so editing inside multi-byte text can be coarse — same limitation as the previous readline-free implementation, acceptable for this tool).
  • Only menu_ask_value was made paste-safe. Other menu-lib primitives (menu_run, menu_pick, menu_guard) use canonical reads where a multi-line paste is not a meaningful input shape; pos-ai-alias is the tool whose "Insert Prompt" step accepted multi-line text.
  • Ctrl-C inside the reader cancels the prompt and continues the script (CANCELLED). Previously any Ctrl-C killed the whole script; behavior is now more forgiving and the terminal is always restored first.
  • CRLF pastes render correctly (CR echoed) and are stored as raw \r only if the terminal emulator delivers them un-mapped; with icrnl on, they normalize to \n — either way the value stays literal text, never input.

[DONE]


Status: COMPLETE. Original objective (paste injection + broken multiline paste in pos ai alias Insert Prompt) is fixed at the shared input layer, verified with pty-level regression harnesses, committed (4306a53), and pushed.