Skip to content

[RFC] Decouple agent-runtime/gateway from the desktop app so KittyLitter works without the Mac #411

Description

@urucoder

Use case

KittyLitter (the mobile app) currently only works while the Local Studio desktop app is
running on the same machine. The Litter bridge gateway — which the phone connects to — is hosted
inside the agent-runtime (a Node/Hono HTTP service that also runs the pi coding agent and
serves /api/agent/*), and that agent-runtime is spawned as a child process of the Electron
desktop app
. When the desktop app is closed, the gateway dies, and the phone loses its
connection.

Users want to keep using the mobile app from anywhere — including when the Mac (or whichever
machine runs the desktop app) is off. That means the gateway must be able to run independently of
the desktop application
, hosted alongside the controller on a machine that stays online.

This is a parent (tracking) issue for the refactor that decouples the runtime layers so the
mobile app no longer depends on the desktop app process — while keeping the desktop app itself
able to reach the gateway and the controller as before.

Current coupling (why the mobile app depends on the desktop app)

The decoupling target is the agent-runtime ↔ Local Studio desktop app relationship. Today the
two are tightly coupled in several ways:

1. Process lifetime

frontend/desktop/logic/agent-runtime-server.ts forks the agent-runtime as a child of the
Electron main process (detached: false) and kills it on app exit (and on stale-launch
timeouts). frontend/desktop/project.mjs and frontend/desktop/logic/app-server.ts do the same.
The runtime only exists while the desktop app runs — which is exactly why the phone cannot work
when the Mac is off.

2. Filesystem/state

The desktop injects LOCAL_STUDIO_DATA_DIR (Electron's userDataDir) when it spawns the runtime.
The runtime reads and writes its durable state there: api-settings.json (controller
backendUrl/apiKey), litter-controller-id, litter-bridge.json (gateway secret + metadata),
projects.json, pi-agent/ models/sessions, connectors, goals, automations and the OAuth vault.
The controller connection and the gateway identity therefore live inside the Mac's user-data
directory
.

3. Controller-URL coupling

The runtime resolves its model backend from settings.backendUrl
(services/agent-runtime/src/pi-runtime-models.ts#mergeControllers → providers
baseUrl: ${controller.url}/v1), and the gateway fetches controller /health, /status, /gpus
and /v1/metrics/vllm from the same setting. The runtime already reaches the configured
controller over HTTP
— this part is already compatible with a remote controller.

4. Dual frontend consumption (the biggest gap)

The Next.js frontend consumes the runtime in two different ways:

  • (a) Proxied over HTTP/api/agent/* routes pass through proxyToAgentRuntime, honoring
    LOCAL_STUDIO_AGENT_RUNTIME_URL. These are already remote-capable.
  • (b) Direct in-process imports — settings, connectors, plugins, projects, skills,
    local-agents, and the /api/proxy/[...path] target import @local-studio/agent-runtime/*
    modules inside the Next.js server process, sharing the same data dir. These are NOT proxied,
    so a remote runtime leaves the desktop unable to manage settings/connectors/plugins/projects
    until they are converted to HTTP calls.

5. Loopback chattiness

The desktop sets LOCAL_STUDIO_FRONTEND_BASE (the runtime calls the frontend for browser/pi
context and OAuth redirects) and LOCAL_STUDIO_AGENT_RUNTIME_URL (the frontend calls the runtime).
google-oauth-loopback.ts binds 127.0.0.1 for the OAuth callback. The two processes share a
data dir, a secret, and loopback URLs.

6. Pairing

frontend/desktop/logic/kittylitter-pairing.ts execs a local kittylitter binary
(kittylitter pair) which reads the runtime's published litter-bridge.json
(loopback http://127.0.0.1:<port>/api/litter-bridge/v1 + secret + controllerId) to produce the
QR payload the phone scans. Both the desktop IPC handler and the
/api/kittylitter/pairing route shell out to this local binary. This is the root of the
KittyLitter is unavailable (ENOENT) error and is inherently same-host.

7. Gateway reachability

server.ts binds hostname: "127.0.0.1" and the gateway metadata URL is hardcoded loopback. The
pairing schema already permits a relay field, but the gateway never sets it — nothing today makes
the gateway reachable from a phone off the local network.

Goal of the refactor: decouple the layers

The refactor should remove the desktop-app dependency from the mobile (KittyLitter) path, while
keeping the desktop app able to reach the gateway and controller as it does today. Concretely:

  1. Promote the agent-runtime to a standalone, deployable service. It already is a discrete
    Node/Hono process (services/agent-runtime/src/server.ts, dist/standalone.mjs) with a health
    endpoint. Treat it as a first-class service that can run on a remote/headless host (systemd /
    container / long-lived process), configured via environment (LOCAL_STUDIO_DATA_DIR,
    BACKEND_URL, API_KEY, port/host bind) instead of Electron-main-injected values.

  2. Decouple the mobile connection from the desktop process. The gateway must be able to run
    remotely and stay alive independently of the desktop app. The phone connects to the gateway
    wherever it lives; nothing in the phone path may require the desktop app to be running.

  3. Move durable identity + controller settings to the runtime/controller side so a remote
    runtime has its own stable controllerId, gateway secret and controller URL — out of the Mac's
    user-data directory.

  4. Close the in-process frontend gap. Convert the settings/connectors/plugins/projects/skills/
    local-agents routes that currently import runtime modules in-process to proxy to the runtime
    over HTTP, completing the "everything through LOCAL_STUDIO_AGENT_RUNTIME_URL" model. This keeps
    the desktop app fully functional while the runtime is remote.

  5. Make pairing remote-capable. Serve the pairing payload from an authenticated gateway/controller
    endpoint (secret→token, controllerId→node_id, optional relay) so a phone can pair without a local
    kittylitter binary reading a loopback file.

  6. Make the gateway reachable off-Mac — configurable bind, TLS/public URL, or tunnel/relay — so
    the phone can reach it over the network, not just 127.0.0.1.

Desired end state

  • Mobile app (KittyLitter) depends on the gateway (wherever it is hosted), not on the
    desktop app process. Works from the phone when the desktop app / Mac is off, as long as the
    gateway host (e.g., the remote controller) is online.
  • Desktop app can still reach the gateway and controller exactly as today (it depends on the
    gateway for the mobile-bridge features), but the two layers are decoupled: the desktop no longer
    owns the runtime's lifecycle, state or pairing.

Acceptance criteria

  • Start the gateway/agent-runtime standalone on a remote host (no desktop app involved) and pair +
    use KittyLitter from a phone against it.
  • Close the desktop app on the machine the runtime is not on and confirm the phone connection
    persists (gateway unaffected).
  • Desktop app still fully functional against a remote runtime: settings, connectors, plugins,
    projects, sessions, model picker, agent turns.
  • Existing local-only flow (desktop + local runtime + local controller) keeps working unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions