fix(agents): address all PR #2299 code review findings

Blocking fixes:
- B1: Return empty restrictions for unknown/custom agents instead of
  EXPLORATION_AGENT_DENYLIST, allowing custom agents full tool access
- B2: Use Object.create(null) consistently across all 5 agent-loading
  result objects to prevent prototype pollution
- B3: Add code comment documenting custom agent bash access trust model
- B4: Mock getOpenCodeConfigDir in opencode-config-agents-reader tests
  to prevent global config dir leakage

Non-blocking fixes:
- N1: Use resolveAgentDefinitionPaths with project boundary enforcement
  in opencode-config-agents-reader for path containment
- N2: Add session-scoped 30s TTL cache to resolveCallableAgents to
  avoid redundant SDK IPC calls per tool invocation
- N3: Extract shared parseToolsConfig into src/shared/parse-tools-config.ts
  replacing 4 duplicated local implementations
- N4: Add .min(1) to AgentDefinitionPathSchema rejecting empty paths
- N5: Add resolve-agent-definition-paths.test.ts covering tilde expansion,
  relative paths, boundary enforcement, and null containmentDir
- N6: Validate agent mode against allowed values instead of bare type
  assertion in opencode-config-agents-reader
This commit is contained in:
YeonGyu-Kim
2026-04-15 10:41:32 +09:00
parent 4c77045c47
commit e5d3fe96c4
14 changed files with 214 additions and 82 deletions
@@ -12,8 +12,8 @@
* R6: No duplicate agent names in output
* R7: Malformed agent entries (null, missing name, non-string name, whitespace-only) are skipped gracefully
*/
const { describe, test, expect, mock } = require("bun:test")
const { resolveCallableAgents } = require("./agent-resolver")
const { describe, test, expect, mock, beforeEach } = require("bun:test")
const { resolveCallableAgents, clearCallableAgentsCache } = require("./agent-resolver")
const { ALLOWED_AGENTS } = require("./constants")
function createMockClient(agents: Array<Record<string, unknown>>) {
@@ -33,6 +33,10 @@ function createFailingClient(error: Error = new Error("API unavailable")) {
}
describe("resolveCallableAgents", () => {
beforeEach(() => {
clearCallableAgentsCache()
})
describe("#given the SDK returns agents successfully", () => {
describe("#when only built-in agents exist", () => {
test("#then every ALLOWED_AGENT appears in the result", async () => {
+20 -1
View File
@@ -8,20 +8,37 @@ type AgentInfo = {
mode?: "subagent" | "primary" | "all";
};
const callableAgentsCache = new Map<string, { agents: string[]; timestamp: number }>();
const CACHE_TTL_MS = 30_000;
export function clearCallableAgentsCache(): void {
callableAgentsCache.clear();
}
/**
* Resolves the set of callable agent names at execute-time by merging the
* hardcoded `ALLOWED_AGENTS` with any additional agents discovered dynamically
* via `client.app.agents()`. Custom agents loaded from registered agent
* directories appear here alongside built-ins.
*
* Results are cached per session for 30s to avoid redundant SDK IPC calls.
*
* Falls back to `ALLOWED_AGENTS` alone if the dynamic lookup fails.
*
* @param client - The plugin client with access to the agent registry
* @param sessionId - Optional session ID for cache scoping
* @returns Array of lowercase callable agent names (excludes primary-mode agents)
*/
export async function resolveCallableAgents(
client: PluginInput["client"],
sessionId?: string,
): Promise<string[]> {
const cacheKey = sessionId ?? "__default__";
const cached = callableAgentsCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return cached.agents;
}
try {
const agentsResult = await client.app.agents();
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
@@ -33,7 +50,9 @@ export async function resolveCallableAgents(
.map((a) => a.name.trim().toLowerCase());
const merged = new Set([...ALLOWED_AGENTS, ...dynamicAgents]);
return [...merged];
const result = [...merged];
callableAgentsCache.set(cacheKey, { agents: result, timestamp: Date.now() });
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log(
@@ -9,6 +9,7 @@
*/
const { describe, test, expect, mock, beforeEach } = require("bun:test")
const { createCallOmoAgent } = require("./tools")
const { clearCallableAgentsCache } = require("./agent-resolver")
type PluginInput = { client: any; directory: string }
@@ -50,6 +51,7 @@ const toolCtx = {
}
beforeEach(() => {
clearCallableAgentsCache()
reserveSubagentSpawnMock.mockClear()
reserveCommitMock.mockClear()
reserveRollbackMock.mockClear()
+2
View File
@@ -1,5 +1,6 @@
const { beforeEach, describe, test, expect, mock } = require("bun:test")
const { createCallOmoAgent } = require("./tools")
const { clearCallableAgentsCache } = require("./agent-resolver")
type PluginInput = { client: any; directory: string }
type BackgroundManager = {
@@ -72,6 +73,7 @@ const toolCtx = {
}
beforeEach(() => {
clearCallableAgentsCache()
assertCanSpawnMock.mockClear()
reserveSubagentSpawnMock.mockClear()
reserveCommitMock.mockClear()