Release v0.103.0 — the log scrub never moves a byte; nothing in /share follows a link; speech honours the transcripts setting - #99
Merged
Conversation
…gated in the journal, security docs corrected Readable-log scrub (webui/logscrub.py, rtpmon, switchboard-config): - The per-poll pass no longer uses the byte-shifting rewrite. While Asterisk appends, every mask is written with os.pwrite over exactly the bytes it replaces and the file is never truncated, so an append cannot be cut or overwritten. v0.100.6's single size re-check left a gap before truncate(). - The rewrite (VERBOSE dropped, canonical sip:***@ and <private-ip>) is kept for the boot pass only, via scrub(..., writer_stopped=True) from the init oneshot. It now also canonicalises the same-length masks and drops blanked VERBOSE lines. - Same-length forms: sip:<one * per char>@, <ip***...> to the address length, and a VERBOSE line kept as its timestamp followed by *. None re-matches. Symlinks in the asterisk-writable share dir: - logscrub opens the log O_NOFOLLOW|O_NONBLOCK and refuses non-regular files. - switchboard-config's chmod/chown loop, the 32 MB trim and the final chown all go through open_share_file(): an fd opened without following links, regular files only. Recognised speech in the add-on journal: - switchboard-stt writes heard= words only when features.json says assistant.transcripts is true; otherwise only the outcome and the length. An unreadable policy withholds the words. - The assistant AGI gates its per-turn line and its TTS failure lines the same way; a TimeoutExpired no longer renders the reply text. - switchboard-tts no longer renders the espeak command line on a timeout. - Comments that called the journal RAM-only, short-lived or wiped by a reboot are corrected; a test now forbids those claims. Docs: SECURITY.md log table covers the journal (verbose trace, account, LAN contact addresses, speech when transcripts are on; about two days, across a reboot), corrects the /data verbose(2) claim, describes both scrub passes and their masks, and lists the three /share jsonl mirrors and the contact_ip / wifi_ssid HA attributes. DOCS.md log_level and assistant_transcripts rows and the en.yaml option description match the new behaviour. Tests: both scrub modes, an append injected mid-pass (and the same injection cutting the boot rewrite), symlink/FIFO/dir entries through the real boot pass, the boot trim through the fd, poller and boot call-site pins; stt main() for every branch and policy plus the option -> features.json bridge; assistant main() with the real say(); an AST scanner for ungated recognised text on stderr with a self-check. Mutation battery: 35 mutants, 35 killed, green before/after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cs aligned SECURITY.md, as written on this branch, said a link planted in /share/switchboard could not redirect a root write, trim, chmod or chown. Only the boot pass and the log scrub worked that way. rtpmon's heartbeat, delivery.record() (the wake-up scheduler and web UI as root, the wake-up AGIs as asterisk), switchboard-callqos and both backup hooks still opened, trimmed and chmodded their ledgers by name. Reproduced in review: with delivery-outcomes.jsonl made a link to a 0600 file outside the directory, record() returned True, appended a line to that file and set its mode to 0620. - rtpmon/poller.py, webui/delivery.py, switchboard-callqos: _open_no_follow() opens with O_NOFOLLOW|O_NONBLOCK and accepts regular files only. _rotate_tail, the ledger appends and delivery's group-write go through that fd. The bytes a rotation leaves are unchanged, so the three copies still agree. - delivery.is_writable(): a link, dangling or not, is not writable. The wake-up reconciler then declines to judge the ring instead of escalating one somebody answered, whose `answered` record the same link refused. - switchboard-backup-pre / -post: the backup-window stamp opens the same way. - SECURITY.md: a table of every writer in the shared folder, as whom it runs and what it does there; what a refused write does; Asterisk's own logger named as outside the rule; backup-window.jsonl listed; the two-day journal reach attributed to the previous boot's log, as the snapshot shows. - DOCS.md: the log_level row names info, debug and trace as the levels whose console carries the dialplan trace, matching SECURITY.md. Tests: - test_share_dir_links.py drives all five appenders (the two backup hooks as subprocesses) against a regular ledger, a link to an outside file, a dangling link, a FIFO with no reader (must not hang) and a FIFO with a reader (must get nothing), with caps shrunk so a trim through a link would show; plus is_writable() on absent, regular, dangling and linked ledgers. - test_wakeup_escalation_paths.py: the scheduler's own _record() and _reconcile_rings() against a linked ledger: nothing written through it, no push, the ring dropped as unjudgeable. - test_logscrub.py: rtpmon run() with the real _heartbeat() against a regular ledger and a link; the boot pass's os-level chown/chmod calls recorded by the inode they land on, which pins the final chown of the log to its fd. - test_ledger_rotation.py: a ledger between the kept half and the cap is not trimmed (the existing under-cap fixture could not see a missing size check). - stt and assistant AGI: an assistant section without the transcripts key keeps the words out of the journal. Mutation: 71 mutants, 71 killed: the implementer's 35 re-run against this tree and 36 for these changes (every helper flag and regular-file check in each copy, each by-path open, the trim's size check, seek and cut, the group-write, both is_writable() guards, the final log chown, and the missing transcripts key). Green baseline before and after; restores sha-verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e follows a link; speech honours the transcripts setting - logscrub: the per-poll pass masks accounts and private IPv4 in place with same-length masks (pwrite, never truncate), so a concurrent Asterisk append cannot be cut; the boot pass (Asterisk stopped) still drops VERBOSE and canonicalises to sip:***@ / <private-ip>. Corrects the 0.100.6 claim. - Every root and asterisk writer in /share/switchboard (share-dir permission fix, scrub, heartbeat, delivery ledger, callqos mirror, backup hooks) opens with O_NOFOLLOW and refuses non-regular files. - Recognised speech in AGI / switchboard-stt stderr is gated on assistant_transcripts (fail-closed when the setting cannot be read). - SECURITY.md / DOCS.md: the journal and /data log carry the full dialplan trace; the /share ledgers and HA attributes (contact_ip, SSID) are listed. Found in the 2026-09-14 live log review. Mutation batteries clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL py/overly-permissive-file flagged the five new O_NOFOLLOW opens: they passed creation mode 0o666, which only the container's umask kept from being world-writable. The shared ledgers need group write (root services and the asterisk-user AGIs append to the same files), not world write, so the mode is now 0o664 — identical effective permissions under umask 022/002, and never world-writable under a permissive one. The three paths not yet in the reviewed baseline for this rule are added with the same justification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes from the 2026-09-14 review of the live logs.
writeandtruncate. An Asterisk append landing between those two steps was cut from the readable copy.pwrite, nevertruncate):sip:*…*@,<ip***…>, VERBOSE lines starred out.sip:***@and<private-ip>.asteriskuser, while root andasteriskwriters opened, trimmed, chmodded and chowned entries by name. Every writer in/share/switchboardnow uses O_NOFOLLOW and refuses anything that is not a regular file:switchboard-sttstderr carried recognised words regardless ofassistant_transcripts. That journal is Supervisor-readable and kept about 2 days, across reboots. It is now gated on the setting and fails closed. Comments that claimed the journal was RAM-only or wiped by a reboot are corrected.contact_ipand the SSID.log_levelandassistant_transcriptssettings accurately.Testing
🤖 Generated with Claude Code