Skip to content

Detect the ChatGPT desktop app as a partially-supported client - #30

Open
Miyamura80 wants to merge 7 commits into
mainfrom
claude/restore-pr-1064-changes-4ygyn2
Open

Detect the ChatGPT desktop app as a partially-supported client#30
Miyamura80 wants to merge 7 commits into
mainfrom
claude/restore-pr-1064-changes-4ygyn2

Conversation

@Miyamura80

@Miyamura80 Miyamura80 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Reimplements edison-watch#1064, whose changes were lost when the desktop client moved into this repo. The original targeted the Electron main process; detection now lives in the Rust daemon, so this is a reimplementation rather than a cherry-pick.

What it does

ChatGPT keeps its MCP servers as Connectors in the user's OpenAI account, not in a local config file. Edison can see the app is installed but can never configure it. Previously it was simply absent from onboarding, so a user with ChatGPT installed finished setup believing every MCP host on their machine was protected.

ChatGPT now appears in both the onboarding wizard and the permanent clients view, marked "Not protected" and excluded from selection, hook counts, and config writes.

Design

Agent::is_manageable() is a declared fact on the Rust trait, not inferred from an empty edison_installs(). JetBrains with no IDE installed returns empty yet is perfectly manageable, so inference would misclassify it.

That fact flows outward as data rather than being re-derived per call site:

Rust  Agent::is_manageable()  ->  AgentInfo.manageable
                                       |  IPC (newline-delimited JSON)
                                       v
Main  AgentFacts.manageable   ->  hookStatus / detectClients
                                       |  preload bridge
                                       v
Renderer  "unmanaged" status, no checkbox, disabled button

The daemon is the enforcement point. retain_manageable() runs inside both enroll and apply_integrations, because those are two independent entry paths and enrollment is additive: an app-side filter guarding only one of them would let chatgpt into selected_agents permanently. Unknown agent names are preserved, so an older app talking to a newer daemon does not lose enrollments.

AgentInfo.manageable carries #[serde(default = "default_true")], so an older daemon that omits the field does not break a newer app.

Also fixed

heal_edison_install counted and logged a heal outside the edison_installs() loop, so an agent with nothing to install still reported as healed. The self-heal signal was permanently non-zero. It now counts only what it actually wrote.

Verification

  • Rust: workspace tests, clippy --all-targets --all-features, fmt --check all clean
  • Desktop: typecheck clean, 21 test files / 192 tests passing
  • Electron e2e under xvfb: 8 passed, 2 skipped (the skipped two need a live backend)
  • Ran the real daemon over a real IPC socket and drove the packaged Electron app to confirm the flag survives the full path

New behavioural tests were mutation-checked against the unfixed code to confirm they fail without the fix.

Known gaps

  • Linux: no official ChatGPT desktop app exists, so default_app_paths() returns empty there and detection never fires. Still true after the July 2026 Codex/ChatGPT merger, which shipped macOS and Windows only.
  • Windows path unverified: %LOCALAPPDATA%\Microsoft\WindowsApps\ChatGPT.exe follows the MSIX execution-alias convention but has not been checked against a real install. If the filename is wrong, detection silently never fires on Windows. Worth a spot-check by someone with a Windows box.
  • Browser users: presence detection only covers the desktop app, so anyone using ChatGPT in a tab still sees no warning. Widening this is a product decision, not addressed here.

Note for reviewers

Building the detectord workspace needs a newer rustc than the crate's declared rust-version = "1.88", because libsqlite3-sys 0.38.1 uses cfg_select. CI resolves stable so it is unaffected, but a pinned older local toolchain will fail to build.


Generated by Claude Code


Summary by cubic

Detects the ChatGPT desktop app as a presence-only client and shows it as “Not protected.” Moves “manageable” into the daemon, removes extra discovery on config reads, adds client‑specific guidance for unmanageable apps, and hardens client id lookups and story mocks.

  • New Features

    • Added chatgpt agent in edison-detectord: presence via app path (macOS ChatGPT.app/ChatGPT Classic.app; Windows %LOCALAPPDATA%\Microsoft\WindowsApps\ChatGPT.exe and Programs\ChatGPT\ChatGPT.exe; none on Linux). Discovers no servers and is never an install target.
    • Introduced Agent::is_manageable() and plumbed to AgentInfo.manageable (defaults to true). IPC/preload carry it; McpClientId includes chatgpt.
    • ClientsView adds an “Unmanaged • Not protected” state; copy is keyed per client with a safe fallback (ChatGPT explains Connectors). Story added to cover this state.
    • mcp:readConfig: on failure for an unmanageable client, returns an explanatory message; the success path no longer triggers a list_agents scan.
  • Bug Fixes

    • The daemon drops unmanageable agents in both enroll and apply_integrations, and prunes any stale names; prevents chatgpt from persisting in selections.
    • Caches the unmanageable set once (LazyLock) to avoid rebuilding agents and re‑emitting discovery warnings during selection filtering.
    • heal_edison_install only counts/logs when it writes an entry, fixing a permanently non‑zero self‑heal signal on agents with no install targets.
    • Onboarding/status: no checkbox for unmanageable clients and excluded from “Configure N” counts; ClientsView shows them instead of hiding.
    • Protocol mapping treats an omitted manageable (older daemons) as true; tests cover the wire mapping. Storybook now restores API stubs after use.
    • ClientsView/story: use Maps for client‑id lookups to avoid inherited‑key collisions; Storybook mock installs/restores in a layout effect to prevent leaking stubs between stories.

