ui/desktop/spike-doc/acp-config-extensions-first-spike.md
Use configured extensions as the first ui/desktop REST-to-ACP migration slice.
This spike is intentionally smaller than chat or sessions. It proves that desktop can talk directly to /acp through goosed during migration, without depending on ACP session ID behavior or chat streaming semantics.
Migrate the read-only configured extensions list behind a feature flag.
Current REST:
GET /config/extensions
Target ACP:
_goose/config/extensions
Do not migrate add/update/delete in this first slice. Those can remain REST until the read path and ACP client plumbing are proven.
/acp._goose/* request dispatch.Desktop REST read path:
ui/desktop/src/components/ConfigContext.tsx
getExtensions as apiGetExtensionsrefreshExtensions() calls apiGetExtensions()apiGetExtensions()Desktop UI consumer:
ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx
extensionsList from useConfig()getExtensions(true) to refreshREST backend:
crates/goose-server/src/routes/config_management.rs
GET /config/extensionsPOST /config/extensionsDELETE /config/extensions/{name}ACP backend:
crates/goose/src/acp/server/extensions.rs
_goose/config/extensions_goose/config/extensions/add_goose/config/extensions/remove_goose/config/extensions/toggleACP client reference:
ui/goose2/src/shared/api/createWebSocketStream.tsui/goose2/src/shared/api/acpConnection.tsui/goose2/src/shared/api/acpApi.tsTreat ui/goose2 as a reference only. Do not share runtime code with it because ui/goose2 is expected to move out of this repo.
REST GET /config/extensions filters hidden extensions:
goose::config::get_all_extensions()
.into_iter()
.filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name()))
ACP _goose/config/extensions currently calls crate::config::extensions::get_all_extensions() and does not apply the same hidden-extension filter.
Recommendation: fix ACP to match REST before enabling this feature flag. The goal is to prove the migration path without introducing UI-visible behavior differences.
Work in small reviewable slices. After each step, stop and review before moving to the next one.
Goal: confirm the exact goosed router composition and ACP transport shape before editing.
Review points:
X-Secret-Key middleware is appliedgoose::acp::transport::create_router causes route collisionsgoose-server to call ACP router codeExpected outcome: a precise patch plan for backend mounting.
Goal: avoid route collisions by exposing only /acp routes for embedding in goosed.
Likely file:
crates/goose/src/acp/transport/mod.rsReason: existing create_router(...) includes /health, /status, and MCP app proxy routes. goosed already owns some of those routes.
Expected outcome: a helper such as create_acp_router(...) or equivalent that only mounts:
/acp POST
/acp GET
/acp DELETE
/acp in goosedGoal: serve REST and ACP from the same goosed process during migration.
Likely files:
crates/goose-server/src/commands/agent.rscrates/goose-server/src/routes/mod.rsExpected shape:
goosed
REST routes protected by X-Secret-Key
/acp protected by ACP token auth
Goal: let renderer connect directly to /acp over WebSocket.
Likely files:
ui/desktop/src/main.tsui/desktop/src/preload.tsExpected renderer-facing value:
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
Goal: create enough client plumbing to initialize ACP and call one custom method.
Suggested files:
ui/desktop/src/acp/createWebSocketStream.ts
ui/desktop/src/acp/acpConnection.ts
ui/desktop/src/acp/acpApi.ts
Reference, not shared dependency:
ui/goose2/src/shared/api/createWebSocketStream.tsui/goose2/src/shared/api/acpConnection.tsui/goose2/src/shared/api/acpApi.tsGoal: call _goose/config/extensions for read-only extension listing when enabled.
Primary integration file:
ui/desktop/src/components/ConfigContext.tsxKeep writes on REST in this slice:
Goal: prove ACP and REST return equivalent visible extension data.
Validation checklist is below.
/acp in goosedAdd the ACP Axum router to goosed agent.
Likely files:
crates/goose-server/src/commands/agent.rscrates/goose-server/src/routes/mod.rsUse existing ACP pieces:
goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}
goose::acp::transport::create_router
Keep REST and ACP as separate route branches.
Watch for route collisions:
/status/mcp-app-proxy/mcp-app-guestThe renderer should connect through direct WebSocket:
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
Do not put /acp behind the REST X-Secret-Key middleware because browser WebSocket cannot set arbitrary headers.
Guardrails:
goosed process.X-Secret-Key./acp?token=... URLs.X-Secret-Key for REST routes during migration._goose/config/extensions parityUpdate ACP _goose/config/extensions to match REST behavior:
The ACP response currently injects a config_key field. Verify the desktop shape expected by ExtensionEntry and normalize on the client if needed.
Suggested files:
ui/desktop/src/acp/createWebSocketStream.ts
ui/desktop/src/acp/acpConnection.ts
ui/desktop/src/acp/acpApi.ts
Base these on ui/goose2, but adapt to Electron.
Differences from ui/goose2:
goosed host.Example URL shape:
const baseUrl = await window.electron.getGoosedHostPort();
const acpUrl = baseUrl.replace(/^http/, 'ws') + '/acp?token=' + encodeURIComponent(token);
If goosed is running HTTPS, this becomes wss://.../acp?....
Add a wrapper such as:
getConfigExtensionsViaAcp(): Promise<ExtensionResponse>
It should call:
_goose/config/extensions
Normalize the response to the current desktop ExtensionResponse shape:
type ExtensionResponse = {
extensions: ExtensionEntry[];
warnings: string[];
};
Suggested name:
acpConfigExtensions
The exact flag mechanism should follow existing desktop feature-flag conventions if present. If there is no suitable framework, use a local env/config gate for the spike.
Primary integration file:
ui/desktop/src/components/ConfigContext.tsxCandidate call sites:
refreshExtensions()Keep these mutation paths on REST for the first spike:
addExtensionremoveExtensiontoggleExtensionAfter REST mutations complete, refresh can use ACP when the flag is enabled.
For the spike, if ACP connection or _goose/config/extensions fails, log the error and fall back to REST.
This fallback is only for the spike. Once a feature area is fully migrated, the matching REST endpoint should be removed.
Compare REST and ACP for the same local config:
enabled valuesbuiltin, stdio, streamable_http, etc.)Do not remove GET /config/extensions after this first read-only spike if writes still use REST sync logic that depends on the endpoint.
Remove the REST extension endpoints only after the full extension config surface has migrated:
GET /config/extensions
POST /config/extensions
DELETE /config/extensions/{name}
Corresponding ACP methods:
_goose/config/extensions
_goose/config/extensions/add
_goose/config/extensions/remove
_goose/config/extensions/toggle
goosed exposes token-authenticated /acp.ui/desktop can open a direct renderer WebSocket to /acp._goose/config/extensions returns REST-equivalent visible extension data.