Bug-hunt round 37: unsanitised device roster, scoped network claim, undocumented 16 MiB cap - #54
Merged
Conversation
startRecorders logs the detected avfoundation microphone roster so an operator can spot a virtual audio driver shadowing the real mic. Device names come from the same unsanitised ffmpeg listing as outputTail's input, but unlike outputTail and lockedBuffer.tail (both wrapped in SafeText/SafeTextLines), this print reached the terminal raw — a crafted device name (a malicious USB/virtual-audio driver) carrying ESC/ANSI bytes or invisible-Unicode bidi overrides could inject terminal escapes or, more pointedly, use bidi reordering to disguise its own roster entry, defeating the very feature meant to reveal it. Factor the roster line into formatAudioRoster (mirroring this file's other pure, unit-testable builders) and route it through session.SafeText, matching every other ffmpeg-derived string this package prints. Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
The Summary and Decisions sections each stated "No LLM and no network
anywhere in the CLI", contradicting the spec's own Design section
("the CLI never calls a model, holds no keys, and adds no network
dependency" — a claim about analyze adding nothing new) and the
network use record's live capture, the demo page's CDN-loaded rrweb
recorder, and the ASR engine's model fetch each independently carry.
Reword both to the scoped form already used everywhere else in the
repo, without touching the correctly-scoped carriers.
Assisted-by: Claude:claude-sonnet-5
timeline.jsonl, transcript.jsonl, interactions.jsonl, and findings.jsonl have each carried a 16 MiB total-size limit since round 33 (session.MaxJSONLBytes), enforced by every reader and writer for those four files. No user-facing doc named it: cli.md's and the instrument-your-own-app how-to's 413 lists for the demo capture endpoints omitted the total-size refusal, the ingest and review sections omitted their own, and session-directory.md never mentioned a size limit on any file. A user hitting the refusal had no documented cause or remedy. Assisted-by: Claude:claude-sonnet-5
release.yml's installer-flag-handling comment said "four" early-return paths; both it and ci.yml actually run six invocations (four flags, two of them also tested with an empty value) — ci.yml's own comment already said six. install.sh's version-stamp comment claimed v0.1.0 predates the version stamp and is refused there, reporting "testimony dev". It doesn't: v0.1.0 was hand-built and hand-stamped before the release workflow existed, so it prints its own tag and passes that gate like every later release. It is refused later, correctly, for lacking a build attestation the workflow did not yet exist to create — a comment correction, not a behaviour change. Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
Correctness review of this round's own PR caught two inaccuracies introduced by its own commits: CHANGELOG.md claimed every reader and writer for the four capped JSONL files "has enforced it since round 33", contradicting the same section's own round-35/36 entries — the last writers (review's verdict append, demo's interactions endpoint) landed in round 36. And CHANGELOG.md/DECISIONS.md both said a pre-attestation release like v0.1.0 "passes [the version-stamp] gate, and is refused later ... by the attestation check" — install.sh runs attestation verification before the version-stamp gate, not after. Also tightened, on the same review's non-blocking notes: session-directory.md's "every reader and writer ... refuses" absolute (analyze's internal re-ingest probe deliberately does not bound the total, though the overall ingest path still refuses); the roster fix's CHANGELOG entry no longer echoes round 27's own "the one class" framing, which it now contradicts; and formatAudioRoster's comment names SafeTextLines as what its siblings actually call, rather than implying they share the exact same call. Assisted-by: Claude:claude-sonnet-5
Docs-accuracy review of this round's own PR caught three defects: - The "four" vs "six" installer-flag-path wording in release.yml was already litigated in round 24 (.abcd/work/DECISIONS.md, "both accurate at their own granularity") and explicitly not changed. This round re-raised and "fixed" the same wording without checking that precedent — reverted release.yml to its original comment and moved the item from this round's nitpicks to its refuted list, as a precedent duplicate of round 24. - The persona-pronoun refutation cited "rounds 14, 24, and 29" as prior occurrences; round 29 does not exist. The third occurrence is round 28, which excluded the identical claim as a precedent duplicate of round 24. - The id-required refutation attributed the timeline package's empty-id skip to "round 27" — round 27 only litigates the Required-column semantics the skip relies on; the skip itself landed in round 10 (merged as PR #27, not round 27). Also tightened two overstated claims in the spc-2 network-claim entry: the Summary's absolute sentence was removed outright (a scoped form already sits two sentences earlier in the same paragraph), not "reworded", and the corrected phrasing is not attributed to a "Design section" it does not appear in. Assisted-by: Claude:claude-sonnet-5
Round-2 docs-accuracy review caught that "two sentences earlier" undercounted: the correctly-scoped sentence is the paragraph's second sentence, the removed one its seventh — five apart, not two. Drop the specific count rather than restate it wrong a second time, matching this file's own round-36 precedent for an unreliable count. Assisted-by: Claude:claude-sonnet-5
Round-2 correctness review caught "see Refuted below" pointing at this entry's Refuted list, which contains no item about analyze's internal re-ingest probe not bounding the total — the reasoning actually lives in rounds 35 and 36's own entries above. Repoint it there. Assisted-by: Claude:claude-sonnet-5
Closed
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.
Round 37 of the autonomous bug-hunt loop (state tracked on #24).
Confirmed findings
Substantive (3)
record's detected audio-device roster reached the terminal unsanitised —internal/record/record.go(startRecorders, previously inline; nowformatAudioRosterininternal/record/recorders.go). The roster prints ffmpeg's raw avfoundation device-name strings so an operator can spot a virtual audio driver shadowing the real microphone. Every other consumer of the identicalprobeSinkbytes —outputTailandlockedBuffer.tail— already passes throughsession.SafeTextLines; this print site did not. A crafted device name (a malicious USB or virtual-audio driver) carrying ESC/ANSI bytes or an invisible-Unicode bidi override could inject terminal escape sequences or, more pointedly, use bidi reordering to hide its own entry from the operator the roster exists to warn — defeating the feature's own purpose. Fixed by factoring the print intoformatAudioRoster, matching this file's other pure, unit-tested builders, and routing it throughsession.SafeText(the single-line variant suffices; the literal newlines in the format string never pass through the sanitiser). New testTestFormatAudioRosterSanitisesDeviceNames, confirmed to fail before the fix and pass after.spc-2-analysis-findings.md's absolute "no network anywhere in the CLI" claim (.abcd/development/specs/open/spc-2-analysis-findings.md:25and its previously-unnoticed sibling in the Decisions section) — flagged as a known, deferred defect in round 36's own decision-log entry. The Summary section already states a correctly-scoped claim two sentences earlier ("the CLI never calls a model, holds no keys, and adds no network dependency" — aboutanalyzeadding nothing new), so its own absolute restatement is removed outright as redundant; the Decisions section's separate "the CLI never calls a model or the network" is reworded to the same scoped form used everywhere else in the repo.record's live capture, the demo page's CDN-loaded rrweb recorder (internal/demo/assets/index.html:112), and the ASR engine's model fetch (docs/explanation/privacy.md,docs/how-to/transcribe-a-recording.md) each falsify the absolute version.The 16 MiB JSONL total-size limit was undocumented —
session.MaxJSONLBytes(internal/session/session.go:433) has boundedtimeline.jsonl/transcript.jsonlreads and writes since round 33, withfindings.jsonl(round 35'sanalyze -ingest, round 36'sreview) andinteractions.jsonl(round 36's demo capture endpoint) closing the last write-side gaps. No user-facing doc named it:docs/reference/cli.md's anddocs/how-to/instrument-your-own-app.md's 413/refusal enumerations omitted it, anddocs/reference/session-directory.mdnever mentioned a size limit on any file. A user hitting the refusal (e.g.demo's "session's captured interactions are at the file size limit; start a new session") had no documented cause or remedy. All three pages now name it, describing the write/load-refusal outcome rather than claiming every internal reader enforces it —analyze's internal re-ingest probe (holdsVerdicts) deliberately does not itself bound the total (see Considered and rejected below).Nitpick (1)
install.sh's version-stamp comment wrongly claimed v0.1.0 is refused there as an unstamped"testimony dev"build. It isn't: v0.1.0 was hand-built and hand-stamped before the release workflow existed, so it prints its own tag and passes that gate like every later release — it is refused earlier, correctly, at the preceding attestation check, for predating the workflow that creates attestations. Comment corrected; no behaviour change (the refusal itself is intentional, fail-closed, and correct — see Considered and rejected below).Considered and rejected
transcript.jsonl/timeline.jsonl'sidfield marked "Required" insession-directory.mddespitemergenot enforcing its presence. Both adversarial reviews killed this:merge's empty-id skip (internal/timeline/timeline.go, landed round 10) is deliberate and documented in three places (timeline.go,analyze/ingest.go,analyze_test.go), and the table's "Required" column tracking shape rather than enforcement was already explicitly litigated in round 27 for a neighbouring field — several other "yes"-marked, unenforced fields (session,text) establish the same pattern.ELOOP-vs-EMLINKsymlink-refusal gap ininternal/session/session.go'sO_NOFOLLOWcheck. Refuted by reading Go's own stdlib source:internal/syscall/unix/nofollow_posix.goexplicitly cites Apple'sopen(2)and puts darwin in theELOOPbucket;EMLINKis confined to FreeBSD/DragonFly, neither a platform testimony ships.release.ymlprivate-repo attestation-fallback comment, re-raised as inaccurate. This is a precedent duplicate of a claim already refuted in round 20 for the identical reason: an unauthenticated tarball fetch against a private repo 404s before the attestation branch is ever reached, so the claimed failure mode cannot occur.03-personas.md's gendered narrative pronouns. A precedent duplicate refuted in rounds 14, 24, and 28: different registers for the same names, co-authored in one commit, and the intents rule is honoured with zero exceptions across all nine intent drafts.release.yml's installer-flag-handling comment's "four early-return flag paths" read againstci.yml's "six" invocations. A precedent duplicate of round 24's identical count read againstinstall.shitself, already settled there as both accurate at their own granularity (four code paths, six invocations exercising them) — reverted to the original comment rather than re-litigated.install.sh --versionclaimed broken for pre-attestation releases under a modern, authenticatedgh. Spun off from the private-repo investigation above. Both refuters agreed the refusal is correct, security-required, fail-closed behaviour against a release with no attestation to verify (a true negative, not a false positive) — the actual, narrower defect was the stale comment describing it, fixed above.internal/record/proc.go'sdroppedflag on an exact-boundary write (unobservable — every code path that could set it wrongly also guarantees the truncation branch that reads it correctly prefixes the ellipsis);demonot handling SIGHUP unlikerecord(no data-loss risk —demo's stream writes are already durableO_APPEND, unlike the ffmpeg-container-finalisation risk SIGHUP handling protects inrecord).session-directory.md's illustrative manifest example differing from the bundled fixture (the page's examples are consistently abridged illustrations throughout, not fixture excerpts — the transcript and findings examples abridge the same way);AGENTS.mdomittingrelease.yml's tag-time-only gates (the section is explicitly scoped to locally-runnable commands plus ci.yml's push/PR gates, never claiming to enumerate every workflow).Merge-gate corrections
Two rounds of post-fix adversarial review ran against this PR's own diff before merge. The correctness reviewer caught an inverted claim (a pre-attestation release "passes the version-stamp gate, then is refused later by attestation" — install.sh actually runs attestation first) and an overclaimed enforcement date ("every reader/writer ... since round 33", when the last two writers landed in round 36); both corrected. The docs-accuracy reviewer caught that the "four vs six" installer-comment change re-litigated a precedent round 24 had already settled — reverted — plus two round-citation errors (a nonexistent "round 29", and "round 27" misattributed for a fix that landed in round 10) and two overstated claims in the spc-2 entry (a deleted sentence described as "reworded"; the correct phrasing misattributed to a "Design section" it doesn't appear in) — all corrected.
Verification
go build,gofmt -l .,go vet ./...,go test ./...,go test -race ./...all clean; pipeline smoke (merge+reportagainst a scratch copy ofexamples/sample-session) succeeded;sh -n install.sh && bash -n install.shclean;release.ymlre-parsed as YAML after the revert. Working tree otherwise untouched.Assisted-by: Claude:claude-sonnet-5