Skip to content

feat(acp): agent catalog UI, profiles, TCP transport, conformance probe (Phase 5) - #64

Draft
BOTOOM wants to merge 6 commits into
devin/1786229141-acp-phase4from
devin/1786229935-acp-phase5
Draft

feat(acp): agent catalog UI, profiles, TCP transport, conformance probe (Phase 5)#64
BOTOOM wants to merge 6 commits into
devin/1786229141-acp-phase4from
devin/1786229935-acp-phase5

Conversation

@BOTOOM

@BOTOOM BOTOOM commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Description

Phase 5, stacked on #63#62#61#60#59. This is the phase that makes "every ACP agent on the market" true in the product rather than only in the backend. The acid test throughout: a registry entry this codebase has never heard of must be findable, installable, configurable and usable through the UI with no code change — asserted by a test that injects a fake registry entry, gives it a profile and launches it.

  • Catalog UI — browse/search the merged catalog from Phase 2 with install/uninstall (installer progress streamed over the Phase 3 gateway) and per-entry install state, auth state and platform availability. An entry with no build for this platform says so instead of offering a button that fails. Reachable from a "Browse ACP agents" entry by the session selector, with per-session agent switching.
  • Profile editor — the Phase 2 variant mechanism, now editable: devin acp --cloud, copilot --acp --port N, a custom non-registry command, per-profile env referencing stored credentials by name only (values never leave the backend), default cwd.
  • TCP transport, deliberately deferred in Phase 2 and now real, with the same lifecycle guarantees as stdio — a socket fails differently, so there's a test running a fixture agent over an ephemeral loopback port asserting connect, full turn, mid-turn peer death surfacing agent_crashed, and clean shutdown.
  • resource_link (R-033, deferred from Phase 4 until there was a workspace surface): referenced files go out with absolute file:// URIs.

Conformance probe (R-017) — per agent it runs, as far as that agent allows: initialize, session/new, a scripted prompt, a slash command, an image block, a permission round-trip, a history probe and cancel, recording protocol version, capabilities, loadSession, advertised commands, auth methods and failures with a real timestamp. An unadvertised method answering -32601 is a result, not an error. The support table in docs/ACP.md is generated from probe results — a hand-maintained matrix is stale the day it's written.

Worth flagging for reviewers, because it nearly shipped as confident misinformation: the first generated table reported Copilot as having neither image support nor history load, contradicting the Phase 0 spike, which measured loadSession: true and promptCapabilities.image: true directly. The cause was the table sourcing request outcomes rather than advertised capabilities — Copilot's image prompt was blocked by Authentication required, and that rendered as "no images". Advertised capability and scripted-probe outcome are now separate, with a third state so that "we couldn't measure this" never renders as "the agent doesn't support it":

Agent Protocol History load capability Advertised commands Image capability Auth methods
acp-sdk-example 1 unsupported unmeasured unmeasured 0
github-copilot-cli 1 supported unmeasured supported 1
claude-acp 1 supported supported supported 0
codex-acp 1 supported unmeasured supported 2
gemini-cli 1 supported unmeasured supported 4
qwen-code 1 supported unmeasured supported 1

These are real runs on a machine with no agent credentials, so rows honestly record what was blocked on auth. A fixture test now pins that an agent advertising image: true/loadSession: true is recorded as such, so a future capability-shape refactor breaks a test instead of quietly downgrading every agent in the docs.

Requirements: R-010..R-018, R-033. Playwright ACs stay unverified — Chromium is installed now, but the harness is blocked on legacy Copilot auth until Phase 8 removes it.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Dependency update

Related Issues

Stacked on #63#62#61#60#59.

Checklist

  • My code compiles without errors (pnpm typecheck)
  • Linter passes (pnpm lint) — scoped Biome clean; repo-wide run is pre-existing-red on unrelated files
  • Tests pass (pnpm test) — backend ACP 53+, extension 10; Playwright blocked on legacy Copilot auth
  • I have added tests for new functionality (if applicable)
  • I have updated documentation (if applicable) — docs/ACP.md is generated, not hand-written
  • My changes follow the project coding conventions

