fix(config): resolve agent/command names from relative paths#28359
Merged
Conversation
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 namedagent(home/home/agent/) — every project agent gets mis-keyed. Instead ofbuild, the key becomes.config/opencode/agents/build. Any code that later looks up an agent by its short name fails.The reporter's environment:
Why it happens
configEntryNameFromPathinpackages/opencode/src/config/entry-name.tssearches the absolute file path for any of these substrings, first-match-wins:For
/home/agent/.config/opencode/agents/build.md:/.opencode/agent/— not present (the path is.config/opencode/, not.opencode/). Skip./.opencode/agents/— not present. Skip./agent/— matches at position 5, inside/home/agent/. Wins./agents/— never tried.indexOfreturns 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 keyteam/.config/opencode/agents/build.Why fixing the helper alone is fragile
I considered the surface-level patches:
lastIndexOfinstead ofindexOf— only fixes the second case. For/home/agent/...there's a single/agent/occurrence, solastIndexOfstill picks position 5.lastIndexOf— handles both reported failures, but silently changes behavior for nested layouts like.opencode/agents/agents/foo.md(today: keyagents/foo; after:foo). The shift would invisibly break any user with that layout who references the agent by its old key fromopencode.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 hasdirright 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— alwaysagent/...oragents/.... The helper now usesstartsWithinstead ofindexOf-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(andcommand.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 atagent//agents/.Why this is safe
path.relative(dir, item)always produces a path rooted atagent/oragents/(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.loadModeunaffected. It passes an empty prefix array and relies on the basename fallback, which still works.Glob.scan({ cwd: dir, absolute: true })returns the scanned absolute path (rooted atdir), not the symlink target, sopath.relative(dir, item)is always clean.command.tshas the identical bug shape).Test plan
packages/opencode/test/config/entry-name.test.ts:agents/build.md,agent/build.md, nestedagents/team/build.md, Windows backslashes, basename fallback.path.relative(dir, item)→ helper), one for the username-agentcase and one for the parent-agents/case. Both fail against the unpatched code with the reporter's exact symptom (e.g..config/opencode/agents/buildinstead ofbuild).bun run typecheckclean.Fixes #25713.