fix(opencode): prevent indexing the entire home directory#1704
fix(opencode): prevent indexing the entire home directory#1704rex-chang wants to merge 7 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/agent-sdk-types.ts | Converts FetchCatalogOptions from a plain object to a discriminated union with scope: "global" |
| packages/server/src/server/agent/providers/opencode/paths.ts | New file: resolveOpenCodeHomeDir returns $PASEO_HOME/opencode-home. Calls resolvePaseoHome which has an I/O side effect (mkdirSync for $PASEO_HOME via ensurePrivateDirectory). |
| packages/server/src/server/agent/providers/opencode/server-manager.ts | Server CWD changed from os.homedir() to resolveHomeDir() ($PASEO_HOME/opencode-home), with mkdirSync to ensure the directory exists before spawning. resolveHomeDir is injectable for testing. |
| packages/server/src/server/agent/providers/opencode-agent.ts | fetchCatalog now handles the global scope by resolving to the neutral opencode-home directory; fs.mkdir is correctly placed inside the try block so the server acquisition is always released. |
| packages/server/src/server/agent/provider-snapshot-manager.ts | Introduces GLOBAL_PROVIDER_SNAPSHOT_KEY sentinel, ProviderCatalogScope discriminated type, and resolveProviderSnapshotTarget to route blank-cwd reads to the global key instead of homedir(). |
| packages/server/src/server/session/provider/provider-catalog-session.ts | Uses isGlobalProviderSnapshotKey to map the internal sentinel back to undefined when emitting snapshot updates to clients; resolveCatalogRequestCwd correctly passes undefined for no-cwd requests. |
| packages/server/src/server/agent/providers/acp-agent.ts | Global scope falls back to homedir() for ACP agents (short-lived probes, no persistent server, no indexing risk). Intentional and consistent with Pi agent's approach. |
| packages/server/src/server/agent/providers/opencode-agent.test.ts | New test verifies acquisition is released when opencode-home mkdir fails; existing global-scope test updated to use scope: "global" and assert the neutral home directory. |
| packages/server/src/server/agent/providers/opencode-server-manager.test.ts | New test verifies server is spawned with cwd: opencode-home instead of homedir(). |
| packages/server/src/server/agent/provider-snapshot-manager.test.ts | New tests verify settings refresh passes scope: "global" to providers and that a homedir() workspace read does not reuse the global snapshot key. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["fetchCatalog(options)"] --> B{options.scope}
B -- global --> C["resolveHomeDir()\n-> $PASEO_HOME/opencode-home"]
B -- workspace --> D["options.cwd\n(project directory)"]
C --> E["fs.mkdir(directory, recursive)\n[inside try block]"]
E --> F["createOpenCodeClient({baseUrl, directory})"]
D --> F
F --> G["providerList(directory)"]
G --> H["return catalog"]
E -- throws --> Z["finally: acquisition.release()"]
H --> Z
SM["OpenCodeServerManager\nstartServer()"] --> R["resolveHomeDir()\n-> $PASEO_HOME/opencode-home"]
R --> S["mkdirSync(serverCwd, recursive)"]
S --> T["spawnServerProcess(cwd: serverCwd)"]
PSM["ProviderSnapshotManager"] --> V{input.cwd}
V -- blank/null --> W["GLOBAL_PROVIDER_SNAPSHOT_KEY\n'paseo:global'"]
V -- path --> X["resolveSnapshotCwd(cwd)\nreal workspace key"]
W --> Y["FetchCatalogOptions\n{scope: 'global', force}"]
X --> YY["FetchCatalogOptions\n{scope: 'workspace', cwd, force}"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["fetchCatalog(options)"] --> B{options.scope}
B -- global --> C["resolveHomeDir()\n-> $PASEO_HOME/opencode-home"]
B -- workspace --> D["options.cwd\n(project directory)"]
C --> E["fs.mkdir(directory, recursive)\n[inside try block]"]
E --> F["createOpenCodeClient({baseUrl, directory})"]
D --> F
F --> G["providerList(directory)"]
G --> H["return catalog"]
E -- throws --> Z["finally: acquisition.release()"]
H --> Z
SM["OpenCodeServerManager\nstartServer()"] --> R["resolveHomeDir()\n-> $PASEO_HOME/opencode-home"]
R --> S["mkdirSync(serverCwd, recursive)"]
S --> T["spawnServerProcess(cwd: serverCwd)"]
PSM["ProviderSnapshotManager"] --> V{input.cwd}
V -- blank/null --> W["GLOBAL_PROVIDER_SNAPSHOT_KEY\n'paseo:global'"]
V -- path --> X["resolveSnapshotCwd(cwd)\nreal workspace key"]
W --> Y["FetchCatalogOptions\n{scope: 'global', force}"]
X --> YY["FetchCatalogOptions\n{scope: 'workspace', cwd, force}"]
Reviews (8): Last reviewed commit: "fix(opencode): pass semantic global cata..." | Re-trigger Greptile
Paseo launches opencode serve with cwd=os.homedir() and refreshes the global provider snapshot with directory=/Users/admin. OpenCode treats that as a workspace and starts location services + bigram indexing for the entire home tree, causing ~466% CPU and ~4GB RAM usage. - Use a neutral scratch directory as the opencode serve cwd. - Use a separate scratch directory for global provider catalog refresh so model/mode discovery no longer triggers home directory indexing. Fixes high CPU/RAM when Paseo starts opencode with no explicit project.
d436aec to
98586df
Compare
…og refresh Switch the home-directory check in fetchCatalog from a string-based path.resolve() comparison to createRealpathAwarePathMatcher, so we catch macOS /private/var/... aliases, symlinks, trailing separators, and Windows casing — consistent with the rest of opencode-agent.ts. Also: - Hoist the matcher to module scope so each fetchCatalog call doesn't rebuild it (the matcher runs realpathSync twice on construction). - Log a debug line when we rewrite the cwd to the scratch path, so it's easy to diagnose missing per-directory config in catalog scope. - Update opencode-agent.test.ts to expect the scratch directory when cwd === os.homedir(), with a comment pointing to the rationale.
| const isHomeDirectory = isHomeDirectoryPath(options.cwd); | ||
| const directory = isHomeDirectory ? this.resolveHomeDir() : options.cwd; | ||
|
|
||
| try { | ||
| if (isHomeDirectory) { |
There was a problem hiding this comment.
Acquisition still leaks if
resolveHomeDir() itself throws
this.resolveHomeDir() on line 1372 is evaluated before the try block that holds the finally { acquisition.release() } guard. In production resolveHomeDir delegates to resolveOpenCodeHomeDir → resolvePaseoHome → ensurePrivateDirectory → mkdirSync. If that synchronous mkdir throws (e.g. ~/.paseo exists as a file, or permission denied), the exception escapes before try is entered, release() is never called, and the retired server's refCount stays at 1 forever — preventing cleanupRetiredServers from ever reaping it.
The PR correctly moved fs.mkdir(directory, …) inside the try block to fix the same class of bug, but resolveHomeDir() carries the same risk and still sits outside it. Moving the resolveHomeDir() call (and the isHomeDirectory branch) inside the try block would close the gap.
Linked issue
None.
Type of change
What does this PR do
Paseo no longer starts OpenCode's shared helper server from the user's home directory when there is no explicit project workspace. The helper server uses
$PASEO_HOME/opencode-home, a neutral OpenCode workspace, so OpenCode does not treat the real home directory as a project and index the whole tree.Provider catalog refresh now carries an explicit semantic scope: global refreshes pass
{ scope: "global" }, while project refreshes pass{ scope: "workspace", cwd }. OpenCode maps global catalog refreshes toopencode-home; providers that still need the old behavior map global to their own default themselves. Global snapshots also use a separate internal key, so an explicit home-directory workspace does not reuse the global catalog cache.The catalog-refresh directory creation is also covered by the server acquisition
finally, so a filesystem error cannot leak an acquired OpenCode server reference.How did you verify it
npm run formatnpm run typechecknpm run lintnpx vitest run packages/server/src/server/agent/provider-snapshot-manager.test.ts packages/server/src/server/agent/providers/opencode-agent.test.ts packages/server/src/server/agent/providers/opencode-server-manager.test.ts packages/server/src/server/session/provider/provider-catalog-session.test.ts packages/server/src/server/session.test.ts --bail=1(224 tests passed)Checklist
npm run typecheckpassesnpm run lintpassesnpm run formatran (Biome)