Skip to content

fix(config): resolve agent/command names from relative paths#28359

Merged
kitlangton merged 3 commits into
devfrom
worktree-fix-entry-name-shorter-pattern
May 19, 2026
Merged

fix(config): resolve agent/command names from relative paths#28359
kitlangton merged 3 commits into
devfrom
worktree-fix-entry-name-shorter-pattern

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

The bug (#25713)

When opencode runs in any environment whose home or parent directory contains the segment /agent/ or /agents/ — most commonly a Docker container running as a user literally named agent (home /home/agent/) — every project agent gets mis-keyed. Instead of build, the key becomes .config/opencode/agents/build. Any code that later looks up an agent by its short name fails.

The reporter's environment:

file path: /home/agent/.config/opencode/agents/build.md
expected agent key: "build"
actual agent key:   ".config/opencode/agents/build"

Why it happens

configEntryNameFromPath in packages/opencode/src/config/entry-name.ts searches the absolute file path for any of these substrings, first-match-wins:

const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]

For /home/agent/.config/opencode/agents/build.md:

  1. /.opencode/agent/ — not present (the path is .config/opencode/, not .opencode/). Skip.
  2. /.opencode/agents/ — not present. Skip.
  3. /agent/matches at position 5, inside /home/agent/. Wins.
  4. /agents/ — never tried.

indexOf returns the leftmost occurrence, so the slice keeps everything from /home/agent/ onward, leaking .config/opencode/agents/ into the key.

A second, related failure surfaces in any path with /agents/ as a parent segment (e.g. /srv/agents/team/.config/opencode/agents/build.md): the first /agents/ wins over the real one further right, producing the key team/.config/opencode/agents/build.

Why fixing the helper alone is fragile

I considered the surface-level patches:

  • lastIndexOf instead of indexOf — only fixes the second case. For /home/agent/... there's a single /agent/ occurrence, so lastIndexOf still picks position 5.
  • Sort patterns longest-first + lastIndexOf — handles both reported failures, but silently changes behavior for nested layouts like .opencode/agents/agents/foo.md (today: key agents/foo; after: foo). The shift would invisibly break any user with that layout who references the agent by its old key from opencode.json. Narrow edge case, but a real one.

Both options also leave the root cause in place: the helper is trying to reverse-engineer "where does the entry's directory start?" from a flat absolute path, when the caller already knows exactly where it starts — every call site does Glob.scan({ cwd: dir, absolute: true }) and has dir right there.

The fix

Anchor the prefix match at the caller by passing path.relative(dir, item) to the helper. The relative path of a globbed agent file is — by definition of the glob {agent,agents}/**/*.md — always agent/... or agents/.... The helper now uses startsWith instead of indexOf-anywhere.

packages/opencode/src/config/entry-name.ts

```ts
function stripPrefix(relativePath: string, prefixes: string[]) {
const normalized = relativePath.replaceAll("\\", "/")
for (const prefix of prefixes) {
if (normalized.startsWith(prefix)) return normalized.slice(prefix.length)
}
}
```

packages/opencode/src/config/agent.ts (and command.ts)

```ts
const name = configEntryNameFromPath(path.relative(dir, item), ["agent/", "agents/"])
```

Patterns collapse from four to two because the .opencode/-prefixed variants are no longer needed — the relative path is already rooted at agent//agents/.

Why this is safe

  • No behavior change for any existing user. path.relative(dir, item) always produces a path rooted at agent/ or agents/ (the glob enforces it), so the new prefix match yields exactly the same key as the old code did for paths the old code resolved correctly. The only inputs whose result changes are the ones that were already buggy.
  • Bug class structurally eliminated. The new helper can no longer be tricked by a parent segment, because it never sees one. There is no "earlier occurrence" to match against — the input begins at the prefix.
  • loadMode unaffected. It passes an empty prefix array and relies on the basename fallback, which still works.
  • Symlinks safe. Glob.scan({ cwd: dir, absolute: true }) returns the scanned absolute path (rooted at dir), not the symlink target, so path.relative(dir, item) is always clean.
  • Same fix lands for commands (Agent name resolution broken when username contains "agent" #25713 is filed for agents but command.ts has the identical bug shape).

Test plan

  • New file packages/opencode/test/config/entry-name.test.ts:
    • Happy-path: agents/build.md, agent/build.md, nested agents/team/build.md, Windows backslashes, basename fallback.
    • Two Agent name resolution broken when username contains "agent" #25713 regression cases that simulate the exact caller flow (path.relative(dir, item) → helper), one for the username-agent case and one for the parent-agents/ case. Both fail against the unpatched code with the reporter's exact symptom (e.g. .config/opencode/agents/build instead of build).
  • All 173 existing config tests still pass.
  • bun run typecheck clean.

Fixes #25713.

Anchor the directory-prefix match at the caller so a parent segment
containing `agent` or `agents` (e.g. a `/home/agent/` home dir, or
`/srv/agents/team/`) can no longer hijack the substring search and leak
intervening path components into the entry's key.

`configEntryNameFromPath` now expects a path already relative to the
directory the caller scanned (i.e. `path.relative(dir, item)`), and
matches prefixes with `startsWith` instead of `indexOf`-anywhere.
The relative path of a globbed agent file is by definition rooted at
`agent/` or `agents/`, so the bug class is structurally eliminated.
`loadMode` was still calling `configEntryNameFromPath(item, [])` with an
absolute path, which only worked via the basename fallback — violating
the helper's new contract that callers pass `path.relative(dir, item)`.
Update it to match the agent/command loaders so the call site can't
silently drift if the helper changes.

Behavior is identical (the mode glob is single-level so basename and
prefix-strip both yield the bare name), but the contract is now uniform.
@kitlangton kitlangton enabled auto-merge (squash) May 19, 2026 15:35
…dows

`path.relative` on Windows produces backslashes (`agents\build.md`) for
paths without a drive prefix, breaking the `expect(relative).toBe("agents/build.md")`
sanity assertion in the #25713 regression test even though the actual
helper-output assertion would still pass (the helper normalizes via
`replaceAll("\\\\", "/")`).

Switch the test to `posix.relative` so the intermediate value is
deterministic across CI runners. Production code is unaffected — the
runtime callers (`agent.ts`, `command.ts`) still use `path.relative` on
the host platform, which the helper continues to normalize internally.
@kitlangton kitlangton merged commit e94d46a into dev May 19, 2026
10 checks passed
@kitlangton kitlangton deleted the worktree-fix-entry-name-shorter-pattern branch May 19, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent name resolution broken when username contains "agent"

1 participant