Absorb decant and canary's remaining judgments; gate Mothball's destructive API - #80
Open
heznpc wants to merge 3 commits into
Open
Absorb decant and canary's remaining judgments; gate Mothball's destructive API#80heznpc wants to merge 3 commits into
heznpc wants to merge 3 commits into
Conversation
decant's cleanup logic already duplicated Modore's storage scan, but two of its judgments had no equivalent here: which Hugging Face models nothing on this machine names, and which registered MCP servers cannot start. Both are ported as stdlib-only scripts on the scree/friction pattern and exposed through the existing read-only MCP surface. The Hugging Face check could not be ported as written. `ContextProbe.swift` returned "unreferenced" whenever its search failed to run -- a search root that did not exist, a failed grep, a mistyped `--projects` argument all produced the same answer as an exhaustive search that found nothing. One typo could therefore mark an entire hub cache safe to delete, and Modore saying "safe to delete" is the one sentence that has to be earned. So the rule is inverted at the point where it is decided: absence of evidence counts only when the search actually happened. A missing root, a file cap reached, or an unreadable subtree now yields `unknown` for every model with the reason named, and `--allow-missing-roots` / `--ignore-unreadable` widen the verdict only when the operator asks for it. `search_is_complete` is one function so the whole rule can be read at once, and every failure path is pinned by a test. Matching is case-insensitive and substring-based for the same reason: over-catching keeps a model, under-catching loses one. mcpaudit carries the same rule where it applies -- a verdict that depends on PATH is withheld when PATH is unusable rather than reported as `dead` -- and two deliberate deviations from decant. Servers are named rather than anonymised to `server#N`, because a hygiene report the operator cannot act on is not a hygiene report, and a server name is configuration metadata of the kind scree already emits. What stays hidden is what was sensitive: `env` is a key count, never keys or values. Neither script ships inside the signed bundle -- they have no Swift caller, so the runtime-completeness test lists them beside friction.py, and giving either one a view means moving it into RUNTIME_FILES. Verified: 121 tests across the four affected suites, full suite 481 passed, release_smoke clean. Live run on this machine -- 5 MCP servers, all healthy; the mistyped-root case returns `verdicts_withheld: true` through the MCP surface rather than a list of false orphans.
Two pieces of the consolidation that had no equivalent here. canary's `get_file_access` inverts the evidence scree already reads: for each path, how many reads, writes, and shell references it received, from how many sessions, and when last. Rule surfaces -- CLAUDE.md, AGENTS.md, settings.json, anything under ~/.claude or ~/.codex -- sort first and are the default view, because a silently edited rule file is the case this exists for. The taxonomy is carried over verbatim. Its content contract is stricter than the original's. canary attached a 200-character excerpt of the shell command to every row; that excerpt is command content, so it is dropped. A path extracted from a command is metadata about which file was touched -- the command that touched it is not. Assistant text, tool results, and non-path tool inputs are discarded in the same pass that reads them, and nested subagent transcripts stay unopened, matching scree's collector. Two things the naive port got wrong, both caught by running it: Paths are canonicalised before aggregation. The same file arrives as an absolute path from a tool input and as `~/...` from a shell command, so aggregating raw strings split one file across several rows -- 1,322 rows where there were 1,212 files, with ~/.claude/settings.json appearing twice. An inverted index that cannot put one file on one row is not doing its job. Dedupe is keyed on the tool_use block id, not on the path. Claude streams one assistant message as multiple lines sharing a message id, which is why dedupe is needed at all; keying it on the path also erased genuine repeat reads within a session, turning a count of touches into a count of sessions. Separately: absorbing Mothball absorbed a second deletion discipline. Modore destroys nothing without a preview-issued single-use token, an owner-only manifest, a remeasure at the boundary, and a receipt. MothballCore's `ArchiveOrchestrator.archive()` is careful on its own terms but is an in-process call taking no token, and the vendor dependency already compiles it into the binary -- only review stood between a future `try orchestrator.archive(...)` in a view action and a path the approval chain never sees. Two tests make that a mechanism: a Modore source naming ArchiveOrchestrator, Restorer, ArchiveRun, or trashItem must also carry the approval-token symbol, so a diff cannot bring one half without showing the other; and MothballService is pinned to the read-only scan-and-classify surface it actually uses. Wiring the archive path stays allowed -- wiring it around the token does not. Both guards were verified by injecting a violation and watching them fail. Verified: 505 tests pass, swift build clean, release_smoke clean. Live run -- 60 sessions, 1,876 paths, 208 rule surfaces; ~/.claude/settings.json shows 3 writes across 10 sessions, and no command text appears anywhere in the payload.
The archive page's copy dated from the display-first stage of the absorption, when vendor/mothball had just been merged and Mothball was still a live standalone product -- so "run the Mothball app separately" was accurate advice at the time. Archiving that repository removed the thing being pointed at, and the copy did not follow. It now says what is actually true: there is no archive execution anywhere yet, and when one is wired it goes through the same preview-and-approval path as every other deletion in this app -- which the guard added alongside this now enforces rather than merely promises. Verified: swift build and 169 Swift tests pass, boundary guards pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Consolidation work: the judgments decant and canary had that Modore did not, plus the boundary the Mothball absorption left open. Three retirements' worth of unique capability, on Modore's own contracts.
1. Hugging Face cache audit — and the defect fixed before porting it
decant's
ContextProbe.isReferencedwas fail-open:A missing search root, a failed grep, or a mistyped
--projectsall produced the same answer as an exhaustive search that found nothing. One typo could mark an entire hub cache safe to delete. decant's own cold-read audit lists this as its top critical finding.Inverted here in one readable function,
search_is_complete:unreferencedunknown+search-root-missingunknown+file-cap-reachedunreferencedunknown+tree-partially-unreadableunreferencedunknown+no-files-read--allow-missing-roots/--ignore-unreadablewiden the verdict only when asked. Matching is case-insensitive and substring-based on purpose: over-catching keeps a model, under-catching loses one.2. MCP config hygiene
Reads
~/.claude.json(including per-project blocks), both Claude Desktop locations, and~/.mcp.json; reportsdead/duplicate/manual-review/unknown.Two deliberate deviations from decant. Servers are named, not anonymised to
server#N— a hygiene report the operator cannot act on is not one, and a server name is configuration metadata of the kind scree already emits. What stays hidden is what was sensitive:envis a key count, never keys or values, pinned by a test that plants a fake API key and asserts neither the value nor the key name appears. And PATH-dependent verdicts are withheld when PATH is unusable rather than reported asdead— the same fail-safe rule as above.3. File-access reverse index
canary's
get_file_access, inverting scree's evidence to path → sessions: reads, writes, shell references, session count, last touch. Rule surfaces (CLAUDE.md, AGENTS.md, settings.json,~/.claude,~/.codex) sort first and are the default view.Stricter content contract than the original. canary attached a 200-character excerpt of the shell command to every row. That excerpt is command content, so it is dropped — a path extracted from a command is metadata about which file was touched; the command that touched it is not. Pinned: a transcript containing
grep -r 'hunter2' /tmp/ws/secrets.txtyields the path and neitherhunter2norgrep.Two things the naive port got wrong, both caught by running it:
~/-relative from a shell command, splitting one file across rows — 1,322 rows for 1,212 files, with~/.claude/settings.jsonlisted twice.4. Mothball's destructive API is now gated by mechanism
Absorbing Mothball absorbed a second deletion discipline. Modore destroys nothing without a preview-issued single-use 64-byte token, a 15-minute owner-only manifest, a remeasure at the boundary, and a receipt.
ArchiveOrchestrator.archive()is careful on its own terms — refuses/and$HOME, verifies before touching the original, moves to Trash rather than unlinking — but it is an in-process call taking no token, and the vendor dependency already compiles it into the binary. Only review stood between a futuretry orchestrator.archive(…)in a view action and a path the approval chain never sees.Two tests make it a mechanism: a Modore source naming
ArchiveOrchestrator,Restorer,ArchiveRun, ortrashItemmust also carry the approval-token symbol, so a diff cannot bring one half without showing the other; andMothballServiceis pinned to the read-only scan-and-classify surface it uses. Wiring the archive path stays allowed — wiring it around the token does not. Both guards were verified by injecting a violation and watching them fail.MCP surface
Three new read-only tools on the existing contract:
hf_orphans,mcp_hygiene,file_access.hf_orphansrestatessearch_completeas a top-levelverdicts_withheldflag so an incomplete search cannot read as "no orphans found"; itsrootsargument is bounded to 8 entries and screened so a value can never be read as an option by the script it is passed to.None of the three scripts ships inside the signed bundle — no Swift caller, so the runtime-completeness test lists them beside
friction.py.Verification
swift buildclean;release_smoke.pycleanverdicts_withheld: truethrough the MCP surface; 60 sessions → 1,876 paths / 208 rule surfaces,~/.claude/settings.jsonat 3 writes across 10 sessions, no command text anywhere in the payloadNot in this PR
balance/bin/cbahard-depends on the decant CLI in 4 places — separate repo, must land before decant is archivedlist_sessionssession rows andget_session_transcriptare not ported; the latter is refused by Modore's no-content contractSupersedes #79, closed automatically when its head branch was renamed from
claude/…to the repository'sfeat/convention. Same commits, same review.