feat(console): local control API for programmatic agent management (CHOO-2570) - #362
feat(console): local control API for programmatic agent management (CHOO-2570)#362abeldantas wants to merge 4 commits into
Conversation
…t (CHOO-2570) Localhost-only HTTP server in the Console main process, modeled on the existing hook-server pattern: binds 127.0.0.1 on an OS-assigned port, token-gated via x-switch-control-token header, with credentials written to a 0600 JSON file in the app data dir. Routes expose the same agent-management operations the UI uses: - GET /agents, GET /agents/:id -- list and read agents - GET /agents/:id/sessions -- list sessions for an agent - POST /agents/:id/sessions -- start a session - DELETE /agents/:id/sessions/:sid -- kill a session - POST /agents/:id/sidecar/restart -- restart sidecar - POST /agents/:id/sidecar/stop -- stop sidecar - GET /agents/:id/sidecar/status -- read sidecar status All routes delegate to existing service functions (getAgents, getSession, sessionService, sidecarController) rather than reimplementing behavior.
There was a problem hiding this comment.
🟡 Changes recommended
There are a few correctness/security/robustness issues in the new control API (notably URL decoding crash potential and error-handling/response behavior) that should be addressed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds a localhost-only “control API” HTTP server to the Switch Console main process so local automation can invoke the same agent/session/sidecar management actions currently available via the UI.
Changes:
- Start/stop a new control API service from the Electron main entrypoint lifecycle.
- Implement a minimal token-gated HTTP server with route matching + JSON helpers.
- Add control API routes for listing/fetching agents, creating/tearing-down sessions, and restarting/stopping/querying sidecar status, plus unit tests for the HTTP server.
File summaries
| File | Description |
|---|---|
| console/apps/switch-console-desktop/src/main/index.ts | Wires the control API service into app startup and shutdown. |
| console/apps/switch-console-desktop/src/main/core/control-api/control-service.ts | Defines control API routes and token file management for local automation. |
| console/apps/switch-console-desktop/src/main/core/control-api/control-server.ts | Implements the localhost-only, token-gated HTTP server and basic routing/body helpers. |
| console/apps/switch-console-desktop/src/main/core/control-api/control-server.test.ts | Adds unit tests for auth gating, routing, and method discrimination. |
Review details
Suppressed comments (2)
console/apps/switch-console-desktop/src/main/core/control-api/control-service.ts:137
- This sidecar route returns
String(err)directly to the caller; log the error and return a stable error message instead (to avoid leaking details and to keep responses predictable).
try {
const { sidecarController } = await import('@main/core/sidecar/controller');
const status = await sidecarController.stop(agent.id);
sendJson(res, 200, { status });
} catch (err) {
sendJson(res, 500, { error: String(err) });
}
console/apps/switch-console-desktop/src/main/core/control-api/control-service.ts:152
- This sidecar route returns
String(err)directly to the caller; log the error and return a stable error message instead (to avoid leaking details and to keep responses predictable).
try {
const { sidecarController } = await import('@main/core/sidecar/controller');
const status = await sidecarController.getStatus(agent.id);
sendJson(res, 200, { status });
} catch (err) {
sendJson(res, 500, { error: String(err) });
}
- Files reviewed: 4/4 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const params: Record<string, string> = {}; | ||
| for (let i = 0; i < route.paramNames.length; i++) { | ||
| params[route.paramNames[i]!] = decodeURIComponent(match[i + 1]!); | ||
| } |
| if (req.headers['x-switch-control-token'] !== this.token) { | ||
| this.log.warn('ControlServer: rejected request with invalid token'); | ||
| res.writeHead(403); | ||
| res.end(); | ||
| return; | ||
| } |
| async initialize(): Promise<void> { | ||
| this.registerRoutes(); | ||
| await this.server.start(); | ||
| this.writeTokenFile(); | ||
| log.info('ControlService: initialized', { port: this.server.getPort() }); | ||
| } |
| if (!result.success) { | ||
| sendJson(res, 422, { error: result.error.type }); | ||
| return; | ||
| } |
| try { | ||
| const { sidecarController } = await import('@main/core/sidecar/controller'); | ||
| const status = await sidecarController.restart(agent.id); | ||
| sendJson(res, 200, { status }); | ||
| } catch (err) { | ||
| sendJson(res, 500, { error: String(err) }); | ||
| } |
The DELETE session handler now checks the teardown Result and returns 500 with the error type on failure instead of always responding 200. Adds 17 tests for the control-service route handlers with mocked dependencies: happy-path and not-found for each route family, creation failure, teardown failure, sidecar error propagation, and token-file lifecycle.
Catch decodeURIComponent errors on path parameters and return 400 instead of crashing the main process. Stop the server if the token file write fails during initialize, so there is no orphaned listener with undiscoverable credentials.
Summary
x-switch-control-tokenheader, credentials written to a 0600 file in the app data dir -- same pattern as the existing hook-serverTest plan
pnpm -r run typecheck)pnpm -r run lint)pnpm -r run format:check)