Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project are documented here. Format loosely follows
## [Unreleased]

### Added
- **Mothball's destructive API is now unreachable from Modore by mechanism, not by intention.** Absorbing Mothball absorbed two different deletion disciplines. Modore destroys nothing without a preview that issues a single-use 64-byte approval token, a 15-minute owner-only manifest, a remeasure at the destructive boundary, and a receipt. MothballCore's `ArchiveOrchestrator.archive()` is careful in its own right — it refuses `/` and `$HOME`, verifies the archive before touching the original, and moves to Trash rather than unlinking — but it is an *in-process* Swift call that takes no token and consumes no manifest, and the vendor dependency already compiles it into Modore's binary. Nothing but review stood between a future `try orchestrator.archive(…)` in a view action and a second deletion path the approval chain never sees. Two tests now pin the boundary: any Modore source file that names `ArchiveOrchestrator`, `Restorer`, `ArchiveRun`, or `trashItem` must also carry the approval-token symbol, so a diff cannot introduce 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.
- **File-access reverse index — `scripts/fileaccess.py`.** Absorbed from canary's `get_file_access` / `getFileAccessAggregates` (that repository is a frozen paper archive; this was the capability Modore had no equivalent for). 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 — with agent rule and config surfaces (CLAUDE.md, AGENTS.md, settings.json, anything under `~/.claude` or `~/.codex`) sorted first and returned by default, because a silently edited rule file is the case the view exists for. The rule-surface taxonomy is carried over verbatim. **The content contract is stricter than the original's**: canary attached a 200-character excerpt of the shell command to every row, and that excerpt is command content, so it is dropped here — a path extracted from a command is metadata about which file was touched, the command that touched it is not. Only tool names and paths are retained; assistant text, tool results, and non-path tool inputs are discarded in the same pass that reads them, and nested subagent transcripts are never opened, matching scree's collector. Paths are canonicalised before aggregation, so a file reached as `/Users/x/.claude/settings.json` from a tool input and as `~/.claude/settings.json` from a shell command is one row rather than several. Dedupe is keyed on the tool_use block id — Claude streams one assistant message as multiple lines sharing a message id, so a path-keyed dedupe would silently collapse a genuine second read. Exposed as the `file_access` MCP tool.
- **Hugging Face cache audit — `scripts/hfscan.py`.** Absorbed from decant's `ContextProbe.swift` (that repository is being retired; this was one of two capabilities Modore did not already have). Derives each cached model's identifier from its hub directory name (`models--Qwen--Qwen2.5-Coder-1.5B-Instruct` → the `Qwen/…` slug and the bare leaf) and searches the given roots for any occurrence, case-insensitively, so that over-catching keeps a model rather than losing one. **The original's central defect is inverted here.** decant returned "unreferenced" whenever the search could not run — a missing search root, a failed grep, a mistyped `--projects` argument all produced the same answer as a genuinely exhaustive search that found nothing, so one typo could mark an entire hub cache safe to delete. Absence of evidence is only evidence of absence when the search actually happened: a root that does not exist, a file cap reached, or a subtree that could not be read now yields `unknown` for every model with the reason stated, and `--allow-missing-roots` / `--ignore-unreadable` widen the verdict only when the operator asks. Metadata-only: file contents are matched in memory and discarded, and the only paths emitted are the files that *do* reference a model, masked through `scree.mask_text`.
- **MCP config hygiene — `scripts/mcpaudit.py`.** Absorbed from decant's `MCPHygiene.swift`. Reads `~/.claude.json` (including servers nested under per-project blocks), both Claude Desktop config locations, and `~/.mcp.json`, and reports entries that cannot start: `dead` (command does not resolve, or a script argument points at a path that is gone), `duplicate` (same command and args as another entry), `manual-review` (an `env` block is present), `unknown` (no command at all). Two deliberate deviations from the original: servers are named rather than anonymised to `server#N`, because a hygiene report the operator cannot act on is not one, and a server name is configuration metadata of the same kind scree already emits — while `env` is reported as a key *count*, never as keys or values. And a verdict that depends on PATH is withheld as `unknown` when PATH is unusable rather than reported as `dead`, the same fail-safe rule hfscan applies to its search. Read-only in the strong sense: the module has no code path that edits a config, disables a server, or starts one, pinned by a test.
- **Two more read-only MCP tools — `hf_orphans`, `mcp_hygiene`.** The audits above, exposed through the existing surface on the same terms as `scree_report` and `friction_scan`: allowlisted by name, annotated read-only and non-destructive, forwarding what the judgment script prints rather than reimplementing it. `hf_orphans` restates `search_complete` as a top-level `verdicts_withheld` flag so an incomplete search cannot be misread as "no orphans found", and its `roots` argument — the one place a caller supplies a path — is bounded to 8 entries and screened so a value can never be read as an option by the script it is passed to.
- **Operator-friction scan — `scripts/friction.py`.** Deterministic port of canary's `lib/sessions/friction.ts` (that repository is now a frozen paper archive; this was its one capability Modore did not already have). Flags the user turns where the operator pushed back on agent behaviour, across nine categories — wrong-action, no-research-assertion, stalling-approval, rule-contamination, over-orchestration-token, stale-repetition, verbosity, tone-attitude, other-ai-friction — at severity 1-3. Keyword and tone matching only, no model in the judgment path. Claude Code and Codex sessions are discovered through scree's own collectors rather than a second traversal; Gemini CLI chats (`~/.gemini/tmp/*/chats`, joined to a workspace through the project registry) and Claude Desktop local-agent sessions are added because scree does not traverse either — its Gemini collector reads only the project registry, and it has no Claude Desktop collector. Content contract: user-authored turns only, quotes capped at 200 characters and masked by default (`--raw-quotes` opts out), nothing written. `--json` serves the structured report. First live run over a 30-day window: 200 sessions, 3,563 user turns, 430 findings (rage 247 / irritation 60 / correction 123).
- **Read-only MCP surface — `scripts/mcp_server.py`.** A zero-dependency stdio JSON-RPC server exposing three tools: `scree_report` (join, retention forecast, sole-copy and orphan verdicts, by section), `friction_scan` (the taxonomy above, filterable by store/category/minimum severity), and `system_scan_summary` (the storage and security scan result already on disk, with its age). Each tool runs `scree.py --json` or `friction.py --json` and forwards the output, so no judgment is reimplemented and the CLI, the Mac app, and the MCP surface cannot disagree. Cleanup, deletion, and scan execution are deliberately not exposed — Modore's destructive path is gated on an on-screen human approval, and an agent-reachable bypass would void it; pinned by a test that exercises every tool and asserts the only processes ever spawned are the two judgment scripts. The read-only contract is enforced where tools are registered, ported from AirMCP's own Swift MCP server (`IOSPreviewContract`): a tool is reachable only if it is on an explicit name allowlist and annotated read-only and non-destructive, so one added without a deliberate edit fails closed rather than being merely unlisted. All results are fenced as untrusted machine-derived data.
- **scree lineage: what the sessions remember versus what the disk still holds.** Every work path appearing in session records is classified alive+git / alive+plain / vanished, using existence and a `.git` presence check only; macOS case-variant ghosts (the same path recorded under different casings) are merged by casefold and reported once with their spellings. First live run: 422 paths — 118 alive git repos, 189 alive plain directories, 115 vanished (session records their only trace), 4 case ghosts. Consumers such as balance's lineup-gap diff read `--json .lineage`; scree itself reports universal facts only.
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ python3 scripts/mcp_server.py --tools # inspect the surface without speaking

