Commit 9773d41
Claude/durability verification gaps jwav0c (#58)
* Fix P0 findings from durability/verification audit: OOM cap, process-tree kill, reference_impact undercounting, release version-tag CI guard
- verify.rs::run_cargo_check: bound stdout/stderr capture to 8 MiB/stream
(read_capped) so a pathological build.rs/proc-macro can't exhaust host
memory before the wall-clock timeout fires; spawn cargo in its own
process group and kill the whole tree (kill_process_tree) on
timeout/output-overflow instead of only the direct cargo PID, which
left descendants (rustc, build scripts, proc-macro servers) running.
Function signature unchanged -- caps are internal constants, not
config-plumbed, to avoid a signature-change risk escalation on this
hub symbol.
- reference_impact (trace.rs): fixed two undercounting bugs. (1) a
textual grep hit in a file that already had a call-edge hit at a
DIFFERENT line was wrongly suppressed because the skip-set was built
from the whole `seen` set instead of just import-edge file-level hits.
(2) must_change_count/likely_change_count/review_count/
textual_only_count were computed after hits.truncate(), silently
under-reporting once the real match count exceeded
REFERENCE_IMPACT_LIMIT.
- release.yml: qualify-release now fails a tag push if Cargo.toml's
committed version doesn't match the tag, preventing the exact
recurring class of bug documented in Cargo.toml's own version-field
comment (stuck at 0.1.1 through v0.1.2-v0.1.4, defeating
scripts/mcp-launcher.sh's verified-download fast path for a whole
release cycle). Deliberately does NOT bump Cargo.toml's version here --
both release binaries and npm packages are versioned straight from the
git tag (sed-stamp / stage-release.sh), not from this file, so bumping
it prematurely without a corresponding tag/publish would just pin
`calm setup --npx` at a phantom, unpublished npm version.
Test coverage: read_capped unit tests (under/over cap), 2 new
reference_impact regression tests (verified to fail against the
pre-fix code, pass against the fix), existing run_cargo_check tests
unchanged and still passing through the new kill path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
* Fail closed the audit ledger's HMAC key instead of silently downgrading to an unkeyed chain (P1)
- ledger.rs: ledger_key_for_conn/compute_event_hash now return Result.
A real on-disk connection whose audit.key can't be read or created
(e.g. a read-only .calm/) propagates LedgerError::KeyUnavailable
instead of collapsing into the same None as a legitimate path-less
(:memory:) connection -- previously that failure silently produced a
plain SHA-256 row indistinguishable at a glance from a genuine
HMAC-signed one, forgeable by anyone with plain SQLite write access.
- txn.rs::append_ledger_in_savepoint: P0-4 (a ledger failure must never
block the write it's auditing) is unchanged -- the existing savepoint
rollback already does the right thing once append() actually signals
failure instead of quietly succeeding with a weak hash. Added a
warn-level log on failure so an operator has a signal that the ledger
is accumulating gaps, instead of finding out only via a manual
verify_chain/completeness check.
- New regression test forces the I/O failure by pre-creating audit.key
as a directory (deterministic, works under root -- a permission-bit
trick doesn't, root bypasses those checks) and asserts append()
returns KeyUnavailable with zero rows written, never a forged-looking
unkeyed row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
* Add per-connection preset handshake so calm connect --preset works when attaching, not just spawning (P1)
- daemon.rs::relay (forwarder side) now writes a one-line preamble
(the requested preset, or empty) to the socket before starting its
pure byte relay -- the forwarder still does zero MCP/JSON-RPC
parsing, ADR-0005's design is unchanged.
- daemon.rs::read_connection_preset_preamble (daemon side) reads that
line byte-by-byte directly off the raw socket (no BufReader, so
nothing gets over-consumed from the MCP stream that immediately
follows), bounded by both a max length and a 2s timeout so a
misbehaving connector can't hang or balloon memory for its own
connection's task -- either bound just falls back to "no preset
requested", since this handshake is a UX affordance, not a security
boundary.
- CalmServer::narrow_connection_preset applies the requested preset to
THIS connection's own ceiling via the same resolve_preset/
current_visible_tool_names/call_tool machinery set_toolset already
uses -- can only ever narrow, never widen past the daemon's real
tool_router (built once at spawn time and still the hard ceiling
ToolRouter::call itself enforces), so a too-wide request is a no-op,
never a privilege escalation.
- New end-to-end test daemon_respects_per_connection_preset: spawns
connection A with no --preset (wide), keeps it alive, then connects
B with --preset orient to the SAME already-live daemon and confirms
B's tools/list is actually narrowed -- confirmed to fail against the
pre-fix code (edit_context leaks into B's list) and pass against the
fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
* Add batch_status tool for multi-file edit observability (P2)
New read-only batch_status tool (txn.rs): takes a caller-supplied list
of tx_ids -- the ones a set of edit_lines/edit_symbol/format_files
calls already returned for one multi-file change -- and reports one
aggregate view (counts by state, which are missing, whether any
failed) instead of requiring a separate edit_transaction_status call
per file.
Deliberately scoped down from a full multi-file transaction/change-set
(new column, schema migration, threading through the core write path):
this is pure read-side aggregation over the existing per-file
EditTransaction journal, so it adds zero risk to edit_lines/edit_symbol
or txn::begin. KNOWN_LIMITATIONS.md's "No multi-file change-set" entry
is updated to describe exactly what this does and does not cover --
the atomicity/rollback gap it describes is unchanged.
Also: regenerated docs/status.generated.md and bumped every
doc-truth-checked "36 tools" reference to 37 (scripts/check-doc-truth.sh,
scripts/check-claims-registry.sh both clean).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
* Add calm guard CLI command: Git/CI-native gate on the staged diff (P2)
New `calm guard --project-root .` command wraps the exact `diff_impact`
tool an MCP agent's own Stage-7 pre-commit gate already runs, against
`git diff --cached`, and exits non-zero when the resulting
aggregate_risk is at or above `--fail-on` (default "high"). Usable
directly as a pre-commit hook or CI step for a change made outside any
MCP session (a teammate's native editor, a bot PR) -- previously
invisible to CALM entirely (KNOWN_LIMITATIONS.md "No Git/CI-native
integration path").
Implementation is pure reuse, not a second risk-analysis path:
CalmServer::diff_impact_json (guardrails.rs) is a thin `pub` wrapper
that constructs a CalmServer, calls the existing pub(crate)
diff_impact tool method directly (in-process, no MCP transport
needed -- same CalmServer::new(...) constructor every test already
uses), and returns serde_json::Value so calm-cli never needs to name
any of this crate's internal tool param/output types.
4 new integration tests spawn the real calm binary against a real git
repo + index: blocks (exit 1) on a signature change to a 3-caller
function, passes (exit 0) on a body-only change under the default
threshold, --json emits parseable diff_impact output, and an invalid
--fail-on value fails loudly instead of silently doing something else.
Also updates KNOWN_LIMITATIONS.md's "No Git/CI-native integration
path" entry to describe what calm guard covers and what's still
missing (a commit-range/PR-diff mode, a publishable GitHub Action).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
* Lay storage foundation for splitting durable state out of index.db
index.db currently holds both the rebuildable symbol/call-graph index
(synchronous=NORMAL) and durable state -- project memory, the edit
transaction journal, the audit ledger, the maintenance outbox -- none
of which are rebuildable from source. All of it shares one file and
one durability posture.
This adds the storage-layer pieces for a real split, without wiring
any production call site to use them yet:
- db::schema::STATE_SCHEMA_SQL / init_state_db: the durable tables as
a schema separate from SCHEMA_SQL / init_db, always created fresh
at their current full shape (no incremental ALTER migrations to
carry forward, unlike the index schema).
- db::conn::open_state_writer: opens a connection at
PRAGMA synchronous=FULL for state.db.
- db::schema::migrate_legacy_durable_tables: one-time, idempotent,
copy-only migration of durable rows out of a pre-split index.db
(never DROPs/DELETEs the legacy rows; INSERT OR IGNORE keyed on
each table's real primary key makes repeat calls safe; audit_ledger
seq is preserved verbatim so the ledger's hash chain survives the
copy).
txn.rs/ledger.rs/maintenance.rs/memory.rs test setup now builds its
connection via init_state_db instead of init_db, since these modules
operate on tables that now live in the state schema.
Rewiring calm-server's actual runtime call sites (edit.rs, txn.rs,
memory.rs, common.rs, lib.rs, ...) to open and write through state.db
is a deliberately separate follow-up -- open_writer's own blast
radius (~320 transitively-affected files per diff_impact) makes it
too large to fold into this groundwork. KNOWN_LIMITATIONS.md and
CHANGELOG.md spell out exactly what's done vs. still pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: t <t@t.com>1 parent 60ff9c9 commit 9773d41
29 files changed
Lines changed: 1635 additions & 209 deletions
File tree
- .claude-plugin
- .github/workflows
- crates
- calm-cli
- src
- tests
- calm-core
- src
- db
- calm-server/src
- __toolsnaps__
- tools
- docs
- plugins/calm/.claude-plugin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
32 | 55 | | |
33 | 56 | | |
34 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
291 | 291 | | |
292 | 292 | | |
293 | 293 | | |
294 | | - | |
| 294 | + | |
295 | 295 | | |
296 | 296 | | |
297 | 297 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
26 | 31 | | |
27 | 32 | | |
28 | 33 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
47 | 59 | | |
48 | 60 | | |
49 | 61 | | |
50 | 62 | | |
51 | 63 | | |
52 | 64 | | |
53 | 65 | | |
54 | | - | |
55 | | - | |
56 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
57 | 80 | | |
58 | 81 | | |
59 | 82 | | |
| |||
137 | 160 | | |
138 | 161 | | |
139 | 162 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
178 | | - | |
179 | | - | |
| 178 | + | |
| 179 | + | |
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| |||
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
| 197 | + | |
| 198 | + | |
197 | 199 | | |
198 | 200 | | |
199 | 201 | | |
200 | 202 | | |
201 | 203 | | |
202 | | - | |
| 204 | + | |
203 | 205 | | |
204 | 206 | | |
205 | 207 | | |
| |||
0 commit comments