mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 00:26:21 +03:00
Updates agent definitions, learnings, CLAUDE.md, and BUILDING.md so
path references point at the new module files instead of the old
monolithic build.sh.
Agent definitions:
.claude/agents/issue-triage.md — table of per-category
investigation paths now points at scripts/patches/*.sh and
scripts/packaging/*.sh instead of "build.sh (search patch_X)".
.claude/agents/electron-linux-specialist.md — patching-functions
table now includes each function's file location; directory tree
illustration reflects the new scripts/ layout.
Documentation:
CLAUDE.md — "Working with Minified
JavaScript" section points at scripts/patches/*.sh; frame-fix
injection attributed to scripts/patches/app-asar.sh; the
version-bump checks now grep scripts/setup/detect-host.sh.
docs/BUILDING.md — automated version
detection paragraph now mentions scripts/setup/detect-host.sh as
the file that holds the URLs.
docs/learnings/cowork-vm-daemon.md — Patch 6 pointer now
says scripts/patches/cowork.sh; line-number references dropped in
favour of anchor-based search (line numbers drift between releases).
docs/learnings/plugin-install.md — Key Files section
points at scripts/patches/cowork.sh for patch_cowork_linux.
Historical changelog-style references (e.g. docs/cowork-linux-handover.md
describing what was "added to build.sh" during initial cowork work)
are intentionally left unchanged — they describe a point-in-time state
of the codebase.
Co-Authored-By: Claude <claude@anthropic.com>
312 lines
11 KiB
Markdown
312 lines
11 KiB
Markdown
# Plugin Install Flow — Learnings
|
||
|
||
## Why This Exists
|
||
|
||
The Directory → "Anthropic & Partners" tab has a non-obvious
|
||
install flow that caused a structural bug (#396) on older
|
||
versions. Key insight: **the renderer that populates
|
||
`pluginContext.mode` and `pluginContext.pluginSource` is served
|
||
remotely from claude.ai in a BrowserView**, not bundled locally.
|
||
Static source inspection only sees the main-process gate; its
|
||
inputs originate in server-rendered JS outside the asar.
|
||
|
||
## Architecture
|
||
|
||
The main window is `https://claude.ai/task/new` loaded in a
|
||
BrowserView. Only ~288 KB of JS lives locally under
|
||
`.vite/renderer/main_window/assets/`; neither `installPlugin` nor
|
||
`pluginContext` appears there.
|
||
|
||
When the user clicks install on a plugin:
|
||
|
||
1. Remote web UI calls `CustomPlugins.installPlugin(pluginId,
|
||
egressAllowedDomains, pluginContext)` via IPC (preload bridge
|
||
→ main process).
|
||
2. Main-process IPC handler validates `pluginContext` via `Qg()`
|
||
(runtime type check):
|
||
`{ mode: string, workspacePath?, settingsLevel?,
|
||
pluginSource?, marketplaceScope?, telemetryAttempt? }`.
|
||
3. Main-process `installPlugin` applies the gate, optionally
|
||
calls the Anthropic API, and falls back to the `claude` CLI if
|
||
the remote path is skipped or fails.
|
||
|
||
The **values of `mode` and `pluginSource` are decided remotely**
|
||
by claude.ai based on which UI surface called install. The
|
||
desktop app has no control over them; it only enforces the gate.
|
||
|
||
## Install Gate (current, 1.3109.0)
|
||
|
||
Location: `index.js:490853` inside the minified app.asar.
|
||
|
||
```js
|
||
const a = s?.pluginSource === "local"; // user-uploaded .zip
|
||
const c = s?.pluginSource === "remote"; // remote marketplace install
|
||
if (!a && (c || s?.mode === "cowork") && (await A0())) {
|
||
// remote API: /api/organizations/{orgId}/plugins/...
|
||
} else {
|
||
// skip, log reason: "local-sourced" |
|
||
// "not-cowork-not-remote" |
|
||
// "sparkplug-disabled"
|
||
}
|
||
// always falls through to CLI install on failure
|
||
```
|
||
|
||
- `A0()` (`index.js:489947`) = GrowthBook flag `"2340532315"` via
|
||
`isFeatureEnabled()`, cached locally. Server-controlled.
|
||
- On CLI fallback for a non-local marketplace like
|
||
`knowledge-work-plugins`, install fails with
|
||
`Plugin "X" not found in marketplace "knowledge-work-plugins"`.
|
||
|
||
## Plugin Listing Filter
|
||
|
||
Four places in 1.3109.0 gate on `A0()`:
|
||
|
||
| Line | Function | If flag off |
|
||
|---|---|---|
|
||
| 490342 | `syncRemotePlugins` | `{newlyInstalled: []}` |
|
||
| 490355 | `getDownloadedRemotePlugins` | `[]` |
|
||
| 491026 | `listAvailablePlugins` | local plugins only |
|
||
| 491060 | `listRemotePluginsPage` | `{plugins: [], hasMore: false}` |
|
||
|
||
**If `A0()` is false, the Anthropic & Partners tab is empty.**
|
||
Users whose account doesn't have the flag enabled server-side
|
||
never see these plugins at all.
|
||
|
||
## Backend Endpoints
|
||
|
||
All served from `https://claude.ai` (base URL from `Jr()` =
|
||
main-window URL). Main-process `net.fetch` adds identity headers
|
||
via an `onBeforeSendHeaders` interceptor at `index.js:504876`:
|
||
|
||
| Header | Value |
|
||
|---|---|
|
||
| `anthropic-client-platform` | `"desktop_app"` (constant) |
|
||
| `anthropic-client-app` | `"com.anthropic.claudefordesktop"` |
|
||
| `anthropic-client-version` | `app.getVersion()` |
|
||
| `anthropic-client-os-platform` | `process.platform` — `"linux"` / `"darwin"` / `"win32"` |
|
||
| `anthropic-client-os-version` | `process.getSystemVersion()` |
|
||
| `anthropic-desktop-topbar` | `"1"` |
|
||
|
||
Key endpoints:
|
||
|
||
| Purpose | URL | Source line |
|
||
|---|---|---|
|
||
| GrowthBook flags | `GET /api/desktop/features` | 190336 |
|
||
| Default marketplaces (Directory source) | `GET /api/organizations/{orgId}/marketplaces/list-default-marketplaces` | — |
|
||
| Account-attached marketplaces (user-added) | `GET /api/organizations/{orgId}/marketplaces/list-account-marketplaces` | — |
|
||
| Directory feed | `GET /api/organizations/{orgId}/plugins/list-plugins?installation_preference=...` | 246164 |
|
||
| Plugin by-id | `GET /api/organizations/{orgId}/plugins/{id}` | 246212 |
|
||
| Plugin by-name | `GET /api/organizations/{orgId}/plugins/by-name/{name}?marketplace_name=...` | 246221 |
|
||
| Plugin download | `GET /api/organizations/{orgId}/plugins/{id}/download` | 246229 |
|
||
|
||
Auth is via the `sessionKey` cookie. `orgId` is read from the
|
||
`lastActiveOrg` cookie by `an()` at `index.js:191235`. No orgId →
|
||
fetchers return null → install falls back to CLI.
|
||
|
||
## Issue #396 Post-Mortem
|
||
|
||
Filed on Claude Desktop 1.1.7714. That version had:
|
||
|
||
**Install gate** (`index.js:230901` in 1.1.7714):
|
||
```js
|
||
if (!c && (a?.mode) === "cowork" && (await Tg())) {
|
||
// remote API
|
||
}
|
||
// reasons: "local-sourced" | "not-cowork" | "sparkplug-disabled"
|
||
```
|
||
|
||
**Listing filter** (`index.js:231032`):
|
||
```js
|
||
if ((s?.mode) !== "cowork" || !(await Tg())) return o; // local only
|
||
// else merge remote
|
||
```
|
||
|
||
**`listRemotePluginsPage`** (`index.js:231066`):
|
||
```js
|
||
if (!(await Tg())) return { plugins: [], hasMore: !1 };
|
||
// else fetch and return
|
||
```
|
||
|
||
`listRemotePluginsPage` gated only on `Tg()`, not on cowork mode,
|
||
so the Directory **showed** remote plugins whenever the sparkplug
|
||
flag was on. But the install gate required `mode === "cowork"`
|
||
specifically. Users browsing the Directory outside a cowork
|
||
session received `pluginContext` without `mode: "cowork"` from
|
||
the renderer → install gate failed → `reason=not-cowork` → CLI
|
||
fallback → "marketplace not found."
|
||
|
||
Structural bug: plugins visible but uninstallable unless the user
|
||
was actively inside a cowork session.
|
||
|
||
**Fixed upstream in 1.3109.0** via two coordinated Anthropic-side
|
||
changes:
|
||
|
||
1. Install gate relaxed to accept `pluginSource === "remote"` as
|
||
equivalent to `mode === "cowork"`.
|
||
2. claude.ai renderer updated to send `pluginSource: "remote"`
|
||
for installs from the Anthropic & Partners Directory
|
||
regardless of cowork session state.
|
||
|
||
PR #435 proposed a client-side Linux-specific short-circuit
|
||
(`process.platform === "linux" || ...`). Correct strategy for the
|
||
bug as it existed; obsolete after upstream fix. Closed as
|
||
obsolete.
|
||
|
||
## Live Investigation Recipe
|
||
|
||
To debug plugin-flow bugs on a running client:
|
||
|
||
### 1. Enable main-process DevTools
|
||
|
||
```bash
|
||
echo '{"allowDevTools": true}' > ~/.config/Claude/developer_settings.json
|
||
```
|
||
|
||
Then fully quit and relaunch the app. Open the (now visible)
|
||
**Enable Main Process Debugger** menu item (under Help when dev
|
||
tools are enabled) — this starts a Node inspector on
|
||
`127.0.0.1:9229`. Connect via `chrome://inspect` in any Chromium
|
||
browser and click **inspect** on the Node target.
|
||
|
||
Source refs:
|
||
- `allowDevTools` schema: `index.js:299085`
|
||
- `developer_settings.json` path: `index.js:299089`
|
||
- Debugger menu: `index.js:494282`
|
||
|
||
### 2. List webContents
|
||
|
||
```js
|
||
require('electron').webContents.getAllWebContents()
|
||
.map(w => ({ id: w.id, type: w.getType(), url: w.getURL() }))
|
||
```
|
||
|
||
Typically three: the find-in-page overlay, the claude.ai
|
||
BrowserView (id 2), and the main window shell (id 1). The
|
||
claude.ai one is where the plugin directory UI lives; open its
|
||
DevTools separately via `webContents.fromId(n).openDevTools()` to
|
||
inspect the renderer-side code.
|
||
|
||
### 3. Check the cached GrowthBook flag state
|
||
|
||
```js
|
||
(async () => {
|
||
const res = await require('electron').net.fetch(
|
||
'https://claude.ai/api/desktop/features');
|
||
const body = await res.json();
|
||
console.log(body.features['2340532315']);
|
||
})();
|
||
```
|
||
|
||
Expected for users with the force rule:
|
||
`{value: true, source: "force", ruleId: "fr_..."}`. If it's
|
||
`{value: false, source: "defaultValue", ruleId: null}`, the user
|
||
won't see any remote plugins — `listAvailablePlugins` and
|
||
`listRemotePluginsPage` filter them out.
|
||
|
||
### 4. Header-spoofing harness
|
||
|
||
Electron only allows one `onBeforeSendHeaders` listener at a
|
||
time. Registering a test listener replaces the app's injector
|
||
(`index.js:504876`), so the harness re-implements the baseline
|
||
injection and adds a per-test override layer:
|
||
|
||
```js
|
||
const { app, session, net } = require('electron');
|
||
|
||
const APP_HEADERS = {
|
||
'anthropic-client-platform': 'desktop_app',
|
||
'anthropic-client-app': 'com.anthropic.claudefordesktop',
|
||
'anthropic-client-version': app.getVersion(),
|
||
'anthropic-client-os-platform': process.platform,
|
||
'anthropic-client-os-version': process.getSystemVersion(),
|
||
'anthropic-desktop-topbar': '1',
|
||
};
|
||
|
||
globalThis.__testOverrides = {};
|
||
globalThis.__testRemove = new Set();
|
||
|
||
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||
{ urls: ['https://claude.ai/*', 'https://claude.com/*'] },
|
||
(d, cb) => {
|
||
const h = { ...d.requestHeaders, ...APP_HEADERS,
|
||
...globalThis.__testOverrides };
|
||
for (const k of globalThis.__testRemove) delete h[k];
|
||
cb({ requestHeaders: h });
|
||
}
|
||
);
|
||
|
||
async function runTest(label, { set = {}, remove = [] } = {},
|
||
url = 'https://claude.ai/api/desktop/features') {
|
||
globalThis.__testOverrides = set;
|
||
globalThis.__testRemove = new Set(remove);
|
||
const res = await net.fetch(url);
|
||
const ct = res.headers.get('content-type') || '';
|
||
const body = ct.includes('json') ? await res.json()
|
||
: await res.text();
|
||
globalThis.__testOverrides = {};
|
||
globalThis.__testRemove = new Set();
|
||
return { label, status: res.status, body };
|
||
}
|
||
```
|
||
|
||
Example: test whether flag depends on OS claim:
|
||
```js
|
||
(async () => {
|
||
const r = await runTest('darwin', {
|
||
set: { 'anthropic-client-os-platform': 'darwin',
|
||
'anthropic-client-os-version': '15.0' } });
|
||
console.log(r.body.features['2340532315']);
|
||
})();
|
||
```
|
||
|
||
If the flag value changes when you spoof OS, the server is
|
||
platform-gating; if not, the gate lives at a different layer
|
||
(account-scoped rule, tier, cohort, or the remote renderer's
|
||
local JS gating).
|
||
|
||
### 5. Breakpoint on the install gate
|
||
|
||
In main-process DevTools **Sources**: Ctrl+P → `index.js` →
|
||
Ctrl+F → search `installPlugin: attempting remote API install`.
|
||
Click the line number to set a breakpoint. Trigger an install in
|
||
the app. When it breaks, inspect `s` (the pluginContext) and
|
||
evaluate `await A0()` in a watch expression.
|
||
|
||
The companion breakpoint on `installPlugin: skipping remote API
|
||
path` tells you which `reason` the gate chose if it failed.
|
||
|
||
## Getting the Minified Source for Any Shipped Version
|
||
|
||
The repo's releases include `reference-source.tar.gz`
|
||
(~6.5 MB) — beautified asar contents of the exact Claude Desktop
|
||
build that was packaged. Much smaller than the AppImage (~133 MB)
|
||
and sufficient for code diffing between versions.
|
||
|
||
```bash
|
||
gh release download "v1.3.23+claude1.1.7714" \
|
||
-R aaddrick/claude-desktop-debian \
|
||
-p 'reference-source.tar.gz' \
|
||
-D /tmp/old-version --clobber
|
||
tar -xzf /tmp/old-version/reference-source.tar.gz -C /tmp/old-version
|
||
# Compare with current: /tmp/old-version/app-extracted/.vite/build/index.js
|
||
```
|
||
|
||
This is how #396's post-mortem was done — side-by-side comparison
|
||
of `installPlugin` (230901 old vs 490853 current) and
|
||
`listAvailablePlugins` (231032 old vs 491026 current) revealed
|
||
both the structural bug and the upstream fix.
|
||
|
||
## Key Files
|
||
|
||
- [`scripts/patches/cowork.sh`](../../scripts/patches/cowork.sh) —
|
||
`patch_cowork_linux()` applies the cowork patches to the asar.
|
||
Patches 1–10 handle cowork mode infrastructure on Linux.
|
||
- [`scripts/cowork-vm-service.js`](../../scripts/cowork-vm-service.js)
|
||
— Linux cowork VM daemon (separate subsystem, see
|
||
[`cowork-vm-daemon.md`](cowork-vm-daemon.md)).
|
||
- Minified install flow in the running app:
|
||
`app.asar.contents/.vite/build/index.js` around line 490853 on
|
||
1.3109.0 (subject to minifier drift — anchor on the log string
|
||
`[CustomPlugins] installPlugin: attempting remote API install`
|
||
when writing patches).
|