- `scree_report` — the join, retention forecast, orphan/sole-copy/lineage judgment, by section, with every truncation reported.
- `friction_scan` — the pushback taxonomy, filterable by store, category, and minimum severity.
- `hf_orphans` — which models in the Hugging Face hub cache no project file names, and how many gigabytes those account for. Reports `search_complete`; when the search could not be exhaustive every model is `unknown`, never `unreferenced`.
- `mcp_hygiene` — registered MCP servers that cannot start: dead command, missing script path, duplicate entry, or an `env` block worth a human look (reported as a key count, never as keys or values).
- `file_access` — the reverse index: which sessions touched which paths, with reads/writes/shell counts. Agent rule surfaces first, because a silently edited `CLAUDE.md` is the case it exists for.
- `system_scan_summary` — the storage and security scan result *already on disk*, with its age, because a stale result read as current is the failure mode here.
- **A thin layer, not a second implementation** — each tool runs `scree.py --json` or `friction.py --json` and forwards what it prints, so the CLI, the Mac app, and the MCP surface cannot disagree about what is true.
- **A thin layer, not a second implementation** — each tool runs one judgment script with `--json` and forwards what it prints, so the CLI, the Mac app, and the MCP surface cannot disagree about what is true.
- **Read-only by contract, enforced at registration** — a tool is reachable only if it is on an explicit allowlist and annotated read-only and non-destructive; one added without a deliberate edit fails closed. Cleanup, deletion, and scan execution are deliberately absent. Modore gates destruction on an approval a human grants on screen; an agent-reachable bypass would not be a feature, it would be the end of that guarantee. Every result is fenced as untrusted data.

