Skip to content

[Architecture] litter-bridge/v1 is live but orphaned: publish a runtime descriptor, docs, and conformance vectors #265

Description

@0xSero

Local Studio ships a complete, signed, versioned mobile bridge that is running on every desktop install and is currently consumed by nothing. The mobile client that was supposed to consume it (0xSero/litter, the KittyLitter phone app) instead SSHes in and guesses filesystem paths.

This issue asks for the small piece that would let the contract actually be depended on. It is not a request to change the protocol.

What exists today

  • shared/agent/litter-bridge.ts — 700 lines, Effect Schema, LITTER_BRIDGE_PROTOCOL_VERSION = 1, onExcessProperty: "error" throughout.
  • services/agent-runtime/src/litter-bridge-gateway.ts — 3,292 lines. Ed25519 per-request signatures over a domain-separated length-prefixed preimage, canonical-JSON body hashing, 60s request lifetime, 30s skew, replay store, opaque device-bound cursors.
  • services/agent-runtime/src/litter-bridge-mutation-ledger.ts — 571 lines, SQLite idempotency ledger with lease/dispatch/settle.
  • test/litter-bridge-gateway.test.ts — 26 tests, including cross-language golden vectors for canonical JSON and the signature preimage.

Verified live on a stock install just now:

$ curl -s -XPOST -H 'content-type: application/json' \
    -H "x-local-studio-litter-bridge-secret: $SECRET" \
    http://127.0.0.1:8081/api/litter-bridge/v1 -d '{}'
{"type":"error","protocolVersion":1,"requestId":"fde0628f-…",
 "error":{"code":"unsupported_version","message":"Gateway request is invalid","retriable":false,…}}

What the consumer actually does

litter's shared Rust layer reads litter-bridge.json — and uses exactly one field from it. From shared/rust-bridge/codex-mobile-client/src/local_studio.rs:

metadata="${agent_dir%/pi-agent}/litter-bridge.json"
pid=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$metadata" 2>/dev/null | head -n 1)
if [ -n "$pid" ] && [ -x "/proc/$pid/exe" ]; then
  program=$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)

url, secret, secretHeader, controllerId, and protocolVersion are all ignored. And because the branch is /proc-gated it never executes on macOS, where the fallback is a hardcoded guess at the Electron bundle layout:

"$app/Contents/Resources/app/frontend/.next/standalone/frontend/node_modules/@earendil-works/pi-coding-agent/dist/cli.js"

That path encodes Next.js standalone output and npm hoisting and the @earendil-works/pi-coding-agent package layout. Any packaging change silently breaks phone access to Local Studio, with no version negotiation and no error that points here.

Requests

1. A documented, machine-readable runtime descriptor.
litter-bridge.json is already the natural discovery point and is already written 0600 with a protocolVersion. Please extend it with the runtime facts the consumer is currently guessing, so path discovery becomes a read instead of a search:

{
  "protocolVersion": 1,
  "url": "http://127.0.0.1:8081/api/litter-bridge/v1",
  "secretHeader": "x-local-studio-litter-bridge-secret",
  "secret": "",
  "controllerId": "",
  "pid": 8389,
  "issuedAt": "2026-07-22T23:59:23.166Z",

  // proposed additions
  "piAgentDir": "/Users/me/.local-studio/pi-agent",
  "piRuntime": {
    "program": "/Applications/Local Studio.app/Contents/MacOS/Local Studio",
    "args": ["/Applications/Local Studio.app/Contents/Resources/.../dist/cli.js"],
    "env": { "ELECTRON_RUN_AS_NODE": "1" }
  }
}

This is information Local Studio already knows for certain at launch and every consumer currently reconstructs by guesswork. alleycat has an independent copy of the same guessing in crates/alleycat/src/local_studio.rs::bundled_pi_runtime().

2. docs/litter-bridge.md. There is currently no prose anywhere: no README section, no ADR, and alleycat appears zero times in this repository. The entire user-facing documentation of the feature is two sentences of UI copy in profile-settings.tsx. Please document the discovery file, the two auth layers, the five served request types, and — importantly — which contract types are defined but not served, because that is not discoverable without reading the router. Today that set is controller_action_request, session_transfer, and the capabilities manifest; the gateway 400s all three.

3. Clarify the intended status of session_transfer.
LitterBridgeSessionAuthoritySchema = ["local-studio", "litter"] plus SessionTransferEnvelope / TransferCursor / TransferAck / ContentHashes describe bidirectional session handoff — start a session in Local Studio, resume it on the phone, and back. This is the single most valuable thing in the contract and it is schema-only. If it is planned, downstream should build toward it; if it is abandoned, it should be deleted so nobody plans around it.

4. Consider publishing the conformance vectors.
A non-TS client must byte-exactly reimplement canonicalLitterBridgeJson and litterBridgeSignaturePreimage to compute bodyHash. The vectors in test/litter-bridge-gateway.test.ts:483-511 are exactly the right fixtures; emitting them as a JSON file under shared/agent/ would let Rust and Swift clients test against them directly. alleycat's crates/local-studio-proto already carries a hand-ported golden vector that will drift silently.

Context

Related work: 0xSero/litter#186. alleycat's Rust half of this protocol (crates/local-studio-proto, ~2,450 lines + 40 passing tests) is currently dead code, so both ends of a working, tested, signed protocol are sitting unused on either side of a gap that is presently bridged by a shell script.

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