Skip to content

Release v0.103.0 — the log scrub never moves a byte; nothing in /share follows a link; speech honours the transcripts setting - #99

Merged
tesseractAZ merged 4 commits into
mainfrom
fix/privacy-and-docs
Sep 15, 2026
Merged

tesseractAZ merged 4 commits into
mainfrom
fix/privacy-and-docs

Conversation

@tesseractAZ

Copy link
Copy Markdown
Owner

Summary

Fixes from the 2026-09-14 review of the live logs.

  • Scrub race (corrects 0.100.6). The per-poll scrub re-checked the size once, then ran write and truncate. An Asterisk append landing between those two steps was cut from the readable copy.
    • While Asterisk runs, the per-poll pass now masks in place with same-length masks (pwrite, never truncate): sip:*…*@, <ip***…>, VERBOSE lines starred out.
    • The boot pass runs before Asterisk starts. It still drops VERBOSE lines and canonicalises masks to sip:***@ and <private-ip>.
  • Symlinks in /share. The directory is writable by the asterisk user, while root and asterisk writers opened, trimmed, chmodded and chowned entries by name. Every writer in /share/switchboard now uses O_NOFOLLOW and refuses anything that is not a regular file:
    • the share-dir permission fix
    • the scrub
    • the heartbeat
    • the delivery ledger
    • the callqos mirror
    • both backup hooks
  • Speech in the add-on journal. AGI and switchboard-stt stderr carried recognised words regardless of assistant_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.
  • Docs.
    • SECURITY.md's destination table now shows that the journal and the /data log carry the full dialplan trace; the old "verbose(2) stops short of verb-3" claim was false.
    • The /share ledgers are listed, along with HA attributes that carry contact_ip and the SSID.
    • DOCS.md now describes the log_level and assistant_transcripts settings accurately.

Testing

  • Full suite passes.
  • New tests append between the check and the write in per-poll mode, and plant a symlink for every writer.
  • Transcript-gate tests cover a missing file, bad JSON and a missing key.
  • Mutation batteries: 35 mutants in the implementation round and 71 after review fixes, all killed.
  • Reviewed adversarially. The major finding (other writers still followed links) was fixed in code, not just in the docs.
  • Leak scan clean.

🤖 Generated with Claude Code

tesseractAZ and others added 3 commits September 14, 2026 18:32
…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>
Comment thread switchboard/rootfs/usr/bin/switchboard-backup-post Fixed
Comment thread switchboard/rootfs/usr/bin/switchboard-backup-pre Fixed
Comment thread switchboard/rootfs/usr/bin/switchboard-callqos Fixed
Comment thread switchboard/rootfs/usr/share/switchboard/rtpmon/poller.py Fixed
Comment thread switchboard/rootfs/usr/share/switchboard/webui/delivery.py Fixed
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>
Comment thread switchboard/rootfs/usr/bin/switchboard-backup-post Dismissed
Comment thread switchboard/rootfs/usr/bin/switchboard-backup-pre Dismissed
Comment thread switchboard/rootfs/usr/bin/switchboard-callqos Dismissed
Comment thread switchboard/rootfs/usr/share/switchboard/rtpmon/poller.py Dismissed
Comment thread switchboard/rootfs/usr/share/switchboard/webui/delivery.py Dismissed
@tesseractAZ
tesseractAZ merged commit 0bf8162 into main Sep 15, 2026
10 checks passed
@tesseractAZ
tesseractAZ deleted the fix/privacy-and-docs branch September 15, 2026 01:39
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.

2 participants