Screenshots (if applicable)

n/a — no UI verification captured while the E2E harness is blocked on legacy Copilot auth.

Link to Devin session: https://app.devin.ai/sessions/bab32da5729e4a95a9cb79f1648f005e
Requested by: @BOTOOM


Open in Devin Review

@BOTOOM BOTOOM self-assigned this Aug 8, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devmentorai-website-cli Ready Ready Preview Aug 10, 2026 2:31pm

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 8 potential issues.

Open in Devin Review

Comment on lines +517 to +521
<AcpProfileEditor
client={acpCatalogClient}
onSaved={handleAcpProfileSelected}
profile={selectedAcpProfile}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Al elegir un perfil guardado y pulsar Guardar se sobrescribe con datos vacíos

El formulario de perfiles se rellena una sola vez al abrirse y no se recarga cuando se elige otro perfil (<AcpProfileEditor profile={selectedAcpProfile}> en apps/extension/src/entrypoints/sidepanel/SidePanel.tsx:517-521), así que al guardar se envían los campos en blanco que el usuario ve y el perfil elegido queda destruido.
Impacto: El usuario pierde el nombre, el comando y la carpeta de trabajo de un perfil ya configurado con un solo clic.

Estado inicial congelado en useState y ruta de guardado

apps/extension/src/components/AcpProfileEditor.tsx:11-18 inicializa todos los estados con useState(profile?...), que solo se evalúa en el primer montaje. En SidePanel.tsx el editor se monta una vez al abrir el modal (inicialmente con profile === undefined, es decir campos vacíos) y luego handleAcpProfileSelected (SidePanel.tsx:350-352) cambia la prop profile sin remontar el componente.

Al pulsar "Save profile", save() (AcpProfileEditor.tsx:19-33) detecta profile definido y llama client.updateProfile(profile.id, input) con name: '', cmd: '', defaultCwd: '', args: [], env: {}. En el backend AcpAgentService.updateProfile (apps/backend/src/acp/catalog/agent-service.ts:133-143) hace merge sin validar defaultCwd absoluto, por lo que persiste el perfil corrupto.

Una solución simple es forzar el remontaje con key={selectedAcpProfile?.id ?? 'new'} o sincronizar el estado con un useEffect sobre profile.

