Skip to content

fix(audit r2): permission rules + markdown tables + /retry + bold streaming + atomic save + UX polish - #56

Merged
yogthos merged 4 commits into
mainfrom
chore/audit-fixes-r2
May 20, 2026
Merged

fix(audit r2): permission rules + markdown tables + /retry + bold streaming + atomic save + UX polish#56
yogthos merged 4 commits into
mainfrom
chore/audit-fixes-r2

Conversation

@yogthos

@yogthos yogthos commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Second sweep of audit fixes — 4 rounds, 15 of 16 outstanding findings resolved.

Round 1 — CRITICAL/HIGH

  • apply_patch, lsp, question added to `PermissionConfig` so per-pattern rules work
  • Markdown tables now render as box-drawn cards (was silently dropping content); enabled `Options::ENABLE_TABLES`

Round 2 — HIGH

  • `/retry` pops the previous assistant response before restoring the prompt
  • `write_line` / `write` apply Bold for bright colors so streaming text reads consistent with the post-redraw viewport
  • `apply_patch` update operation normalizes CRLF (Windows files now work)
  • `apply_patch` clears the cache once after the batch instead of per-op

Round 3 — MEDIUM

  • `save_session` is now atomic (write-temp + fsync + rename) — no more truncated session files
  • Auto-compact failure renders as a framed alert with recovery hints instead of a single dim line
  • `set_label` uses `ensure_back_compat_initialized` for consistency
  • Bash command splitting works without `semantic-bash` feature (best-effort `&&`/`;`/`||` split + command-substitution detection)

Round 4 — LOW

  • Prompt indicator + input text + search bar get Bold-glow for bright colors
  • Avatar updates to Alert immediately when permission prompt appears (was waiting for next keystroke)

Skipped

  • grep `glob_to_regex` "doesn't escape `.`" — false positive; line 43 already pushes `\.`

Totals

  • 612 tests pass, 0 fail
  • Both build profiles -> 0 warnings
  • Audit findings: 22 of 24 from the prior round + 15 of 16 from this round = 37 of 40 fixed across both audits; remaining 3 are doc/style nitpicks (restrictive-mode docstring clarity, etc.)

Yogthos added 4 commits May 20, 2026 19:24
…tables

Round 1 of the follow-up audit fixes.

## PermissionConfig: apply_patch, lsp, question (CRITICAL/HIGH)

Three tools called `check_perm(..., "apply_patch", _)` /
`check_perm(..., "lsp", _)` / `check_perm(..., "question", _)`
but their keys were never registered in `PermissionConfig`. Users
could only allow/deny them via the global `*` default — no
per-pattern rules.

Each added as `Option<ToolPerm>` on `PermissionConfig` and wired
into `checker.rs`'s tool-rule map. Users can now write rules like:

  "apply_patch": { "**/*.rs": "allow", "**": "ask" }
  "lsp": "allow"
  "question": "ask"

## Markdown tables now render (CRITICAL)

`Tag::Table` / `TableHead` / `TableRow` / `TableCell` had empty
match arms; pipe-delimited tables in agent replies silently
dropped. `pulldown_cmark::Parser::new` doesn't emit table events
by default either — needs `Options::ENABLE_TABLES`.

Now enables tables and renders as box-drawn cards:

  │ col1 │ col2 │ col3 │
  ├──────┼──────┼──────┤
  │ a    │ b    │ c    │
  │ d    │ e    │ f    │

Column widths computed from longest cell content, capped to fit
inside `max_width` so a runaway cell can't break alignment.
Header row paints in `theme::header()`, body in `theme::agent()`,
separator in `theme::dim()`. Long cells truncate with `…` to
preserve the right border.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
Round 2 — HIGH fixes.

## /retry pops the previous assistant response

