feat(discovery): real macOS and Linux branches for day-zero scanners (#1956) - #2747
Conversation
|
Verdict: Approve This extends the day-zero discovery scanners from Windows-only to real macOS and Linux branches (apps, bookmarks, history, email), and — the more valuable half — replaces a wall of silent The one thing to keep honest: the new macOS and Linux branches are exercised only through a patched Real-world evidenceN/A in this run — no 🔍 Technical detailsIssues🟡 macOS/Linux branches unvalidated on real hardware ( Nits🟢 Bootstrap prints a "Skipped" line for every off-platform source ( Strengths
|
Summary
On macOS and Linux, four of the six day-zero discovery sources returned nothing — and returned it silently. Measured on this Mac against
main: installed apps0, bookmarks0, history0, email0, with no error and no log line, indistinguishable from "you own no browsers and have no email". The same machine on this branch returns 39 applications, 908 bookmark facts across both Chrome profiles, and 50 history domains; anything that genuinely has no scanner for the running OS now names itself instead of vanishing.Why
Day-zero bootstrap is the one flow whose entire job is making a brand-new user's agent useful on first run. A source that quietly returns
[]there doesn't degrade the experience, it removes it — and the user has no way to tell a missing platform branch from an empty machine. That is precisely the failure modeCLAUDE.md's fail-loudly rule exists to prevent, and it was shipping on two of the three supported platforms.The fix is real per-platform branches for the four Windows-only scanners, plus one importable predicate (
unsupported_reason) that turns every remaining "no branch here" case into a named, surfaced reason rather than an empty list.Linked issue
Closes #1956
Changes
.appbundles /.desktopentries), bookmarks and history (Chromium + Firefox + Safari), and email (Keychain attributes, Apple Mail plists, Thunderbird, Evolution). Windows keeps its existing bodies verbatim.unsupported_reason()is the single source of truth for "no scanner here", and every entry point consults it —scan_all, both Agent UI SSE streams, and bothgaia memory bootstrappaths. This is what converts a silent[]into a log line and an SSElogevent, and it also closes the pre-existing silent no-op forwindows_userassist/macos_app_usagein the UI.Default. This machine hasDefaultandProfile 1; previously half its bookmarks and history were invisible. Applies on all three platforms — see deviation 6.XDG_DATA_DIRS/XDG_DATA_HOMEfor apps, plus the Snap and Flatpak roots for Chromium and Firefox. Stock Ubuntu ships Chromium as a snap, which never writes to~/.config.Path.globis not used for profile or mail enumeration. It swallowsPermissionErrorand yields nothing — the exact silent-empty result this work exists to remove.os.scandirwith explicit handlers replaces it.-wal/-shmsidecars. Safari'sHistory.dband Firefox'splaces.sqliterun in WAL mode, so the most recent visits — what a 30-day scan is actually after — live in the sidecar until checkpoint.#1956 Acceptance Criteria — Proof
AC 1 — "macOS + Linux scanners implemented behind platform branches; no silent empty returns off Windows" ✅
Measured on darwin 25.1, same machine, same home directory:
main@9bf0042ainstalled_appsbrowser_bookmarksbrowser_historyemail_accountsThe "no silent empty" half is proven by the live CLI run under Evidence:
windows_userassistemits a named INFO line on darwin instead of nothing, and three TCC denials each emit an actionable warning.email_accountslegitimately finds 0 on this box — Apple Mail is TCC-denied (warned, twice), Thunderbird is not installed, and no Keychain internet-password matched the mail-host allowlist. Zero with four warnings is a different user experience from zero in silence, which is the whole point.AC 2 — "Existing
scan_macos_app_usagewired into the discovery path" ✅ already satisfied before this PRThe triage bot's correction on the issue thread was right:
scan_macos_app_usagewas already registered inscan_alland already called from the memory router. No re-wiring was needed and none was done. Its detection logic is untouched; only its platform guard changed, from a barereturn []to a reported skip — see deviation 1.AC 3 — "Code-level tests pass on Mac; macOS/Linux runtime runs flagged
UNVALIDATED" ✅120 discovery tests plus the router suite pass on this Mac; the Linux and Windows branches are exercised via
patch("sys.platform", …)against fixture homes. macOS is runtime-validated here (CLI output below). Linux and Windows areUNVALIDATED— no hardware available; flagged individually in Deviations 3, 4, and 6 for AMD to confirm.Deviations from the plan
scan_macos_app_usagemust have zero diff linesif sys.platform != "darwin": return []— a silent empty for a source registered in_PLATFORM_SUPPORT_log_unsupported. Detection logic unchanged. The invariant was written beforeunsupported_reasonexisted; leaving it would have exempted this scanner from the PR's own rule./Applications+ Launch Services"lsregisterlives at an undocumented framework path, dumps megabytes, and is slow/Applications+~/Applicationsbundle scan. No Launch Services./System/Applicationsdeliberately excluded — ~40 Apple stock apps would swamp the review list..desktop/ apt / flatpak / snap"apt list --installedis slow, Debian-only, and lists non-GUI packages.desktopfiles only, across XDG dirs plus the Flatpak and Snap export dirs — how those two surface GUI apps anyway. No package-manager subprocess.UNVALIDATEDon Linux hardware.secret-toolcannot enumerate — it requires exact attribute key/value pairs to search. Blanket enumeration is not an API that exists.~/.config/evolution/sources/*.source. No libsecret. This is a factual correction to the issue.UNVALIDATEDon Linux hardware.Defaultbrowser profileUNVALIDATEDon real Windows.plistlibandconfigparserare stdlib.discovery.pyused stdlibloggingCLAUDE.mdrequiresgaia.logger.get_logger_DISCOVERY_SOURCESomitsemail_accountswhilescan_allincludes itBeyond the plan, the following came out of review and are in the diff: XDG-based Linux app-dir resolution, Snap/Flatpak Chromium roots, WAL sidecar copying, the
subprocess.CREATE_NO_WINDOWguard (the symbol does not exist off Windows, which made the win32 cold-state assertions vacuous), and extending the platform gate tostream_inferenceand_bootstrap_infer— the plan had only coveredstream_discovery.Evals: not required. This change touches no system prompt, tool docstring, tool registration, JSON tool schema, error classifier, default model, tokenizer config, or tool-call parser — it is filesystem/subprocess scanning behind an unchanged
List[Dict]contract. No baseline category (context_retention,rag_quality,tool_selection) exercises discovery. The only indirect path to a prompt is approved facts → memory, which is data gated behind the user's explicit[Y/n/q]approval.Test plan
python util/lint.py --all→ALL QUALITY CHECKS PASSED, Failed: 0python -m pytest tests/unit/test_memory_discovery.py tests/unit/test_memory_router.py -q→ 244 passed, 2 skipped. One pre-existing failure,TestReconcileEndpoint::test_reconcile_returns_503_when_no_agent_and_faiss_missing, is environmental: it asserts 503 for "faiss unavailable" but faiss is installed in a full local venv. It is untouched by this diff, exercises an endpoint this branch does not modify, and passes in CI, which installs without faiss.python -m pytest tests/unit/agents/test_discovery.py -q→ 49 passed (pre-existing discovery suite, unmodified, no regressions)python -m pytest tests/unit/ -q→ 7960 passed, 170 skipped, 5 failed. All five are pre-existing and environmental, none in a file this branch touches: the faissreconciletest above, threetest_cli_smoke.py::test_gaia_binary_on_pathcases (console-script shims not on PATH in a bare venv), andtest_skills_cli.py::test_skills_package_ships_in_the_wheel(setup.py package list vs. a working tree containingsrc/gaia/apps/webui/node_modules).TestColdEmptyHomeruns all four scanners against an empty temp home on each platform; asserts[], no exception, and no ERROR record. This is the new-user state, which a primed dev home hides.TestKeychainContractShapeandTestCredentialManagerContractShapeassert the shape of the outgoing subprocess call, not merely that it fired: exact argv list, never a shell string,timeoutset,-g/-wnever passed (so no secret is read and no auth modal appears).python -m gaia.cli memory bootstrap --discover, answerqat the first prompt. Expect Chrome bookmarks and history from bothDefaultandProfile 1, ~39/Applicationsentries, one actionable Full-Disk-Access warning per TCC-blocked source, an explicit "no scanner for 'windows_userassist' on darwin" line, and0 approved, 0 skipped— nothing stored.UNVALIDATEDhere): same command; confirm the branches in deviations 3, 4, and 6.Evidence
python -m gaia.cli memory bootstrap --discoveron darwin 25.1, answeringq:Four denials named with a remedy, one unsupported source named by OS, 1009 findings, and nothing stored — the review gate is intact. On
mainthis same machine produced zero findings from these sources and zero log lines about it.Per-source counts and multi-profile proof from the same box:
All 50 history facts and every email fact remain
sensitive=Trueunconditionally — the sensitivity contract is unchanged.GET /api/memory/stream-discoveryagainst an empty home on darwin:Previously this streamed "Nothing found" — the UI could not distinguish an unsupported platform from an empty machine.
{"type": "finding" | "log" | "error" | "done"}) is unchanged and no source key was added or removed, somemoryApi.tsandMemoryDashboard.tsxneed no frontend change.Checklist
Closes #1956).python util/lint.py --all,pytest tests/unit/).docs/sdk/sdks/memory.mdxanddocs/spec/agent-memory-architecture.mdupdated together. No new CLI command or user-facing feature, so no new guide and nodocs.jsonentry.