Suggested change
<AcpProfileEditor
client={acpCatalogClient}
onSaved={handleAcpProfileSelected}
profile={selectedAcpProfile}
/>
<AcpProfileEditor
client={acpCatalogClient}
key={selectedAcpProfile?.id ?? 'new'}
onSaved={handleAcpProfileSelected}
profile={selectedAcpProfile}
/>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +39 to +41
...(profile.transport === 'tcp'
? { transport: 'tcp' as const, host: profile.host ?? '127.0.0.1', port: profile.port }
: {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Los perfiles por red que no indican un comando local se rechazan al arrancar

Un perfil personalizado se rechaza cuando no tiene comando (if (!profile.cmd) throw en apps/backend/src/acp/catalog/launch-resolver.ts:28) incluso cuando el nuevo modo por red no lanza ningún programa local, de modo que no se puede conectar a un agente que ya está escuchando.
Impacto: El usuario que configura host y puerto sin comando recibe un error de arranque y nunca llega a conectarse.

Camino TCP no necesita `cmd` pero la resolución lo exige

Con transport: 'tcp', AgentLauncher.launch (apps/backend/src/acp/launcher.ts:127-131) solo usa spec.host/spec.port y jamás usa spec.cmd. Sin embargo AgentLaunchResolver.resolve sigue exigiendo profile.cmd para perfiles custom/sin entrada de catálogo antes de construir el launchSpec con transport: 'tcp'.

El editor de la UI (apps/extension/src/components/AcpProfileEditor.tsx:20-28) envía custom: true, cmd: '' cuando no hay agentId, por lo que un perfil TCP creado desde la interfaz siempre cae en esa rama y falla con agent_launch_failed. El propio test tuvo que añadir un cmd: 'copilot' artificial (apps/backend/tests/acp/catalog-phase2.test.ts:393) para pasar.

Posible arreglo: omitir la validación de cmd cuando profile.transport === 'tcp' y validar en su lugar port.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +54 to +64
onClick={() => {
setBusy(entry.id);
void client.installAgent(entry.id).then((installed) => {
setEntries((current) =>
current.map((candidate) =>
candidate.id === installed.id ? installed : candidate
)
);
setBusy(null);
});
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Si falla la instalación de un agente el botón queda inhabilitado para siempre

La instalación se marca como en curso y solo se limpia si termina bien (.then(...) sin catch en apps/extension/src/components/AcpCatalogView.tsx:56-63), así que un fallo deja el botón bloqueado sin ningún mensaje.
Impacto: Tras un error de instalación el usuario no puede reintentar ni sabe qué pasó; hay que cerrar y reabrir el panel.

Promesa sin manejo de rechazo

setBusy(entry.id) se ejecuta antes de la llamada; setBusy(null) solo está dentro del callback de éxito. Si client.installAgent rechaza (por ejemplo agent_launch_failed o desconexión del WebSocket, ver apps/extension/src/services/acp-client.ts:188-190), el estado busy permanece igual al id de la entrada y el botón sigue disabled. Además la promesa rechazada no se captura, generando un unhandled rejection.

Conviene usar try/finally (o .catch(...).finally(...)) y exponer el error al usuario.

Suggested change
onClick={() => {
setBusy(entry.id);
void client.installAgent(entry.id).then((installed) => {
setEntries((current) =>
current.map((candidate) =>
candidate.id === installed.id ? installed : candidate
)
);
setBusy(null);
});
}}
onClick={() => {
setBusy(entry.id);
void client
.installAgent(entry.id)
.then((installed) => {
setEntries((current) =>
current.map((candidate) =>
candidate.id === installed.id ? installed : candidate
)
);
})
.catch(() => undefined)
.finally(() => setBusy(null));
}}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +83 to +90
<select
aria-label="ACP profile"
className="ml-2 rounded border px-1"
onChange={(event) => {
const profile = profiles.find((candidate) => candidate.id === event.target.value);
if (profile) onProfileSelected?.(profile);
}}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 El primer perfil de la lista aparece elegido pero el botón para usarlo sigue apagado

La lista de perfiles muestra el primero como opción visible sin comunicarlo hacia fuera (falta de valor controlado y de aviso inicial en apps/extension/src/components/AcpCatalogView.tsx:83-90), así que quien no cambie la opción tiene el botón de iniciar sesión deshabilitado.
Impacto: El usuario ve un perfil elegido en pantalla pero no puede usarlo hasta cambiar a otro y volver.

Select no controlado y sin notificación inicial

El <select> no recibe value ni defaultValue y solo llama onProfileSelected en onChange. El navegador muestra la primera <option> por defecto, pero SidePanel mantiene selectedAcpProfile === undefined (apps/extension/src/entrypoints/sidepanel/SidePanel.tsx:44), lo que deja el botón "Use selected profile for ACP session" con disabled={!selectedAcpProfile} (SidePanel.tsx:524-526). Al seleccionar la primera opción no se dispara change, por lo que el estado nunca se rellena.

Una opción es notificar el primer perfil al cargar (en refresh) y/o convertir el select en controlado con un estado local de id.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread scripts/generate-acp-support-table.ts Outdated
import fs from 'node:fs';
import { type AcpProbeReport, renderSupportTable } from '../apps/backend/src/acp/probe.js';

const reportPath = process.argv[2] ?? 'acp-probe-results.json';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 El generador de la tabla de compatibilidad busca los resultados en una ruta inexistente

El generador lee por defecto un archivo en la raíz del repositorio (process.argv[2] ?? 'acp-probe-results.json' en scripts/generate-acp-support-table.ts:4) mientras que los resultados se escriben dentro de la carpeta de documentación, por lo que ejecutarlo sin argumentos falla.
Impacto: Regenerar la tabla de agentes tal como está documentado da un error de archivo no encontrado.

Desajuste con el script de sondeo

scripts/run-acp-probes.ts:83-86 escribe en path.resolve('docs/acp-probe-results.json'), pero el generador espera acp-probe-results.json en el cwd cuando no se pasa argumento; fs.readFileSync lanzará ENOENT.

Suggested change
const reportPath = process.argv[2] ?? 'acp-probe-results.json';
const reportPath = process.argv[2] ?? 'docs/acp-probe-results.json';
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +354 to +359
const handleStartAcpSession = useCallback(async () => {
if (!selectedAcpProfile) return;
await acpCatalogClient.connect();
await acpCatalogClient.createSession(selectedAcpProfile.id, selectedAcpProfile.defaultCwd);
setShowAcpAgents(false);
}, [acpCatalogClient, selectedAcpProfile]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 La sesión ACP creada desde el modal se descarta

handleStartAcpSession crea una sesión contra un AcpClient independiente (acpCatalogClient) y descarta el AcpSessionRecord devuelto; ni useSessions ni useChat (que usa su propia instancia de AcpClient) se enteran. El resultado visible es que el modal se cierra y no cambia nada en la interfaz, aunque en el backend queda una sesión y una conexión de agente vivas (apps/backend/src/acp/gateway.ts:358-405). Además el panel abre un segundo WebSocket permanente con reconexión automática que nunca se cierra.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/backend/src/acp/probe.ts Outdated
Comment on lines +108 to +110
report.checks.cancel = await connection.probeRequest('session/cancel', {
sessionId: session.sessionId,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 session/cancel se sondea como petición cuando el protocolo lo define como notificación

probeRequest('session/cancel', ...) envía una petición JSON-RPC con id; en ACP la cancelación es una notificación (así la envía AgentConnection.cancel, apps/backend/src/acp/connection.ts:271-278). Por eso los resultados guardados muestran -32601 Method not found para agentes que sí soportan cancelar, lo que puede llevar a interpretar mal la tabla generada.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +127 to +131
if (spec.transport === 'tcp') {
if (!spec.port) throw new AcpError('agent_launch_failed', 'TCP profile requires a port');
const socket = net.createConnection({ host: spec.host ?? '127.0.0.1', port: spec.port });
return this.track(new AgentProcess(socket, stderrLimit));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 El endpoint de sondeo de conformidad permite conectarse a hosts y puertos arbitrarios y arrancar comandos sin validación

El nuevo método ui/agents.probe del gateway (apps/backend/src/acp/gateway.ts:347-348) resuelve el perfil y ejecuta runConformanceProbe, que abre la conexión definida por el perfil. Con el nuevo transporte TCP, un perfil creado vía ui/agents.create_profile puede fijar host/port arbitrarios (apps/backend/src/acp/catalog/launch-resolver.ts:39-41, apps/backend/src/acp/launcher.ts:127-131) y el backend abrirá una conexión saliente a ese destino desde la máquina del usuario, incluyendo direcciones internas. Ni host ni port se validan (rango, loopback, formato) en AcpAgentService.createProfile (apps/backend/src/acp/catalog/agent-service.ts:103-115), que solo valida que defaultCwd sea absoluto.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Revisión atendida en 755a955: guardar un perfil seleccionado ya no lo sobrescribe con datos vacíos, los perfiles TCP no exigen comando local, un fallo de instalación libera el estado busy y muestra el error, el selector de perfiles es controlado (el botón de uso queda habilitado), el generador de la tabla de soporte lee docs/acp-probe-results.json, la sesión creada desde el modal se conserva y se selecciona, session/cancel se sondea como notificación (sin id JSON-RPC) y ui/agents.probe deja de permitir sondear destinos TCP arbitrarios.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant