Skip to content

feat(discovery): real macOS and Linux branches for day-zero scanners (#1956) - #2747

Merged
kovtcharov-amd merged 2 commits into
amd:mainfrom
alexey-tyurin:feat/1956-discovery-macos-linux
Aug 6, 2026
Merged

feat(discovery): real macOS and Linux branches for day-zero scanners (#1956)#2747
kovtcharov-amd merged 2 commits into
amd:mainfrom
alexey-tyurin:feat/1956-discovery-macos-linux

Conversation

@alexey-tyurin

Copy link
Copy Markdown
Contributor

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 apps 0, bookmarks 0, history 0, email 0, 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 mode CLAUDE.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

  • Four scanners gain real macOS and Linux branches. Apps (.app bundles / .desktop entries), 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 both gaia memory bootstrap paths. This is what converts a silent [] into a log line and an SSE log event, and it also closes the pre-existing silent no-op for windows_userassist / macos_app_usage in the UI.
  • Every browser profile is read, not just Default. This machine has Default and Profile 1; previously half its bookmarks and history were invisible. Applies on all three platforms — see deviation 6.
  • Permission denials are reported with a remedy instead of swallowed at debug level. A TCC-blocked Safari or Apple Mail now emits one actionable warning naming System Settings → Privacy & Security → Full Disk Access, and the rest of the scan continues.
  • Linux paths follow the environment rather than a hardcoded prefixXDG_DATA_DIRS/XDG_DATA_HOME for apps, plus the Snap and Flatpak roots for Chromium and Firefox. Stock Ubuntu ships Chromium as a snap, which never writes to ~/.config.
  • Path.glob is not used for profile or mail enumeration. It swallows PermissionError and yields nothing — the exact silent-empty result this work exists to remove. os.scandir with explicit handlers replaces it.
  • SQLite snapshots copy the -wal/-shm sidecars. Safari's History.db and Firefox's places.sqlite run in WAL mode, so the most recent visits — what a 30-day scan is actually after — live in the sidecar until checkpoint.
  • Docs corrected in the same change: the SDK methods table and the spec's discovery-source table both described these as Windows-only sources.

#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:

Source main @ 9bf0042a this branch
installed_apps 0 39
browser_bookmarks 0 908 (4 sensitive)
browser_history 0 50 (50 sensitive)
email_accounts 0 0 — reported, not silent (see note)

The "no silent empty" half is proven by the live CLI run under Evidence: windows_userassist emits a named INFO line on darwin instead of nothing, and three TCC denials each emit an actionable warning. email_accounts legitimately 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_usage wired into the discovery path"already satisfied before this PR

The triage bot's correction on the issue thread was right: scan_macos_app_usage was already registered in scan_all and 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 bare return [] 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 are UNVALIDATED — no hardware available; flagged individually in Deviations 3, 4, and 6 for AMD to confirm.

Deviations from the plan

# Plan / issue said Reality Resolution
1 scan_macos_app_usage must have zero diff lines Its guard was if sys.platform != "darwin": return [] — a silent empty for a source registered in _PLATFORM_SUPPORT Deviation. The guard now reports via _log_unsupported. Detection logic unchanged. The invariant was written before unsupported_reason existed; leaving it would have exempted this scanner from the PR's own rule.
2 macOS apps via "/Applications + Launch Services" lsregister lives at an undocumented framework path, dumps megabytes, and is slow /Applications + ~/Applications bundle scan. No Launch Services. /System/Applications deliberately excluded — ~40 Apple stock apps would swamp the review list.
3 Linux apps via ".desktop / apt / flatpak / snap" apt list --installed is slow, Debian-only, and lists non-GUI packages .desktop files only, across XDG dirs plus the Flatpak and Snap export dirs — how those two surface GUI apps anyway. No package-manager subprocess. UNVALIDATED on Linux hardware.
4 Linux email via "libsecret/gnome-keyring" secret-tool cannot enumerate — it requires exact attribute key/value pairs to search. Blanket enumeration is not an API that exists. Thunderbird profiles + Evolution ~/.config/evolution/sources/*.source. No libsecret. This is a factual correction to the issue. UNVALIDATED on Linux hardware.
5 Spec describes apps + email as Windows-only sources Cross-platform after this change Spec table updated in this PR per the "a functional change updates every doc that describes it" rule. Flagging in case the maintainer wants spec edits split out.
6 Windows reads only the Default browser profile Multi-profile globbing now applies on all platforms Windows gains coverage. Validated on macOS only — UNVALIDATED on real Windows.
7 Module docstring: "stdlib only. Windows-focused." False after this change Updated to "Cross-platform (Windows / macOS / Linux)". Still no third-party deps — plistlib and configparser are stdlib.
8 discovery.py used stdlib logging CLAUDE.md requires gaia.logger.get_logger Swapped. This file gains ~20 new log calls, so it had to comply before adding them.
9 Router _DISCOVERY_SOURCES omits email_accounts while scan_all includes it The Agent UI surfaces no email discovery on any OS Out of scope, unchanged. Surfacing sensitive email data in a new UI is a product call, not a platform fix. Worth a follow-up issue.

Beyond 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_WINDOW guard (the symbol does not exist off Windows, which made the win32 cold-state assertions vacuous), and extending the platform gate to stream_inference and _bootstrap_infer — the plan had only covered stream_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 --allALL QUALITY CHECKS PASSED, Failed: 0
  • python -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 faiss reconcile test above, three test_cli_smoke.py::test_gaia_binary_on_path cases (console-script shims not on PATH in a bare venv), and test_skills_cli.py::test_skills_package_ships_in_the_wheel (setup.py package list vs. a working tree containing src/gaia/apps/webui/node_modules).
  • Cold-state checkTestColdEmptyHome runs 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.
  • Call-validity checkTestKeychainContractShape and TestCredentialManagerContractShape assert the shape of the outgoing subprocess call, not merely that it fired: exact argv list, never a shell string, timeout set, -g/-w never passed (so no secret is read and no auth modal appears).
  • On a Mac: python -m gaia.cli memory bootstrap --discover, answer q at the first prompt. Expect Chrome bookmarks and history from both Default and Profile 1, ~39 /Applications entries, one actionable Full-Disk-Access warning per TCC-blocked source, an explicit "no scanner for 'windows_userassist' on darwin" line, and 0 approved, 0 skipped — nothing stored.
  • On Linux / Windows (AMD hardware, UNVALIDATED here): same command; confirm the branches in deviations 3, 4, and 6.

Evidence

  • CLIpython -m gaia.cli memory bootstrap --discover on darwin 25.1, answering q:
=== GAIA Memory Bootstrap — System Discovery ===
Scanning your system for projects, apps, and more...
Nothing is stored without your approval.

WARNING | discovery._extract_safari_bookmarks | Cannot read Safari bookmarks at
  ~/Library/Safari/Bookmarks.plist: [Errno 1] Operation not permitted. Grant Full Disk
  Access to the terminal or app running GAIA in System Settings → Privacy & Security →
  Full Disk Access, then re-run discovery.
WARNING | discovery._safe_copy_and_query_sqlite | Cannot read Safari history at
  ~/Library/Safari/History.db: [Errno 1] Operation not permitted. Grant Full Disk Access …
WARNING | discovery._scan_apple_mail | Cannot read Apple Mail accounts at ~/Library/Mail:
  [Errno 1] Operation not permitted. Grant Full Disk Access …
WARNING | discovery._scan_apple_mail | Cannot read Apple Mail accounts at
  ~/Library/Containers/com.apple.mail/…/com.apple.mail.plist: [Errno 1] Operation not permitted. …
INFO    | discovery._log_unsupported | no scanner for 'windows_userassist' on darwin
  (supported: win32) — nothing was scanned. Add a branch in
  src/gaia/agents/base/discovery.py and register the platform in _PLATFORM_SUPPORT,
  or open an issue at https://github.com/amd/gaia/issues.
Found 1009 items. Review each one:

  [Y] = approve (default)   [n] = skip   [q] = quit review

  (1/1009) Project 'Zoom' in Documents/ — unknown [unclassified]
    Approve? [Y/n/q]:   Review stopped.

✅ Discovery complete: 0 approved, 0 skipped.

Four denials named with a remedy, one unsupported source named by OS, 1009 findings, and nothing stored — the review gate is intact. On main this 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:

profiles found: [('Chrome (Default)', 'Default'), ('Chrome (Profile 1)', 'Profile 1')]
installed_apps       ->   39 items (0 sensitive)
browser_bookmarks    ->  908 items (4 sensitive)
browser_history      ->   50 items (50 sensitive)
email_accounts       ->    0 items (0 sensitive)
macos_app_usage      ->    3 items (0 sensitive)
recent_file_types    ->    2 items (0 sensitive)

All 50 history facts and every email fact remain sensitive=True unconditionally — the sensitivity contract is unchanged.

  • HTTP API / RESTGET /api/memory/stream-discovery against an empty home on darwin:
GET /api/memory/stream-discovery -> 200 text/event-stream; charset=utf-8
data: {"type": "log", "message": "  App usage frequency (Windows): no scanner for
  'windows_userassist' on darwin (supported: win32) — nothing was scanned."}

Previously this streamed "Nothing found" — the UI could not distinguish an unsupported platform from an empty machine.

  • Agent exposed in the Agent UI (Memory Dashboard → Profile Setup) — outstanding. The discovery and inference SSE streams feeding this panel both changed, so a before→after browser screenshot is still owed. The event contract ({"type": "finding" | "log" | "error" | "done"}) is unchanged and no source key was added or removed, so memoryApi.ts and MemoryDashboard.tsx need no frontend change.
  • MCP tools / servers — N/A. No MCP server, tool, or bridge surface is touched.

Checklist

  • I have linked a GitHub issue above (Closes #1956).
  • I have described why this change is being made, not just what changed.
  • I have run linting and tests locally (python util/lint.py --all, pytest tests/unit/).
  • I have attached real-world evidence matched to the surface I changed — CLI, HTTP API, and MCP are covered above; the Agent UI screenshot is still outstanding.
  • I have updated documentation if user-visible behavior changed — docs/sdk/sdks/memory.mdx and docs/spec/agent-memory-architecture.md updated together. No new CLI command or user-facing feature, so no new guide and no docs.json entry.

@github-actions github-actions Bot added documentation Documentation changes cli CLI changes tests Test changes agents labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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 except: pass fallbacks with an explicit platform gate so an unsupported source is named ("GAIA cannot look here") instead of masquerading as "you have none". The code is defensive, reads addresses-only (never secrets), and the test suite is unusually thorough: subprocess call-shape contracts, cold-empty-home coverage, map↔branch drift detection, and a WAL-sidecar test. Nothing here blocks merge.

The one thing to keep honest: the new macOS and Linux branches are exercised only through a patched sys.platform against a fake $HOME — the test file says so plainly ("UNVALIDATED on real hardware; fixture-covered only"). A single real gaia memory bootstrap --infer run on an actual Mac (and Linux if reachable) is the evidence that closes that gap; the hermetic tests gate the logic, not the surface.

Real-world evidence

N/A in this run — no evidence-bundle.md was produced (no-inference lane), and I couldn't fetch the PR description here to see whether a real run is linked, so this verdict rests on static review plus the committed tests. The changed surfaces that a real run would exercise are the gaia memory bootstrap --infer CLI output and the Agent UI stream-discovery / stream-inference SSE logs. If a real gaia memory bootstrap transcript on macOS/Linux is already linked in the description, disregard the nudge below.

🔍 Technical details

Issues

🟡 macOS/Linux branches unvalidated on real hardware (tests/unit/test_memory_discovery.py:2557)TestLinuxBranches and TestMacOSBranches drive the new branches with patch("sys.platform", ...) and a fixture home, and the docstring is candid that no Linux hardware ran them. That's fine for CI logic-gating, but the SQL against Safari History.db / Accounts.plist layouts and the Evolution/Thunderbird path shapes are exactly the kind of thing a fixture can't prove. Recommend attaching one real gaia memory bootstrap --infer run (Mac at minimum) to the PR before/at merge — a nudge, not a blocker, given the honest disclosure.

Nits

🟢 Bootstrap prints a "Skipped" line for every off-platform source (src/gaia/cli.py:6650) — on macOS, _collect_signal("windows_userassist", …) prints Skipped: no scanner for 'windows_userassist' on darwin … on every run. This is intentional (report, never silent) and consistent with the fail-loudly rule, so leave it if desired — just flagging that a normal user sees a "Skipped" line for a platform they'll never have. If it reads as noise, consider demoting the cross-platform-expected skips to the log and keeping stdout for genuine failures.

Strengths

  • Fail-loudly done right. The core win isn't the new OS coverage — it's swapping ~a dozen except Exception: pass blocks for unsupported_reason() + logged, correlation-id'd reporting across the CLI, the SSE router, and scan_all. _log_unsupported even warns on map↔branch drift (map says supported, no branch exists) rather than returning a quiet [].
  • Contract-shape tests, not just invocation tests. TestKeychainContractShape / TestCredentialManagerContractShape assert the outgoing argv is a list (never a shell string), that -g/-w (the secret-reading, prompt-raising flags) are never passed, and that a timeout is set — testing the validity of the boundary call, which is exactly the gaia init --profile npu fails with status 400 (missing user. prefix for model name) #1655-class gap CLAUDE.md calls out.
  • Cold-state coverage. TestColdEmptyHome and the isolated-home fixture verify the empty/new-machine path returns [] without errors on all three platforms — the state a day-zero feature actually targets.
  • Privacy care. _collect_plist_emails harvests only under account-identity keys, with a dedicated test proving correspondents/signatures aren't scooped up; Keychain reads attributes only.
  • Docs (memory.mdx, agent-memory-architecture.md) updated in the same change to match the new behavior, including the "reported, never silently empty" contract.

@kovtcharov-amd
kovtcharov-amd added this pull request to the merge queue Aug 6, 2026
Merged via the queue into amd:main with commit c103f6c Aug 6, 2026
109 checks passed
@itomek itomek mentioned this pull request Aug 10, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents cli CLI changes documentation Documentation changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Discovery — real macOS + Linux branches for bookmarks/history/email/apps

3 participants