Commit bf5e98d
chore: audit fixes (5 rounds — CRITICAL/HIGH/MEDIUM/LOW) (#54)
* fix(audit r1): path traversal, compress crashes, question permission
Round 1 of the audit fixes — security + crashes.
## Path traversal in session storage (CRITICAL)
`save_session` / `load_session` / `delete_session` interpolated
`session.id` directly into the `{id}.json` filename. Session ids
are normally UUIDs but they round-trip through JSON on disk; a
tampered-with file with `id: "../../etc/passwd"` could escape the
sessions directory on save or read arbitrary files on load.
New `validate_session_id` gate: accepts `[A-Za-z0-9._-]+` only,
explicitly rejects `.`, `..`, slashes, backslashes. Tested with
the usual escape attempts.
## Compress bounds + leaf-tracking (HIGH)
`Session::compress(_, first_kept_index, _)` did `messages.drain(..first_kept_index)`
with no bounds check — an out-of-range index from a buggy caller
panicked. Now clamped to `messages.len()` so misuse degrades to
"summarize everything" instead of crashing the agent.
Branched-session compaction also had a latent leaf-tracking bug:
if `tree.leaf_id` pointed at a branch leaf that was in the dropped
set (e.g. user forked, then compressed the alternate branch), the
leaf id was left dangling. New post-prune check re-anchors the leaf
to the first kept message (or the summary if everything was dropped).
## QuestionTool routes through permission (HIGH)
`QuestionTool::call` injected user input into the LLM's tool result
without any permission check, unlike `TaskTool` / `WriteTool` /
`BashTool` etc. Added `permission: Option<PermCheck>` +
`ask_tx: Option<AskSender>` to the struct, new `.with_permission()`
builder, and a `check_perm(&self.permission, &self.ask_tx, "question", &summary)`
at the top of `call`. Wired through `builder.rs` so production
paths get the gate; existing tests construct without permission
and continue to pass.
## Test plan
- [x] 2 new tests in `session::storage::tests` (UUID accept,
traversal reject).
- [x] `cargo test --features plugin` -> 606 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
* fix(audit r2): on-complete dispatch, turn-hook slots, harness-response, warn unknown ops
Round 2 — silent failures.
## on-complete now fires (CRITICAL)
`on-complete` was in `HOOK_NAMES` (so plugins defining it got
auto-aliased on load) but no host site dispatched. Plugins that
defined `on-complete` ran forever without ever seeing the event.
Now fires from `AgentEvent::Done` right after `on-response` + the
pending-prompt + store_response sequence, so plugins observe a
fully-complete turn (response stored, pending prompts processed).
## Turn-hook slot reset (HIGH)
`on-turn-start` / `on-turn-end` bypassed `dispatch_tool_hook`'s
slot-clear step, so a plugin calling `(harness/block ...)` from
inside a turn hook would leave the slot set and spuriously block
the *first* tool of the next turn. Both turn hooks now reset
`harness-block` / `harness-mutate-input` / `harness-replace-result`
explicitly after the dispatch returns.
## harness-response cleared after store (HIGH)
`mgr.store_response()` wrote `harness-response` and left it set
indefinitely. Plugins reading the var in a later hook saw stale
text from previous turns. Now cleared with
`(set harness-response nil)` immediately after store, matching the
pattern other slots already use.
## drain_tree_ops warns on unknown op verbs (MEDIUM)
`parse_tree_op_line` silently dropped lines it didn't recognize
(forward-compat — good), but with no diagnostic a typo'd op verb
in a plugin would fail with no trace. Added a `tracing::warn!`
at `dirge::plugin` target so confused plugin authors can spot the
typo in the log.
## Test plan
- [x] `cargo test --features plugin` -> 606 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
* fix(audit r3): markdown theme sweep, cursor flicker, avatar idle, atomic init
Round 3 — theme + UI polish.
## Markdown theme sweep (CRITICAL)
`src/ui/markdown.rs` had 13 hardcoded color literals
(`Color::Cyan`, `Color::DarkYellow`, `Color::DarkGrey`) used for
headings, code blocks, blockquotes, and bullets. Under the
phosphor theme these forced cyan + yellow accents that broke
palette coherence. Swept to `theme::header()`, `theme::tool()`,
and `theme::dim()` so palette swaps cascade through markdown
rendering.
`bullet_prefix(col: Color)` matched on `Color::DarkGrey` as a
sentinel for "this is a blockquote." With the dim color now
themed away from DarkGrey, the sentinel was broken. Refactored
to `bullet_prefix(in_blockquote: bool)` — explicit, theme-safe.
## Cursor flicker on the right (HIGH)
`draw_bottom` called `draw_panel` while the cursor was visible —
the panel's MoveTo loop walked the hardware cursor across the
right-hand panel one cell at a time, visibly flickering. Now
hides the cursor BEFORE panel + avatar paints, places it at the
final input position, then re-shows.
## Avatar resets to Idle on user submit (MEDIUM)
After a turn completed the avatar stuck on Done forever — the
next user prompt didn't visually "wake" it. Now all three
user-message commit sites set the avatar to Idle so the brief
moment between submit and first agent token shows a neutral
face that transitions to Thinking/Speaking naturally.
## Atomic back-compat init (MEDIUM)
`ensure_message_store_initialized()` and
`ensure_tree_initialized()` were called as a pair in every
mutation method; a panic between them could leave the session
half-initialized (tree rebuilt but store empty, or vice versa).
New `ensure_back_compat_initialized()` runs both in one call.
All five mutation sites (add_message, pop_last_message,
switch_to_leaf, fork_at, compress) updated.
## Test plan
- [x] `cargo test --features plugin` -> 606 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
* fix(audit r4): permission gates, schema corrections, single-compaction record
Round 4 — schema + permission hygiene.
## TaskStatusTool routes through permission
`task_status` was the only background-task tool that didn't call
`check_perm`. The wait=true path can hold the parent turn open for
up to 10 minutes; both wait=true and wait=false leak subagent
state to the caller LLM. Now gated identically to `task`.
## PlanEnterTool / PlanExitTool stay un-gated (documented)
These tools surface a user-confirmation dialog via `plan_tx` /
`PlanSwitchResponse::{Accepted,Rejected}` — that IS the gate. A
second `check_perm` call would double-ask the user. Added a
comment in both `call` methods so future readers don't try to
"fix" the missing check.
## WebFetch max_chars: number -> integer
Schema declared `"type": "number"` for what's a `usize` in code.
LLM-emitted floats would round-trip as 0 (when fractional) or
panic in `.chars().take(n)`. Now declared as integer with a
minimum of 1.
## LSP workspaceSymbol query: schema now documents the contract
Schema marks `query` as a regular optional field, but the call
path errors when query is missing AND operation == workspaceSymbol.
Added that constraint to the parameter description so the LLM
knows when to pass it (the runtime check stays as the source of
truth).
## Compress now replaces the compactions list instead of appending
`Compaction::first_kept_index` is meaningful only for the *latest*
compaction record — keeping a list of records from earlier
compresses left stale `first_kept_index` values that no longer
matched the post-drain message indices. The LLM context already
folds older summaries into the new summary via `previous_summary`
(captured in `slash.rs:109` before compress runs), so dropping
the historical list is lossless. Fixes the multi-compaction
accounting drift flagged in the audit.
## Test plan
- [x] `cargo test --features plugin` -> 606 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
* fix(audit r5): glob cache, fork docs, switch-session diagnostics, pop fallback
Round 5 — low hygiene.
## GlobTool gets the dual-constructor pattern
Other read tools (Read/Grep/FindFiles/ListDir) follow a
`new(perm, ask_tx) / with_cache(perm, ask_tx, cache)` pattern.
`GlobTool` had only `new`, so glob calls re-walked the filesystem
every invocation even when bash/write/edit cache-clears
explicitly invalidated other tools' caches.
Added `with_cache(perm, ask_tx, cache)` and wired it through
`builder.rs`. Reuses the same `ToolCache` so a `bash`/`write`
mutation clears glob results too.
## pop_last_message: tree-corruption fallback
If `tree.entries` somehow lacks the popped message's id (data
corruption, external mutation), the old code wiped `tree.leaf_id`
to None, leaving the tree dangling on branched sessions. New
fallback uses the previous message's id from the linear cache so
the leaf stays anchored to a real node.
## fork_at: documented root behaviour
`fork_at` at the conversation root clears `messages` and sets
`leaf_id = None` — useful but surprising. Docstring now spells
this out, including the note that sibling branches survive.
## switch-session: ambiguity error lists candidate ids
`prefix 'ab' matches 5 sessions` was un-debuggable. Now lists
the first 3 matching session ids (short form) so the plugin
author / user can pick a longer prefix immediately.
## Test plan
- [x] `cargo test --features plugin` -> 606 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
---------
Co-authored-by: Yogthos <yogthos@gmail.com>1 parent b25d58a commit bf5e98d
14 files changed
Lines changed: 340 additions & 62 deletions
File tree
- src
- agent
- tools
- plugin
- session
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
197 | | - | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
198 | 202 | | |
199 | 203 | | |
200 | 204 | | |
| |||
217 | 221 | | |
218 | 222 | | |
219 | 223 | | |
220 | | - | |
221 | | - | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
222 | 229 | | |
223 | 230 | | |
224 | 231 | | |
| |||
279 | 286 | | |
280 | 287 | | |
281 | 288 | | |
282 | | - | |
283 | | - | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
284 | 293 | | |
285 | 294 | | |
286 | 295 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
16 | 22 | | |
17 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
18 | 44 | | |
19 | 45 | | |
20 | 46 | | |
| |||
95 | 121 | | |
96 | 122 | | |
97 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
98 | 135 | | |
99 | 136 | | |
100 | 137 | | |
| |||
150 | 187 | | |
151 | 188 | | |
152 | 189 | | |
153 | | - | |
154 | | - | |
| 190 | + | |
| 191 | + | |
155 | 192 | | |
156 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
157 | 197 | | |
| 198 | + | |
158 | 199 | | |
159 | 200 | | |
160 | 201 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
179 | | - | |
| 179 | + | |
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
65 | 71 | | |
66 | 72 | | |
67 | 73 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
55 | | - | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
56 | 75 | | |
57 | 76 | | |
58 | 77 | | |
| |||
116 | 135 | | |
117 | 136 | | |
118 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
119 | 149 | | |
120 | 150 | | |
121 | 151 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
7 | 6 | | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
15 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
16 | 32 | | |
17 | 33 | | |
18 | 34 | | |
| |||
52 | 68 | | |
53 | 69 | | |
54 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
55 | 75 | | |
56 | 76 | | |
57 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
| 94 | + | |
| 95 | + | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2063 | 2063 | | |
2064 | 2064 | | |
2065 | 2065 | | |
2066 | | - | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
| 2071 | + | |
| 2072 | + | |
| 2073 | + | |
2067 | 2074 | | |
2068 | 2075 | | |
2069 | 2076 | | |
| |||
0 commit comments