Skip to content

Make invalid states unrepresentable: ScopedPath, MemoryConfig, TruncationReason, NonZero caps - #9

Merged
devfire merged 1 commit into
mainfrom
type-driven-safety
Aug 25, 2026
Merged

Make invalid states unrepresentable: ScopedPath, MemoryConfig, TruncationReason, NonZero caps#9
devfire merged 1 commit into
mainfrom
type-driven-safety

Conversation

@devfire

@devfire devfire commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Summary

Implements findings 1–4 from the type-safety review: move runtime validation into the compile-time type system so whole classes of bugs become compile errors.

1. ScopedPath newtype (scope enforcement as a type)

Scope::check now returns ScopedPath, constructible only by scope validation. grep/find/cat require &ScopedPath(viaAsRef`), so a tool handler that forgets the scope check no longer compiles — previously it compiled fine and was a security hole.

2. MemoryConfig enum

Disabled | Enabled { dir, extra } replaces CodeMcpServer's (Option<PathBuf>, Option<String>) pair. The impossible state "extra instructions without a memory dir" is no longer constructible; the memories handler and get_info match one source of truth.

3. TruncationReason enum

Replaces freeform truncation-reason strings ("byte_cap", "line_cap", "max_results") scattered as literals across cat/grep. Snake_case serialization keeps the wire contract byte-identical.

4. Validated rates and caps

  • PeerLimiter::new returns Result<_, InvalidRate>, rejecting zero/negative/NaN capacity/refill — the inputs that would panic Duration::from_secs_f64 in release builds where the old debug_assert is compiled out. per_minute takes NonZeroU32; --initialize-rate-per-min is NonZeroU32.
  • max_results / max_bytes / max_lines are NonZeroUsize end to end (JSON args → options → drain_capped), so a zero cap is rejected at deserialization instead of producing empty "truncated" output.

Verification

  • cargo test: 44 passed, 0 failed (43 prior + new InvalidRate contract test)
  • cargo clippy --all-targets: 0 warnings; cargo fmt --check: clean
  • Live smoke over streamable HTTP: initialize + session, real grep call, cat max_lines: 2"truncation_reason": "line_cap" (enum serializes to the exact old string), max_results: 0 → deserialization error, --initialize-rate-per-min 0 → clap error, /etc/passwdOut of project scope

Notes

  • Tool tests now obtain inputs via scoped_in() (testutil), which routes tempdirs through a real Scope — the same path production takes.
  • load_memory intentionally keeps &Path: the memory dir is server-configured, not client input under project scope, so ScopedPath would assert a false proof.
  • Remaining review findings (StreamMode overload in grep_count, Option<StringOrVec> collapse) not included.

…tionReason, NonZero caps/rates

- Scope::check now returns ScopedPath, a newtype constructible only by
  scope validation; grep/find/cat require it, so skipping the check is a
  compile error rather than a security hole
- MemoryConfig enum replaces (Option<PathBuf>, Option<String>): the
  impossible 'instructions without a memory dir' state is no longer
  constructible
- TruncationReason enum replaces freeform truncation-reason strings;
  wire contract (byte_cap/line_cap/max_results) unchanged
- PeerLimiter::new validates capacity/refill (guards the
  Duration::from_secs_f64 panic on non-finite input) and per_minute
  takes NonZeroU32; --initialize-rate-per-min is NonZeroU32
- max_results/max_bytes/max_lines are NonZeroUsize through args ->
  options -> tools, so zero caps are rejected at the JSON boundary

44 tests pass; clippy and fmt clean; wire smoke-tested via streamable
HTTP (initialize, grep, cat truncation reason, zero-cap and
out-of-scope rejection)
@devfire devfire self-assigned this Aug 25, 2026
@devfire
devfire merged commit 110f3b0 into main Aug 25, 2026
6 checks passed
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