Skip to content

feat(console): local control API for programmatic agent management (CHOO-2570) - #362

Open
abeldantas wants to merge 4 commits into
mainfrom
feat/console-local-control-api
Open

feat(console): local control API for programmatic agent management (CHOO-2570)#362
abeldantas wants to merge 4 commits into
mainfrom
feat/console-local-control-api

Conversation

@abeldantas

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a localhost-only HTTP server in the Console main process so local automation can trigger the same agent-management actions the UI exposes, without clicking through the GUI
  • Binds 127.0.0.1 on an OS-assigned port, token-gated via x-switch-control-token header, credentials written to a 0600 file in the app data dir -- same pattern as the existing hook-server
  • Routes: list/get agents, list/start/kill sessions, restart/stop/status sidecar -- all delegating to existing service functions

Test plan

  • Unit tests cover route dispatch, token auth rejection, path-parameter matching, method discrimination (11 tests)
  • Typecheck passes (pnpm -r run typecheck)
  • Lint clean (pnpm -r run lint)
  • Format clean (pnpm -r run format:check)
  • Full node test suite passes (277 files, 3029 tests)

…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.
@abeldantas
abeldantas requested a review from amaudruz as a code owner September 3, 2026 17:00
Copilot AI lite review requested due to automatic review settings September 3, 2026 17:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +71 to +74
const params: Record<string, string> = {};
for (let i = 0; i < route.paramNames.length; i++) {
params[route.paramNames[i]!] = decodeURIComponent(match[i + 1]!);
}
Comment on lines +56 to +61
if (req.headers['x-switch-control-token'] !== this.token) {
this.log.warn('ControlServer: rejected request with invalid token');
res.writeHead(403);
res.end();
return;
}
Comment on lines +18 to +23
async initialize(): Promise<void> {
this.registerRoutes();
await this.server.start();
this.writeTokenFile();
log.info('ControlService: initialized', { port: this.server.getPort() });
}
Comment on lines +84 to +87
if (!result.success) {
sendJson(res, 422, { error: result.error.type });
return;
}
Comment on lines +116 to +122
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.
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.

2 participants