Skip to content

fix(approver): фраза підтвердження Touch ID читається з /dev/tty без EAGAIN - #67

Merged
0design merged 3 commits into
mainfrom
claude/approver-tty-read-20261028
Oct 28, 2026
Merged

0design merged 3 commits into
mainfrom
claude/approver-tty-read-20261028

Conversation

@0design

@0design 0design commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Що змінилося

Після цієї зміни фразу підтвердження add touchid / remove touchid можна ввести в інтерактивному терміналі. Раніше команда падала ще до того, як людина встигала щось надрукувати.

Причина. confirmRegistration у src/approver-setup.mjs читала фразу через readSync(process.stdin.fd). Коли код звертається до process.stdin, Node створює tty-потік: libuv заново відкриває термінал на дескрипторі 0 у неблокуючому режимі. Через це readSync одразу кидав EAGAIN (відтворено на Node 22.14 у трьох терміналах, зокрема в Terminal.app).

Виправлення.

  • Фраза читається з /dev/tty. Його відкриває guardedOpenSync('/dev/tty', 'r'), тобто через ту саму межу persistence.mjs, яку перевіряє check:persistence-writes.
  • Нове відкриття отримує окремий опис файлу, і той завжди блокуючий. Тому неважливо, хто й коли перевів дескриптори 0/1/2 у неблокуючий режим. process.stdin код більше не чіпає.
  • Чому не очікування на EAGAIN через Atomics.wait: такий варіант і далі читав би спільний із Node дескриптор 0 і опитував би його в циклі. Причина залишилася б, змінився б лише симптом.

Межі не послаблені.

  • Перевірка TTY лишилася: tty.isatty(0) && tty.isatty(1). Це номери дескрипторів, тому неблокуючий потік не вмикається.
  • Якщо керуючого термінала немає (/dev/tty не відкривається), команда відмовляє з тим самим кодом HUMAN_CONFIRMATION_REQUIRED.
  • Відкритий дескриптор також перевіряється через isatty. Після читання він закривається у finally.
  • Без змін: точний збіг фрази, межа 64 символи, обрізання \r. Як і раніше, точна фраза, після якої введення закінчується без переводу рядка, вважається підтвердженням. Це зафіксовано тестом як незмінна поведінка.
  • Що змінилося для рецензента: фраза тепер приходить із керуючого термінала процесу (/dev/tty), а не з того термінала, яким є дескриптор 0. В обох випадках це термінал, де сидить людина, і зміна в межах моделі загроз D-0392.
  • У продукт не додано script, expect чи підстановок pty.

Окремим комітом перезакріплено артефакт Core у реєстрі (registry/catalog.source.json, catalog.json, SHA256SUMS). Без цього не проходить перевірка закріпленого артефакту: зараз вона червона і на main («Core artifact does not match catalog pin»).

Тести

  • Новий негативний тест: мок readSync на дескрипторі 0 кидає EAGAIN, а фраза приходить через /dev/tty. Тест перевіряє, що підтвердження проходить, stdin не читається жодного разу, а дескриптор термінала закривається.
  • Ці ж тести прогнано проти старого src/approver-setup.mjs із main: 13 з 14 падають з EAGAIN або на новій перевірці відсутнього /dev/tty. Тобто вони справді ловлять цю помилку.
  • Неправильна фраза для remove: yes, порожнє введення, пробіл у кінці, пробіл на початку, інший регістр, фраза іншої операції (add touchid), задовга фраза, правильна фраза після 65 символів сміття. Усі варіанти відмовляють, допоміжна програма не викликається, дескриптор термінала закривається.
  • Без TTY: якщо isatty повертає false для stdin або лише для stdout, команда відмовляє, а /dev/tty навіть не відкривається. Спроба передати фразу через pipe у CLI так само відмовляє.
  • Відсутній керуючий термінал (ENXIO) дає HUMAN_CONFIRMATION_REQUIRED.
  • Повний набір Q-Core у чистому середовищі: 1733 з 1733. Пройшли також check:persistence-writes і npm run test:package, включно з точним закріпленням артефакту.

Ручна перевірка в справжньому псевдотерміналі (лише локально, не в продукті)

  • Окремий скрипт запускався через Python pty.fork.
  • Він підміняв домашню теку на тимчасову і перевіряв це до запуску. Компіляцію та Touch ID він замінював заглушкою, яка зупиняє виконання одразу після confirmRegistration.
  • Перед викликом скрипт звертався до process.stdin.fd, щоб відтворити умову помилки.
Введення Старий код Новий код
add touchid\n EAGAIN до введення дійшло до кроку після підтвердження
yes\n — HUMAN_CONFIRMATION_REQUIRED (не підтверджено)
100 символів — HUMAN_CONFIRMATION_REQUIRED
echo add touchid | (без TTY) — HUMAN_CONFIRMATION_REQUIRED (потрібен інтерактивний TTY)

Справжній Touch ID не викликався, у ~/.q-core нічого не реєструвалося: до і після перевірки цієї теки не існувало. remove через псевдотермінал не запускався, його покривають модульні тести.

🤖 Generated with Claude Code

0design and others added 3 commits October 28, 2026 16:58
… not from stdin

Node's stdin tty stream reopens fd 0 non-blocking, so readSync(process.stdin.fd)
failed with EAGAIN before the person typed "add touchid" / "remove touchid".
The phrase is now read from a fresh /dev/tty open (its own blocking file
description); process.stdin is never touched. The TTY check stays and uses
tty.isatty(0) and tty.isatty(1); a missing controlling terminal is refused as
HUMAN_CONFIRMATION_REQUIRED. Exact phrase match and the 64-character bound are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; correct phrase at end of input still confirms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@0design
0design merged commit 8f90f7f into main Oct 28, 2026
2 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