### 2. Why is my PC this busy?
Expand All @@ -71,7 +74,10 @@ A fan that will not stop, CPU/GPU load while idle, an unknown process, a strange
- **Two OS editions under one brand**: Modore for Windows and Modore for Mac share the same promise — explain local machine state in plain language without deleting anything automatically.
- **Mac Edition — AI-agent session audit**: `scree` (above) is the flagship Mac capability — cross-tool join, retention forecast, orphan/sole-copy/lineage judgment, metadata-only.
- **Mac Edition — operator-friction scan**: `friction` classifies the turns where the operator pushed back on agent behaviour across Claude Code, Codex, Gemini CLI, and Claude Desktop transcripts — nine categories, severity 1-3, deterministic keyword/tone matching, user-authored turns only, quotes masked by default.
- **Read-only MCP surface**: a zero-dependency stdio MCP server exposing scree, friction, and the existing storage/security scan summary to an agent mid-session. Judgment only — no cleanup, no deletion, no scan execution.
- **Mac Edition — Hugging Face cache audit**: `hfscan` cross-references every cached model against the code on this machine and reports which ones nothing names. An incomplete search withholds the verdict instead of guessing.
- **Mac Edition — MCP config hygiene**: `mcpaudit` reads the registered MCP servers and reports the entries that cannot start. It never edits a config, disables a server, or starts one.
- **Mac Edition — file-access reverse index**: `fileaccess` answers "which sessions touched this file, how often, and when last", rule surfaces first. Only paths and tool names are kept — the command a path came from is never emitted.
- **Read-only MCP surface**: a zero-dependency stdio MCP server exposing scree, friction, the two audits above, and the existing storage/security scan summary to an agent mid-session. Judgment only — no cleanup, no deletion, no scan execution.
- **Mac Edition scanner**: Bash + JXA collectors for macOS security context, launchd/login items, Gatekeeper/SIP/XProtect, network/listening ports, installed-app size, and developer-runtime incidents. Every collector reports `ok`, `permission_denied`, `unavailable`, `timed_out`, or `failed`; a missing required collector can never become a safe verdict.
- **Mac Edition app**: the native SwiftUI app presents one incident judgment followed by evidence, likely impact, and approval-gated recovery; bounded local history keeps the judgment without storing raw commands or URLs. Browser automation is grouped into roots with PID, parent, elapsed time, channel, profile type, and a privacy-preserving controller label.
- **Windows Edition**: PowerShell 5.1+ scanner focused on Korean banking/government plugin context, Windows Defender, Sysinternals-backed signature/autoruns coverage, networking, startup entries, scheduled tasks, recent installs, and the 5-minute idle CPU monitor.
Expand Down Expand Up @@ -240,7 +246,10 @@ modore/
├── scripts/
│ ├── scree.py AI-agent session & residue audit (metadata-only)
│ ├── friction.py operator-pushback scan over the same session stores
│ ├── mcp_server.py read-only MCP surface (scree · friction · scan summary)
│ ├── hfscan.py Hugging Face hub cache: which models nothing here names
│ ├── mcpaudit.py MCP config hygiene: registered servers that cannot start
│ ├── fileaccess.py reverse index: which sessions touched which paths
│ ├── mcp_server.py read-only MCP surface (scree · friction · hfscan · mcpaudit · fileaccess · scan summary)
│ ├── menu.ps1 Windows interactive menu
│ ├── scanner.ps1 Windows scanner
│ ├── monitor.ps1 Windows 5-min idle monitor
Expand Down
24 changes: 23 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ Every app build embeds an explicit runtime allowlist under `Contents/Resources/r
mask-by-default Markdown export for records about to expire. There is no bulk-export path, and
no cleanup recipe consumes scree output.

## Absorbed audits (hfscan, mcpaudit, fileaccess)

- `scripts/hfscan.py` judges the Hugging Face hub cache: it derives each cached model's identifier
from its hub directory name and searches the given roots for any occurrence. Absorbed from
decant's `ContextProbe.swift` with that module's central defect inverted — an incomplete search
(missing root, file cap reached, unreadable subtree) reports every model `unknown`, never
`unreferenced`. Absence of evidence counts only when the search actually ran; the escape hatches
are explicit flags, never a side effect of a mistyped argument.
- `scripts/mcpaudit.py` judges MCP configuration: which registered servers cannot start. Absorbed
from decant's `MCPHygiene.swift`. `env` is reported as a key count only, and any verdict that
depends on PATH is withheld when PATH is unusable — the same fail-safe rule as above.
- `scripts/fileaccess.py` inverts scree's evidence to path → sessions: reads, writes, shell
references, session count, and last touch, with agent rule surfaces sorted first. Absorbed from
canary's `get_file_access`. Its content contract is *stricter* than the original's — canary
attached a 200-character excerpt of the shell command to every row, and 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. Nested subagent transcripts stay unopened, matching
scree's collector.
- All three are metadata-only, write nothing, and start nothing. None ships inside the signed app
bundle: they have no Swift caller, so they stay CLI-and-MCP surfaces, and a test pins that
giving one a view means moving it into `RUNTIME_FILES`.

## Cleanup invariants

- No caller-supplied deletion path.
Expand All @@ -74,7 +96,7 @@ Changes to outbound networking, signature verification, cleanup targets, standal

## Verification map

- `python3 -I -B -m pytest tests/ -q`: rule/report/runtime contracts and destructive-boundary tests in isolated fixtures — including `tests/test_scree.py`, which pins scree's no-content-leak, masking, and single-session-export contracts.
- `python3 -I -B -m pytest tests/ -q`: rule/report/runtime contracts and destructive-boundary tests in isolated fixtures — including `tests/test_scree.py`, which pins scree's no-content-leak, masking, and single-session-export contracts, and `tests/test_hfscan.py`, which pins that every way a reference search can fail yields a withheld verdict rather than a false orphan.
- `swift test --package-path macos/Modore -Xswiftc -warnings-as-errors -Xswiftc -strict-concurrency=complete`: native model, selection, history, presentation, and runtime-staging tests under the CI compiler policy.
- `python3 -I -B scripts/release_smoke.py`: OS-specific source allowlists plus secret/PII/archive-structure audit.
- `scripts/package_macos_release.sh --local`: strict Universal 2 standalone app/DMG build under `dist/local/`, clearly unsigned for distribution and never overwriting a release artifact. Git, Swift/Xcode, Python audit, signing, and disk-image tools run from a minimal environment; metadata records the selected developer directory and Swift version.
Expand Down
Loading