From 455480cb8bbb62e16beee340b59f427f85de30eb Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 29 Apr 2026 22:08:30 +0200 Subject: [PATCH] Use registry API for agent detection Agent detection was walking ad-hoc registry shapes and could miss agents exposed only through the canonical registry helpers. This makes getAgentDefinition and resolveAgent the preferred lookup path, uses AGENT_IDS/getAgentDefinitions for default scans, and keeps unknown ids as structured unavailable results without executing anything. Constraint: User limited edits to src/agents/detect.js and test/agents-detect.test.js Rejected: Probing unknown ids as shell commands | would execute untrusted command names and hide registry drift Confidence: high Scope-risk: narrow Tested: node --test test/agents-detect.test.js --- src/agents/detect.js | 48 +++++++++++++++++++- test/agents-detect.test.js | 93 +++++++++++++++++++++++++++++++------- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src/agents/detect.js b/src/agents/detect.js index f9dc89b8..5ba57626 100644 --- a/src/agents/detect.js +++ b/src/agents/detect.js @@ -2,6 +2,19 @@ const registry = require('./registry'); const { run } = require('../core/runtime'); function registryEntries() { + if (typeof registry.getAgentDefinitions === 'function') { + const definitions = registry.getAgentDefinitions(); + if (Array.isArray(definitions)) { + return definitions; + } + } + + if (Array.isArray(registry.AGENT_IDS) && typeof registry.getAgentDefinition === 'function') { + return registry.AGENT_IDS + .map((agentId) => registry.getAgentDefinition(agentId)) + .filter((entry) => entry && typeof entry === 'object'); + } + const source = registry.agents || registry.AGENTS || @@ -24,6 +37,20 @@ function registryEntries() { } function findAgent(agentId) { + if (typeof registry.getAgentDefinition === 'function') { + const entry = registry.getAgentDefinition(agentId); + if (entry) return entry; + } + + if (typeof registry.resolveAgent === 'function') { + try { + const entry = registry.resolveAgent(agentId); + if (entry) return entry; + } catch (_error) { + return null; + } + } + if (typeof registry.getAgent === 'function') { const entry = registry.getAgent(agentId); if (entry) return entry; @@ -32,6 +59,21 @@ function findAgent(agentId) { return registryEntries().find((entry) => entry.id === agentId); } +function registryAgentIds() { + if (Array.isArray(registry.AGENT_IDS)) { + return [...registry.AGENT_IDS]; + } + + if (typeof registry.getAgentDefinitions === 'function') { + const definitions = registry.getAgentDefinitions(); + if (Array.isArray(definitions)) { + return definitions.map((entry) => entry && entry.id).filter(Boolean); + } + } + + return registryEntries().map((entry) => entry.id).filter(Boolean); +} + function normalizeDetectCommand(detectCommand) { if (Array.isArray(detectCommand)) { const [cmd, ...args] = detectCommand; @@ -84,7 +126,9 @@ function detectionResult(entry, available, command, error = null) { function detectAgent(agentId) { const entry = findAgent(agentId); if (!entry) { - return detectionResult({ id: agentId, label: agentId }, false, null, `unknown agent: ${agentId}`); + const known = registryAgentIds(); + const suffix = known.length > 0 ? ` (known agents: ${known.join(', ')})` : ''; + return detectionResult({ id: agentId, label: agentId }, false, null, `unknown agent: ${agentId}${suffix}`); } const { cmd, args, command } = normalizeDetectCommand(entry.detectCommand); @@ -101,7 +145,7 @@ function detectAgent(agentId) { } function detectAgents(agentIds) { - const ids = Array.isArray(agentIds) ? agentIds : registryEntries().map((entry) => entry.id); + const ids = Array.isArray(agentIds) ? agentIds : registryAgentIds(); return ids.map((agentId) => detectAgent(agentId)); } diff --git a/test/agents-detect.test.js b/test/agents-detect.test.js index db77b60d..0054e864 100644 --- a/test/agents-detect.test.js +++ b/test/agents-detect.test.js @@ -36,13 +36,14 @@ function withMockedDetection({ registry, run }, fn) { } } -test('detectAgent reports an available registered agent without launching it', () => { +test('detectAgent uses getAgentDefinition for codex registry entries', () => { const calls = []; const registry = { - agents: [ - { id: 'codex', label: 'Codex', detectCommand: ['codex', '--version'] }, - { id: 'claude', label: 'Claude', detectCommand: ['claude', '--version'] }, - ], + getAgentDefinition: (agentId) => ( + agentId === 'codex' + ? { id: 'codex', label: 'Codex', detectCommand: ['codex', '--version'] } + : null + ), }; withMockedDetection( @@ -69,11 +70,41 @@ test('detectAgent reports an available registered agent without launching it', ( ]); }); +test('detectAgent uses resolveAgent for claude registry entries', () => { + const registry = { + getAgentDefinition: () => null, + resolveAgent: (agentId) => { + if (agentId === 'claude') { + return { id: 'claude', label: 'Claude Code', detectCommand: 'claude --version' }; + } + throw new Error(`Unknown agent id: ${agentId}`); + }, + }; + + withMockedDetection( + { + registry, + run: () => ({ status: 0, stdout: 'claude 1.2.3\n', stderr: '' }), + }, + ({ detectAgent }) => { + assert.deepEqual(detectAgent('claude'), { + id: 'claude', + label: 'Claude Code', + available: true, + command: 'claude --version', + error: null, + }); + }, + ); +}); + test('detectAgent reports command failures as unavailable with error text', () => { const registry = { - agents: [ - { id: 'claude', label: 'Claude', detectCommand: { command: 'claude', args: ['--version'] } }, - ], + getAgentDefinition: (agentId) => ( + agentId === 'claude' + ? { id: 'claude', label: 'Claude', detectCommand: { command: 'claude', args: ['--version'] } } + : null + ), }; withMockedDetection( @@ -93,20 +124,24 @@ test('detectAgent reports command failures as unavailable with error text', () = ); }); -test('detectAgents preserves requested order and supports registry maps', () => { +test('detectAgents with no args returns all registry agents', () => { const registry = { - codex: { label: 'Codex', detectCommand: 'codex --version' }, - gemini: { label: 'Gemini', detectCommand: ['gemini', '--version'] }, + AGENT_IDS: ['codex', 'claude', 'gemini'], + getAgentDefinition: (agentId) => ({ + codex: { id: 'codex', label: 'Codex', detectCommand: 'codex --version' }, + claude: { id: 'claude', label: 'Claude', detectCommand: 'claude --version' }, + gemini: { id: 'gemini', label: 'Gemini', detectCommand: 'gemini --version' }, + }[agentId]), }; withMockedDetection( { registry, - run: (cmd) => ({ status: cmd === 'gemini' ? 1 : 0, stdout: '', stderr: '' }), + run: () => ({ status: 0, stdout: '', stderr: '' }), }, ({ detectAgents }) => { - assert.deepEqual(detectAgents(['gemini', 'codex']).map((agent) => agent.id), ['gemini', 'codex']); - assert.deepEqual(detectAgents(['gemini', 'codex']).map((agent) => agent.available), [false, true]); + assert.deepEqual(detectAgents().map((agent) => agent.id), ['codex', 'claude', 'gemini']); + assert.deepEqual(detectAgents().map((agent) => agent.available), [true, true, true]); }, ); }); @@ -144,7 +179,13 @@ test('detectAgent reports unknown agents without running commands', () => { withMockedDetection( { - registry: { agents: [] }, + registry: { + AGENT_IDS: ['codex', 'claude'], + getAgentDefinition: () => null, + resolveAgent: () => { + throw new Error('unknown'); + }, + }, run: () => { callCount += 1; return { status: 0, stdout: '', stderr: '' }; @@ -156,10 +197,30 @@ test('detectAgent reports unknown agents without running commands', () => { label: 'ghost', available: false, command: null, - error: 'unknown agent: ghost', + error: 'unknown agent: ghost (known agents: codex, claude)', }); }, ); assert.equal(callCount, 0); }); + +test('detectAgent reports successful mocked commands as available', () => { + const registry = { + getAgentDefinition: (agentId) => ( + agentId === 'codex' + ? { id: 'codex', label: 'Codex', detectCommand: 'codex --version' } + : null + ), + }; + + withMockedDetection( + { + registry, + run: () => ({ status: 0, stdout: 'codex 1.2.3\n', stderr: '' }), + }, + ({ detectAgent }) => { + assert.equal(detectAgent('codex').available, true); + }, + ); +});