Skip to content

codypendent v0.12.0 (build 99)

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Aug 21:15

v0.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

  • DownloadAllowlist failed open. An empty allowlist permitted any domain,
    against a documented "default is closed: no domain allowed".
  • WASM read_file TOCTOU. 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 arbitrary organization_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_file truncated in place (set_len(0) then write_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_from was written as edit:<uuid> and compared as text against the
    fixed-width seq: 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_usd could
    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 own RunStarted event. 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_pane formatted
    every run, render_issues had a .skip() with no matching .take(), and
    render_journey had 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 BY no 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 old correct() still hold edit: rows, and those stay
    mis-ranked; the code fix governs new corrections only.
  • Remaining .skip()-without-.take() sites in render_backtrack,
    render_provider_picker, render_add_model_pick, render_model_picker and
    render_context_pane — known, not missed.
  • PauseRun/ResumeRun and the pending-prompt queue still have no desktop
    surface.
  • Release binaries are still unsigned; codypendent install desktop remains
    the supported path on macOS because gh sets 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_SECRET is a hard startup requirement that
    nothing in this repository sets.