Written for commit b9dcc97. Summary will update on new commits.

Review in cubic

claude added 4 commits July 31, 2026 11:05
Reimplements Edison-Watch/edison-watch#1064 against the current
architecture. That PR landed in the old `client_2/` tree, where the
Electron main process probed for clients itself; detection has since
moved into the detector daemon, so the same behaviour is rebuilt here
rather than ported line for line.

ChatGPT exposes MCP through server-side Connectors, hosted in the OpenAI
account rather than in a local config file. Edison can see the app is
installed but cannot read, write, hook, or proxy anything for it, so it
belongs in the wizard's existing "we only support local MCP servers, not
Connectors" section next to Claude Desktop and Claude Cowork.

Detection (daemon):
- New `ChatGpt` agent, behind a `chatgpt` cargo feature. Presence is
  probed from the app itself, not a config path: `ChatGPT.app` /
  `ChatGPT Classic.app` on macOS (post-merger the unified Chat + Work +
  Codex app ships as `ChatGPT.app`), the Store execution alias and a
  direct install on Windows, nothing on Linux. It discovers no servers
  and is never an install target.

Advisory-only in the app:
- `chatgpt` added to `McpClientId` + `CLIENT_DISPLAY`, marked
  `connectorOnly` with a label to show where a config path would go.
- `applyIntegrations` drops connector-only clients, so the wizard's
  default "select everything" never asks the daemon to install into an
  app with no config file.
- Setup status is reported over the managed clients only. Both answers
  it could give for ChatGPT mislead: "gateway not configured" blames the
  user for something they can't fix, and "nothing applicable" paints an
  unprotected app green.
- `readConfig` explains the Connectors situation instead of surfacing
  the daemon's "no user-scope config" error.
- Wizard: ChatGPT joins the partially-supported set; fixed the banner's
  "a equivalent" typo.

The Codex CLI stays a separate, fully-supported client.

Testing: desktop typecheck + 18 vitest files pass; new
`connectorOnlyClients.test.ts` pins the no-install / no-status
behaviour; new Rust unit tests cover the probe. Detection itself is
verified by tests and logic-trace, not a live run - CI here can't launch
macOS/Windows or the real Electron binary. Note that `cargo test` for
the full daemon can't run in this sandbox: `libsqlite3-sys` 0.38.1's
build script needs a newer stable toolchain than the one installed, a
pre-existing condition unrelated to this change. The `edison-detectord`
lib, its tests, clippy and rustfmt all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqPs27HH3TkQ6hQeJ5mAK
Follow-up to the ChatGPT detection commit, from a code-quality review of it.
Fixes a real defect that commit introduced, plus one it made permanent.

The bug: filtering unmanageable agents inside `applyIntegrations` guarded
one caller of two. `bootstrap.ts` sends the saved app selection straight to
`enroll` on every start, using its own private dash-to-underscore helper,
and never saw the filter. The selection is additive daemon-side (only
`unenroll` removes), so a default onboarding run on a Mac with ChatGPT.app
put `chatgpt` in `selected_agents` for good. The previous commit message
and docblock both claimed this could not happen.

`heal_edison_install` then counted and logged a self-heal for it on every
reconcile pass - every fs event plus every 20s tick - because the log and
`healed += 1` sat outside the loop over `edison_installs()`. So the
self-heal signal was permanently non-zero and a genuine heal became
indistinguishable from background noise. That was already latent for
JetBrains with no IDE installed; ChatGPT made it certain.

Both are fixed at the layer that owns the fact:

