codypendent v0.12.0 (build 99)
Pre-releasev0.12.0 — the desktop catches up, and four security fixes
If you are running v0.11.0, upgrade. This release fixes a terminal escape
injection reachable from ordinary model output — see the security section.
Two things landed since v0.11.0: the desktop app went from a shell to something
close to the TUI's equal, and an external adversarial review produced fixes to
four high-severity bugs, two systemic patterns and fifteen medium findings.
Security — read this part
Terminal escape injection on the main transcript path
Raw model output reached the terminal without ANSI/OSC sanitization. With the fix
reverted, the terminal byte stream literally carried:
\x1b]52;c;cHduZWQ=\x07 OSC 52 — overwrites the user's CLIPBOARD
\x1b]0;pwned-by-osc0\x07 window title
\x1b[2J screen clear
Any prompt-injected repository, malicious ACP agent, or hostile tool output could
reach it. The crate already had sanitizers and applied them everywhere except
the highest-volume text path.
Five ingest points were affected, not one: ModelStreamDelta, NoteAppended, the
RunStarted objective, and the tool name and label on ToolStarted/ToolCompleted.
Fixing only the first would have left the hole open through a tool label.
Worth recording how it was nearly missed: the first analysis concluded ratatui
already neutralises these escapes and the finding was theoretical. A byte-level
test disproved that — Buffer::set_stringn does filter control graphemes, but
that path serves Block titles and Lists, not Paragraph, which is what draws
every transcript row.
Three more
DownloadAllowlistfailed open. An empty allowlist permitted any domain,
against a documented "default is closed: no domain allowed".- WASM
read_fileTOCTOU. Canonicalize-then-open-by-pathname let a symlink
swapped in between the two reads escape the manifest's granted roots. - Missing organization authorization on pairing. Any authenticated user could
create a pairing challenge naming an arbitraryorganization_id.
Also: artifact blobs were written world-readable (0644) while able to carry
Secret-classified content; doctor's "read-only" probe followed symlinks and
could truncate a planted target; the shell env denylist missed GIT_PAGER,
PAGER, EDITOR, LESSOPEN, MAKEFILES, PYTHONBREAKPOINT and CARGO.
Data loss and correctness
edit_filetruncated in place (set_len(0)thenwrite_all), so a
mid-write failure destroyed the user's file. Now temp + rename with an fsync.
This was the worst outcome in the review.- Memory corrections broke temporal queries in both directions.
valid_fromwas written asedit:<uuid>and compared as text against the
fixed-widthseq:form — and"e" < "s", so every correction sorted before
every revision. A corrected fact appeared in all historical as-of queries
and the fact it replaced in none, exactly inverting the store's headline
invariant. See "Known gaps" — existing databases need a migration that is not
in this release. - The ACP bridge bound a turn to the wrong run, and its cancel path could kill
another client's run. - Workflow budgets charged only the last attempt, so
maximum_cost_usdcould
be exceeded by up to the retry factor. - Webhook dedup burned the replay key before dispatch, so a transient sink
failure lost the event permanently with no signal. - The workflow conductor could resurrect a completed run to Paused or
Cancelled; it now uses the CAS the driver already used.
Desktop app
51 of 66 TUI surfaces now exist in the desktop client, up from 6 components and
13 bridge commands.
- Steering a live run — previously absent entirely. It keeps accepted,
queued and applied apart, because the daemon does: an acceptance is never
presented as a queued turn. - Cancel now confirms, showing the objective and live elapsed time read from
the run's ownRunStartedevent. Attached mid-run and never saw it? Both render
as unknown rather than zero. - Model, provider, mode and API-key surfaces; council builder, browser and
results; repository picker; session library and lifecycle; workflow, kanban and
blackboard; skills, memory, docs, plugins, context; a command palette; first-run
onboarding. - API keys travel one way only — presence crosses as
stored | env(NAME) | missing | unknown, never key material.
The repository picker refuses $HOME and non-checkouts. That is not
belt-and-braces: it is where the v0.11 code-graph incident could have returned.
Performance
The repository had no benchmarks and no profiler, which is why two real
performance bugs had survived behind comments asserting they were cheap. It now
has seven criterion targets with deterministic corpora including CJK and ZWJ
emoji.
- TUI scrolling froze on long sessions. The transcript was re-measured in
full every frame — O(total graphemes) with a heap allocation per row — and the
build pass paid it a second time. 71.07 ms → 2.63 ms per frame (27×), and
the win grows with session length. - Three siblings of the same bug found and fixed:
render_runs_paneformatted
every run,render_issueshad a.skip()with no matching.take(), and
render_journeyhad no windowing at all. - The session-library projection adds +35 µs (+55%) to every ledger append —
about 198 ms of SQLite writes for a run of 2000 streamed fragments. Previously
unmeasured; now collapsed from three statements to one. - A repository-map
ORDER BYno index covered, so SQLite built a temporary
B-tree over every node in the repository.
Measured and rejected, because the discipline matters as much as the wins:
strip = "symbols" was a real 22% binary-size saving, removed because it strips
what a panic backtrace needs. lto = "thin" made the binary larger for 2.12×
the CPU. Batching ledger appends was rejected outright — append_next_event
claims its sequence inside the INSERT precisely because read-then-write raced.
Known gaps
- A data migration for the memory fix is NOT in this release. Databases that
already ran the oldcorrect()still holdedit:rows, and those stay
mis-ranked; the code fix governs new corrections only. - Remaining
.skip()-without-.take()sites inrender_backtrack,
render_provider_picker,render_add_model_pick,render_model_pickerand
render_context_pane— known, not missed. PauseRun/ResumeRunand the pending-prompt queue still have no desktop
surface.- Release binaries are still unsigned;
codypendent install desktopremains
the supported path on macOS becauseghsets no quarantine attribute. - Carried forward from v0.11.0: the control plane and remote runner are
source-only and not in the release binaries; no user can be created through
the control-plane HTTP API;JWT_SECRETis a hard startup requirement that
nothing in this repository sets.