Skip to content

Commit 60ff9c9

Browse files
Eilodonclaude
andauthored
Claude/calm security gaps z3hlla (#57)
* Close P0 trust gaps: verify_change TOCTOU, HTTP capability firewall, ledger HMAC, permissions, risk_rules, memory quarantine Fixes and hardens the highest-severity gaps found in a code-level audit comparing this repo's actual behavior against its "safety net" claims: - verify_change now binds to the transaction's proposed_digest (checked before AND after cargo check runs), closing a TOCTOU where a concurrent write could get bound to someone else's verification receipt - run_cargo_check now has a wall-clock timeout (config.verification. timeout_secs, default 120s) instead of blocking indefinitely on a hung build.rs/proc-macro/registry fetch - Non-loopback `calm serve --http` now forces a capability-derived "remote-safe" preset (every tool with read_only_hint=true, computed live off the tool router) instead of "full,-edit", which only ever excluded edit_lines/edit_symbol/format_files -- remember, verify_change, retry_maintenance, scip_refresh, lsp_refresh, set_toolset, and pattern_debt_register were all still reachable over an authenticated remote connection despite the "read-only" framing - The audit ledger is now HMAC-SHA256-signed (a new 0600 .calm/audit.key) instead of a plain unkeyed SHA-256 chain -- SQLite file write access alone can no longer forge a chain that still passes verify_chain - `calm init` creates .calm/ atomically at 0700 (matching the daemon's own posture) instead of an umask-derived create_dir_all; `calm doctor --fix` retightens an already-loose .calm/ and its sensitive files - `calm setup --npx` pins to this binary's own version by default instead of an unpinned `npx -y @eilodon/calm-mcp` (--track latest opts out) - New `.calm/config.json` risk_rules: a path-glob-to-minimum-risk floor so a low-fan-in but security-sensitive file (auth/, CI workflows, migrations) can't read as low risk purely because few callers exist yet - `remember` quarantines injection-flagged notes; `recall` excludes them from its ambient/broad paths (query search, list-all) by default, mirroring edit_context's existing related_notes ambient-surfacing gate - KNOWN_LIMITATIONS.md: an honest catalog of what's still open (sandboxed multi-language verification, multi-file change-sets, a reference-impact tool, change-kind-aware risk, structured evidence grounding, per- connection daemon profiles, the CLI name collision, Git/CI integration) and why each is deliberately deferred rather than half-built All new/changed behavior covered by tests (cargo test --workspace, including new integration tests spawning the real binary); cargo clippy --workspace --all-targets -D warnings clean; fmt clean on every touched file; doc-truth/gen-status/claims-registry checks and this repo's own fitness-check all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQRHnBxA3q9pCt77dHHRUB * Close Tier 1+2 security/UX gaps: indexer DoS caps, signature-aware risk gate, HTTP hardening, reference_impact tool, structured edit citation - Indexer now skips files over 8 MiB before reading them and bounds a single tree-sitter parse to 5s, closing two concrete gaps behind SECURITY.md's "malicious repo indexing DoS" stance. - compute_touch_risk escalates risk to "high" when an edit's own proposed content actually changes a touched function/method's signature text (semantic comparison, not just line overlap -- a whole-body replace that leaves the signature untouched no longer false-positives). - calm serve --http now caps request body size and concurrent in-flight requests as defense-in-depth (still not a substitute for a reverse proxy's real rate limiting). - New reference_impact tool merges call edges, import edges, and a textual grep into one classified reference list for rename/removal planning -- closes the exact gap behind two real benchmarks/b7_task_correctness misses (a bare re-export invisible to the call graph alone). - edit_lines/edit_symbol gained an optional `cites` param: an exact qualified-name match against edit_context's fresh caller list, closing the "paste a caller name into an unrelated sentence" gaming path for callers that opt in; the free-text `reason` path remains for backward compatibility. - KNOWN_LIMITATIONS.md, CHANGELOG.md, and all doc-truth-checked tool-count references updated to match (35 -> 36 tools). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQRHnBxA3q9pCt77dHHRUB --------- Co-authored-by: Eilodon <noreply@anthropic.com>
1 parent 86e1aad commit 60ff9c9

42 files changed

Lines changed: 3486 additions & 199 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"plugins": [
99
{
1010
"name": "calm",
11-
"description": "Adds the CALM MCP server: call-graph-aware locate/source/edit_symbol/diff_impact and 35 tools total for navigating and safely editing a codebase, backed by a multi-tier resolver (syntactic -> Stack Graphs -> SCIP) instead of grep.",
11+
"description": "Adds the CALM MCP server: call-graph-aware locate/source/edit_symbol/diff_impact and 36 tools total for navigating and safely editing a codebase, backed by a multi-tier resolver (syntactic -> Stack Graphs -> SCIP) instead of grep.",
1212
"author": {
1313
"name": "Eilodon"
1414
},

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Full stage-by-stage guide, all 8 Mandatory Rules, the Preset table, and Tool Qui
1212

1313
---
1414

15-
> 35 tools. 8 stages. Every response carries `suggested_next` — follow it.
15+
> 36 tools. 8 stages. Every response carries `suggested_next` — follow it.
1616
---
1717

1818
## Core Principles
@@ -262,7 +262,7 @@ remember("auth-flow", "OAuth callback must validate state param — see incident
262262
| 1 Orient | `repo_overview`, `hotspots`, `fitness_report`, `test_gap_hotspots` (coreness × test-coverage-gap ranking) | Directory scanning, README reading |
263263
| 2 Locate | `locate`, `search`, `file_overview` | `grep`, file search |
264264
| 3 Inspect | `source`, `symbol_info`, `understand`, `symbols_batch` (batch source+callers/callees for several exact `qualified_name`s) | `cat` / full file read |
265-
| 4 Trace | `callers`, `callees`, `path`, `dependencies` | Manual call tracing |
265+
| 4 Trace | `callers`, `callees`, `path`, `dependencies`, `reference_impact` (rename/removal reference surface — merges call edges, imports, and textual matches) | Manual call tracing |
266266
| 5 Pre-Edit | `edit_context` | *(no native equivalent)* |
267267
| 6 Edit | `edit_symbol`, `edit_lines` (preferred), `format_files` (rustfmt via stdin, safe replacement for shelling out), `pattern_debt_register`/`pattern_debt_status` (track a duplicated bug pattern) | native `Edit`/`Write` (fallback for new/untracked files) |
268268
| 7 Verify | `diff_impact` | *(no native equivalent)* |
@@ -288,10 +288,10 @@ remember("auth-flow", "OAuth callback must validate state param — see incident
288288
| Preset | Registered Tools | Use when |
289289
|--------|-----------------|----------|
290290
| `orient` | `repo_overview`, `locate`, `dependencies`, `hotspots`, `fitness_report`, `indexing_status` | Exploration only, no edits |
291-
| `trace` | `repo_overview`, `search`, `locate`, `symbol_info`, `source`, `callers`, `callees`, `path`, `dependencies`, `indexing_status` | Call graph traversal |
291+
| `trace` | `repo_overview`, `search`, `locate`, `symbol_info`, `source`, `callers`, `callees`, `path`, `dependencies`, `reference_impact`, `indexing_status` | Call graph traversal |
292292
| `edit` | `repo_overview`, `search`, `locate`, `symbol_info`, `source`, `callers`, `callees`, `edit_context`, `edit_lines`, `edit_symbol`, `diff_impact`, `indexing_status`, `edit_transaction_status`, `maintenance_status`, `retry_maintenance`, `repair_consistency`, `verify_change` | Code modification workflow |
293293
| `compound` | `repo_overview`, `locate`, `hotspots`, `fitness_report`, `source`, `understand`, `edit_context`, `diff_impact`, `session_context`, `indexing_status`, `remember`, `recall` | Full workflow, no raw graph traversal |
294-
| `full` | All 35 tools | Default; use when workflow spans multiple stages |
294+
| `full` | All 36 tools | Default; use when workflow spans multiple stages |
295295
`--preset` is set once at server startup and cannot change mid-session. Use `full` (default) when the workflow spans multiple stages. Use specific presets only when scope is locked to one stage.
296296

297297
Beyond the 5 named presets above, `--preset`/`config.json`'s `preset` field also accept a **composable toolset spec**: a comma-separated list of toolset (module-domain) names — `trace`, `locate`, `orient`, `memory`, `guardrails`, `recover`, `scip`, `lsp`, `security`, `testgap`, `inspect`, `edit`, `patterndebt` — optionally prefixed with `-` to subtract that toolset instead of adding it. E.g. `--preset "trace,security"` unions two toolsets; `--preset "full,-edit"` is every tool except the edit toolset's (`edit_symbol`/`edit_lines`/`format_files`). This is a different, finer-grained axis than the 5 named presets (which are hand-curated cross-cutting workflow bundles, not toolset unions) — an unrecognized token in either syntax is a hard startup error, never a silent full-access fallback.

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ git tags in [Releases](https://github.com/Eilodon/CALM/releases).
99
### Added
1010
- Opt-in WS-6 first-slice verification (`docs/plans/2026-08-03-ws6-verification-pipeline-execution-plan.md`): `[verification] rust_check_on_write` (default off) routes a `.rs` write through the durable transaction's `VERIFY_PENDING` state instead of straight to `Done`; new `verify_change(tx_id)` tool runs `cargo check` scoped to the nearest Cargo package and advances the transaction to `Done`/`Failed` -- a failed check does not revert the file already written to disk
1111
- `plugins/calm/.claude-plugin/plugin.json`'s `version` is now checked against `Cargo.toml`'s (`scripts/check-doc-truth.sh`) so the Claude Code plugin manifest can't silently drift from the release it bundles again
12+
- `verify_change` now binds to the transaction's `proposed_digest`, checked both immediately before and immediately after `cargo check` runs -- a concurrent write can no longer get bound to someone else's verification receipt (`VERIFICATION_SNAPSHOT_CHANGED`)
13+
- `[verification] timeout_secs` (default 120s): `cargo check` is killed if it hangs (a stuck `build.rs`/proc-macro/registry fetch) instead of blocking the tool call indefinitely
14+
- `calm init` now creates `.calm/` atomically at `0700` (matching the daemon's own posture) instead of a plain `create_dir_all` at the umask default; `calm doctor --fix` additionally retightens an already-loose `.calm/` and its sensitive files (`index.db`, `memory.key`, `daemon.log`, `audit.log`, `daemon.sock`)
15+
- Non-loopback `calm serve --http` now forces a capability-derived `remote-safe` preset (every tool declaring `read_only_hint = true`, computed live off the tool router) instead of the old `full,-edit` toolset exclusion, which only ever disabled `edit_lines`/`edit_symbol`/`format_files` -- `remember`, `verify_change`, `retry_maintenance`, `scip_refresh`, `lsp_refresh`, `set_toolset`, and `pattern_debt_register` are now also excluded by default over an unauthenticated-by-default remote transport
16+
- The audit ledger (`audit_ledger`) is now HMAC-SHA256-signed (keyed by a new 0600 `.calm/audit.key`, separate from `memory.key`) instead of a plain unkeyed SHA-256 chain -- an actor with only SQLite file write access can no longer forge a chain that still passes `verify_chain`
17+
- `calm setup --npx` now pins the written entry to `@eilodon/calm-mcp@<this binary's own version>` by default instead of an unpinned `npx -y @eilodon/calm-mcp`, so a cold `npx` invocation always resolves to the same release; `--track latest` opts back into the old unpinned behavior
18+
- `.calm/config.json` `risk_rules` (default empty): a path-glob-to-minimum-risk floor (e.g. `{glob: "**/auth/**", minimum: "high"}`) that the write gate can never classify below, closing the gap where a low-fan-in but security-sensitive file read as low risk regardless of caller count
19+
- `remember` now quarantines a note whose content trips the prompt-injection heuristic (still saved, same detection-only philosophy) and `recall` excludes quarantined notes from its ambient/broad paths (FTS `query`, no-args list-all) by default -- an exact `topic` lookup still always returns it, mirroring `edit_context`'s existing `related_notes` ambient-surfacing gate
20+
- `KNOWN_LIMITATIONS.md`: an honest catalog of what CALM doesn't do yet and why each gap is deliberately deferred rather than half-built
21+
- Indexing now skips any file over 8 MiB (`read_source_capped`, checked via a cheap `metadata()` stat before ever reading the file) and bounds a single tree-sitter parse to 5s (`Parser::set_timeout_micros`) -- a pathologically huge or deeply-nested file can no longer hang or balloon the indexer's memory
22+
- `compute_touch_risk` now escalates risk to `"high"` when an edit's own proposed content actually changes a touched function/method's signature TEXT (not just overlaps its line range -- a whole-body replace that leaves the signature byte-for-byte identical does not escalate), reusing `diff_impact`'s own `is_signature_semantically_changed`/`escalate_risk_if_signature_changed`
23+
- `calm serve --http` now caps request body size (16 MiB, `axum::extract::DefaultBodyLimit`) and concurrent in-flight requests (64, `tower::limit::ConcurrencyLimitLayer`) as defense-in-depth against the unbounded-resource gap a bare `axum::Router` had; still not a substitute for a reverse proxy's real rate limiting
24+
- New `reference_impact` tool: merges call edges, import edges naming a symbol, and a repo-wide textual grep into one classified reference list (`must_change`/`likely_change`/`review`/`textual_only`) for rename/removal planning -- closes the exact gap behind two real `benchmarks/b7_task_correctness` misses (a bare re-export statement invisible to the call graph alone)
25+
- `edit_lines`/`edit_symbol` gained an optional `cites` param: the EXACT `qualified_name` of a caller `edit_context` returned this session, checked by equality rather than the existing `reason` field's word-boundary substring search -- closes the "paste a real caller name into an unrelated sentence" gaming path for callers that opt in; the free-text `reason` path remains for backward compatibility
1226

1327
## [0.5.0] - 2026-08-03
1428

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ Roadmap items currently open, roughly in priority order:
1717
1. **Hook adapters for other MCP hosts.** `calm init --hooks[=nudge|enforce]` now scaffolds the PreToolUse/PostToolUse deny-hook wiring generically (backed by `calm-core::hooks`/`hooks_check`), but it only targets Claude Code's hook schema so far. VS Code/GitHub Copilot uses the *same* `PreToolUse` / `permissionDecision: deny` shape, so that port should be close to a direct translation. Cursor (`beforeMCPExecution`) and Windsurf (Cascade Hooks) use different shapes and need their own adapters.
1818
2. **Resolver accuracy.** False positives/negatives in `coreness`/`is_hub` classification, edge cases in the SCIP-overlay caching key. Also useful: getting more of the optional SCIP cross-reference providers (Go, Java, C#, PHP, C, Ruby — see `install_hint` in `repo_overview`'s `health_summary`) easier to install, since call-graph precision for those languages is capped without one installed.
1919
3. **New Tier-0.5 → Tier-0 promotions.** SQL and Dart both already ship (SQL has its own standalone `sqlparser`-based indexer — real grammar, not regex — but deliberately stops short of a call graph, since "calls" isn't a coherent concept across SQL dialects; Dart is Tier-0.5 with the known zero-call-edge grammar limit noted above) — the next candidates are whichever Tier-0.5 language your own report from the "try CALM on a Tier-0.5 language" bullet above turns up as highest-friction.
20+
4. **A sandboxed, multi-language execution broker for verification.** `verify_change` runs one check (`cargo check`, unsandboxed beyond a wall-clock timeout and digest binding) — see `KNOWN_LIMITATIONS.md`'s "Verification is single-language..." entry. Extending to `go test`/`tsc --noEmit`/`pytest` needs a shared network/filesystem/env execution-policy abstraction built first, not each language bolted directly onto a bare subprocess call.
21+
5. **`reference_impact(symbol, operation="rename")`.** `benchmarks/b7_task_correctness` has two real failing rename cases today from composing `callers`/`edit_context` by hand instead of having a tool that merges call edges + SCIP references + imports/re-exports + textual matches into one classified list. See `KNOWN_LIMITATIONS.md`'s "No unified reference-impact tool" entry.
22+
6. **Change-kind-aware risk classification.** `compute_touch_risk` sees caller-count/hub-status and (as of this pass) a path-based `risk_rules` floor, but not whether an edit is a comment tweak vs. a signature break vs. an auth-check removal. Needs the diff content turned into a classified axis and folded into the risk model. See `KNOWN_LIMITATIONS.md`.
23+
7. **`calm guard --staged` / a publishable GitHub Action.** The only integration point today is an MCP client calling CALM's tools directly — a native editor edit or a teammate's local commit is invisible to CALM. See `KNOWN_LIMITATIONS.md`'s "No Git/CI-native integration path" entry.
24+
8. **Multi-file change-set / transaction.** Today every `edit_lines`/`edit_symbol`/`format_files` call is its own independent, file-scoped `EditTransaction` — a multi-file refactor has no aggregate success/failure view. See `KNOWN_LIMITATIONS.md`.
25+
26+
`KNOWN_LIMITATIONS.md` has the full list (including smaller items not listed here) with the reasoning for why each is deliberately deferred rather than half-built.
2027

2128
Please open a GitHub Issue before starting on anything larger than a small fix, so effort doesn't collide with what's already in progress.
2229

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ tracing-opentelemetry = { version = "0.33" }
237237
# 0.8 + rmcp 2.2.0 (transport-streamable-http-server) resolve and build
238238
# clean together (both on http 1.x / tower-service 0.3.x).
239239
axum = "0.8"
240+
# Same `http` feature as axum above -- already present transitively in
241+
# Cargo.lock (pulled in by axum/hyper's own stack) at this exact version, so
242+
# declaring it directly adds no new supply-chain surface. `limit` is the
243+
# only feature enabled: `ConcurrencyLimitLayer`, used for basic HTTP-
244+
# transport DoS hardening (docs/http-transport.md already documents this
245+
# transport has no built-in protection -- see serve_http's doc comment for
246+
# what this closes and what it deliberately doesn't).
247+
tower = { version = "0.5", default-features = false, features = ["limit"] }
240248

241249
# Internal
242250
calm-core = { path = "crates/calm-core" }

0 commit comments

Comments
 (0)