Previously /retry only restored the last user prompt into the
editor and left the failed assistant reply in the session. On
retry the agent saw its own bad answer as context. Now calls
`undo_last(session)` first to pop the trailing assistant message
(plus the user message if `undo_last`'s pair logic fires), then
restores the user prompt to the editor and re-renders.

## write_line + write now apply Bold for bright colors

`render_viewport` applied `Attribute::Bold` to bright tones for
the phosphor glow effect; the streaming paths (`write_line` and
`write`) didn't. Streamed agent text rendered flat until the
next full repaint shifted it to bold. Both streaming paths now
check `theme::is_bright` and wrap output in `Bold` /
`NormalIntensity`, matching the viewport's per-row paint.

## apply_patch CRLF normalization on update

`apply_patch`'s `update` operation did a raw substring match on
the file's on-disk bytes. The LLM almost always emits `\n` for
line breaks even when the file is CRLF on disk, so the substring
match failed on Windows-style files. `edit.rs` already had this
normalization; copying the same pattern here:

1. Read original.
2. If CRLF detected, build a `\n`-normalized working copy for
   matching.
3. Normalize `old_text` and `new_text` to `\n` for the match +
   replace.
4. Re-apply CRLF on write-back so the file's line endings aren't
   silently changed.

## apply_patch clears cache once after the batch

Per-op `cache.clear()` was wasteful — a 5-op batch cleared 5
times. Now the cache is cleared exactly once after all ops in the
batch finish (success or partial failure).

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
…lback split

Round 3 — MEDIUM fixes.

## Atomic session save

`save_session` did `fs::write(path, json)` directly. A crash mid-
write left a truncated `.json` that `find_recent_sessions` would
skip silently — the user would notice their session was missing
without knowing why. Two concurrent dirge processes saving the
same id could also interleave bytes.

Now write-temp-then-rename:
1. Write to `dir/.{id}.json.tmp`.
2. `sync_all()` (best-effort; non-fatal on unsupported FS).
3. `rename(tmp, target)`.
4. Clean up the tmp if rename fails.

Rename is atomic on every OS we target as long as both paths
live on the same filesystem (they do — same dir).

## Auto-compact failure is visible now

The old auto-compact error was a single dim red line that
scrolled past unnoticed. Users kept typing into an over-full
context and got mysterious context-length errors next turn.
Replaced with a framed alert:

  ╭─ ⚠ AUTO-COMPACT FAILED ─...─╮
  │ cause: <error>
  │ context is over the threshold — replies may start
  │ hitting context-length errors. Try /compress
  │ manually, /clear to start fresh, or restart with
  │ a larger context_window in config.
  ╰─...─╯

Same style as the permission alert; impossible to miss.

The success-path banner also got a soft accent treatment
(`▒░ auto-compacting context ░▒`) so the user sees auto-compact
running rather than wondering why the next prompt is slow.

## set_label uses ensure_back_compat_initialized

Every other mutation method (`add_message`, `pop_last_message`,
`switch_to_leaf`, `fork_at`, `compress`) calls
`ensure_back_compat_initialized`. `set_label` was still using the
older split call. Switched for consistency so future label-aware
code reading `message_store` doesn't trip on an uninitialized store.

## Bash segment splitting without `semantic-bash` feature

Without the tree-sitter feature, complex compound commands like
`safe_cmd && rm -rf /` were checked as a single string against
the bash permission rules. If `safe_cmd && rm` didn't match any
deny pattern, the dangerous part squeaked through.

Now does a best-effort coarse split on `&&` / `;` / `||` and
checks each segment separately. Command substitution / subshell
constructs (`$(...)`, backticks, `<(...)`, `>(...)`) still need
the full parser, so when one is detected we fall back to
whole-command check — that surfaces the unfamiliar form before
any segment runs, letting the user explicitly allow or deny.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
…on alert

Round 4 — LOW fixes (final audit round).

## Prompt + input + search bar get Bold-glow

The bottom row (prompt indicator, input text, search bar)
rendered without `Attribute::Bold` for their bright colors. Chat
above bloomed; the bottom area looked dimmer. Each site now
checks `theme::is_bright` and wraps the write in Bold /
NormalIntensity, matching the viewport's per-row paint.

Status row stays unbolded — `theme::dim()` is dim by design
(two-tone phosphor depth) so `is_bright` correctly returns false.

## Avatar updates instantly on permission alert

Setting the avatar state to `Alert` happened before the alert
box rendered, but the bottom-row repaint that actually shows the
new face waited for the next event (typically the user's
keystroke answering the prompt). The face still showed the
in-flight tool (Reading/Writing/Bash) while the alert was up.

Now `draw_bottom` runs explicitly right after the state change so
the `(O_O)` Alert face appears at the same moment the alert box
does.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.

Note: the previously-flagged "grep.rs glob_to_regex doesn't escape
dots" finding turned out to be a false positive — line 43 of
`grep.rs` does push `\\.` for `.` correctly. Skipped.
@yogthos
yogthos merged commit 2559208 into main May 20, 2026
1 check passed
@yogthos
yogthos deleted the chore/audit-fixes-r2 branch May 20, 2026 23:31
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…eaming + atomic save + UX polish (dirge-code#56)

* fix(audit): permission rules for apply_patch/lsp/question + markdown tables

Round 1 of the follow-up audit fixes.

## PermissionConfig: apply_patch, lsp, question (CRITICAL/HIGH)

Three tools called `check_perm(..., "apply_patch", _)` /
`check_perm(..., "lsp", _)` / `check_perm(..., "question", _)`
but their keys were never registered in `PermissionConfig`. Users
could only allow/deny them via the global `*` default — no
per-pattern rules.

Each added as `Option<ToolPerm>` on `PermissionConfig` and wired
into `checker.rs`'s tool-rule map. Users can now write rules like:

  "apply_patch": { "**/*.rs": "allow", "**": "ask" }
  "lsp": "allow"
  "question": "ask"

## Markdown tables now render (CRITICAL)

`Tag::Table` / `TableHead` / `TableRow` / `TableCell` had empty
match arms; pipe-delimited tables in agent replies silently
dropped. `pulldown_cmark::Parser::new` doesn't emit table events
by default either — needs `Options::ENABLE_TABLES`.

Now enables tables and renders as box-drawn cards:

  │ col1 │ col2 │ col3 │
  ├──────┼──────┼──────┤
  │ a    │ b    │ c    │
  │ d    │ e    │ f    │

Column widths computed from longest cell content, capped to fit
inside `max_width` so a runaway cell can't break alignment.
Header row paints in `theme::header()`, body in `theme::agent()`,
separator in `theme::dim()`. Long cells truncate with `…` to
preserve the right border.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.

* fix(audit r2): /retry, streaming bold, apply_patch cache + CRLF

Round 2 — HIGH fixes.

## /retry pops the previous assistant response

Previously /retry only restored the last user prompt into the
editor and left the failed assistant reply in the session. On
retry the agent saw its own bad answer as context. Now calls
`undo_last(session)` first to pop the trailing assistant message
(plus the user message if `undo_last`'s pair logic fires), then
restores the user prompt to the editor and re-renders.

## write_line + write now apply Bold for bright colors

`render_viewport` applied `Attribute::Bold` to bright tones for
the phosphor glow effect; the streaming paths (`write_line` and
`write`) didn't. Streamed agent text rendered flat until the
next full repaint shifted it to bold. Both streaming paths now
check `theme::is_bright` and wrap output in `Bold` /
`NormalIntensity`, matching the viewport's per-row paint.

## apply_patch CRLF normalization on update

`apply_patch`'s `update` operation did a raw substring match on
the file's on-disk bytes. The LLM almost always emits `\n` for
line breaks even when the file is CRLF on disk, so the substring
match failed on Windows-style files. `edit.rs` already had this
normalization; copying the same pattern here:

1. Read original.
2. If CRLF detected, build a `\n`-normalized working copy for
   matching.
3. Normalize `old_text` and `new_text` to `\n` for the match +
   replace.
4. Re-apply CRLF on write-back so the file's line endings aren't
   silently changed.

## apply_patch clears cache once after the batch

Per-op `cache.clear()` was wasteful — a 5-op batch cleared 5
times. Now the cache is cleared exactly once after all ops in the
batch finish (success or partial failure).

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.

* fix(audit r3): atomic save, auto-compact UX, set_label init, bash fallback split

Round 3 — MEDIUM fixes.

## Atomic session save

`save_session` did `fs::write(path, json)` directly. A crash mid-
write left a truncated `.json` that `find_recent_sessions` would
skip silently — the user would notice their session was missing
without knowing why. Two concurrent dirge processes saving the
same id could also interleave bytes.

Now write-temp-then-rename:
1. Write to `dir/.{id}.json.tmp`.
2. `sync_all()` (best-effort; non-fatal on unsupported FS).
3. `rename(tmp, target)`.
4. Clean up the tmp if rename fails.

Rename is atomic on every OS we target as long as both paths
live on the same filesystem (they do — same dir).

## Auto-compact failure is visible now

The old auto-compact error was a single dim red line that
scrolled past unnoticed. Users kept typing into an over-full
context and got mysterious context-length errors next turn.
Replaced with a framed alert:

  ╭─ ⚠ AUTO-COMPACT FAILED ─...─╮
  │ cause: <error>
  │ context is over the threshold — replies may start
  │ hitting context-length errors. Try /compress
  │ manually, /clear to start fresh, or restart with
  │ a larger context_window in config.
  ╰─...─╯

Same style as the permission alert; impossible to miss.

The success-path banner also got a soft accent treatment
(`▒░ auto-compacting context ░▒`) so the user sees auto-compact
running rather than wondering why the next prompt is slow.

## set_label uses ensure_back_compat_initialized

Every other mutation method (`add_message`, `pop_last_message`,
`switch_to_leaf`, `fork_at`, `compress`) calls
`ensure_back_compat_initialized`. `set_label` was still using the
older split call. Switched for consistency so future label-aware
code reading `message_store` doesn't trip on an uninitialized store.

## Bash segment splitting without `semantic-bash` feature

Without the tree-sitter feature, complex compound commands like
`safe_cmd && rm -rf /` were checked as a single string against
the bash permission rules. If `safe_cmd && rm` didn't match any
deny pattern, the dangerous part squeaked through.

Now does a best-effort coarse split on `&&` / `;` / `||` and
checks each segment separately. Command substitution / subshell
constructs (`$(...)`, backticks, `<(...)`, `>(...)`) still need
the full parser, so when one is detected we fall back to
whole-command check — that surfaces the unfamiliar form before
any segment runs, letting the user explicitly allow or deny.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.

* fix(audit r4): bold streaming for prompt/input/search, avatar redraw on alert

Round 4 — LOW fixes (final audit round).

## Prompt + input + search bar get Bold-glow

The bottom row (prompt indicator, input text, search bar)
rendered without `Attribute::Bold` for their bright colors. Chat
above bloomed; the bottom area looked dimmer. Each site now
checks `theme::is_bright` and wraps the write in Bold /
NormalIntensity, matching the viewport's per-row paint.

Status row stays unbolded — `theme::dim()` is dim by design
(two-tone phosphor depth) so `is_bright` correctly returns false.

## Avatar updates instantly on permission alert

Setting the avatar state to `Alert` happened before the alert
box rendered, but the bottom-row repaint that actually shows the
new face waited for the next event (typically the user's
keystroke answering the prompt). The face still showed the
in-flight tool (Reading/Writing/Bash) while the alert was up.

Now `draw_bottom` runs explicitly right after the state change so
the `(O_O)` Alert face appears at the same moment the alert box
does.

## Test plan

- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.

Note: the previously-flagged "grep.rs glob_to_regex doesn't escape
dots" finding turned out to be a false positive — line 43 of
`grep.rs` does push `\\.` for `.` correctly. Skipped.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
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.

1 participant