Skip to content

fix(opencode): prevent indexing the entire home directory#1704

Open
rex-chang wants to merge 7 commits into
getpaseo:mainfrom
rex-chang:fix/opencode-home-indexing
Open

fix(opencode): prevent indexing the entire home directory#1704
rex-chang wants to merge 7 commits into
getpaseo:mainfrom
rex-chang:fix/opencode-home-indexing

Conversation

@rex-chang

@rex-chang rex-chang commented Jun 24, 2026

Copy link
Copy Markdown

Linked issue

None.

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

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 to opencode-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 format
  • npm run typecheck
  • npm run lint
  • npx 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

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • UI changes include screenshots or video for every affected platform (not applicable, no UI changes)
  • Tests added or updated where it made sense

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a regression where Paseo would start the OpenCode shared helper server from the user's home directory, causing OpenCode to treat ~ as a workspace and index the entire home tree. The fix introduces a neutral $PASEO_HOME/opencode-home directory for both the server CWD and global catalog probes, backed by an injectable resolveHomeDir seam for testing.

  • FetchCatalogOptions is refactored from a plain { cwd, force } object into a discriminated union { scope: "global", force } | { scope: "workspace", cwd, force }, eliminating the fragile options.cwd === os.homedir() comparison that was previously flagged.
  • ProviderSnapshotManager now uses an opaque "paseo:global" sentinel key (instead of homedir()) so global catalog state is stored independently from any real workspace path, and the getProviderDiagnostic path is updated to use the global scope without touching the home directory.
  • The mkdir for opencode-home is correctly placed inside the try...finally block in fetchCatalog, ensuring the server acquisition is released even when directory creation fails (verified by a new test).

Confidence Score: 5/5

Safe to merge — the fix correctly redirects both the shared helper server CWD and the global catalog probe away from the user's home directory, and all previously identified acquisition-leak paths have been closed.

The core logic change is focused and correct: the OpenCode server is now spawned from $PASEO_HOME/opencode-home rather than ~, eliminating the workspace-indexing regression. The FetchCatalogOptions discriminated union removes the fragile cwd === homedir() comparison. The fs.mkdir for opencode-home is inside the try...finally block so the server acquisition is released on directory creation failure — a new test directly verifies this path. The snapshot manager's global scope is backed by an opaque sentinel key rather than a real filesystem path, making the separation explicit and type-safe throughout the stack.

No files require special attention.

Important Files Changed

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}"]
Loading
%%{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}"]
Loading

Reviews (8): Last reviewed commit: "fix(opencode): pass semantic global cata..." | Re-trigger Greptile

Comment thread packages/server/src/server/agent/providers/opencode-agent.ts Outdated
Comment thread packages/server/src/server/agent/providers/opencode-agent.ts Outdated
Comment thread packages/server/src/server/agent/providers/opencode/server-manager.ts Outdated
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.
@rex-chang rex-chang force-pushed the fix/opencode-home-indexing branch from d436aec to 98586df Compare June 24, 2026 11:57
rex-chang and others added 4 commits June 24, 2026 20:46
…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.
Comment thread packages/server/src/server/agent/providers/opencode-agent.ts Outdated
Comment on lines +1371 to +1375
const isHomeDirectory = isHomeDirectoryPath(options.cwd);
const directory = isHomeDirectory ? this.resolveHomeDir() : options.cwd;

try {
if (isHomeDirectory) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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 resolveOpenCodeHomeDirresolvePaseoHomeensurePrivateDirectorymkdirSync. 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.

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