- `Agent::is_manageable()`, declared (not inferred from an empty
  `edison_installs`, which conflates "no target right now" with "never has
  one" - the JetBrains case). `enroll` and `apply_integrations` filter on
  it, closing both doors with one guard and pruning any stale name a
  previous build let through.
- `heal_edison_install` reports only what it wrote.
- `AgentInfo.manageable` carries it to the app, defaulting to true so an
  older daemon doesn't drop real clients out of setup status.

That removes the app's second, hand-maintained copy of the same fact:
`connectorOnly` off `ClientDisplay` (whose header promises it mirrors the
shared agent-registry, which has no such field), and `MANAGED_CLIENT_LIST`
and `isConnectorOnly` deleted.

Two user-facing corrections that the app-side flag was hiding:

- ChatGPT is reported in ClientsView again, under a new `unmanaged`
  status ("Not Protected", amber). Excluding it meant the user was warned
  once during onboarding and never again about an app running unprotected;
  the alternative of `mcpApplicable: false` would have painted it green.
- The wizard no longer renders a checked checkbox for it. Selecting it did
  nothing, and it inflated the next step's "Configure N Apps" count.

`PARTIALLY_SUPPORTED_IDS` is now documented as the presentation grouping it
is - Claude Desktop/Cowork are manageable and still belong under that
warning - which is the distinction a story comment previously inverted.

Tests: dropped two that asserted properties of the language rather than of
this code (`Arc<dyn Agent>` compiles by construction; `[].any()` is false)
and covered `default_app_paths`, which is the only function here with real
logic and whose failure mode is silent. Added Rust tests for the selection
guard and a renderer test for the checkbox, both verified to fail against
the unfixed code. Dropped a dead electron mock.

The full detectord workspace now builds and tests here after a toolchain
update (`libsqlite3-sys` needed a newer stable rustc), so unlike the
previous commit the daemon changes are verified rather than inferred:
cargo test --workspace, clippy --all-targets --all-features, fmt. Desktop
typecheck and 18 vitest files pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqPs27HH3TkQ6hQeJ5mAK
The "Not Protected" state has no other visual coverage: it can only be
reached with a daemon that reports `manageable: false`, and the agent that
does (ChatGPT) is never installed on Linux, so neither CI nor a sandbox run
can produce it from real detection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqPs27HH3TkQ6hQeJ5mAK
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Detect ChatGPT desktop app as installed-but-unmanageable client

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add ChatGPT desktop presence detection, surfaced as “Not protected” in UI.
• Plumb a daemon-owned manageable flag through IPC to prevent bogus selection/config.
• Fix self-heal reporting to count only real writes; add coverage across Rust/TS.
Diagram

graph TD
A["edison-detectord: ChatGpt Agent"] --> B["Daemon: agents/build + list_agents"] --> C["Protocol: AgentInfo.manageable"] --> D["Desktop main: getAgentFacts/toFacts"] --> E["IPC: detectClients + getHookStatus"] --> F["Renderer: AppsStep + ClientsView"]
B --> G["Daemon ops: retain_manageable"] --> H["Enrollment/apply: selection filtered"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Infer manageability from empty install targets
  • ➕ Avoids adding a new trait method and protocol field
  • ➕ Less plumbing through IPC and UI types
  • ➖ Incorrect for agents that are manageable but currently have no install targets (e.g., JetBrains without an IDE installed)
  • ➖ Conflates 'not installed yet' with 'never manageable', leading to silent misclassification
2. Filter connector-only clients only in the desktop app
  • ➕ No daemon-side selection changes; faster UI-only iteration
  • ➕ Keeps daemon simpler in the short term
  • ➖ Does not protect the second entry path (enroll vs apply_integrations); additive selection would still persist bad values
  • ➖ Weakens the daemon as the single enforcement point; higher long-term risk
3. Separate 'presence-only apps' from Agents entirely
  • ➕ Clear conceptual separation between manageable agents and advisory detections
  • ➕ Could avoid overloading the Agent interface over time
  • ➖ Adds parallel reporting paths and UI wiring
  • ➖ Higher refactor scope than needed for this incremental capability

Recommendation: Keep the PR’s approach: a declared Agent::is_manageable() capability that the daemon reports and enforces, with a backward-compatible default (true) in the protocol. This cleanly handles JetBrains-style “no targets right now” cases, prevents unmanageable agents from ever entering enrollment via either path, and still reports ChatGPT as installed so users are not misled.

Files changed (26) +720 / -43

Enhancement (14) +424 / -35
agent.rsAdd declared manageability capability to Agent trait +19/-0

Add declared manageability capability to Agent trait

• Introduces 'Agent::is_manageable()' (defaulting to true) to distinguish presence-only agents from manageable ones. Codifies that manageability must not be inferred from empty install targets.

crates/detectord/crates/edison-detectord/src/agent.rs

chatgpt.rsImplement ChatGPT presence-only agent (no config, no installs) +201/-0

Implement ChatGPT presence-only agent (no config, no installs)

• Adds a new 'ChatGpt' Agent that detects installation via app/executable paths (macOS/Windows) and reports no servers, no watch targets, and 'is_manageable=false'. Includes focused unit tests for install detection and platform-specific probe paths.

crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs

mod.rsRegister chatgpt client module behind feature gate +7/-0

Register chatgpt client module behind feature gate

• Adds the 'chatgpt' module and re-exports 'ChatGpt' when the feature is enabled. Keeps it outside common/transport gates since it has no config parsing.

crates/detectord/crates/edison-detectord/src/clients/mod.rs

agents.rsInclude ChatGpt agent in daemon discovery set +6/-1

Include ChatGpt agent in daemon discovery set

• Adds 'ChatGpt::discover()' to the daemon’s agent build list with documentation that it is detect-only. Ensures the app can be warned when ChatGPT is installed.

crates/detectord/crates/mcp_detector_daemon/src/agents.rs

protocol.rsAdd 'manageable' to AgentInfo with backward-compatible default +9/-0

Add 'manageable' to AgentInfo with backward-compatible default

• Extends 'AgentInfo' with a 'manageable: bool' field defaulting to true for older daemons/agents. Documents the meaning for connector-only clients like ChatGPT.

crates/detectord/crates/mcp_detector_daemon/src/protocol.rs

displayMeta.tsAdd per-client configLabel display metadata for config-less clients +17/-1

Add per-client configLabel display metadata for config-less clients

• Extends display metadata with optional 'configLabel' to avoid blank config-path UI for clients with no local config. Adds ChatGPT display metadata including an advisory connectors label.

packages/desktop/src/main/clients/displayMeta.ts

agents.tsPlumb manageable into AgentFacts with safe defaulting +11/-2

Plumb manageable into AgentFacts with safe defaulting

• Adds 'manageable' to 'AgentFacts' and defaults unknown/older-daemon values to true. Updates conversion from daemon 'AgentInfo' accordingly.

packages/desktop/src/main/detectord/agents.ts

protocol.tsExtend desktop-side AgentInfo typing with optional manageable +10/-0

Extend desktop-side AgentInfo typing with optional manageable

• Adds optional 'manageable?: boolean' to match the daemon protocol and documents backward-compat expectations. Enables safe parsing of older daemon responses.

packages/desktop/src/main/detectord/protocol.ts

types.tsAdd chatgpt to McpClientId as detect-only client +4/-0

Add chatgpt to McpClientId as detect-only client

• Introduces 'chatgpt' as a recognized client id with documentation that it will not appear as a discovered server client id. Ensures consistent typing across main/renderer.

packages/desktop/src/main/discovery/types.ts

ipcHandlers.tsExpose manageable and config labels via detectClients IPC +7/-2

Expose manageable and config labels via detectClients IPC

• Extends 'mcp:detectClients' IPC payload to include 'manageable'. Substitutes an advisory config label when the daemon reports a null config path.

packages/desktop/src/main/ipc/ipcHandlers.ts

ipcHandlersMcpSubmit.tsHandle readConfig for unmanageable clients with user-facing explanation +14/-1

Handle readConfig for unmanageable clients with user-facing explanation

• Adds a pre-check using 'getAgentFacts()' to short-circuit config reads for unmanageable clients. Returns explanatory content instead of producing an un-actionable daemon error for connector-only clients.

packages/desktop/src/main/ipc/ipcHandlersMcpSubmit.ts

hookStatus.tsAdd manageable to hook status and exclude unmanageable from scoring +15/-3

Add manageable to hook status and exclude unmanageable from scoring

• Extends hook status entries with 'manageable' and uses it to disable MCP/hook applicability for unmanageable clients. Prevents UI from rendering misleading “not configured” states for connector-only apps.

packages/desktop/src/main/runtime/hookStatus.ts

ClientsView.tsxRender unmanageable clients as 'Not Protected' status bucket +54/-3

Render unmanageable clients as 'Not Protected' status bucket

• Adds 'manageable' to status models and introduces a new 'unmanaged' client status with dedicated UI copy, tooltip, badge, and grouping. Ensures unmanageable clients are visible but not scored against setup conditions.

packages/desktop/src/renderer/src/components/main/ClientsView.tsx

AppsStep.tsxDisable selection for unmanageable clients and show 'Not protected' tag +50/-22

Disable selection for unmanageable clients and show 'Not protected' tag

• Extends detected client model with 'manageable', disables toggling and checkbox rendering when false, and ensures such clients never enter the local selection state. Updates partially-supported grouping to include ChatGPT while keeping capability authority with the daemon.

packages/desktop/src/renderer/src/components/onboarding/AppsStep.tsx

Bug fix (1) +78 / -4
ops.rsEnforce manageability in selections; fix heal counting; add tests +78/-4

Enforce manageability in selections; fix heal counting; add tests

• Adds 'retain_manageable()' and applies it to both 'enroll' and 'apply_integrations' (preserving unknown names). Fixes 'heal_edison_install' to only log/count heals when an actual install write occurred, and adds unit tests for selection filtering behavior.

crates/detectord/crates/mcp_detector_daemon/src/ops.rs

Tests (4) +114 / -1
hookStatus.test.tsUpdate hookStatus test fixtures for manageable field +1/-0

Update hookStatus test fixtures for manageable field

• Extends the 'AgentFacts' factory helper to include 'manageable: true'. Keeps existing hook status tests compatible with the new shape.

packages/desktop/src/main/tests/hookStatus.test.ts

unmanageableClients.test.tsAdd main-process tests for unmanageable client reporting +83/-0

Add main-process tests for unmanageable client reporting

• Adds Vitest coverage asserting that ChatGPT remains visible in hook status but is marked unmanageable and not scored against setup conditions. Verifies backward-compat default behavior when the daemon omits the field.

packages/desktop/src/main/tests/unmanageableClients.test.ts

renderSmoke.test.tsxAdd onboarding test for unmanageable client UI behavior +28/-1

Add onboarding test for unmanageable client UI behavior

• Updates mock detectClients payloads to include manageable and adds a test asserting ChatGPT is shown but not selectable and does not inflate the selected-app count. Guards against regressions where a disabled selection is still counted.

packages/desktop/src/renderer/src/tests/renderSmoke.test.tsx

mockApi.tsUpdate renderer mock API types for manageable field +2/-0

Update renderer mock API types for manageable field

• Extends test mock client type to include 'manageable'. Keeps renderer tests and fixtures aligned with the updated IPC payload.

packages/desktop/src/renderer/src/testing/mockApi.ts

Documentation (2) +9 / -1
README.mdDocument ChatGPT as detected but not managed +2/-0

Document ChatGPT as detected but not managed

• Adds a note explaining that ChatGPT is presence-detected only because its MCP servers are account-hosted Connectors. Clarifies that the wizard flags it for removal rather than claiming protection.

packages/desktop/README.md

integrations.tsClarify daemon-side enforcement of unmanageable client filtering +7/-1

Clarify daemon-side enforcement of unmanageable client filtering

• Updates comments to document that unmanageable clients are not filtered in the app and must be enforced by the daemon due to multiple entry paths (enroll vs apply_integrations).

packages/desktop/src/main/detectord/integrations.ts

Other (5) +95 / -2
Cargo.tomlEnable optional chatgpt agent feature by default +4/-0

Enable optional chatgpt agent feature by default

• Adds a 'chatgpt' cargo feature and includes it in the default feature set. Documents that ChatGPT detection is presence-only with no parser dependencies.

crates/detectord/crates/edison-detectord/Cargo.toml

index.d.tsUpdate preload API typings for detectClients manageable field +1/-1

Update preload API typings for detectClients manageable field

• Extends the exposed preload type signature for 'detectClients()' to include 'manageable'. Keeps renderer typings aligned with IPC payloads.

packages/desktop/src/preload/index.d.ts

index.tsUpdate preload API implementation typing for detectClients +1/-1

Update preload API implementation typing for detectClients

• Updates the TypeScript return type for 'api.mcp.detectClients' to include 'manageable'. No behavioral changes beyond surface typing alignment.

packages/desktop/src/preload/index.ts

ClientsView.stories.tsxAdd storybook scenario with an unmanageable client +70/-0

Add storybook scenario with an unmanageable client

• Introduces a story that includes ChatGPT as 'manageable: false' to demonstrate the permanent client status surface. Ensures design coverage for the 'Not Protected' state.

packages/desktop/src/renderer/src/components/main/ClientsView.stories.tsx

AppsStep.stories.tsxExtend onboarding stories with manageable vs detect-only examples +19/-0

Extend onboarding stories with manageable vs detect-only examples

• Adds 'manageable' to mock client entries and introduces a ChatGPT example rendered as detect-only. Clarifies the distinction between “partially supported” grouping and manageability.

packages/desktop/src/renderer/src/components/onboarding/AppsStep.stories.tsx

@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Unneeded list_agents in readConfig ✓ Resolved 🐞 Bug ➹ Performance
Description
The mcp:readConfig IPC handler now calls getAgentFacts() (which triggers a full list_agents
discovery pass) just to check manageable, before performing the actual readConfig request. This
adds an extra heavy daemon RPC on a UI path that may be invoked repeatedly (e.g., expanding config
previews).
Code

packages/desktop/src/main/ipc/ipcHandlersMcpSubmit.ts[R139-144]

  ipcMain.handle("mcp:readConfig", async (_event, client: string) => {
+    // An unmanageable client has no local config, so asking the daemon for one
+    // only produces an error the user can do nothing about. Say what's actually
+    // going on instead. The daemon is the authority on which those are.
+    const facts = await getAgentFacts();
+    if (facts?.get(client as McpClientId)?.manageable === false) {
Relevance

●●● Strong

Team frequently accepts removing extra/noisy work in desktop main paths (robustness/perf) in PR
#23/#26.

PR-#23
PR-#26

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler explicitly calls getAgentFacts() before readConfig; getAgentFacts() invokes
listAgents(), which in the daemon performs a build/discovery pass that is documented as expensive.

packages/desktop/src/main/ipc/ipcHandlersMcpSubmit.ts[137-156]
packages/desktop/src/main/detectord/agents.ts[72-82]
crates/detectord/crates/mcp_detector_daemon/src/ops.rs[49-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mcp:readConfig` performs an additional `list_agents` request (via `getAgentFacts`) solely to determine whether a client is manageable, then proceeds to call `readConfig` for manageable clients. This adds extra daemon work and latency.

### Issue Context
`getAgentFacts()` calls `daemon.listAgents()`; the daemon notes adapter building/discovery is expensive.

### Fix Focus Areas
- packages/desktop/src/main/ipc/ipcHandlersMcpSubmit.ts[137-156]
- packages/desktop/src/main/detectord/agents.ts[72-82]

### Suggested direction
- Prefer calling `daemon.readConfig(...)` directly and, when it fails with the daemon’s “has no user-scope config” / “unknown agent” error, translate that to the friendly Connectors message for unmanageable clients (avoids the extra `list_agents` call).
- Alternatively, cache agent facts in the main process (short TTL) so `mcp:readConfig` can consult the cache without re-triggering discovery each time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Redundant agent builds ✓ Resolved 🐞 Bug ➹ Performance
Description
retain_manageable() calls agents::build() to compute unmanageable names, re-running expensive agent
discovery and re-emitting any discover-failed warnings each time it filters a selection. This is now
invoked multiple times in apply_integrations() (and enroll), adding avoidable filesystem/config
scanning and log noise to normal flows.
Code

crates/detectord/crates/mcp_detector_daemon/src/ops.rs[R27-30]

+fn retain_manageable(agents: &mut Vec<String>) {
+    let unmanageable: Vec<&'static str> = agents::build()
+        .iter()
+        .filter(|a| !a.is_manageable())
Relevance

●● Moderate

No prior accepted/rejected guidance on caching agents::build(); only related refactors in ops.rs (PR
#24).

PR-#24

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new helper computes unmanageable agents by rebuilding all agents; the daemon explicitly calls
out that building adapters is expensive and agents::build() logs warnings on discover failures.
This work is now performed additionally during apply/enroll selection filtering, including twice per
apply_integrations call.

crates/detectord/crates/mcp_detector_daemon/src/ops.rs[21-40]
crates/detectord/crates/mcp_detector_daemon/src/ops.rs[49-57]
crates/detectord/crates/mcp_detector_daemon/src/agents.rs[11-22]
crates/detectord/crates/mcp_detector_daemon/src/ops.rs[137-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`retain_manageable()` rebuilds the entire agent set (`agents::build()`) just to derive a static-ish list of “unmanageable” agent names. This can be expensive (agent constructors scan configs/dirs) and may duplicate warning logs on every call.

### Issue Context
`apply_integrations()` calls `retain_manageable()` twice in one request (for `wanted` and again for `e.selected_agents`). The daemon already documents that building adapters is not free.

### Fix Focus Areas
- crates/detectord/crates/mcp_detector_daemon/src/ops.rs[21-40]
- crates/detectord/crates/mcp_detector_daemon/src/ops.rs[137-156]
- crates/detectord/crates/mcp_detector_daemon/src/agents.rs[11-23]

### Suggested direction
- Compute the unmanageable-name set once per operation and reuse it (e.g., have `retain_manageable` accept `&[&str]`), **or**
- Cache the computed unmanageable set using `OnceLock`/`LazyLock` so `agents::build()` isn’t re-run on every selection filter.

Keep the “unknown names are preserved” behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. macOS test assumes home dir ✓ Resolved 🐞 Bug ☼ Reliability
Description
The macOS unit test for default_app_paths() unconditionally expects dirs::home_dir() to exist and
asserts exactly two candidates per bundle name. In environments where home_dir() is unavailable,
this test will panic/fail even though production code already handles None gracefully.
Code

crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs[R165-168]

+        assert_eq!(ends_with("ChatGPT.app"), 2);
+        assert_eq!(ends_with("ChatGPT Classic.app"), 2);
+        assert!(paths.iter().any(|p| p.starts_with("/Applications")));
+        let home = dirs::home_dir().expect("a home dir");
Relevance

●● Moderate

No Rust test-history found; team accepted test portability/robustness changes in PR #27.

PR-#27

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Production path generation conditionally includes home-based candidates, but the macOS test requires
a home dir and assumes two candidates unconditionally, making it more brittle than the code it
tests.

crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs[76-91]
crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs[154-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The macOS test uses `dirs::home_dir().expect(...)` and fixed count assertions that assume the home directory is always resolvable. Production code treats `home_dir() == None` as a valid situation.

### Issue Context
`default_app_paths()` already conditionally adds `~/Applications` candidates only when `home_dir()` is `Some`.

### Fix Focus Areas
- crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs[76-91]
- crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs[154-174]

### Suggested direction
- Keep asserting that `/Applications/<bundle>` entries are present.
- Only assert `~/Applications/<bundle>` entries (and the “count == 2” expectations) when `dirs::home_dir()` returns `Some(home)`; otherwise assert the “count == 1” case and skip the home-path assertion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread crates/detectord/crates/mcp_detector_daemon/src/ops.rs Outdated
Comment thread packages/desktop/src/main/ipc/ipcHandlersMcpSubmit.ts Outdated
Comment thread crates/detectord/crates/edison-detectord/src/clients/chatgpt.rs Outdated
Three fixes from review on #30.

`mcp:readConfig` asked `getAgentFacts()` up front to decide whether a
client was manageable, so every successful read paid for a `list_agents`
- a full agent-discovery pass plus a workspace hook scan across the
user's projects. AppsStep re-reads every expanded client on refresh, so
that was one wasted scan per open panel, not one per read. The check now
runs only after a read has already failed, which is the only case it can
change the answer for. Behaviour is identical; the happy path is one RPC
again.

`retain_manageable` derived the unmanageable set by rebuilding every
agent. `is_manageable()` is declared per type and no filesystem state
feeds it, so it cannot change while the process runs - it is now computed
once. `apply_integrations` alone was rebuilding the whole agent set twice
more per request and re-emitting each constructor's "discover failed"
warning along the way.

The macOS path test required a home dir that the code it tests treats as
optional, so it could fail on a state the code handles correctly.

Covers the readConfig handler for the first time: the Connectors message,
the passthrough of a real error, and a guard on the extra discovery call.
That guard fails against the previous ordering.

Copy link
Copy Markdown
Contributor Author

All three review findings fixed in e58a16e.

readConfig extra list_agents — real, and the cost is worse than a single extra RPC. AppsStep re-reads every currently-expanded client on refresh, so the eager check meant one full discovery pass per open panel, each including a workspace hook scan across the user's projects.

Fixed by moving the check after the read rather than by matching on the daemon's error string, which would have coupled this handler to error text the daemon is free to reword. An unmanageable client always fails the read, so the check still runs whenever it can change the answer, and never when it can't. Same output, one RPC on the happy path.

retain_manageable rebuilding the agent set — real. Computed once now, via LazyLock. Worth stating why that's safe rather than just cheap: is_manageable() is declared per agent type and no filesystem state feeds it, so unlike the rest of what agents::build() reports it cannot change while the process runs. The suggested alternative of caching agents::build() wholesale would not be safe, since installing an IDE mid-session must still be picked up.

macOS test assuming a home dir — correct, and the sharper version of the point is that the test asserted more than the code promises, so it could fail on a state the code handles correctly. Now asserts 2 candidates per bundle when home_dir() is Some, 1 when None.

The handler had no test coverage at all before this, which is how the ordering problem got in. Added three: the Connectors message, passthrough of a genuine error, and a guard asserting no discovery pass on a successful read. The guard was mutation-checked — it fails against the previous ordering.

Green locally: 195 desktop tests, 156 Rust tests, clippy and fmt clean.


Generated by Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/desktop/src/main/discovery/types.ts">

<violation number="1" location="packages/desktop/src/main/discovery/types.ts:20">
P2: ChatGPT can now be represented as the client of a discovered MCP server, which conflicts with the stated detect-only contract and can route such an entry into submission. A separate installed/detectable-client type (or a server-client type that excludes `chatgpt`) would keep ChatGPT presence-only at the type boundary.</violation>
</file>

<file name="packages/desktop/src/main/detectord/agents.ts">

<violation number="1" location="packages/desktop/src/main/detectord/agents.ts:61">
P3: The daemon-to-UI `manageable` boundary is not covered by the current tests, so a regression could silently turn ChatGPT into a selectable client or lose the older-daemon fallback. A focused `getAgentFacts`/protocol-mapping test covering both `manageable: false` and an omitted field would protect this compatibility behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// Detect-only: its MCP servers are server-side Connectors, so it never
// appears as the `client` of a discovered server - only in the installed-app
// list, where the wizard flags it as partially supported.
| 'chatgpt'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ChatGPT can now be represented as the client of a discovered MCP server, which conflicts with the stated detect-only contract and can route such an entry into submission. A separate installed/detectable-client type (or a server-client type that excludes chatgpt) would keep ChatGPT presence-only at the type boundary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop/src/main/discovery/types.ts, line 20:

<comment>ChatGPT can now be represented as the client of a discovered MCP server, which conflicts with the stated detect-only contract and can route such an entry into submission. A separate installed/detectable-client type (or a server-client type that excludes `chatgpt`) would keep ChatGPT presence-only at the type boundary.</comment>

<file context>
@@ -14,6 +14,10 @@ export type McpClientId =
+  // Detect-only: its MCP servers are server-side Connectors, so it never
+  // appears as the `client` of a discovered server - only in the installed-app
+  // list, where the wizard flags it as partially supported.
+  | 'chatgpt'
 
 export type McpServerTransport = 'stdio' | 'http' | 'sse'
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fixing this one — leaving the thread open so a human can overrule.

The type is wider than reality, agreed. But the reachability claim doesn't hold: ChatGpt::discover() returns Ok(Vec::new()) unconditionally, and the daemon is the only source of discovered servers, so no entry can carry client: 'chatgpt' to route into submission.

Splitting McpClientId into installed-client and server-client variants would touch every discovery and submission signature to encode a state the daemon cannot produce. And since daemon payloads are parsed rather than compiled, the boundary would still need a cast — which is exactly where the guarantee would be lost, so the refactor would buy less safety than it appears to.

Worth revisiting if ChatGPT ever gains a local config, since that's the change that would make this reachable.


Generated by Claude Code

Comment thread packages/desktop/src/main/detectord/agents.ts
Comment thread packages/desktop/src/renderer/src/components/main/ClientsView.tsx Outdated
Comment thread packages/desktop/src/renderer/src/components/main/ClientsView.stories.tsx Outdated
Three of four findings from cubic on #30.

The "not protected" wording named ChatGPT's reason while being selected
by `manageable`, which is a capability the daemon can set on any client.
The next presence-only client would have been told its servers were
Connectors in an OpenAI account. Specific advice is now keyed by client
id, with a fallback that claims only what the flag itself guarantees.

`toFacts` is where `manageable` crosses from daemon JSON into app types,
including the `?? true` that keeps an older daemon's omitted field from
reading as "unmanageable". Nothing covered it - the existing test asserts
on UNKNOWN_AGENT_FACTS, a different constant. Both directions are now
tested through the real `getAgentFacts`; flipping the default to `false`
fails the second.

The ClientsView story replaced `window.api.mcp.getHookStatus` and never
put it back. Storybook renders a file's stories on one page, so the next
story added here would have inherited these four clients. Swapped in the
initialiser and restored on unmount - reading the previous value during
render would capture this stub as the "original" on a re-render.

Copy link
Copy Markdown
Contributor Author

Three of cubic's four fixed in ffa0dc6. Declining the fourth, with reasoning.

manageable: false shows ChatGPT-specific copy (P3) — fixed, and this was the one worth catching. The copy named ChatGPT's reason while being selected by a generic capability flag, so the next presence-only client would have been told its servers were Connectors in an OpenAI account. Reason text is now keyed by client id with a fallback that claims only what the flag guarantees. Took the point rather than the suggested diff: dropping to generic copy everywhere would lose the Connectors explanation, which is the actionable part for the only client that has this status today.

toFacts mapping untested (P3) — correct, and my existing test was weaker than it looked: it asserts on UNKNOWN_AGENT_FACTS, a different constant, so the ?? true in toFacts had no coverage at all. Now tested in both directions through the real getAgentFacts. Flipping the default to false fails the omitted-field test.

Story leaks its getHookStatus stub (P3) — correct. Fixed, and reviewing my own first attempt caught a second-order version of the same bug: snapshotting the previous value during render captures the stub itself on a re-render. The swap and snapshot now happen in a useState initialiser, restored on unmount.

chatgpt in McpClientId permits it as a discovered server's client (P2) — not fixing. The type is wider than reality, but the reachability claim doesn't hold: ChatGpt::discover() returns Ok(Vec::new()) unconditionally, so the daemon cannot emit a server carrying that client, and it is the only source of discovered servers. Splitting McpClientId into installed-client and server-client variants would touch every discovery and submission signature to encode a state the daemon can't produce — and since daemon payloads are parsed rather than compiled, the boundary would still need a cast, which is exactly where the guarantee would be lost. Happy to revisit if ChatGPT ever gains a local config, since that's the change that would make it reachable.

Green locally: 197 desktop tests, both typecheck projects clean.


Generated by Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/desktop/src/renderer/src/components/main/ClientsView.stories.tsx Outdated
Comment thread packages/desktop/src/renderer/src/components/main/ClientsView.tsx Outdated
Both findings from cubic's second pass, on the fixes from ffa0dc6.

The story installed its stub during render and removed it in an effect.
React repeats renders - StrictMode double-invokes, concurrent renders get
thrown away - and a repeat would capture the stub itself as the value to
restore. Install and restore now both live in the effect, so they cannot
disagree. `useLayoutEffect` because ClientsView fetches in a passive
effect and every layout effect runs before any passive one.

Client ids come from the daemon and were read out of object literals,
which answer inherited keys as though they were entries: a client named
`constructor` took a function where the fallback belongs. Both maps in
this file are now Maps, which have no such keys. `CLIENT_NAMES` was not
flagged but has the same lookup and would have rendered that function as
a display name.

Covers ClientsView for the first time: a known unmanageable client gets
its specific advice, an unknown one gets the generic fallback. The
unknown id in the test is `constructor`, so the test fails against the
object-literal lookup.

Copy link
Copy Markdown
Contributor Author

Both fixed in b9dcc97. Fair catches — these were on my previous round of fixes, not the original PR.

Story mock installed during render (P2) — correct. I'd moved the swap into a useState initialiser specifically to avoid recapturing the stub on re-render, but that solves only the same-render case: React repeats renders, and StrictMode double-invokes initialisers, so a second pass would still snapshot the stub as the value to restore. Install and restore now both live in the effect, where they can't disagree.

Used useLayoutEffect rather than useEffect deliberately: ClientsView fetches in a passive effect, and every layout effect runs before any passive one, so the stub is in place before the component asks. With useEffect the decorator would race the fetch.

Object lookup answering inherited keys (P3) — correct. Fixed at the data structure rather than the call site: both maps are now Maps, which have no inherited keys, so no lookup in this file can hit the problem again.

Extended it to CLIENT_NAMES, which you didn't flag but has the identical CLIENT_NAMES[s.client] ?? s.client lookup on the same daemon-supplied id — that one would have rendered Object.prototype.constructor as a client's display name. It's pre-existing, but I added entries to it in this PR and leaving the file half-hardened seemed worse than the slightly wider diff.

ClientsView had no renderer coverage at all, which is why both rounds of copy changes went unguarded. Added a test: a known unmanageable client gets its specific advice, an unknown one gets the generic fallback. The unknown id in the fixture is literally constructor, so the test fails against the object-literal lookup — verified.

Green locally: 198 desktop tests, both typecheck projects clean.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants