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:
@@ -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 () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user