Skip to content

Fix 24 correctness, robustness, and security issues across the core crates#79

Merged
davidrhodus merged 1 commit into
mainfrom
fix/core-review-fixes
Jul 10, 2026
Merged

Fix 24 correctness, robustness, and security issues across the core crates#79
davidrhodus merged 1 commit into
mainfrom
fix/core-review-fixes

Conversation

@davidrhodus

Copy link
Copy Markdown
Contributor

Summary

A correctness-focused review of the core and agentic-coding crates (hi-agent, hi-ai, hi-tools, hi-cli, hi-lsp, hi-tui; mlx/cuda excluded) surfaced 24 major issues. Each was verified against a concrete failure scenario before fixing.

Crashes on untrusted input

  • <|> slice panic: streamed text containing <|> (e.g. Haskell's alternative operator in a code answer) produced a reversed slice range and crashed the turn — in both hi-ai stream filters and hi-agent's strip_chatml_tokens. The suffix search now starts after the <| prefix.
  • Unbounded SSE indices: one corrupt frame ("index": 9999999999) forced a multi-GB allocation in the OpenAI and Anthropic stream parsers. Indices are now capped at 512.
  • Byte-slice panics: API-key masking (--show-config, /provider edit) and the TUI's Ctrl-R history preview panicked on multi-byte characters; both now truncate by chars.

Silent data loss / corruption

  • Config saves deleted six profile fields: the hand-written Serialize impl omitted minimal_tools, curate_skills, explore_subagents, write_subagents, planner_model, skeptic_model — any /model switch or profile edit permanently destroyed them. Replaced with derived serialization so a new field can't be silently dropped again.
  • /provider edit wiped uncovered fields (max_tokens, fallback, tool_mode, …) in both the REPL and TUI flows; edits now start from the existing profile.
  • Non-UTF-8 files corrupted on edit: edit/multi_edit/apply_patch lossily decoded (e.g. Latin-1) files and wrote the mangled string back, destroying every non-ASCII byte file-wide. They now refuse with a clear error pointing at bash/iconv.
  • Session JSONL interleaving: records >8 KB were split across multiple write() calls, so two hi -c processes (or a fleet child on --session-file) could corrupt each other's lines — silently dropped on resume. Each record is now one write_all on the O_APPEND fd.
  • TUI transcript overwrite: the collapsed-reads merge (⏺ read 6 files · 743 lines) could replace committed assistant prose; it now merges only while its own line is still the last entry.
  • Post-finish garbage discarded completed answers: a heartbeat/sentinel frame after finish_reason threw away the fully-streamed answer and re-billed a retry.

Security

  • web_download had no SSRF check despite the module doc claiming protection — a prompt-injected URL could pull cloud-metadata/localhost content into a readable workspace file. It now runs the same validator as web_fetch (initial hop; redirect re-validation noted as follow-up).

Hangs and stuck states

  • LSP: concurrent requests on one client stole each other's responses (120 s hangs); a timeout mid-frame permanently desynced the JSON-RPC stream with no recovery; and the synced-doc dedup map survived crashes//lsp off, so respawned servers silently reported "no problems found". Round-trips now serialize behind an io mutex, reads are two-phase (cancel-safe wait, then committed frame read) with poison-on-desync + respawn, and the dedup map is invalidated on respawn, disable, and failed syncs.
  • Stale .memory.lock after a SIGKILL disabled memory persistence for all future sessions; locks older than 10 minutes are now broken.
  • Failed goals were permanently dead: rederive_status never returned a Failed goal to Active after a plan revision activated new work, contradicting its own doc and disabling auto-drive.
  • Loop firings could hang forever on a child that wedged after closing stdout, leaking the in-flight slot and eventually stalling every loop; child.wait() is now bounded like run_fix.
  • Bash output readers aborted at the first invalid-UTF-8 byte — output lost, and a child filling the pipe stalled until the 600 s timeout. They now read raw bytes and lossy-decode per line.

Agent-loop correctness

  • replace_last_nudge erased the current turn: a stale verify marker from an earlier turn triggered the pop path mid-turn, destroying the model's just-completed work and leaving consecutive user messages (invalid transcript; debug-assert panic). It now replaces only when the tail is exactly the prior nudge's response cycle.
  • Pre-pass dependency violations: explore/delegate/record_decision ran before earlier mutations in the same batch, handing subagents a pre-edit tree (and tripping respects_deps in debug builds). They now dispatch inside the dep-aware scheduler.
  • Context gauge clobbered by side calls: finalize/skeptic/curate/memory/explore/compaction requests overwrote context_used, so a ~3 K recap at the end of a 150 K session reported "ctx 2%" and silently skipped auto-compaction. Side calls now count spend without touching the gauge.
  • Interrupt flag leak: an Esc landing as a batch's last tool finished cancelled the next round's (or next turn's) entire batch with fabricated "interrupted" results.
  • Missing tool-call index collapsed parallel calls into slot 0 ("readread", concatenated JSON args) on local OpenAI-compat servers; routing now uses a new-call heuristic.
  • Anthropic usage backfill sets estimated and context_occupancy; memory bullets ending in a sentence period are no longer misclassified as paths and dropped; REPL provider flows honor --config.

Test plan

  • cargo test -p hi-agent -p hi-ai -p hi-tools -p hi -p hi-lsp -p hi-tui — 829 tests pass
  • cargo clippy --all-targets on the six crates — only the four pre-existing hi-tui test warnings remain
  • The <|> panic and prose-bullet drop were reproduced standalone before fixing

Known issues left for follow-up

  1. Merged config save design: saving writes the merged global+local config to a single file — with a local hi.toml present, /model copies global profiles (including literal API keys) into the project file, and removing a globally-defined profile resurrects on next launch. Needs a decision on file ownership.
  2. verify_skipped_for_prose_only_changes is flaky under full-suite parallelism (pre-existing): tests share the crate cwd, the verifier snapshots it, and a concurrent test's extension-less scratch file mid-window makes the change-set look non-prose. Harness refactor needed.
  3. web_download follows redirects without re-validating (initial-hop check only).

🤖 Generated with Claude Code

…rates

Findings from a full review of hi-agent, hi-ai, hi-tools, hi-cli, hi-lsp,
and hi-tui (mlx/cuda excluded). Every fix was verified against a concrete
failure scenario; the full suite (829 tests) passes.

Crashes on untrusted input:
- Streamed text containing `<|>` produced a reversed slice range and
  panicked in strip_special_tokens_chunk/strip_special_tokens (hi-ai) and
  strip_chatml_tokens (hi-agent); search the suffix after the `<|` prefix
- Bound SSE tool-call/content-block indices (OpenAI + Anthropic) so one
  corrupt frame can't force a multi-GB resize_with allocation
- Char-safe API-key masking in --show-config and /provider edit; char-safe
  Ctrl-R history preview truncation in the TUI

Silent data loss / corruption:
- Profile serializer omitted six fields (minimal_tools, curate_skills,
  explore_subagents, write_subagents, planner_model, skeptic_model) —
  every config save deleted them; switch to derived Serialize
- /provider edit (REPL + TUI) wiped fields the form doesn't cover; edits
  now start from the existing profile
- edit/multi_edit/apply_patch corrupted non-UTF-8 files via lossy decode
  + write-back; they now refuse with a clear error
- Session JSONL appends split >8KB records across write() calls, letting
  concurrent writers interleave mid-line; single write_all per record
- TUI explore-run merge overwrote committed prose lines; merge only while
  the run's summary line is still the last transcript entry
- A malformed SSE frame after finish_reason discarded (and re-billed) a
  fully-streamed answer; tolerated like the transport-error case

Security:
- web_download never SSRF-validated model-supplied URLs despite the module
  doc claiming it did; validate before handing to curl/aria2c

Hangs and stuck states:
- LSP: serialize request round-trips (concurrent calls stole each other's
  responses → 120s hangs); two-phase cancel-safe reads with poison-on-
  desync + respawn (a timeout mid-frame permanently broke the stream);
  invalidate the synced-doc map on respawn, /lsp off, and failed syncs
- Stale .memory.lock after a crash disabled memory writes forever; break
  locks older than 10 minutes
- A Failed long-horizon goal never returned to Active on plan revision,
  permanently disabling auto-drive
- Loop firings hung forever on a child that wedged after closing stdout;
  bound child.wait() like run_fix does
- Bash output readers aborted on the first invalid-UTF-8 byte, losing
  output and stalling until timeout on a full pipe; read bytes per line

Agent-loop correctness:
- replace_last_nudge matched a stale verify marker from a previous turn
  and erased the current turn's transcript, leaving consecutive user
  messages; replace only when the tail is exactly the prior nudge cycle
- explore/delegate/record_decision ran in a pre-pass before earlier
  mutations in the same batch (stale tree for subagents, respects_deps
  debug-assert panic); they now dispatch through the dep-aware scheduler
- Side model calls (finalize/skeptic/curate/memory/explore/compaction)
  clobbered context_used, breaking the ctx gauge and auto-compaction
- Interrupt flag leaked past batch end, spuriously cancelling the next
  round's tool calls
- Missing tool-call `index` collapsed parallel calls into slot 0
  ("readread", concatenated args); route by new-call heuristic instead
- Anthropic usage backfill now sets `estimated` and context_occupancy
- Memory bullets ending in a sentence period were misclassified as paths
  and silently dropped by grounded verification
- REPL provider add/edit/remove honored --config instead of always
  writing to the default config path

Known issues deliberately not addressed here: the merged global+local
config save design (can copy global profiles into ./hi.toml), redirect
re-validation in web_download, and the pre-existing shared-cwd race that
makes verify_skipped_for_prose_only_changes flaky under full-suite
parallelism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@davidrhodus
davidrhodus merged commit d2b229b into main Jul 10, 2026
1 of 3 checks passed
@davidrhodus
davidrhodus deleted the fix/core-review-fixes branch July 10, 2026 16:54
davidrhodus added a commit that referenced this pull request Jul 10, 2026
* Apply rustfmt to the review-fix code (unblock CI)

The CI `fmt · test` job runs `cargo fmt --all --check` before the test
step, and several hand-written lines from the recent review fixes (#79,
#81, #82 and the loop-firing timeout) didn't match rustfmt — mostly long
assert!/match/call expressions that rustfmt reflows across multiple lines.
Format failing blocked the Test step from ever running, so CI showed red
despite a green local suite.

Formatting-only: `git diff -w` shows nothing but line reflows; the four
touched crates' tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix two clippy errors surfaced on stable 1.97

CI's stable toolchain moved to rust 1.97, whose tightened lints error
(under -D warnings) on two pre-existing spots the 1.93 CI never flagged.
Once the fmt fix let the Test job reach the Clippy step, they blocked it:

- web.rs: redundant `&` in a format! arg (useless_borrows_in_formatting)
- app/run.rs: sort_by comparator that is really a keyed reverse sort
  (unnecessary_sort_by) → sort_by_key(Reverse(..))

Both are behavior-preserving. Verified with the CI toolchain:
`cargo +stable clippy -p hi-agent -p hi-ai -p hi-tools -p hi-tui -p hi
-p hi-eval -- -D warnings` is clean, fmt --check clean, tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.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