Skip to content

fix: thread agent command through model discovery endpoint#1905

Closed
mjaverto wants to merge 2 commits intopaperclipai:masterfrom
mjaverto:fix/opencode-models-command-passthrough
Closed

fix: thread agent command through model discovery endpoint#1905
mjaverto wants to merge 2 commits intopaperclipai:masterfrom
mjaverto:fix/opencode-models-command-passthrough

Conversation

@mjaverto
Copy link
Copy Markdown

@mjaverto mjaverto commented Mar 27, 2026

Summary

  • The GET /adapters/opencode_local/models endpoint called listOpenCodeModels() with no arguments, always falling back to the default opencode command. When the default binary was unavailable (e.g. stale nix store path), it returned [] even though agents had a working custom command in their adapterConfig.
  • The route now accepts an optional agentId query param, looks up the agent's persisted adapterConfig.command, and passes it through — never exposing raw user input to process execution.
  • Added test for the command option passthrough.

Test plan

  • pnpm tsc --noEmit passes
  • pnpm vitest run packages/adapters/opencode-local/src/server/models.test.ts — 4/4 pass
  • Manual: configure an agent with a custom command in adapterConfig, hit GET /adapters/opencode_local/models?agentId=<id>, verify models populate

Fixes #1904

🤖 Generated with Claude Code

…el discovery

The models listing endpoint called listOpenCodeModels() with no arguments,
so it always fell back to the default "opencode" command. When the default
binary was unavailable (e.g. stale nix store path), the endpoint returned []
even though agents had a working custom command in their adapterConfig.

Thread an optional { command } through the call chain. The route accepts an
optional agentId query param and resolves the command from the agent's
persisted adapterConfig — never from raw user input — to avoid command
injection.

Fixes paperclipai#1904

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR threads an agent's persisted adapterConfig.command through the GET /adapters/:type/models route so that agents using a non-default opencode binary get their models populated correctly. The approach — accepting an agentId query param, looking it up in the DB, and extracting the stored command rather than accepting raw user input — is the right design. The plumbing in models.ts, registry.ts, and types.ts is clean and correct.\n\nHowever, the route handler contains a critical bug that renders the entire feature non-functional:\n\n- svc.getById(companyId, agentId) passes the wrong argument. The service's getById(id: string) accepts exactly one parameter. JavaScript silently discards the second argument, so companyId is used as the agent ID in the DB query. The query will never match an agent, command will always be undefined, and the default opencode binary continues to be used — exactly the problem the PR intends to fix.\n- The call should be svc.getById(agentId) with a subsequent agent.companyId === companyId guard (to prevent cross-company command extraction).\n- The PR description is missing the "Thinking Path" format required by CONTRIBUTING.md — please add a brief top-down narrative explaining the problem and the fix.

Confidence Score: 4/5

Not safe to merge as-is — the core feature silently does nothing due to the wrong argument being passed to getById.

One P1 defect: svc.getById(companyId, agentId) passes companyId as the id, agentId is ignored, agent lookup always returns null, command is never extracted. The fix is a one-line change. All other parts of the PR (types, registry, models, test) are correct.

server/src/routes/agents.ts — the getById call on line 673 must be corrected before merging.

Important Files Changed

Filename Overview
server/src/routes/agents.ts Route now reads agentId from query param and looks up adapterConfig.command, but calls svc.getById(companyId, agentId) — passing companyId as the id argument and silently discarding agentId, so the agent is never found and the command is never passed through.
packages/adapters/opencode-local/src/server/models.ts listOpenCodeModels now accepts optional { command? } and forwards it to discoverOpenCodeModelsCached — straightforward and correct.
packages/adapters/opencode-local/src/server/models.test.ts Adds test covering the new command option path; verifies graceful fallback with an invalid binary, which is the expected behavior.
server/src/adapters/registry.ts listAdapterModels now accepts optional opts and forwards them to adapter.listModels — clean and backward-compatible.
packages/adapter-utils/src/types.ts listModels signature updated to accept optional opts; change is backward-compatible since opts is optional.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: server/src/routes/agents.ts
Line: 673-676

Comment:
**Wrong argument passed to `getById` — feature is silently non-functional**

`svc.getById` (defined in `server/src/services/agents.ts` line 241) accepts exactly **one** parameter, `id: string`. Calling it as `svc.getById(companyId, agentId)` passes `companyId` as the `id` argument; JavaScript silently ignores the second argument `agentId`. As a result the DB query runs `WHERE agents.id = <companyId>`, which will never match an agent row. `agent` comes back `null`, `command` is never assigned, and the entire command-passthrough feature silently no-ops — the bug this PR aims to fix is still present.

All other call sites in `agents.ts` pass a single ID: `svc.getById(req.actor.agentId)`, `svc.getById(id)`, etc.

The call should be `svc.getById(agentId)` with an explicit company-ownership guard to prevent cross-company data leakage:

```suggestion
      const agent = await svc.getById(agentId);
      if (agent && agent.companyId === companyId && agent.adapterConfig && typeof agent.adapterConfig.command === "string") {
        command = agent.adapterConfig.command;
      }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: thread command option through listA..." | Re-trigger Greptile

getById takes a single id param — was incorrectly called with (companyId, agentId)
which silently passed companyId as the id, making the feature a no-op.
Added explicit companyId ownership check to prevent cross-company data leakage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mjaverto
Copy link
Copy Markdown
Author

Addressed in f41a8d3: Fixed svc.getById(companyId, agentId)svc.getById(agentId) with explicit agent.companyId === companyId guard. The inline thread is resolved.

Re: CONTRIBUTING.md Thinking Path format — will add if required by maintainers, but keeping the PR description focused on the technical fix for now.

@mjaverto
Copy link
Copy Markdown
Author

Closing in favor of a new PR with the correct approach — passing command directly from the UI form instead of looking up by agentId.

@mjaverto mjaverto closed this Mar 27, 2026
mjaverto added a commit to mjaverto/paperclip that referenced this pull request Mar 27, 2026
The models endpoint previously required an agentId to look up the
configured command binary from the database. This meant users had to
save their agent config before the Model dropdown would work — broken
UX when configuring a new command path.

Now the UI sends the command value directly from the live form state,
so model discovery works immediately as the user types — no save needed.

Security note: we considered the agentId/DB-lookup approach (server-
sourced command) vs direct client parameter. The direct approach was
chosen because:
- local_trusted mode (primary deployment) is single-user on localhost
- The codebase already trusts user-supplied commands in agent config
  save and test-environment endpoints at the same privilege level
- spawn() uses shell:false with hardcoded args ["models"], so the
  attacker controls only the binary path, not arguments
- An attacker would need filesystem write access to place a malicious
  binary, at which point they already have code execution
- Input validation rejects shell metacharacters as defense-in-depth

Additional improvements from code review:
- 800ms debounce on command input to prevent subprocess storm
- staleTime: 60s on all adapter model queries (matches server cache)
- Shell metacharacter validation on server route

Replaces paperclipai#1905

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

GET /adapters/opencode_local/models ignores per-agent command config — returns [] when default opencode binary is unavailable

1 participant