# Lorca Documentation

> Lorca is a Grok bot alternative: a Rust CLI for macOS, Linux, and Windows, a native macOS AppKit client, and phone apps. The CLI is a localhost websocket service and the agent loop; the AppKit client is a UI for that...

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6)
- [GitHub repository](https://github.com/egoist/lorca)

## Repository Metadata

- Repository: egoist/lorca

- Generated: 2026-09-26T20:21:57.938Z
- Updated: 2026-09-26T20:38:30.205Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 18

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/01-overview.md) - What the project exposes, its primary entry points, runtime assumptions, and links to the highest-value pages.
- 02. [Installation](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/02-installation.md) - Prerequisites, install command, first run, the expected success signal, and one recovery note.
- 03. [Quickstart](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/03-quickstart.md) - The shortest source-backed path from install to a first successful invocation, with the expected output.
- 04. [Architecture](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/04-architecture.md) - The system's main components, boundaries, and data flow as the repository itself documents them.
- 05. [Configuration reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/05-configuration-reference.md) - Config keys, environment variables, defaults, required values, and where each one is read.
- 06. [CLI reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/06-cli-reference.md) - Commands, flags, arguments, defaults, exit behavior, and examples for the command-line interface.
- 07. [HTTP API reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/07-http-api-reference.md) - Routes, methods, request and response shapes, authentication, and error responses.
- 08. [crates/relay reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/08-crates-relay-reference.md) - Reference for the code under `crates/relay`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 09. [crates/agent reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/09-crates-agent-reference.md) - Reference for the code under `crates/agent`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 10. [crates/cli reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/10-crates-cli-reference.md) - Reference for the code under `crates/cli`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 11. [mobile reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/11-mobile-reference.md) - Reference for the code under `mobile`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 12. [Data model reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/12-data-model-reference.md) - Schemas, entities, fields, relationships, migrations, and validation rules.
- 13. [macos reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/13-macos-reference.md) - Reference for the code under `macos`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 14. [web reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/14-web-reference.md) - Reference for the code under `web`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 15. [crates/provider-auth reference](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/15-crates-provider-auth-reference.md) - Reference for the code under `crates/provider-auth`: what it exports, how it is invoked, its options and defaults, and its error cases.
- 16. [Deployment and operations](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/16-deployment-and-operations.md) - Build, containerize, deploy, configure runtime environments, and observe the running system.
- 17. [Testing](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/17-testing.md) - How to run the test suites, what they cover, fixtures, and how continuous integration executes them.
- 18. [Changelog and migration](https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/18-changelog-and-migration.md) - Version history, breaking changes, and upgrade steps between versions.

## Source File Index

- `.env.example`
- `.github/workflows/release-cli.yml`
- `ARCHITECTURE.md`
- `Cargo.toml`
- `CHANGELOG.md`
- `crates/agent/src/lib.rs`
- `crates/agent/src/types.rs`
- `crates/cli/src/api.rs`
- `crates/cli/src/config.rs`
- `crates/cli/src/lib.rs`
- `crates/cli/src/main.rs`
- `crates/cli/src/model.rs`
- `crates/cli/src/provider_auth.rs`
- `crates/cli/tests/provider_settings.rs`
- `crates/provider-auth/src/lib.rs`
- `crates/relay/Dockerfile`
- `crates/relay/src/db.rs`
- `crates/relay/src/main.rs`
- `crates/relay/src/routes.rs`
- `docs/agent/harness.md`
- `docs/agent/providers.md`
- `docs/releasing-cli.md`
- `docs/releasing-mac.md`
- `macos/Package.swift`
- `macos/Sources/Lorca/App/AppDelegate.swift`
- `macos/Sources/Lorca/App/CLILauncher.swift`
- `macos/Sources/Lorca/main.swift`
- `macos/Sources/Lorca/Model/Protocol.swift`
- `mobile/index.ts`
- `mobile/modules/lorca-core/index.ts`
- `package.json`
- `README.md`
- `web/content/docs/cli.mdx`
- `web/content/docs/get-started.mdx`
- `web/content/docs/index.mdx`
- `web/src/router.tsx`
- `web/src/routeTree.gen.ts`

---

## 01. Overview

> What the project exposes, its primary entry points, runtime assumptions, and links to the highest-value pages.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/01-overview.md
- Generated: 2026-09-26T20:14:10.314Z

### Source Files

- `web/content/docs/index.mdx`
- `README.md`
- `ARCHITECTURE.md`

---
title: "Overview"
description: "What the project exposes, its primary entry points, runtime assumptions, and links to the highest-value pages."
---

Lorca ships three runnable surfaces: a Rust `lorca` CLI (localhost websocket + agent loop), a native macOS AppKit client that launches the bundled CLI, and an Expo phone app that pairs as a Device and speaks the relay protocol directly. Bots run on **Runners** you own (`macos`, `linux`, `windows`); phones and tablets (`ios`, `ipados`, `android`) are Devices only.

<Info>
Production installs use `app.lorca` and `~/.lorca` on port `4862`. Dev installs use `app.lorca.dev`, `LORCA_HOME=~/.lorca-dev`, and port `4863`.
</Info>

## What you get

| Surface | Path | Role |
| --- | --- | --- |
| macOS app | `macos/` | AppKit UI; speaks only to the local CLI over `127.0.0.1` websocket |
| CLI | `crates/cli` | Identity, pairing, E2E crypto, credentials, sync, `lorca serve` |
| Agent loop | `crates/agent` | Inference, tools, streaming, cancellation, bot orchestration |
| Relay | `crates/relay` | Store-and-forward API (axum + SQLite or Postgres); opaque blobs + public keys |
| Phone app | `mobile/` | Expo (iOS/Android); pairs as a Device; never a Runner |
| Website | `web/` | TanStack Start landing + MDX docs |

Persistent named bots, 1:1 chats, group chats (1–6 bots), handoff, and orchestration. Identity is a local key pair; Devices pair; traffic to the network is end-to-end encrypted.

## Runtime shape

```mermaid
flowchart TB
  subgraph ui ["UI"]
    App["Lorca.app / AppKit\nmacos/"]
    Mobile["Phone app\nmobile/"]
  end
  subgraph local ["Local runtime"]
    CLI["lorca CLI\ncrates/cli + crates/agent\nkeys + agent loop"]
  end
  subgraph net ["Network"]
    Relay["Relay\ncrates/relay\npublic keys + ciphertext"]
  end
  App -->|"localhost websocket\n127.0.0.1"| CLI
  CLI -->|"HTTPS\nsigned requests + blobs"| Relay
  Mobile -->|"relay protocol\n(Device core)"| Relay
```

Hard constraints from the architecture:

1. The app talks **only** to the local CLI; the CLI holds keys, talks to the relay, and talks to models.
2. The CLI owns the agent loop (inference, tools, streaming, cancellation, orchestration).
3. The relay is zero-knowledge: opaque blobs and public keys; auth is a signature challenge.
4. A bot runs on **one** Runner (that Device’s CLI).
5. Provider credentials belong to the account and sync as an encrypted `credentials` blob to every paired Device.

## Primary entry points

### Dev commands

```bash
bun run dev          # build CLI + app, launch app, rebuild on change; relay on 0.0.0.0:8787
bun run mobile       # Expo Metro for the phone app
bun run mobile:ios   # build/run iOS simulator (Xcode)
bun run relay        # local relay on 0.0.0.0:8787 (point LORCA_RELAY_URL at it)
cargo test           # agent loop, crypto, and SSE tests
bun run reset        # stop everything; wipe identity, credentials, chats, prefs
bun run web          # docs/landing at http://localhost:3000
```

### CLI as local service

The macOS app ships the CLI binary, starts `lorca serve` unless one already answers on the port, and restarts it if it exits. If the CLI is down, the app shows a native empty state with launcher status and the manual `lorca serve` command.

| Build | Home | Listen port |
| --- | --- | --- |
| Production (`Lorca` / `app.lorca`) | `~/.lorca` | `4862` |
| Development (`Lorca Dev` / `app.lorca.dev`) | `~/.lorca-dev` via `LORCA_HOME` | `4863` |

Self-host the relay anywhere; clients set `LORCA_RELAY_URL`. Pairing also accepts `lorca pair <string>` (or QR from the desktop app on phones).

## Devices vs Runners

Every Device records `os` in its machine metadata. Role is derived from `os` alone—no opt-in flag.

| `os` | Role | Can |
| --- | --- | --- |
| `macos`, `linux`, `windows` | **Runner** | Hold keys, sync, and run assigned bots/Jobs with account credentials |
| `ios`, `ipados`, `android` | Device | Hold keys, configure providers, read/write chats, create bots for Runners, pair Devices |

Peers learn Runner status from the decrypted metadata blob; the relay never learns which Devices are Runners.

## Identity and sync (short)

| Layer | Local | On the relay |
| --- | --- | --- |
| Master secret (32 bytes) | `~/.lorca/identity.json` (0600); backup as base32 phrase | — |
| Content / signing keys | Derived via HKDF from master | Public keys; identity id = `hash(pubkey)` |
| Account DEK | Encrypts roster, chats, messages, machine metadata, credentials | Sealed `key` blob to content pubkey |
| Machine keypair | `~/.lorca/machine.json` | Public keys, attested by the identity |

Recovery: restore the master secret from the backup phrase → re-derive content keys → unwrap DEKs from the relay. Unpairing (`device.unpair` / `identity.forget`) hits `DELETE /v1/machines/{machine_pubkey}`; revoked keys never authenticate again (`410 Gone`).

## Domain model (plaintext on Devices)

```
Identity 1──* Device
Device   1──* Bot          (Runner only)
Identity 1──* Chat
Chat     *──* Bot          (dm: exactly 1 · group: 1–6)
Chat     1──* Message
Bot      1──* Routine
Device   1──* Plugin       (MCP on a Runner)
Bot      1──* Job          (turn on the bot's Runner)
```

A new account ships with **Chef** (chief of staff)—ordinary bot, renameable. Jobs are sealed envelopes to the Runner’s box key and deleted once run.

## AI providers

| Kind | Providers |
| --- | --- |
| API key | DeepSeek, Anthropic, OpenCode Zen, OpenCode Go |
| Subscription | ChatGPT, Grok (SuperGrok or X Premium+) |

Credentials connect once on any Device and reach every paired Device as the encrypted `credentials` blob. A bot runs with them on whichever Runner it is assigned to.

## License

GPL-3.0.

## Next

<CardGroup>
  <Card title="Installation" href="/installation">Prerequisites, install command, first run, and recovery.</Card>
  <Card title="Quickstart" href="/quickstart">Shortest path from install to a first successful invocation.</Card>
  <Card title="Architecture" href="/architecture">Processes, identity layers, pairing, and data flow.</Card>
  <Card title="CLI reference" href="/cli-reference">Commands, flags, and `lorca serve` behavior.</Card>
  <Card title="crates/relay" href="/ref-crates-relay">Relay API, storage backends, and zero-knowledge surface.</Card>
</CardGroup>

Next: open [Installation](/installation) and run `bun run dev` (about one command; relay comes up on `0.0.0.0:8787`).

---

## 02. Installation

> Prerequisites, install command, first run, the expected success signal, and one recovery note.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/02-installation.md
- Generated: 2026-09-26T20:13:55.772Z

### Source Files

- `web/content/docs/get-started.mdx`
- `README.md`
- `web/content/docs/cli.mdx`

---
title: "Installation"
description: "Prerequisites, install command, first run, the expected success signal, and one recovery note."
---

Install Lorca as the macOS desktop app (bundles the CLI at `Lorca.app/Contents/Resources/bin/lorca`) or as the standalone `lorca` CLI on macOS, Linux, or Windows. The CLI holds keys, talks to the relay and AI providers, and runs bots; a computer running `lorca serve` is a Runner.

## Prerequisites

| Requirement | Detail |
| --- | --- |
| AI account | One API key (DeepSeek, Anthropic, OpenCode Zen, OpenCode Go) or a ChatGPT / Grok subscription (SuperGrok or X Premium+) |
| Desktop app | macOS 14 or later |
| Windows CLI bots | [Git for Windows](https://git-scm.com/downloads/win) — bots run commands in its bash |
| Time | About 5–10 minutes for install + identity |

<Note>
Bots cannot answer until at least one provider is connected. You can skip provider setup at first launch and finish later in **Settings › Providers** or with `lorca provider set`.
</Note>

## Install

<Tabs>
<Tab title="macOS app">

<Steps>
<Step title="Download and place the app">
[Download Lorca for Mac](/download), open the disk image, and drag Lorca to Applications. The app updates itself; use **Check for Updates…** in the Lorca menu.
</Step>
<Step title="Launch">
Open Lorca from Applications. On first launch you create, restore, or pair an identity (next section).
</Step>
</Steps>

</Tab>
<Tab title="CLI (Apple silicon / Linux)">

```bash
curl -fsSL https://lorca.app/install-cli.sh | sh
```

The script downloads the latest release, checks checksums, installs `lorca` to `~/.local/bin`, and adds that folder to your PATH. Run the same command again to update; a running `lorca serve` keeps the old binary until it restarts.

</Tab>
<Tab title="CLI (Windows)">

```powershell
irm https://lorca.app/install-cli.ps1 | iex
```

Same install behavior as the shell script (`~/.local/bin`, PATH update, checksum check). Install Git for Windows before you rely on bot shell commands.

</Tab>
</Tabs>

### Install options (CLI)

| Variable | Effect |
| --- | --- |
| `LORCA_VERSION` | Pin a release (example: `1.0.0`) |
| `LORCA_INSTALL_DIR` | Install directory instead of `~/.local/bin` |
| `LORCA_NO_MODIFY_PATH` | Leave PATH unchanged (`1`) |

<CodeGroup>

```bash title="Pin version (Unix)"
curl -fsSL https://lorca.app/install-cli.sh | LORCA_VERSION=1.0.0 sh
```

```powershell title="Pin version (Windows)"
$env:LORCA_VERSION = '1.0.0'
irm https://lorca.app/install-cli.ps1 | iex
```

</CodeGroup>

## First run

### Desktop app (about 5 minutes)

<Steps>
<Step title="Create identity">
Choose **Create**. Lorca makes a local key pair and shows a **backup phrase**: thirteen groups of four characters.
</Step>
<Step title="Save the phrase offline">
Write the phrase down. It is the only way to restore the account on another computer; nobody can reset it for you.
</Step>
<Step title="Name the first bot">
Every account starts with one bot (default name Chef). Keep it or rename.
</Step>
<Step title="Connect a provider">
Paste an API key, or sign in to ChatGPT or Grok in the browser. Skip only if you will connect later in **Settings › Providers**.
</Step>
</Steps>

Already have an account: choose **Restore** and enter the phrase, or **Pair** with a string from **Pair a Device** / `lorca pair` on a device that is already set up.

### Headless Runner (CLI)

```bash
export LORCA_RELAY_URL=https://relay.example.com
lorca pair 'lorca://pair?…'   # from Pair a Device… or `lorca pair` on an existing Device
lorca serve
```

`lorca serve` is the default when no command is given. The machine appears in your Device list so you can create bots for it from the desktop app or phone.

## Success signal

| Path | What “it worked” looks like |
| --- | --- |
| macOS app | First-run flow finishes; you can open Chef’s chat and send a message after a provider is connected |
| CLI install | `lorca` is on your PATH (unless `LORCA_NO_MODIFY_PATH=1`); `lorca identity show` or `lorca status` runs |
| Headless Runner | After `lorca pair` + `lorca serve`, the computer shows up in the Device list |

Data lives under `~/.lorca/` by default (`LORCA_HOME` overrides). Local websocket port defaults to `4862` (`LORCA_PORT`).

## Recovery

Lost machine, same identity: restore with the backup phrase.

```bash
lorca identity restore <phrase>
```

In the app, choose **Restore** on first launch and type the phrase.

<Tip>
If install or startup looks wrong, run `lorca doctor`. It checks the home folder, identity, port, and relay, and reports what is wrong.
</Tip>

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Shortest path from install to a first successful invocation.
</Card>
<Card title="CLI reference" href="/cli-reference">
Commands, flags, environment variables, and headless Runner setup.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`LORCA_HOME`, `LORCA_PORT`, `LORCA_RELAY_URL`, model defaults, and where they are read.
</Card>
</CardGroup>

Next: open [Quickstart](/quickstart) and run your first successful invocation (about 2 minutes once installed).

---

## 03. Quickstart

> The shortest source-backed path from install to a first successful invocation, with the expected output.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/03-quickstart.md
- Generated: 2026-09-26T20:14:13.028Z

### Source Files

- `web/content/docs/get-started.mdx`
- `README.md`

---
title: "Quickstart"
description: "The shortest source-backed path from install to a first successful invocation, with the expected output."
---

Install the macOS desktop app, create a local identity key pair, connect one AI provider, then send the first message in Chef's chat. Lorca is a localhost websocket CLI plus agent loop; the AppKit client is the UI for that CLI and launches the bundled binary.

<Info>
Time: about 10–15 minutes if you already have a supported AI account. Desktop path requires macOS 14 or later.
</Info>

## Prerequisites

| Need | Detail |
| --- | --- |
| Machine | macOS 14+ desktop app (this path). CLI-only setup is covered under [CLI reference](/cli-reference). |
| AI account | One of: DeepSeek, Anthropic, OpenCode Zen, or OpenCode Go API key; or ChatGPT / Grok subscription (SuperGrok or X Premium+) |
| Outcome signal | Chef's chat accepts a message and the bot can answer (provider connected) |

## First successful run (desktop)

<Steps>
<Step title="1. Install the app">
Download Lorca for Mac, open the disk image, and drag Lorca to Applications. The app updates itself; **Check for Updates…** is in the Lorca menu.

For install variants and recovery, see [Installation](/installation).
</Step>

<Step title="2. Create your identity">
On first launch, choose **Create**. Lorca makes a key pair on this computer and shows a **backup phrase**: thirteen groups of four characters.

<Warning>
The backup phrase is your identity. It is the only way to restore your account on another computer, and nobody can reset it for you. Keep it somewhere safe and offline.
</Warning>

Already have an account? Choose **Restore** and type your phrase, or **Pair** and paste a pairing string from another set-up computer (**Pair a Device** in the app, or `lorca pair` in a terminal).
</Step>

<Step title="3. Name your first bot">
Every account starts with one bot, Chef. Keep the name or pick your own.
</Step>

<Step title="4. Connect a provider">
Paste a DeepSeek, Anthropic, OpenCode Zen, or OpenCode Go API key, or sign in to ChatGPT or Grok (browser sign-in). You can skip and connect later in **Settings › Providers**, but bots cannot answer until one provider is connected.
</Step>

<Step title="5. Send the first message">
Open Chef's chat and describe your work. Chef proposes a small team and creates it when you agree.

- **Return** — send
- **`+`** — attach files
- **Microphone** — dictate
</Step>
</Steps>

### Expected result

After step 5:

- Identity exists on this machine (key pair created or restored).
- At least one provider is connected.
- Chef's chat accepts your message; Chef proposes a small team when you describe your work.

If the provider step was skipped, the UI still opens, but bots cannot answer until a provider is connected.

## Local development path (optional)

From a checkout, one command builds the CLI and app, launches the app, and starts a local relay:

```bash
bun run dev          # builds CLI + app, launches app, rebuilds on change; relay on 0.0.0.0:8787
```

Related one-liners from the same surface:

```bash
bun run relay        # local relay on 0.0.0.0:8787 (set LORCA_RELAY_URL to use it)
bun run mobile       # Expo / Metro for the phone app
cargo test           # agent loop, crypto, and SSE tests
bun run reset        # stop everything and wipe identity, credentials, chats, prefs (-y, --build, --relay)
```

<Check>
After `bun run dev`, the desktop app launches and a relay listens on `0.0.0.0:8787`. Point clients with `LORCA_RELAY_URL` when you need that relay.
</Check>

## Phone as a follow-on Device

A phone pairs as a Device (never a Runner): scan a QR from the desktop app or paste a string from `lorca pair`, then talk to the relay with the same keys, envelopes, and jobs as the CLI. Bots keep running on your Runner machines.

```bash
bun run mobile       # Metro for the dev client
bun run mobile:ios   # build and run on the iOS simulator (Xcode)
```

## Recovery notes

| Situation | Action |
| --- | --- |
| New computer, same account | **Restore** with the backup phrase, or **Pair** with a pairing string |
| Pairing string needed | **Pair a Device** in the desktop app, or `lorca pair` |
| Wipe local state (dev) | `bun run reset` (flags: `-y`, `--build`, `--relay`) |
| No bot replies | Connect a provider in **Settings › Providers** |

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, install command, first-run success signal, and recovery.
</Card>
<Card title="Overview" href="/overview">
What Lorca exposes: CLI, AppKit client, relay, mobile, and entry points.
</Card>
<Card title="CLI reference" href="/cli-reference">
Commands such as `lorca pair`, flags, and CLI-only setup without the desktop UI.
</Card>
<Card title="Architecture" href="/architecture">
CLI websocket service, agent loop, relay, identity/pairing, and Device vs Runner.
</Card>
</CardGroup>

Next: Open Lorca, confirm a provider under **Settings › Providers**, then send one message in Chef's chat.

---

## 04. Architecture

> The system's main components, boundaries, and data flow as the repository itself documents them.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/04-architecture.md
- Generated: 2026-09-26T20:16:06.838Z

### Source Files

- `ARCHITECTURE.md`

---
title: "Architecture"
description: "The system's main components, boundaries, and data flow as the repository itself documents them."
---

Lorca is three cooperating processes: a local UI (AppKit or Expo), a Device core (`lorca` / `lorca-mobile`), and a zero-knowledge relay (`lorca-relay`). The UI never holds keys or talks to models; the core owns identity, encryption, sync, and the agent loop on Runners; the relay stores public keys and ciphertext only.

## System boundaries

```mermaid
flowchart TB
  subgraph clients [Clients you run]
    Mac["macos/ AppKit<br/>Lorca.app"]
    Phone["mobile/ Expo<br/>iOS · Android"]
  end

  subgraph device [Device core]
    CLI["crates/cli · lorca<br/>keys · local WS · sync"]
    Agent["crates/agent · lorca-agent<br/>loop · tools · providers"]
    Mobile["crates/mobile · lorca-mobile<br/>UniFFI Core"]
  end

  subgraph network [Network]
    Relay["crates/relay · lorca-relay<br/>axum · SQLite or Postgres"]
  end

  subgraph external [External]
    Models["Model providers<br/>API keys · OAuth"]
    Push["APNs · FCM"]
  end

  Mac -->|"ws://127.0.0.1:4862/ws"| CLI
  CLI --> Agent
  Phone --> Mobile
  Mobile -->|"same JSON API"| CLI
  CLI -->|"HTTPS · signed · ciphertext"| Relay
  Mobile -->|"HTTPS · signed · ciphertext"| Relay
  Agent --> Models
  Relay --> Push
```

| Process | Owns | Does not own |
| --- | --- | --- |
| **App** (`macos/`, `mobile/`) | Chat UI, onboarding chrome, notifications presentation | Keys, plaintext store of account secrets, model calls |
| **CLI / core** (`crates/cli`, `crates/mobile`) | Identity, pairing, DEK, credentials merge, sync outbox, jobs; on Runners: agent loop | Relay plaintext, other Devices' machine secrets |
| **Relay** (`crates/relay`) | Public keys, opaque blobs, envelopes, presence, push fan-out | Nicknames, chat text, credentials, Runner role |

<Note>
Production installs use `app.lorca`, `~/.lorca`, and port `4862`. Dev installs use `app.lorca.dev`, `LORCA_HOME=~/.lorca-dev`, and port `4863`. They are separate apps and data directories.
</Note>

## Hard constraints

1. The desktop app speaks only to the local CLI over localhost WebSocket (`ws://127.0.0.1:<port>/ws`). It launches bundled `lorca serve` unless something already answers on the port.
2. The CLI owns the agent loop: inference, tools, streaming, cancellation, orchestration.
3. The relay is zero-knowledge: opaque blobs and public keys; auth is a signature challenge plus HMAC bearer.
4. Desktop `os` values (`macos`, `linux`, `windows`) are **Runners**. Phones and tablets (`ios`, `ipados`, `android`) are Devices only — never Runners.
5. Provider credentials belong to the account: one encrypted `credentials` blob, merged by `changed_at` on every Device.
6. A bot runs on exactly one Runner.

## Identity and crypto layers

| Layer | What | Where |
| --- | --- | --- |
| Master secret (32 bytes) | Root; backup as base32 phrase | `~/.lorca/identity.json` (identity device only, mode `0600`) |
| Content keypair | X25519 via HKDF from master | Secret local; public key on relay |
| Identity signing key | Ed25519 via HKDF from master | Identity id = `hash(pubkey)` |
| Account DEK | XChaCha20-Poly1305 | Sealed `key` blob on relay; in pairing reply |
| Machine keypair | Ed25519 sign + X25519 box | `~/.lorca/machine.json` |
| Job envelopes | Sealed to Runner box key | Relay ciphertext; deleted after run |
| Push key | HKDF from account DEK | Derived on every Device |

AEAD envelopes are `nonce(24) || ciphertext` with blob kind as associated data. Sealed boxes use `crypto_box_seal`. Recovery = restore master phrase → re-derive content keys → unwrap DEKs from the relay.

## Domain model

Plaintext lives on Devices. The relay never sees entity fields as columns.

```text
Identity 1──* Device
Device   1──* Bot        (Runner only)
Identity 1──* Chat
Chat     *──* Bot        (dm: 1 bot · group: 1–6 bots)
Chat     1──* Message
Bot      1──* Routine
Device   1──* Plugin     (MCP on that Runner)
Bot      1──* Job        (turn on the bot's Runner)
```

| Entity | On Device | On relay |
| --- | --- | --- |
| Identity | Master + content + signing keys | Public key |
| Device | Machine keypair, `os` | Machine pubkey + encrypted `machine` blob |
| Bot / Routine | Decrypted roster | Inside encrypted `roster` blobs |
| Credentials | `credentials.json` | Encrypted `credentials` blob |
| Chat / Message | Local SQLite + DEK | Encrypted `chat` / `file` blobs |
| Job | Created anywhere; run on assigned Runner | Sealed envelope to Runner box key |

## Cross-device turn flow

```mermaid
sequenceDiagram
  participant AppB as App on Device B
  participant CLIB as CLI / core B
  participant Relay as lorca-relay
  participant CLIA as CLI Runner A
  participant Model as Provider

  AppB->>CLIB: chats.send
  CLIB->>Relay: PUT encrypted chat + job envelope
  Relay-->>CLIA: sync nudge blobs
  CLIA->>Relay: GET envelopes for machine
  CLIA->>Model: run_agent_loop_continue
  Model-->>CLIA: stream events
  CLIA->>Relay: PUT encrypted reply chunks
  Relay-->>CLIB: sync nudge
  CLIB->>AppB: message.* / job.*
```

1. Any paired Device may create a Job; the assigned Runner decrypts and runs it.
2. Streaming replies re-send growing text at paragraph ends (or sentence ends after 1.5 s silence), encrypted before upload.
3. Hard Stop seals `job_cancel` to the Device listing the turn; the Runner seals `job_result` back to the requester.
4. If the Runner is offline, the envelope waits on the relay until fetched.

## Component map

:::files
lorca/
  ARCHITECTURE.md          # source-of-truth design doc
  Cargo.toml               # workspace members
  crates/agent/            # lorca-agent: loop, tools, providers
  crates/provider-auth/    # ChatGPT + Grok OAuth / PKCE
  crates/cli/              # lorca library + binary (runner + server features)
  crates/mobile/           # UniFFI Core over cli without runner/server
  crates/relay/            # lorca-relay + Dockerfile
  crates/markdown/         # shared Markdown FFI
  macos/                   # AppKit SPM; bundles CLI
  mobile/                  # Expo; modules/lorca-core
  web/                     # TanStack Start site + Fumadocs
  scripts/                 # bun:dev, relay, release, l10n
:::

### `crates/cli` feature split

| Feature | Ships | Used by |
| --- | --- | --- |
| `cli` (default) | `runner` + `server` + binary | Desktop `lorca` |
| `runner` | Agent loop, tools, MCP, providers | Desktop Runners |
| `server` | Local axum WebSocket | Desktop app launcher |
| `provider-auth` | Credential connect/disconnect | Desktop + phone |

`crates/mobile` depends on `lorca` with `default-features = false, features = ["provider-auth"]`: same JSON API and sync, no agent loop.

### Local data directory (`LORCA_HOME`, default `~/.lorca`)

| Path | Role |
| --- | --- |
| `identity.json` | Master secret (identity device) |
| `machine.json` | Machine secret, public keys, account DEK, `name`, `os` |
| `credentials.json` | Merged account providers |
| `settings.json` | Relay URL |
| `lorca.sqlite3` | Devices, bots, chats, messages, outbox, jobs |
| `files/<id>` | Attachment bytes |
| `plugins/` | Per-Runner MCP installs (Runners) |
| `workspaces/<bot id>/` | Bot memory and workdir |

## Protocols

### App ↔ CLI (localhost)

- Endpoint: `ws://127.0.0.1:4862/ws` (or `4863` for Lorca Dev / `LORCA_PORT`).
- Requests: `{ id, method, params }` → `{ id, result }` or `{ id, error: { message } }`.
- Events: `{ event, data }` — `snapshot`, `roster.changed`, `message.*`, `job.*`, `relay.status`, `identity.changed`, …
- First load: `bootstrap` snapshot; clients buffer events until it applies.

Phone path: `Core.start` / `request` / `wake` in `crates/mobile` expose the same methods and event frames without a WebSocket.

### CLI ↔ relay

| Concern | Mechanism |
| --- | --- |
| Protocol gate | Header `Lorca-Protocol: <n>` (currently `1`); below `--min-protocol` → `426` |
| Machine auth | Challenge → sign → 1-hour HMAC bearer |
| Identity writes | Signed `{ payload, signature }` (register / attest) |
| Blobs | `PUT/GET /v1/blobs`, kinds `roster` \| `chat` \| `job` \| `machine` \| `credentials` \| `key` \| … |
| Files | Binary ciphertext on `/v1/files/{id}` (up to ~100 MiB) |
| Sync | `GET /v1/sync` WebSocket nudges `{type:"blobs"|"machines"}` |
| Cross-Runner verbs | Sealed `request` / `response` (`memory.*`, `plugins.*`, `bash.*`, `permission.answer`) |

Without a relay URL, the CLI runs single-Device. `LORCA_RELAY_URL` wins over saved settings; release macOS may set `LORCA_DEFAULT_RELAY_URL=https://relay.lorca.app`.

## Relay storage surface

The relay keeps: identity and machine public keys; blob ids, kinds, seq, size, slot, group; recipient on envelopes; presence; revoked machine keys; APNs/FCM tokens; pairing mailboxes (10-minute TTL).

Default quota: **5 GiB** ciphertext per identity (`--quota-bytes`, `0` = unlimited). File ciphertext lives in `--files-dir` or S3 (`--s3-bucket`); rows keep metadata with empty ciphertext.

Backends: SQLite (one process, WAL) or Postgres (`postgres://…`, multi-process via `NOTIFY` + advisory locks). Pick with `--db` / `LORCA_RELAY_DB`.

## Dev entry points

| Command | What starts |
| --- | --- |
| `bun run dev` | Builds CLI + Lorca Dev, launches app, local relay on `0.0.0.0:8787` |
| `bun run relay` | `lorca-relay` on `0.0.0.0:8787` |
| `bun run mobile` / `mobile:dev` | Expo phone Device |
| `cargo test` | Agent loop, crypto, SSE |
| `lorca serve` | Local Device core (default bind `127.0.0.1:4862`) |

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">
    What the project exposes and the highest-value entry points.
  </Card>
  <Card title="crates/relay reference" href="/ref-crates-relay">
    Relay routes, storage, quotas, and deploy knobs.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Binary commands, local API surface, and Runner features.
  </Card>
  <Card title="crates/agent reference" href="/ref-crates-agent">
    Agent loop, tools, providers, and harness behavior.
  </Card>
  <Card title="HTTP API reference" href="/http-api-reference">
    Relay `/v1` routes, auth, blobs, sync, and push.
  </Card>
</CardGroup>

Next: open `ARCHITECTURE.md` at the repo root and skim **Three processes** plus **Protocols** (about two minutes).

---

## 05. Configuration reference

> Config keys, environment variables, defaults, required values, and where each one is read.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/05-configuration-reference.md
- Generated: 2026-09-26T20:15:43.455Z

### Source Files

- `crates/cli/src/config.rs`
- `.env.example`
- `web/content/docs/cli.mdx`

---
title: "Configuration reference"
description: "Config keys, environment variables, defaults, required values, and where each one is read."
---

Lorca configuration splits across four surfaces: Device CLI env + `~/.lorca/settings.json`, `lorca-relay` flags/env, install-script env, and macOS/mobile launch overrides. Set `LORCA_HOME` and `LORCA_RELAY_URL` first for a Runner; everything else is optional.

## Config surfaces

| Surface | Who reads it | Persist? |
| --- | --- | --- |
| CLI flags `--home`, `--port` | `lorca` | No (process) |
| `LORCA_*` env on the Device | `lorca`, bash tool, providers | No |
| `$LORCA_HOME/settings.json` | `Settings` in the CLI | Yes (mode `0600`) |
| Pairing / `machine.json` relay URL | `App::relay_url` | Yes |
| `lorca-relay` flags + `LORCA_RELAY_*` | Relay process | Deploy config |
| `.env` (repo root) | Bun `dev` / `relay` scripts; relay process when launched that way | Local only |
| macOS app launch env | `CLILauncher` → child `lorca` | App defaults + env |

```text
Device Runner                         Relay host
─────────────────                     ─────────────────
LORCA_HOME / --home                   LORCA_RELAY_BIND / --bind
LORCA_PORT / --port                   LORCA_RELAY_DB / --db
settings.json                         LORCA_RELAY_SECRET
  relay_url, marketplace_url          files dir or S3 + push keys
LORCA_RELAY_URL (wins)                .env.example documents push/secret
LORCA_DEFAULT_RELAY_URL (fallback)
credentials.json (provider keys)
```

## Device home layout

Default home: `~/.lorca` (`~/.lorca-dev` when the macOS Dev app launches the CLI). Override with `--home` or `LORCA_HOME`.

| Path under home | Role |
| --- | --- |
| `identity.json` | Identity keys |
| `machine.json` | This Device; may store relay URL from pairing |
| `credentials.json` | Provider API keys / OAuth tokens |
| `lorca.sqlite3` | Local DB |
| `settings.json` | Runtime settings (`relay_url`, `marketplace_url`) |
| `files/` | Attachment bytes |
| `plugins/` | Installed plugins + secrets |
| `workspaces/<bot-id>/` | Per-bot workspace / memory |

`ensure_home` creates the directory and sets Unix mode `0700` on the home dir; JSON writes use `0600`.

## `settings.json`

Loaded from `Config::settings_path()`. Missing file → empty defaults.

<ParamField body="relay_url" type="string | null">
Saved relay URL. Trimmed; trailing `/` stripped. Overridden by `LORCA_RELAY_URL` when that env is non-empty.
</ParamField>

<ParamField body="marketplace_url" type="string | null">
Extra marketplace index URL listed beside the bundled index. Wins over `LORCA_MARKETPLACE_URL`.
</ParamField>

Change relay at runtime via the app **Settings › Advanced › Relay URL**, or the local API that calls `App::set_relay_url` (writes `settings.json` and forgets the relay bearer token).

## Relay URL resolution

Effective URL for sync / pairing (highest wins):

1. `LORCA_RELAY_URL` (non-empty after trim)
2. `settings.json` → `relay_url`
3. `machine.json` relay URL stored at pairing
4. If `LORCA_DEV` is set and not `0`: `http://<LAN-IPv4>:8787` (`DEV_RELAY_PORT`)
5. `LORCA_DEFAULT_RELAY_URL` (macOS production app injects `https://relay.lorca.app` when unset)

No URL → single-Device mode. `lorca doctor` reports relay as not configured; `lorca identity restore` needs a relay.

## Device CLI environment

Global flags also bind these env vars (`lorca --help`).

### Core

| Variable | Default | Read in |
| --- | --- | --- |
| `LORCA_HOME` | `~/.lorca` | `Config::load`, CLI `--home` |
| `LORCA_PORT` | `4862` | `Config::load`, CLI `--port`; macOS Dev default port is `4863` via app prefs |
| `LORCA_RELAY_URL` | unset | `Settings::effective_relay_url` / `App::relay_url` |
| `LORCA_DEFAULT_RELAY_URL` | unset (prod app sets production relay) | `default_relay_url()` |
| `LORCA_DEV` | unset | `dev_relay_url()`; `scripts/dev.ts` sets `1` on the app |
| `LORCA_SHELL` | `/bin/bash` if present else `/bin/sh`; Windows: Git for Windows `bash.exe` or `bash` | `BashTool::new` |
| `LORCA_MARKETPLACE_URL` | unset | Marketplace index fetch (after settings) |
| `LORCA_COMPACTION` | on (any value except `0`) | `turns.rs`; `0` disables compaction |
| `LORCA_MEMORY_FLUSH` | on | `0` disables pre-compaction memory flush |
| `LORCA_OAUTH_NO_BROWSER` | unset | `1` skips opening a browser for MCP OAuth |

### Provider model defaults

Used when a bot does not pick a model. Built-in fallbacks if env is also unset:

| Variable | Built-in default |
| --- | --- |
| `LORCA_DEEPSEEK_MODEL` | `deepseek-flash` |
| `LORCA_ANTHROPIC_MODEL` | `claude-opus-5` |
| `LORCA_CHATGPT_MODEL` | `gpt-6-sol` |
| `LORCA_GROK_MODEL` | `grok-4.7` |
| `LORCA_OPENCODE_MODEL` | `deepseek-v4.1-flash` |
| `LORCA_OPENCODE_GO_MODEL` | `glm-5.3-flash` |

### Provider base URLs / issuers

Credential `base_url` wins when set. Else env, else built-in root:

| Variable | Default root |
| --- | --- |
| `LORCA_DEEPSEEK_BASE_URL` | `https://api.deepseek.com` (Anthropic-compatible path `/anthropic` appended when missing) |
| `LORCA_ANTHROPIC_BASE_URL` | `https://api.anthropic.com` |
| `LORCA_OPENCODE_BASE_URL` | `https://opencode.ai/zen` |
| `LORCA_OPENCODE_GO_BASE_URL` | `https://opencode.ai/zen/go` |
| `LORCA_GROK_BASE_URL` | provider default unless set |
| `LORCA_GROK_ISSUER` | xAI OAuth endpoints unless set |

Provider **secrets** live in `credentials.json` via `lorca provider set` / app Settings — not in these env vars on the Device CLI path.

<Note>
The `lorca_agent` library’s `EnvProviderFactory` is a separate BYOK surface for embedding the agent crate: `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, plus `DEEPSEEK_BASE_URL` / `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` / `GROK_BASE_URL`. The shipping CLI does not use those names for account credentials.
</Note>

## `lorca-relay` configuration

Every flag has a matching env var (`lorca-relay --help`). CLI flag wins over env when both are passed through the usage parser.

### Required for durable multi-Device deploys

| Variable / flag | Default | Notes |
| --- | --- | --- |
| `LORCA_RELAY_SECRET` / `--secret` | random per boot | **Set in production.** Unset → every restart logs Devices out |
| `LORCA_RELAY_BIND` / `--bind` | `127.0.0.1:8787` | Docker/Railway: image uses `[::]:$PORT` when bind unset |
| `LORCA_RELAY_DB` / `--db` | `lorca-relay.db` | SQLite path or `postgres://…` URL |

### Limits and housekeeping

| Variable | Default | Meaning |
| --- | --- | --- |
| `LORCA_RELAY_QUOTA_BYTES` | `5368709120` (5 GiB) | Per-identity ciphertext cap; `0` = none |
| `LORCA_RELAY_IP_PER_MINUTE` | `60` | Unauthed routes; burst = same; `0` = off |
| `LORCA_RELAY_IDENTITY_PER_SECOND` | `50` | Authed; burst 10×; `0` = off |
| `LORCA_RELAY_MIN_PROTOCOL` | `0` | Older clients get `426` |
| `LORCA_RELAY_INACTIVE_DAYS` | `365` | Delete idle identities; `0` = keep |
| `LORCA_RELAY_CONCURRENT_UPLOADS` | `3` | Large uploads at once; `0` = off |
| `LORCA_RELAY_TRUST_PROXY` | `false` | Use last `X-Forwarded-For` hop; set only behind a rewriting proxy |
| `LORCA_RELAY_METRICS_TOKEN` | unset | Enables `GET /metrics` with that bearer; unset → no route |

### File storage

| Variable | Default / constraint |
| --- | --- |
| `LORCA_RELAY_FILES_DIR` | DB path with `.files` extension; conflicts with S3 |
| `LORCA_RELAY_S3_BUCKET` | Requires `--s3-endpoint` / `LORCA_RELAY_S3_ENDPOINT` |
| `LORCA_RELAY_S3_ENDPOINT` | Required with bucket |
| `LORCA_RELAY_S3_REGION` | `auto` |
| `LORCA_RELAY_S3_PREFIX` | `""` |
| `LORCA_RELAY_S3_ACCESS_KEY` | Falls back to `AWS_ACCESS_KEY_ID` |
| `LORCA_RELAY_S3_SECRET_KEY` | Falls back to `AWS_SECRET_ACCESS_KEY` |

Postgres multi-replica deploys need shared `LORCA_RELAY_SECRET` and S3-compatible attachment storage.

### Push notifications

Documented in `.env.example`. Keys may be file paths or inline text (`-----BEGIN…` or `{…}` JSON).

| Variable | Required with | Default |
| --- | --- | --- |
| `LORCA_RELAY_APNS_KEY` | key id + team id | — |
| `LORCA_RELAY_APNS_KEY_ID` | with key | — |
| `LORCA_RELAY_APNS_TEAM_ID` | with key | — |
| `LORCA_RELAY_APNS_TOPIC` | — | `app.lorca` (dev scripts use `app.lorca.dev`) |
| `LORCA_RELAY_FCM_SERVICE_ACCOUNT` | — | — |
| `LORCA_RELAY_APNS_URL` | tests only | production APNs |
| `LORCA_RELAY_FCM_URL` | tests only | production FCM |

Without push keys, sync still works; phones get no push.

### Test-only relay env

| Variable | Use |
| --- | --- |
| `LORCA_RELAY_TEST_POSTGRES` | Integration tests against Postgres |
| `LORCA_RELAY_TEST_S3` | MinIO/S3 test endpoint |
| `LORCA_RELAY_TEST_S3_KEYS` | `access:secret` for that endpoint |

## Install script environment

`web/public/install-cli.sh` and `install-cli.ps1`:

| Variable | Default | Effect |
| --- | --- | --- |
| `LORCA_VERSION` | latest release | Pin version (`1.0.0`) |
| `LORCA_INSTALL_DIR` | `~/.local/bin` | Binary destination |
| `LORCA_NO_MODIFY_PATH` | unset | `1` = do not edit shell PATH |
| `LORCA_DOWNLOAD_URL` | GitHub releases base | Mirror / alternate releases root |
| `LORCA_HOME` | `~/.lorca` | Installer checks for existing `machine.json` |

## macOS app and mobile build

| Variable | Reader | Effect |
| --- | --- | --- |
| `LORCA_HOME` | `CLILauncher` | Defaults to `~/.lorca` or `~/.lorca-dev` for Dev bundle |
| `LORCA_DEFAULT_RELAY_URL` | `CLILauncher` | Prod app sets `https://relay.lorca.app` if unset |
| `LORCA_PORT` | `Preferences.cliPort` | Overrides stored CLI port |
| `LORCA_CLI` | `CLILaunchWorker` | Path to `lorca` binary override |
| `LORCA_MOCK` | `AppStore` | `1` = mock mode |
| `LORCA_TRACE_STARTUP` | `StartupTrace` | `1` = launch timings |
| `RUST_LOG` | `CLILauncher` | Defaults to `lorca=info` for the child |
| `LORCA_MOBILE_VARIANT` | `mobile/app.config.ts` | `development` → dev client |
| `LORCA_IOS_BUILD_NUMBER` | `app.config.ts` / release script | iOS build number (default `"1"`) |

## Quick verification

<Steps>
<Step title="Check Device config">
Run `lorca doctor`. Expect home, identity, port, and relay checks; missing relay is OK for single-Device use.
</Step>
<Step title="Confirm effective relay">
Run `lorca status` and inspect the JSON for relay / device state after setting `LORCA_RELAY_URL` or `settings.json`.
</Step>
<Step title="Confirm relay process">
`curl http://127.0.0.1:8787/v1/health` → `{"ok":true,"service":"lorca-relay"}` (adjust host/port to your bind).
</Step>
</Steps>

<Warning>
Unset `LORCA_RELAY_SECRET` on a public relay invalidates every Device token on each restart. Set a stable secret before pairing phones or extra Runners.
</Warning>

## Related pages

<CardGroup>
<Card title="CLI reference" href="/cli-reference">
Commands, flags, and doctor/status behavior for `lorca`.
</Card>
<Card title="crates/relay reference" href="/ref-crates-relay">
Relay binary exports, storage modes, and error cases.
</Card>
<Card title="Deployment and operations" href="/deployment">
Build, containerize, and run relay environments.
</Card>
<Card title="Installation" href="/installation">
Install CLI / app and first-run success signals.
</Card>
</CardGroup>

Next: open `~/.lorca/settings.json` (or set `LORCA_RELAY_URL`) and run `lorca doctor` to confirm the Device sees the intended home and relay.

---

## 06. CLI reference

> Commands, flags, arguments, defaults, exit behavior, and examples for the command-line interface.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/06-cli-reference.md
- Generated: 2026-09-26T20:14:22.288Z

### Source Files

- `web/content/docs/cli.mdx`
- `crates/cli/src/main.rs`

---
title: "CLI reference"
description: "Commands, flags, arguments, defaults, exit behavior, and examples for the command-line interface."
---

`lorca` is the binary that holds keys, serves the local websocket API the app connects to, runs the agent loop, and syncs with the relay. Unknown flags error. With no subcommand, it runs `serve`.

```bash
lorca [--home <path>] [--port <u16>] [serve|identity|pair|provider|status|doctor] …
```

## Global flags

<ParamField body="home" type="PathBuf" optional>
  Data directory for keys, credentials, and chats. Long flag `--home`. Env: `LORCA_HOME`. Default: `~/.lorca`.
</ParamField>

<ParamField body="port" type="u16" optional>
  Local websocket port for the app. Long flag `--port`. Env: `LORCA_PORT`. Default: `4862`.
</ParamField>

Both flags are global and apply to every subcommand.

## Environment

| Variable | Effect |
| --- | --- |
| `LORCA_HOME` | Data directory. Default `~/.lorca`. |
| `LORCA_PORT` | Local port. Default `4862`. |
| `LORCA_RELAY_URL` | Relay URL for multi-device sync. Unset = single-device mode. |
| `LORCA_SHELL` | Shell bots use for commands. Default `/bin/bash`; on Windows, Git for Windows bash. |
| `LORCA_DEEPSEEK_MODEL`, `LORCA_ANTHROPIC_MODEL`, `LORCA_CHATGPT_MODEL`, `LORCA_GROK_MODEL` | Default model when a bot does not pick one. |

Tracing logs go to stderr. Default filter when `RUST_LOG` / env filter is unset: `lorca=info,lorca_agent=info`.

## Command map

| Command | Role |
| --- | --- |
| `lorca serve` | Local API + agent loop + relay sync (default). |
| `lorca identity …` | Create, restore, or show identity. |
| `lorca pair [string]` | Start pairing or join with a pairing string. |
| `lorca provider …` | Connect, disconnect, or list AI providers. |
| `lorca status` | Pretty-printed JSON snapshot of this device. |
| `lorca doctor` | Local setup checks with ✔ / ✘ lines. |

---

## `lorca serve`

Runs the local websocket the app connects to. Default when `command` is omitted (`parent_pid: None`, `ready_stdout: false`).

On start it primes runtime names, resumes sent jobs, closes stale shell rows, refreshes installed marketplace plugins from the bundled index, optionally watches a parent PID, loads login-shell environment for bots, and spawns relay sync and routines before `ws::serve`.

<ParamField body="parent-pid" type="u32" optional>
  Long flag `--parent-pid`. Exit when this process is gone. The app passes its own PID so a killed app does not leave a stale CLI holding the port. Polls every 1s; on parent exit shuts down shell sessions and `process::exit(0)`.
</ParamField>

<ParamField body="ready-stdout" type="bool" optional>
  Long flag `--ready-stdout`. Write a JSON readiness message to stdout once the local server is listening.
</ParamField>

<RequestExample>
```bash
lorca serve
lorca serve --parent-pid 12345 --ready-stdout
```
</RequestExample>

### Unix signals

On Unix, `SIGTERM`, `SIGINT`, and `SIGHUP` first shut down bot shell sessions, then restore the default handler and re-raise the same signal so the process exits as usual.

### Headless Runner

```bash
export LORCA_RELAY_URL=https://relay.example.com
lorca pair 'lorca://pair?…'
lorca serve
```

After pairing and serve, the computer appears in the Device list and can host bots.

---

## `lorca identity`

### `lorca identity new`

Creates a new identity on this device and prints the backup phrase.

<ParamField body="name" type="string" optional>
  Long flag `--name`. Device name for the new identity.
</ParamField>

<ResponseExample>
```text
Identity created on this Device.

Backup phrase (write it down; it is the identity):

  word1 word2 … wordN

No relay configured. Set LORCA_RELAY_URL to sync with other Devices.
```
</ResponseExample>

If `LORCA_RELAY_URL` is set, one sync flush (up to 8s) runs after create.

### `lorca identity restore`

Restores from a backup phrase. Needs a relay.

<ParamField body="phrase" type="Vec<String>" required>
  Positional words of the backup phrase (joined with spaces for restore).
</ParamField>

<ParamField body="name" type="string" optional>
  Long flag `--name`.
</ParamField>

Prints: `Identity restored. Run \`lorca serve\` to sync.`

### `lorca identity show`

Prints identity id, public keys, device name/OS, whether this device holds the master, and registration. If none: `No identity on this Device. Run \`lorca identity new\`.`

```text
identity id:   …
identity key:  …
machine key:   …
device name:   name (os)
holds master:  true|false
registered:    true|false
```

---

## `lorca pair`

### Start pairing (no string)

Creates a pairing string and waits until the other device joins or pairing fails.

```bash
lorca pair
# optional: lorca pair --name "laptop"
```

Prints instructions, then polls status every 1s:

- `completed` → prints paired device name, flushes outbox once, exits 0
- `failed` → bails with the status error message (or `pairing failed`)

### Join with a string

```bash
lorca pair 'lorca://pair?…' --name "runner"
```

On success: `Paired as <name>. Run \`lorca serve\` to sync.`

<ParamField body="pairing_string" type="string" optional>
  Positional. From the device that holds the identity. Omit to create one here.
</ParamField>

<ParamField body="name" type="string" optional>
  Long flag `--name`. Name for this device when joining.
</ParamField>

---

## `lorca provider`

Manages account provider credentials. Prefer a running `lorca serve`: set/remove call `ws://127.0.0.1:<port>/ws` so the app sees the change immediately. If nothing listens, the CLI dispatches locally and runs one sync flush.

Allowed kinds: `deepseek`, `anthropic`, `opencode`, `opencode-go`, `chatgpt`, `grok`.

### `lorca provider set <kind> [api-key]`

| Kind | How it connects |
| --- | --- |
| `deepseek`, `anthropic`, `opencode`, `opencode-go` | API key (positional or stdin) |
| `chatgpt`, `grok` | Browser sign-in only |

<ParamField body="kind" type="string" required>
  Provider id. Unknown values bail: `Unknown provider {kind}. Use one of: …`.
</ParamField>

<ParamField body="api_key" type="string" optional>
  Positional API key. Omit to read from stdin (keeps the key out of shell history). Prompt `API key: ` on stderr when stdin is a terminal.
</ParamField>

<ParamField body="base-url" type="string" optional>
  Long flag `--base-url`. Custom API root (proxy or compatible server). Ignored / rejected for `chatgpt` and `grok`.
</ParamField>

Constraints:

- `chatgpt` / `grok` with `api_key` or `base_url` set → bail: `{kind} connects with a browser sign-in and takes no API key`
- For those kinds, prints `Finish the sign-in in the browser…` then calls `providers.connect_<kind>` with `{}`
- `opencode-go` maps to method `providers.connect_opencode_go`

After success, prints each provider as `{kind}` left-padded to 10 columns plus `detail`.

### `lorca provider remove <kind>`

Disconnects that provider on every device via `providers.disconnect` with `{ "kind": kind }`.

### `lorca provider list`

Lists providers and connection detail from the credentials file (same set a running serve would hold). No websocket call.

---

## `lorca status`

Prints `app.snapshot()` as pretty JSON (devices, bots, chats, providers, and related state). Exits after print.

```bash
lorca status
```

---

## `lorca doctor`

Checks local setup; always returns `Ok(())` from main after printing lines.

| Check | Pass signal | Fail detail |
| --- | --- | --- |
| `home` | `config.home` is a directory | path string |
| `identity` | identity present | `run \`lorca identity new\`` |
| `port` | `127.0.0.1:<port>` bind succeeds | `{port} busy (lorca serve running?)` |
| `relay` | health OK when URL set | unreachable URL, or `not configured (LORCA_RELAY_URL); single-Device mode` |
| `providers` | at least one connected kind | `none connected` |

Each line: `✔` or `✘`, then label and detail.

---

## Exit and error behavior

| Situation | Behavior |
| --- | --- |
| Success paths above | `Ok(())` / normal process exit |
| `--parent-pid` parent gone | shell shutdown, then exit code `0` |
| Pairing `failed` | `anyhow::bail` with error string |
| Unknown provider kind | bail with allowed-kinds list |
| chatgpt/grok with key/url | bail with browser-sign-in message |
| Serve websocket closed mid-call | bail `lorca serve closed the connection` |
| Provider RPC error | mapped from JSON `error.message` |
| Unknown CLI flags | error (`unknown_flags = "error"`) |

Provider set/remove against a live serve send JSON-RPC over websocket:

```json
{ "id": 1, "method": "<method>", "params": { … } }
```

Reply is the message with matching `id`; `error.message` becomes the CLI error.

---

## Install and binary location

On Apple silicon Mac or Linux without the app:

```bash
curl -fsSL https://lorca.app/install-cli.sh | sh
```

On Windows PowerShell:

```powershell
irm https://lorca.app/install-cli.ps1 | iex
```

Install script options: `LORCA_VERSION`, `LORCA_INSTALL_DIR`, `LORCA_NO_MODIFY_PATH=1`. Default install path: `~/.local/bin`. On macOS with the app, the bundled binary is `Lorca.app/Contents/Resources/bin/lorca`. App service logs: `~/Library/Logs/Lorca/cli.log`.

On Windows, install Git for Windows so bots can run commands in its bash.

## Data under home

Everything under `~/.lorca/` (or `LORCA_HOME`): identity and machine keys, provider credentials, settings, chats, attachments, plugins, and one workspace folder per bot. Readable only by the user.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Prerequisites, install command, first run, and recovery notes.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Shortest path from install to a first successful invocation.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Config keys, environment variables, defaults, and where each is read.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Library surface under `crates/cli`: exports, invocation, options, errors.
  </Card>
  <Card title="HTTP API reference" href="/http-api-reference">
    Local websocket / RPC methods the CLI and app use.
  </Card>
</CardGroup>

Next: Run `lorca doctor` and confirm every line shows ✔ before connecting providers.

---

## 07. HTTP API reference

> Routes, methods, request and response shapes, authentication, and error responses.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/07-http-api-reference.md
- Generated: 2026-09-26T20:16:05.717Z

### Source Files

- `crates/cli/src/api.rs`
- `crates/relay/src/routes.rs`

---
title: "HTTP API reference"
description: "Routes, methods, request and response shapes, authentication, and error responses."
---

`lorca-relay` exposes an Axum HTTP API under `/v1/*` (default bind `127.0.0.1:8787`, protocol `1`). Clients send ciphertext and public keys only; the relay stores and forwards opaque blobs between an identity’s machines. A second, localhost surface exists on `lorca serve` (`GET /`, `GET /ws`).

<Note>
Send `Lorca-Protocol: 1` on every `/v1/*` call except `/v1/health`. Clients below `--min-protocol` get `426 Upgrade Required`.
</Note>

## Surfaces

| Surface | Bind | Auth | Role |
| --- | --- | --- | --- |
| Relay REST + sync WS | `LORCA_RELAY_BIND` / `127.0.0.1:8787` | Bearer HMAC token (or signed identity request) | Cross-device mailbox |
| Local app server | `127.0.0.1:$LORCA_PORT` (default `4862`) | Loopback only | Desktop/mobile JSON methods over `/ws` |

## Conventions

| Rule | Value |
| --- | --- |
| Encoding | base64url without padding for keys, signatures, ciphertext in JSON |
| JSON body limit | 6 MiB (`MAX_BODY_BYTES`) |
| Non-file blob ciphertext | 1…4 MiB |
| File upload | raw body, `Content-Type: application/octet-stream`, up to 100 MiB + 40-byte envelope |
| Blob / group / slot / file ids | 1–64 chars of `[A-Za-z0-9._-]`, not `.` or `..` |
| Blob kinds | `roster`, `chat`, `job`, `job_cancel`, `job_result`, `request`, `response`, `machine`, `credentials`, `key`, `file` |
| Success with no body | `204 No Content` |
| Error body | `{"error":"<message>"}` (plus extras on protocol refusal) |

## Authentication

### Signed identity request

Used by `POST /v1/identities`.

```json
{
  "payload": "<base64url(JSON)>",
  "signature": "<base64url(ed25519(payload_bytes))>"
}
```

Decoded payload must include `identity_pubkey`, `ts` (unix seconds), and the route-specific fields. Signature is over the raw payload bytes with the identity Ed25519 key. Skew window: ±5 minutes (`SIGNED_REQUEST_SKEW`).

### Machine challenge → bearer token

1. `POST /v1/auth/challenge` with `{ "machine_pubkey": "..." }` → `{ "nonce", "expires_in": 120 }`
2. Sign the nonce bytes with the machine key; `POST /v1/auth/verify`
3. Use `Authorization: Bearer <token>` on authenticated routes

Token TTL: 1 hour. Token body is HMAC-SHA256 over `identity_pubkey|machine_pubkey|expires_at` with `LORCA_RELAY_SECRET`. A revoked machine returns `410 Gone` even if the token is still within TTL.

### Public vs authenticated routes

**Per-IP limited (no bearer):** `POST /v1/identities`, `POST /v1/auth/challenge`, `POST /v1/auth/verify`, `POST /v1/pair/{nonce}/request`, `GET /v1/pair/{nonce}/reply`

**Bearer required:** everything else under `/v1/` except health (and the two public pairing mailbox endpoints above)

**Unauthenticated probes:** `GET /`, `GET /v1/health`

**Metrics bearer:** `GET /metrics` uses `LORCA_RELAY_METRICS_TOKEN` (route is `404` when unset)

## Errors

| Status | When |
| --- | --- |
| `400` | Bad JSON, unknown kind, invalid id/key/size, wrong file content type path |
| `401` | Missing/bad bearer, bad signature, expired challenge/token |
| `403` | Pairing owned by another identity |
| `404` | Unknown blob/file/machine/pairing; metrics when token unset |
| `409` | Identity content-key mismatch; deleted group; pairing already has a request |
| `410` | Machine unpaired |
| `413` | Storage quota exceeded |
| `415` | File upload without `application/octet-stream` |
| `426` | Client protocol below `--min-protocol` |
| `429` | IP or identity rate limit; includes `Retry-After` |
| `503` | Large upload waited 30s for a slot; `Retry-After: 5` |
| `500` | Database/internal failure (`{"error":"Database error"}` etc.) |

Protocol refusal body:

```json
{
  "error": "This relay needs a newer Lorca",
  "min_protocol": 1,
  "protocol": 1
}
```

## Rate limits and quotas

| Limit | Default | Applies to |
| --- | --- | --- |
| IP | 60/min (`LORCA_RELAY_IP_PER_MINUTE`) | Public routes above |
| Identity | 50/s burst×10 (`LORCA_RELAY_IDENTITY_PER_SECOND`) | Bearer routes |
| Concurrent large uploads | 3 (`LORCA_RELAY_CONCURRENT_UPLOADS`), bodies &gt; 1 MiB | `PUT /v1/blobs`, `PUT /v1/files/{id}` |
| Quota | 5 GiB/identity (`LORCA_RELAY_QUOTA_BYTES`; `0` = unlimited) | Inserts |

Behind a reverse proxy set `LORCA_RELAY_TRUST_PROXY=true` so limits use `X-Forwarded-For`.

## Endpoint inventory

| Method | Path | Auth | Summary |
| --- | --- | --- | --- |
| `GET` | `/` | none | Plain text: `Lorca Relay is running...` |
| `GET` | `/v1/health` | none | Liveness + protocol |
| `GET` | `/metrics` | metrics token | Prometheus text |
| `POST` | `/v1/identities` | signed | Register identity + machine |
| `POST` | `/v1/auth/challenge` | none | Start machine login |
| `POST` | `/v1/auth/verify` | none | Redeem challenge → token |
| `DELETE` | `/v1/identity` | bearer | Delete whole identity |
| `GET` | `/v1/machines` | bearer | List machines + presence |
| `DELETE` | `/v1/machines/{machine_pubkey}` | bearer | Revoke one machine |
| `PUT` | `/v1/blobs` | bearer | Upload non-file blob |
| `GET` | `/v1/blobs` | bearer | Page blobs since `seq` |
| `GET` | `/v1/blobs/{id}` | bearer | One non-file blob |
| `DELETE` | `/v1/blobs/{id}` | bearer | Delete blob (and file object) |
| `PUT` | `/v1/files/{id}` | bearer | Upload attachment bytes |
| `GET` | `/v1/files/{id}` | bearer | Download attachment bytes |
| `DELETE` | `/v1/groups/{group}` | bearer | Delete chat group + attachments |
| `GET` | `/v1/groups/{group}/blobs` | bearer | Page group slots backwards |
| `GET` | `/v1/sync` | bearer | WebSocket presence + signals |
| `PUT` | `/v1/push/token` | bearer | Register APNs/FCM token |
| `DELETE` | `/v1/push/token` | bearer | Clear this machine’s token |
| `POST` | `/v1/push` | bearer | Queue push ciphertext |
| `POST` | `/v1/pair` | bearer | Open pairing mailbox |
| `DELETE` | `/v1/pair/{nonce}` | bearer | Cancel pairing |
| `POST` | `/v1/pair/{nonce}/request` | none | Joiner posts sealed request |
| `GET` | `/v1/pair/{nonce}/request` | bearer | Host reads request |
| `POST` | `/v1/pair/{nonce}/reply` | bearer | Host posts sealed reply |
| `GET` | `/v1/pair/{nonce}/reply` | none | Joiner polls reply |

## Health and metrics

:::endpoint GET /v1/health Liveness
No auth. Not gated by `Lorca-Protocol`.

<ResponseExample>
```json
{ "ok": true, "service": "lorca-relay", "protocol": 1 }
```
</ResponseExample>
:::

:::endpoint GET /metrics Prometheus scrape
Requires `Authorization: Bearer <LORCA_RELAY_METRICS_TOKEN>`. Without the env/flag the route returns `404`. Wrong token → `401` with empty body. Content-Type: `text/plain; version=0.0.4; charset=utf-8`. Metric names are prefixed `lorca_relay_`.
:::

## Identities and machines

:::endpoint POST /v1/identities Register identity and attest a machine
Signed by the identity key. Idempotent when the same `content_pubkey` is reused; different content key → `409`.

<ParamField body="payload" type="string" required>
base64url JSON with `identity_pubkey`, `content_pubkey`, `machine.machine_pubkey`, `machine.box_pubkey` (32-byte X25519 as base64url), `ts`
</ParamField>
<ParamField body="signature" type="string" required>
base64url Ed25519 over payload bytes
</ParamField>

<ResponseExample>
```json
{
  "identity_pubkey": "...",
  "machine_pubkey": "..."
}
```
</ResponseExample>
:::

:::endpoint POST /v1/auth/challenge Request login nonce
<RequestExample>
```json
{ "machine_pubkey": "<base64url ed25519>" }
```
</RequestExample>
<ResponseExample>
```json
{ "nonce": "<base64url>", "expires_in": 120 }
```
</ResponseExample>
:::

:::endpoint POST /v1/auth/verify Exchange signature for bearer token
<RequestExample>
```json
{
  "machine_pubkey": "...",
  "nonce": "...",
  "signature": "<ed25519(nonce bytes)>"
}
```
</RequestExample>
<ResponseExample>
```json
{
  "token": "<body>.<hmac>",
  "expires_at": 1710000000,
  "identity_pubkey": "...",
  "machine_pubkey": "..."
}
```
</ResponseExample>

Unknown/expired challenge → `401`. Unknown machine → `404`. Unpaired machine → `410`.
:::

:::endpoint GET /v1/machines List machines
Returns `{ "machines": [...], "now": <unix> }`. Each machine: `machine_pubkey`, `box_pubkey`, `last_seen`, `created_at`, `online`.
:::

:::endpoint DELETE /v1/machines/{machine_pubkey} Revoke a machine
`204` on success. Drops pending sealed envelopes, closes its sync socket, publishes a `machines` signal. Unknown machine → `404`.
:::

:::endpoint DELETE /v1/identity Delete the identity
`204`. Removes machines, blobs, attachments, push tokens, and usage. Paired clients that reconnect see `410` and forget the identity.
:::

## Blobs and files

:::endpoint PUT /v1/blobs Store a non-file blob
JSON body. `kind` must be in `KINDS` and must not be `file` (use the file route).

<ParamField body="id" type="string">
Optional client id; default UUID
</ParamField>
<ParamField body="kind" type="string" required>
Blob kind (not `file`)
</ParamField>
<ParamField body="ciphertext" type="string" required>
base64url ciphertext, 1…4 MiB decoded
</ParamField>
<ParamField body="recipient_machine_pubkey" type="string">
Sealed to one machine
</ParamField>
<ParamField body="slot" type="string">
Named slot for replaceable messages
</ParamField>
<ParamField body="keep_first" type="boolean">
With `slot`: keep the first version when replacing
</ParamField>
<ParamField body="group" type="string">
Chat/group id for later group delete/paging
</ParamField>

<ResponseExample>
```json
{ "id": "...", "seq": 42 }
```
</ResponseExample>

Duplicate id → `{ "id", "seq", "existing": true }`. Quota → `413`. Deleted group → `409`.
:::

:::endpoint GET /v1/blobs Page the identity log
Query: `since` (seq, default `0`), `kinds` (comma list; default all non-file), `limit` (1–500, default 200). Page capped at ~8 MiB of ciphertext.

<ResponseExample>
```json
{
  "blobs": [
    {
      "id": "...",
      "kind": "chat",
      "recipient_machine_pubkey": null,
      "seq": 42,
      "ciphertext": "...",
      "created_at": 1710000000
    }
  ],
  "seq": 42
}
```
</ResponseExample>

Pull until a page is empty; refresh when the sync socket says `blobs`. Asking for `file` in `kinds` → `400`.
:::

:::endpoint GET /v1/blobs/{id} Fetch one non-file blob
Returns a single `BlobOut` object. File kind → `400` (use `/v1/files/{id}`). Missing → `404`.
:::

:::endpoint DELETE /v1/blobs/{id} Delete a blob
`204`. For `file` kinds also deletes the object store key.
:::

:::endpoint PUT /v1/files/{id} Upload attachment ciphertext
Raw body. Optional query `?group=`. Requires `Content-Type: application/octet-stream`.

<ResponseExample>
```json
{ "id": "...", "seq": 7, "existing": false }
```
</ResponseExample>
:::

:::endpoint GET /v1/files/{id} Download attachment ciphertext
Returns `application/octet-stream` body. Missing row or missing object → `404`.
:::

:::endpoint DELETE /v1/groups/{group} Delete a chat group
`204`. Removes the group’s blobs/attachments and refuses future blobs for that group (`409 Group was deleted`). Idempotent.
:::

:::endpoint GET /v1/groups/{group}/blobs Page a chat backwards
Query: `before` (slot place/seq upper bound; omit for newest), `limit` (1–500, default 100).

<ResponseExample>
```json
{
  "slots": [
    { "place": 100, "blobs": [ { "id": "...", "kind": "chat", "seq": 100, "ciphertext": "...", "created_at": 1, "recipient_machine_pubkey": null } ] }
  ],
  "has_more": true
}
```
</ResponseExample>
:::

## Sync WebSocket

:::endpoint GET /v1/sync Presence and wake signals
Upgrade with bearer auth (same protocol header rules as other `/v1` routes).

While open the machine is **online**; `last_seen` updates on open/close. Server pings every 25s and drops a silent peer after ~50s.

Server → client text frames (signals only, no payload data):

```json
{"type":"blobs"}
```

```json
{"type":"machines"}
```

`blobs` means pull `GET /v1/blobs` (or the relevant sealed recipient path). `machines` means refresh `GET /v1/machines`. Client traffic can be any WS message to reset the silence timer; the relay does not parse client JSON on this socket.
:::

```mermaid
sequenceDiagram
  participant Device
  participant Relay
  Device->>Relay: POST /v1/auth/verify
  Relay-->>Device: bearer token
  Device->>Relay: GET /v1/sync (Bearer)
  Relay-->>Device: WS open (online)
  Note over Relay: blob inserted for identity
  Relay-->>Device: {"type":"blobs"}
  Device->>Relay: GET /v1/blobs?since=N
  Relay-->>Device: {"blobs":[...],"seq":M}
```

## Push

:::endpoint PUT /v1/push/token Register device push token
<ParamField body="platform" type="string" required>
`apns` or `fcm`
</ParamField>
<ParamField body="token" type="string" required>
APNs: hex ≤200 chars. FCM: printable ASCII ≤4096
</ParamField>
<ParamField body="environment" type="string">
`sandbox` for APNs development; otherwise `production`
</ParamField>

`204` on success.
:::

:::endpoint DELETE /v1/push/token Unregister this machine
`204`.
:::

:::endpoint POST /v1/push Queue encrypted push
Body `{ "ciphertext": "<base64url>" }` decoded size 1…2560 bytes. Delivery is async after the response.

<ResponseExample>
```json
{ "queued": 2 }
```
</ResponseExample>

`queued` is how many registered tokens this relay can actually send (APNs/FCM configured).
:::

## Pairing mailbox

TTL: 10 minutes. Ciphertext for request/reply: ≤64 KiB decoded.

| Step | Caller | Call |
| --- | --- | --- |
| 1 | Host (bearer) | `POST /v1/pair` → `{ "nonce", "expires_at" }` |
| 2 | Joiner (no auth) | `POST /v1/pair/{nonce}/request` `{ "ciphertext" }` → `204` |
| 3 | Host | `GET /v1/pair/{nonce}/request` → `{ "ciphertext": "..." \| null }` |
| 4 | Host | `POST /v1/pair/{nonce}/reply` `{ "ciphertext" }` → `204` |
| 5 | Joiner | `GET /v1/pair/{nonce}/reply` → `{ "ciphertext": "..." \| null }` |
| Cancel | Host | `DELETE /v1/pair/{nonce}` → `204` |

Second request on the same nonce → `409 Pairing already has a request`. Wrong identity on host routes → `403`. Unknown/expired → `404`.

## Local `lorca serve` HTTP

`lorca serve` binds `127.0.0.1` only (port `4862` / `LORCA_PORT`).

| Method | Path | Behavior |
| --- | --- | --- |
| `GET` | `/` | Plain text `lorca` |
| `GET` | `/ws` | WebSocket JSON RPC |

Request:

```json
{ "id": 1, "method": "hello", "params": {} }
```

Response:

```json
{ "id": 1, "result": { ... } }
```

or `{ "id": 1, "error": { "message": "..." } }`. Async app events arrive as `{ "event", "data" }` (and related event shapes). Method catalog lives in `crates/cli/src/api.rs` (`hello`, `bootstrap`, identity/pair/device, bots/chats, providers, …)—see the CLI reference for invocation from the command line.

On ready (with `--ready-stdout`), the process prints one JSON line to stdout: `{ "event": "ready", "port": <n> }`.

## Quick verify

<Steps>
<Step title="Health">
```bash
curl -s "$RELAY/v1/health"
```
Expect `{"ok":true,"service":"lorca-relay","protocol":1}`.
</Step>
<Step title="Root banner">
```bash
curl -s "$RELAY/"
```
Expect `Lorca Relay is running...`.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="crates/relay reference" href="/ref-crates-relay">
Binary flags, storage backends, push config, and deploy defaults.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`LORCA_RELAY_*` env vars and CLI equivalents.
</Card>
<Card title="CLI reference" href="/cli-reference">
`lorca serve`, local port, and method-style invocation.
</Card>
<Card title="Architecture" href="/architecture">
How Devices, blobs, and the relay fit together.
</Card>
</CardGroup>

Next: run `curl -s http://127.0.0.1:8787/v1/health` against a local relay (or your deployed URL) and confirm `"protocol":1`.

---

## 08. crates/relay reference

> Reference for the code under `crates/relay`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/08-crates-relay-reference.md
- Generated: 2026-09-26T20:18:58.874Z

### Source Files

- `crates/relay/src/main.rs`
- `crates/relay/src/routes.rs`

---
title: "crates/relay reference"
description: "Reference for the code under `crates/relay`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

Run the binary in one command:

```bash
cargo run -q -p lorca-relay -- --bind 127.0.0.1:8787 --db lorca-relay.db
```

`crates/relay` is the **binary-only** crate `lorca-relay`: a zero-knowledge store-and-forward HTTP + WebSocket service. It keeps identity/machine public keys and opaque ciphertext, issues HMAC bearer tokens after an Ed25519 challenge, and never decrypts payloads. There is no `lib.rs`. The public surface is the `lorca-relay` binary (see `crates/relay/Cargo.toml` `[[bin]]`) and the HTTP API wired in `crates/relay/src/main.rs` and `crates/relay/src/routes.rs`.

## Invoke

| Path | Command / signal |
| --- | --- |
| Local binary | `cargo run -p lorca-relay -- [flags]` or `target/debug/lorca-relay` / `target/release/lorca-relay` |
| Repo npm script | `bun run relay` → `mkdir -p temp`, then bind `0.0.0.0:8787`, `--db temp/lorca-relay.db`, `--apns-topic app.lorca.dev` |
| Docker | `docker build -f crates/relay/Dockerfile -t lorca-relay .` then `docker run -p 8787:8787 -v lorca-relay:/data lorca-relay` |
| Success | First log: `lorca-relay listening` with `bind`, `db`, `files`, `push`; `GET /` → `Lorca Relay is running...`; `GET /v1/health` → `{"ok":true,"service":"lorca-relay","protocol":1}` |
| Shutdown | `SIGTERM` or Ctrl-C cancels sync sockets, then `db.close()` |

`temp/lorca-relay.db` is a **runtime** SQLite path created by `bun run relay` (`mkdir -p temp`). The directory `temp/` is gitignored; it is not a committed repository file. The binary default without that script is `lorca-relay.db` in the process working directory.

Docker `CMD` listens on `[::]:${PORT:-8787}` unless `LORCA_RELAY_BIND` is set. Clients point at the relay with `LORCA_RELAY_URL` (CLI) or Settings › Advanced › Relay URL.

:::files
crates/relay/
  Cargo.toml          # package lorca-relay, [[bin]] path = src/main.rs
  Dockerfile
  README.md
  src/
    main.rs           # Args, AppState, bind + graceful shutdown
    routes.rs         # HTTP/WS router + ApiError
    routes/tests.rs
    auth.rs           # signed requests, bearer tokens
    db.rs + db/       # Store trait; sqlite + postgres
    store.rs          # local dir or S3 for file ciphertext
    hub.rs            # in-process sync-socket signals
    limit.rs          # IP / identity / large-upload limits
    push.rs           # APNs + FCM
    metrics.rs        # GET /metrics
    sweep.rs          # housekeeping + identity delete
:::

## Options and defaults

Every flag maps to an env var (`lorca-relay --help`). Secret-bearing values hide env contents in help.

<ParamField body="--bind" type="SocketAddr" default="127.0.0.1:8787">
`LORCA_RELAY_BIND`. Listen address. Image default becomes `[::]:$PORT` when `PORT` is set.
</ParamField>

<ParamField body="--db" type="string" default="lorca-relay.db">
`LORCA_RELAY_DB`. SQLite path, or `postgres://` / `postgresql://` URL for shared multi-process storage.
</ParamField>

<ParamField body="--secret" type="string">
`LORCA_RELAY_SECRET`. HMAC material for bearer tokens (SHA-256 of the string). Unset → random per boot; every Device must re-auth after restart. Required when multiple processes share Postgres.
</ParamField>

<ParamField body="--quota-bytes" type="u64" default="5368709120">
`LORCA_RELAY_QUOTA_BYTES`. Ciphertext cap per identity (5 GiB). `0` disables.
</ParamField>

<ParamField body="--ip-per-minute" type="u32" default="60">
`LORCA_RELAY_IP_PER_MINUTE`. Public routes (register, auth, pairing mailbox). Burst equals the rate. `0` disables.
</ParamField>

<ParamField body="--identity-per-second" type="u32" default="50">
`LORCA_RELAY_IDENTITY_PER_SECOND`. Authenticated routes; burst is 10×. `0` disables.
</ParamField>

<ParamField body="--min-protocol" type="u32" default="0">
`LORCA_RELAY_MIN_PROTOCOL`. Clients with `Lorca-Protocol` below this get `426` on `/v1/*` except `/v1/health`. Missing header counts as protocol `0`. Relay speaks protocol `1`.
</ParamField>

<ParamField body="--inactive-days" type="u32" default="365">
`LORCA_RELAY_INACTIVE_DAYS`. Daily sweep deletes identities with no machine seen, blob write, or open socket for this many days. `0` keeps all.
</ParamField>

<ParamField body="--metrics-token" type="string">
`LORCA_RELAY_METRICS_TOKEN`. Enables `GET /metrics` for that bearer. Unset → route returns `404`.
</ParamField>

<ParamField body="--concurrent-uploads" type="usize" default="3">
`LORCA_RELAY_CONCURRENT_UPLOADS`. Concurrent bodies over 1 MiB. Wait over 30 s → `503` + `Retry-After: 5`. `0` disables.
</ParamField>

<ParamField body="--trust-proxy" type="bool" default="false">
`LORCA_RELAY_TRUST_PROXY`. Take client IP from the last `X-Forwarded-For` hop. Set only behind a proxy that overwrites that header.
</ParamField>

### File ciphertext

| Flag / env | Default / notes |
| --- | --- |
| `--files-dir` / `LORCA_RELAY_FILES_DIR` | DB path with `.files` extension (`lorca-relay.files`); conflicts with `--s3-bucket` |
| `--s3-bucket` / `LORCA_RELAY_S3_BUCKET` | Requires `--s3-endpoint` |
| `--s3-endpoint` / `LORCA_RELAY_S3_ENDPOINT` | R2 / S3 / MinIO endpoint URL |
| `--s3-region` / `LORCA_RELAY_S3_REGION` | `auto` |
| `--s3-prefix` / `LORCA_RELAY_S3_PREFIX` | empty |
| `--s3-access-key` / `LORCA_RELAY_S3_ACCESS_KEY` | Falls back to `AWS_ACCESS_KEY_ID` |
| `--s3-secret-key` / `LORCA_RELAY_S3_SECRET_KEY` | Falls back to `AWS_SECRET_ACCESS_KEY` |

With a `postgres://` DB and no `--files-dir`, the local files dir still defaults beside `lorca-relay.db` (not the URL).

### Push

| Flag / env | Notes |
| --- | --- |
| `--apns-key` / `LORCA_RELAY_APNS_KEY` | `.p8` text or file path; requires key id + team id |
| `--apns-key-id` / `LORCA_RELAY_APNS_KEY_ID` | 10-character Apple key id |
| `--apns-team-id` / `LORCA_RELAY_APNS_TEAM_ID` | Apple team id |
| `--apns-topic` / `LORCA_RELAY_APNS_TOPIC` | Bundle id; default `app.lorca` |
| `--fcm-service-account` / `LORCA_RELAY_FCM_SERVICE_ACCOUNT` | Firebase JSON text or file path |

`LORCA_RELAY_APNS_URL` and `LORCA_RELAY_FCM_URL` redirect push clients to a test server (env-only, not CLI flags).

## Constants that bound requests

| Constant | Value | Role |
| --- | --- | --- |
| `PROTOCOL` | `1` | Reported in `/v1/health`; group paging + `DELETE /v1/identity` |
| `TOKEN_TTL` | 3600 s | Bearer lifetime |
| `CHALLENGE_TTL` | 120 s | Auth challenge lifetime |
| `PAIRING_TTL` | 600 s | Pairing mailbox lifetime |
| `SIGNED_REQUEST_SKEW` | 300 s | Max clock skew on signed identity requests |
| `MAX_BLOB_BYTES` | 4 MiB | Inline blob ciphertext |
| `MAX_FILE_BLOB_BYTES` | ~100 MiB + 40 | Attachment upload body limit |
| `MAX_BODY_BYTES` | 6 MiB | Default JSON body limit |
| `MAX_PUSH_BYTES` | 2560 | Push ciphertext |
| `LARGE_UPLOAD` | 1 MiB | Semaphore threshold |
| `PING_SECONDS` | 25 | Sync WebSocket ping interval |

Blob kinds: `roster`, `chat`, `job`, `job_cancel`, `job_result`, `request`, `response`, `machine`, `credentials`, `key`, `file`. Kind `file` must use `/v1/files/{id}`.

## HTTP surface

| Method | Path | Auth |
| --- | --- | --- |
| `GET` | `/` | none |
| `GET` | `/v1/health` | none |
| `GET` | `/metrics` | bearer = `--metrics-token` |
| `POST` | `/v1/identities` | signed identity payload |
| `POST` | `/v1/auth/challenge` | none (IP-limited) |
| `POST` | `/v1/auth/verify` | machine signature over nonce |
| `GET` | `/v1/sync` | bearer → WebSocket |
| `DELETE` | `/v1/identity` | bearer |
| `GET` / `DELETE` | `/v1/machines`, `/v1/machines/{machine_pubkey}` | bearer |
| `GET` / `PUT` | `/v1/blobs` | bearer |
| `GET` / `DELETE` | `/v1/blobs/{id}` | bearer |
| `GET` / `PUT` | `/v1/files/{id}` | bearer; PUT needs `application/octet-stream` |
| `DELETE` | `/v1/groups/{group}` | bearer |
| `GET` | `/v1/groups/{group}/blobs` | bearer |
| `POST` | `/v1/push` | bearer |
| `PUT` / `DELETE` | `/v1/push/token` | bearer |
| `POST` / `DELETE` | `/v1/pair`, `/v1/pair/{nonce}` | bearer (owner) |
| `POST` / `GET` | `/v1/pair/{nonce}/request` | join posts without auth; owner GETs |
| `POST` / `GET` | `/v1/pair/{nonce}/reply` | owner posts; join GETs without auth |

<RequestExample>
```bash
curl -s http://127.0.0.1:8787/v1/health
```
</RequestExample>

<ResponseExample>
```json
{"ok":true,"service":"lorca-relay","protocol":1}
```
</ResponseExample>

<RequestExample>
```http
PUT /v1/files/att-1?group=chat-abc HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/octet-stream

<binary ciphertext>
```
</RequestExample>

<ResponseExample>
```json
{"id":"att-1","seq":42,"existing":false}
```
</ResponseExample>

## Error responses

JSON errors use `{ "error": "<message>" }` unless noted. Rate limits add `Retry-After`.

| Status | When |
| --- | --- |
| `400` | Bad base64url/key/signature shape, unknown kind, invalid id/slot/group, empty/oversized ciphertext, wrong blob route for `file`, bad push platform/token |
| `401` | Missing/malformed/expired bearer, bad signature, unknown/expired challenge, signed request too old |
| `403` | Pairing not owned by caller (`Not your pairing`) |
| `404` | Unknown machine/blob/file; metrics when token unset; missing file object (`The file is no longer stored`); unknown/expired pairing |
| `409` | Identity content key mismatch; group already deleted; pairing already has a request |
| `410` | Machine unpaired / revoked |
| `413` | Storage quota exceeded |
| `415` | File upload without `application/octet-stream` |
| `426` | Client protocol below `--min-protocol` — body includes `min_protocol` and `protocol` |
| `429` | IP or identity rate limit (`Too many requests` + `Retry-After`) |
| `503` | Large-upload semaphore wait timed out (`Too many uploads at once; try again`) |
| `500` | Database / internal failures (`Database error`) |
| `204` | Successful deletes / push-token updates / pairing posts with no body |

Startup failures (missing S3 keys, unreadable APNs/FCM key path, bind/DB open errors) exit the process via `anyhow` before serving.

Postgres with a self-signed peer and web PKI roots fails TLS unless the URL includes `sslmode=disable` (common on Railway private networking).

## Storage and multi-process

```text
Client ──HTTP/WS──► lorca-relay (axum)
                      │
                      ├─ db::Store ──► SQLite file  OR  Postgres (+ LISTEN/NOTIFY)
                      ├─ FileStore ──► local .files dir  OR  S3/R2/MinIO
                      ├─ Hub (this process) ──► sync socket signals
                      └─ Pusher ──► APNs / FCM (optional)
```

- **SQLite**: one process owns the file.
- **Postgres**: many processes share DB + must share `--secret` and an S3 bucket for files. Without `--secret`, tokens from one process fail on another. Without S3, other hosts cannot read attachment objects.
- Housekeeping: `Store::tick` every 60 s; `sweep` hourly after a 10-minute delay (stale sealed envelopes, deleted-group marks) and daily (inactive identities, usage recount, orphan file objects older than 1 day).

## Related pages

<CardGroup>
  <Card title="HTTP API reference" href="/http-api-reference">
    Route-level request and response shapes across the product surface.
  </Card>
  <Card title="Architecture" href="/architecture">
    Identity, pairing, and zero-knowledge relay protocol.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env vars and defaults across CLI, relay, and clients.
  </Card>
  <Card title="Deployment and operations" href="/deployment">
    Docker, Railway, Postgres, and S3 runbooks.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Client that talks to this relay (`LORCA_RELAY_URL`, sync session).
  </Card>
</CardGroup>

Next: run `cargo run -q -p lorca-relay -- --bind 127.0.0.1:8787 --db lorca-relay.db` and `curl -s http://127.0.0.1:8787/v1/health`.

---

## 09. crates/agent reference

> Reference for the code under `crates/agent`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/09-crates-agent-reference.md
- Generated: 2026-09-26T20:16:25.690Z

### Source Files

- `crates/agent/src/lib.rs`
- `docs/agent/harness.md`
- `docs/agent/providers.md`

---
title: "crates/agent reference"
description: "Reference for the code under `crates/agent`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`lorca-agent` (`crates/agent`) is Lorca’s Rust agent runtime: stream one assistant turn, run tool calls, drain steering/follow-up queues, and emit lifecycle events until the model stops. Depend on the Cargo package `lorca-agent`; import as `lorca_agent`.

```toml
lorca-agent = { path = "crates/agent" }
```

Smoke run (about one command once a key is set):

```bash
DEEPSEEK_API_KEY=… cargo run -p lorca-agent --example chat -- deepseek/deepseek-flash
```

## Surfaces

| Surface | Type | Role |
| --- | --- | --- |
| `agent_loop` / `run_agent_loop*` | functions | Stateless turn loop over an `AgentContext` |
| `Agent` | struct | Owns transcript + queues between prompts |
| `Provider` | trait | Model adapter → `AssistantEvent` stream |
| `Tool` | trait | Model-callable function with JSON Schema args |
| `AgentHarness` | struct | General agent: skills, templates, compaction, retry, hooks |

```text
Host (CLI / harness / your app)
        │
        ▼
 Agent / AgentHarness / run_agent_loop
        │
        ├─► Provider.stream(ModelRequest) ──► AssistantEvents
        ├─► Tool.execute(...)              ──► ToolResult
        └─► mpsc / EventBus                ──► AgentEvent / HarnessEvent
```

## Crate layout

:::files
crates/agent/
  Cargo.toml          # package name: lorca-agent
  examples/chat.rs    # terminal harness demo
  src/
    lib.rs            # re-exports
    agent.rs          # Agent, AgentOptions, AgentError
    agent_loop.rs     # loop, LoopHooks, LoopError
    harness/          # AgentHarness, factory, skills, templates
    providers/        # Anthropic, OpenAI-compat, ChatGPT, Grok
    tools/            # read, write, edit, bash, grep, find, ls
    compaction.rs
    retry.rs
    request.rs
    types.rs
:::

## Public exports (`lib.rs`)

### Core re-exports

| Export | Kind |
| --- | --- |
| `Agent`, `AgentError`, `AgentHandle`, `AgentMessageQueue`, `AgentOptions`, `QueueMode` | Agent API |
| `agent_loop`, `run_agent_loop`, `run_agent_loop_continue` | Loop entry points |
| `AgentContext`, `AgentLoopConfig`, `LoopHooks`, `NoHooks`, `EventSink` | Loop config/hooks |
| `ToolExecutionMode`, `TurnUpdate`, before/after tool contexts | Loop control |
| `Provider`, `ModelRequest`, `AssistantEvent`, `AssistantEventStream`, `ToolSpec` | Provider contract |
| `Tool`, `ToolError`, `ToolResult`, `ToolUpdateFn` | Tool contract |
| `AgentEvent`, `AgentMessage`, `AssistantMessage`, `StopReason`, `ThinkingLevel`, `Usage`, … | Transcript + events |
| `RequestOptions`, `RequestOptionsPatch`, `RequestHooks`, `ResponseInfo` | Per-call request settings |
| `RetryPolicy`, `is_retryable_error`, `is_context_overflow` | Retry helpers |
| `ModelInfo` (via `models`) | Catalog snapshot |
| `now_ms()` | Unix epoch ms |

### Modules (not all re-exported at crate root)

`harness`, `providers`, `tools`, `compaction`, `estimate`, `transform`, `schema`, `json`, `sse`, `login_shell`, `models`, `retry`, `request`.

Host code in `crates/cli` imports `lorca_agent` and wires providers/tools into turns; the CLI feature flag is `runner` → `dep:lorca-agent`.

## Invoke: three paths

### 1. `Agent` (multi-prompt conversation)

```rust
use std::sync::Arc;
use lorca_agent::providers::AnthropicProvider;
use lorca_agent::tools::coding_tools;
use lorca_agent::{Agent, AgentEvent, AgentOptions};
use tokio::sync::mpsc;

let provider = AnthropicProvider::deepseek(&std::env::var("DEEPSEEK_API_KEY")?, None);
let mut options = AgentOptions::new(Arc::new(provider));
options.system_prompt = "You are a concise coding assistant.".into();
options.tools = coding_tools(std::env::current_dir()?);
let mut agent = Agent::new(options);

let (tx, mut rx) = mpsc::channel(256);
tokio::spawn(async move { while rx.recv().await.is_some() {} });

let added = agent.prompt("Summarize README.md", tx).await?;
// agent.messages holds the full transcript
```

`prompt` accepts `&str`, `String`, `Vec<ContentPart>`, one `AgentMessage`, `Vec<AgentMessage>`, or `PromptInput::with_images(...)`.

Drain the event channel on another task. A held-but-unread receiver stalls the run when the buffer fills; a dropped receiver is fine.

### 2. Low-level loop (host owns transcript)

```rust
use lorca_agent::{agent_loop, AgentContext, AgentLoopConfig, AgentMessage};
use tokio_util::sync::CancellationToken;

let context = AgentContext {
    system_prompt: "...".into(),
    messages: history,
    tools,
    cache_points: vec![],
};
let config = AgentLoopConfig::new(provider);
let cancel = CancellationToken::new();
let (rx, join) = agent_loop(vec![AgentMessage::user("hi")], context, config, cancel);
let produced = join.await?;
```

### 3. `AgentHarness` (skills, compaction, model switch)

```rust
use std::sync::Arc;
use lorca_agent::harness::{
    build_system_prompt, load_skills, AgentHarness, EnvProviderFactory,
    HarnessOptions, ModelIdentity, ProviderFactory, SystemPromptParts,
};
use lorca_agent::tools::coding_tools;

let factory: Arc<dyn ProviderFactory> = Arc::new(EnvProviderFactory::new());
let provider = factory.provider(&ModelIdentity::parse("deepseek/deepseek-flash"), None)?;
let cwd = std::env::current_dir()?;
let skills = load_skills(&[cwd.join("skills")]);

let mut options = HarnessOptions::new(provider);
options.factory = Some(factory);
options.system_prompt = build_system_prompt(&SystemPromptParts {
    cwd: Some(&cwd),
    coding_tools: true,
    skills: &skills.skills,
    ..Default::default()
});
options.tools = coding_tools(cwd);
let mut harness = AgentHarness::new(options);

let result = harness.prompt("Summarize README.md").await?;
// result.outcome: Completed | Aborted | Failed { error } | Declined
```

Also: `harness.skill(name, extra)`, `harness.prompt_from_template(name, args)`, `harness.continue_run()`, `harness.compact(instructions)`.

## Options and defaults

### `AgentOptions`

| Field | Default |
| --- | --- |
| `provider` | required (`AgentOptions::new`) |
| `system_prompt` | `""` |
| `tools` | `[]` |
| `messages` | `[]` |
| `hooks` | `NoHooks` |
| `steering_mode` | `QueueMode::OneAtATime` |
| `follow_up_mode` | `QueueMode::OneAtATime` |
| `tool_execution` | `ToolExecutionMode::Parallel` |
| `retry` | `None` (one try) |
| `request` | empty `RequestOptions` |

### `HarnessOptions`

| Field | Default |
| --- | --- |
| `provider` | required |
| `factory` | `None` (`set_model` / `set_thinking_level` fail without it) |
| `thinking_level` | `None` |
| `system_prompt` | `""` |
| `tools` / `active_tools` | `[]` / `None` (all tools visible) |
| `resources` | empty skills + templates |
| `request` | empty |
| `retry` | `RetryPolicy::default()` — enabled, 3 retries, 1s base, 60s cap |
| `compaction` | enabled; `reserve_tokens: 16384`; `keep_recent_tokens: 20000`; `max_input_tokens: 0` |
| `steering_mode` / `follow_up_mode` | `QueueMode::All` |
| `tool_execution` | `Parallel` |
| `messages` | `[]` |

<Warning>
`Agent` defaults queue drain to `OneAtATime`. `AgentHarness` defaults to `All`. Match the host you are using before assuming drain behavior.
</Warning>

### `AgentLoopConfig`

| Field | Default |
| --- | --- |
| `hooks` | `NoHooks` |
| `tool_execution` | `Parallel` |
| `sink` | `None` |
| `retry` | `None` |
| `request` | empty |

### `RequestOptions`

| Field | Default | Effect |
| --- | --- | --- |
| `headers` | empty | Merged over adapter headers |
| `timeout` | `None` | Whole-request timeout |
| `session_id` | `None` | Sent as `x-session-affinity` / `prompt_cache_key` where supported |
| `metadata` | empty | Anthropic forwards `user_id` |
| `hooks` | `None` | `api_key`, `before_payload`, `after_response` |

### `RetryPolicy`

```rust
RetryPolicy { enabled: true, max_retries: 3, base_delay_ms: 1_000, max_delay_ms: Some(60_000) }
```

HTTP adapters also retry pre-stream failures (408/409/429/5xx/transport) with `max_retries` typically `2` and `DEFAULT_MAX_RETRY_DELAY_MS = 60_000`.

### `CompactionSettings`

Compaction runs when `context_tokens > window - reserve_tokens` (window must be known and non-zero).

## Handles while a run is active

### `AgentHandle`

| Method | Effect |
| --- | --- |
| `steer(message)` | Inject after current turn’s tools, before next model call |
| `follow_up(message)` | Inject when the run would otherwise stop |
| `abort()` | Cancel active run → `StopReason::Aborted` |
| `clear_*_queue()` / `clear_all_queues()` | Drop queued messages |
| `has_queued_messages()` / `is_running()` | Status |

### `HarnessHandle`

Same steering/follow-up/abort idea, plus `next_run(message)` (new run after current ends), `cancel_queued(id)`, and `queued()`. Each queue call returns a message id and emits `queue_update`.

## Events

### `AgentEvent` (loop / `Agent`)

Tagged `type` when serialized: `agent_start`, `agent_end`, `turn_start`, `turn_end`, `retry`, `message_start`, `message_update`, `message_end`, `tool_execution_start`, `tool_execution_update`, `tool_execution_end`.

### `HarnessEvent`

Adds `run_start` / `run_end`, `retry_scheduled`, `config_update`, `compaction_start` / `compaction_end`, `usage`, `handler_error`, plus renamed tool events (`tool_start` / `tool_update` / `tool_end`).

## Providers

`Provider::stream` never returns `Err`. Failures are `AssistantEvent::Error { message, aborted }`.

| Adapter | Auth | Default model | Notes |
| --- | --- | --- | --- |
| `AnthropicProvider::anthropic` | `x-api-key` | `claude-opus-5` | Messages API; server web search/fetch |
| `AnthropicProvider::deepseek` | `x-api-key` | `deepseek-flash` | `https://api.deepseek.com/anthropic` |
| `OpenAiCompatProvider` | bearer | (required) | `/chat/completions` |
| `OpenAiResponsesProvider` | bearer | (required) | API-key `/responses` |
| `ChatGptProvider` | OAuth `TokenSource` | `gpt-6-sol` | Codex responses backend |
| `GrokProvider` | OAuth `GrokTokenSource` | `grok-4.7` | `https://api.x.ai/v1` |

### `EnvProviderFactory` credentials

| Provider id | Key / tokens | Base URL override |
| --- | --- | --- |
| `deepseek` | `DEEPSEEK_API_KEY` | `DEEPSEEK_BASE_URL` (appends `/anthropic` if missing) |
| `anthropic` | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` |
| `openai` | `OPENAI_API_KEY` | `OPENAI_BASE_URL` (default `https://api.openai.com/v1`) |
| `chatgpt` | `with_chatgpt(TokenSource)` | — |
| `grok` | `with_grok(GrokTokenSource)` | `GROK_BASE_URL` |

`ModelIdentity::parse("provider/model")` splits on the first `/`.

`ThinkingLevel`: `off` | `minimal` | `low` | `medium` | `high` | `xhigh` | `max`. Adapters map to provider-native effort/budget knobs.

## Tools

`coding_tools(cwd)` returns seven tools bound to one working directory: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`. Relative paths and `~` resolve via `resolve_to_cwd`.

`coding_tools_with_sessions(cwd, sessions)` uses a shared bash session and, on Unix, adds `bash_input` / `bash_output`.

Implement `Tool`: `name`, `description`, `parameters` (JSON Schema), `execute(...) -> Result<ToolResult, ToolError>`. Prefer `Err(ToolError)` over encoding failure in content. `ToolResult::terminating()` hints the loop to stop only when every tool in the batch sets it.

## Errors

### `AgentError`

| Variant | When |
| --- | --- |
| `Busy` | `prompt` / `continue_run` while a run is active — use `steer` / `follow_up` / wait |
| `NoMessages` | `continue_run` with empty transcript |
| `LastIsAssistant` | Transcript ends on assistant and neither steering nor follow-up is queued |

### `LoopError`

| Variant | When |
| --- | --- |
| `Empty` | Continue with no messages in context |
| `LastIsAssistant` | Continue from an assistant message without queued injects |

### `HarnessError`

| Variant | When |
| --- | --- |
| `Busy` | Run already active |
| `NoMessages` / `LastIsAssistant` | Same continue rules as `Agent` |
| `UnknownSkill` / `UnknownTemplate` | Name missing from `resources` |
| `NoFactory` | `set_model` / `set_thinking_level` without `factory` |
| `Provider(String)` | Factory/build failure (missing key, unknown provider, …) |
| `Compaction(String)` / `NothingToCompact` | Manual/auto compaction failure or empty cut |

### Provider / stream failures (not Rust `Err`)

| Case | Result |
| --- | --- |
| HTTP/auth/malformed response | `AssistantEvent::Error { aborted: false }` → `StopReason::Error` |
| Cancellation | `Error { aborted: true }` → `StopReason::Aborted` |
| Stream ends without terminal event | Loop synthesizes error (`Provider stream ended before completion`) or aborted |
| Transient pre-stream failure with retry | `AgentEvent::Retry` / `HarnessEvent::RetryScheduled`, then retry |
| Context overflow (harness) | Compact + one retry of the turn |

`is_retryable_error` matches overloaded/rate-limit/5xx/transport patterns and never quota/billing exhaustion.

`ToolError(String)` — tool execution failure; schema validation rejects bad args before `execute`.

## Stop reasons

| `StopReason` | Meaning |
| --- | --- |
| `Stop` | Normal end |
| `Length` | Hit max tokens / incomplete |
| `ToolUse` | Assistant emitted tool calls |
| `Error` | Provider/runtime failure |
| `Aborted` | Cancellation |

## Verification

1. Unit/integration: `cargo test -p lorca-agent`
2. Live DeepSeek (ignored): `DEEPSEEK_API_KEY=… cargo test -p lorca-agent live_deepseek -- --ignored --nocapture`
3. Interactive: `cargo run -p lorca-agent --example chat -- deepseek/deepseek-flash` — expect streamed text and `[tool]` lines

## Related pages

<CardGroup>
  <Card title="Architecture" href="/architecture">
    How agent, CLI, relay, and clients bound each other.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    How the CLI hosts `lorca_agent` for turns, tools, and providers.
  </Card>
  <Card title="crates/provider-auth reference" href="/ref-crates-provider-auth">
    OAuth token helpers used by ChatGPT and Grok adapters.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env keys and runtime settings the host layers on this crate.
  </Card>
</CardGroup>

Next: open `crates/agent/examples/chat.rs` and run `cargo run -p lorca-agent --example chat -- deepseek/deepseek-flash` with `DEEPSEEK_API_KEY` set.

---

## 10. crates/cli reference

> Reference for the code under `crates/cli`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/10-crates-cli-reference.md
- Generated: 2026-09-26T20:20:46.013Z

### Source Files

- `web/content/docs/cli.mdx`
- `crates/cli/src/main.rs`
- `crates/cli/src/lib.rs`

---
title: "crates/cli reference"
description: "Reference for the code under `crates/cli`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

Run `lorca serve` (or plain `lorca`) to bind `ws://127.0.0.1:<port>/ws`. The package under `crates/cli` is both the Device/Runner library (crate name `lorca`, entry `crates/cli/src/lib.rs`) and the `lorca` binary (`crates/cli/src/main.rs`, feature `cli`).

## Package and features

| Item | Value |
| --- | --- |
| Package | `lorca` (`crates/cli/Cargo.toml`) |
| Library | `crates/cli/src/lib.rs` → crate name `lorca` |
| Binary | `crates/cli/src/main.rs` → `lorca` (`required-features = ["cli"]`) |
| Default features | `cli` |
| Workspace version | from root `Cargo.toml` workspace package |

| Feature | Pulls in | Role |
| --- | --- | --- |
| `cli` | `runner`, `server`, `usage`, `tracing-subscriber` | Full command-line binary |
| `runner` | `provider-auth`, `lorca-agent`, MCP deps | Bots, tools, providers |
| `server` | `axum` | Local websocket for the app |
| `provider-auth` | `lorca-provider-auth` | Connect/disconnect providers without the agent loop |

Phone builds link the core through `lorca-mobile` with `default-features = false` and `provider-auth` only (no `cli` binary surface).

## Invoke the binary

Default command when none is given: `serve`.

```bash
lorca
lorca serve
lorca --home ~/.lorca --port 4862 status
```

Logging goes to **stderr**. Default filter when `RUST_LOG` is unset: `lorca=info,lorca_agent=info`.

### Global options

<ParamField body="home" type="path" optional>
Long flag `--home`. Env `LORCA_HOME`. Default `~/.lorca` (or `./.lorca` if no home directory). Data directory for keys, DB, credentials, plugins, workspaces.
</ParamField>

<ParamField body="port" type="u16" optional>
Long flag `--port`. Env `LORCA_PORT`. Default `4862`. Local websocket bind port (`127.0.0.1` only).
</ParamField>

Unknown flags error (`unknown_flags = "error"` on the CLI derive).

## Commands

| Command | Purpose |
| --- | --- |
| `serve` | Local API + sync + agent runtime (default) |
| `identity new\|restore\|show` | Create, restore, or print this Device’s identity |
| `pair [string]` | Start pairing or join with a pairing string |
| `provider set\|remove\|list` | Connect, disconnect, or list providers |
| `status` | Pretty-printed JSON snapshot |
| `doctor` | Local health checks (always exits successfully after printing) |

### `lorca serve`

```bash
lorca serve [--parent-pid <pid>] [--ready-stdout]
```

<ParamField body="parent_pid" type="u32" optional>
`--parent-pid`. When that process exits, the CLI shuts down shell sessions and exits `0`. Used by the desktop app so a killed UI does not leave a stale server on the port.
</ParamField>

<ParamField body="ready_stdout" type="bool" optional>
`--ready-stdout`. After bind succeeds, writes one JSON line to stdout and flushes: `{"event":"ready","port":<u16>}`. Logs stay on stderr.
</ParamField>

On start, `serve`:

1. Primes bot names and resumes sent jobs
2. Closes stale shell rows left by a previous quit
3. On Unix, installs SIGTERM/SIGINT/SIGHUP handlers that shut down shell sessions before re-raising the signal
4. Refreshes installed marketplace plugins against the bundled index
5. Spawns login-shell environment capture, relay sync, and routines
6. Binds `127.0.0.1:<port>` and serves `/` (`"lorca"`) and `/ws`

<RequestExample>
```bash
lorca serve --ready-stdout --port 0 --home /tmp/lorca-test
```
</RequestExample>

<ResponseExample>
```json
{"event":"ready","port":54321}
```
</ResponseExample>

Port `0` lets the OS assign a free port; the readiness line reports the actual port. A failed bind exits without writing readiness (stdout stays empty).

### `lorca identity`

```bash
lorca identity new [--name <device-name>]
lorca identity restore <phrase...> [--name <device-name>]
lorca identity show
```

| Subcommand | Success signal | Failure |
| --- | --- | --- |
| `new` | Prints backup phrase (base32 lowercase, no padding, groups of four → 13 groups for a 32-byte master). Creates lead bot `Chef` on this Runner. | Already has identity: remove `LORCA_HOME` first |
| `restore` | `Identity restored. Run lorca serve to sync.` | Needs relay URL; phrase must unwrap DEK from relay; fails if identity already present |
| `show` | Prints identity id, keys, device name/os, master-hold flag, registered flag | Prints guidance to run `identity new` when empty |

`new` without a relay prints a note to set `LORCA_RELAY_URL`. With a relay, it runs one outbox flush (8s timeout).

### `lorca pair`

```bash
lorca pair                          # host: print string, wait
lorca pair '<lorca://pair?…>' [--name <device-name>]
```

| Mode | Behavior |
| --- | --- |
| No argument | Prints a pairing string, polls until `completed` or `failed`, then flushes outbox once |
| With string | Joins as a Device; prints `Paired as …. Run lorca serve to sync.` |

Failed host wait bails with the status error string (or `pairing failed`).

### `lorca provider`

```bash
lorca provider set <kind> [api-key] [--base-url <url>]
lorca provider remove <kind>
lorca provider list
```

Kinds: `deepseek`, `anthropic`, `opencode`, `opencode-go`, `chatgpt`, `grok`.

| Kind group | Connect flow |
| --- | --- |
| API-key kinds | Key from argv or stdin; optional `--base-url` |
| `chatgpt`, `grok` | Browser sign-in only; API key / `--base-url` bail |

When `lorca serve` is listening on the configured port, provider mutations go over `ws://127.0.0.1:<port>/ws` so the live app updates immediately. Otherwise the CLI dispatches locally and flushes the outbox once.

### `lorca status` / `lorca doctor`

- `status` — pretty-printed JSON from `App::snapshot()`
- `doctor` — prints ✔/✘ lines for home, identity, port, relay, providers; always returns success after printing

## Local HTTP / websocket surface

Bound address: `127.0.0.1:<port>` only (`crates/cli/src/ws.rs`).

| Route | Behavior |
| --- | --- |
| `GET /` | Plain text `lorca` |
| `GET /ws` | JSON-RPC + push events |

Request shape: `{ "id", "method", "params" }` → `{ "id", "result" }` or `{ "id", "error": { "message" } }`. Push events: `{ "event", "data" }` (see `crates/cli/src/events.rs`).

## Environment variables

| Variable | Role |
| --- | --- |
| `LORCA_HOME` | Data directory (default `~/.lorca`) |
| `LORCA_PORT` | Local bind port (default `4862`) |
| `LORCA_RELAY_URL` | Relay URL; wins over `settings.json` |
| `LORCA_DEFAULT_RELAY_URL` | Fallback when nothing else names a relay |
| `LORCA_DEV` | Non-empty and not `0`: use LAN `http://<lan-ip>:8787` when no relay is set |
| `LORCA_SHELL` | Shell for bot commands (read in `lorca-agent` bash tool; default `/bin/bash` or Git for Windows bash) |
| `LORCA_*_MODEL` | Default model overrides (`DEEPSEEK`, `ANTHROPIC`, `OPENCODE`, `OPENCODE_GO`, `CHATGPT`, `GROK`) |
| `LORCA_*_BASE_URL` | Provider API roots when credential has none |
| `LORCA_GROK_ISSUER` | Grok OAuth issuer override |
| `LORCA_MARKETPLACE_URL` | Extra marketplace index beside the bundled one |
| `LORCA_COMPACTION` | `0` disables compaction |
| `LORCA_MEMORY_FLUSH` | `0` disables memory flush before compaction |
| `LORCA_OAUTH_NO_BROWSER` | `1` skips opening a browser for OAuth |
| `RUST_LOG` | Tracing filter (stderr) |

Relay resolution order: `LORCA_RELAY_URL` → `settings.json` `relay_url` → machine pairing URL → `LORCA_DEV` LAN relay → `LORCA_DEFAULT_RELAY_URL`.

## Home directory layout

Under `LORCA_HOME` (mode `0700` on Unix; private JSON files `0600`):

:::files
~/.lorca/
  identity.json      # master secret (identity device)
  machine.json       # this Device’s machine keys + metadata
  credentials.json   # account provider credentials
  settings.json      # relay_url, marketplace_url
  lorca.sqlite3      # local store
  files/             # attachments
  plugins/           # installed plugins + secrets
  workspaces/<bot>/  # per-bot workspace and memory
:::

Dev desktop builds often use `LORCA_HOME=~/.lorca-dev` and port `4863`.

## Library surface (`lorca`)

`crates/cli/src/lib.rs` exports modules used by the binary, mobile bindings, and tests.

| Module | Role |
| --- | --- |
| `api` | JSON-RPC method dispatch for `/ws` |
| `app` | Loaded Device state, snapshot, relay helpers |
| `config` | `Config`, `Settings`, paths, defaults |
| `credentials` | `PROVIDER_KINDS`, connect/merge/save |
| `identity`, `keys`, `pairing`, `crypto` | Identity and pairing |
| `ws` | Local axum server (`server` feature) |
| `sync`, `relay`, `runtime`, `routines` | Relay sync and Runner loops |
| `providers`, `shell`, `turns`, `local_review` | Runner-only (`runner` feature) |
| `provider_auth` | Optional connect flows (`provider-auth` feature) |

## Error cases

| Situation | Behavior |
| --- | --- |
| Port already bound | `serve` fails: `cannot bind 127.0.0.1:<port>: …. Is another lorca serve running?`; no readiness line |
| Unknown provider kind | Bail: `Unknown provider … Use one of: deepseek, anthropic, …` |
| API key on chatgpt/grok | Bail: `<kind> connects with a browser sign-in and takes no API key` |
| Identity already exists | `identity new` / `restore` bail |
| Restore without relay | Bail: set a relay URL first |
| Pairing failed | Bail with status error string |
| Live serve closed mid-RPC | Bail: `lorca serve closed the connection` |
| Serve RPC error | Mapped from websocket `error.message` |

Provider CLI changes against a running serve keep the app’s in-memory view in sync; offline changes sync on the next `serve` outbox flush.

## Related pages

<CardGroup>
  <Card title="CLI reference" href="/cli-reference">
    Product-facing commands, install scripts, and Runner setup.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Config keys, env vars, and where each value is read.
  </Card>
  <Card title="crates/relay reference" href="/ref-crates-relay">
    Relay binary, storage backends, and HTTP routes.
  </Card>
  <Card title="crates/agent reference" href="/ref-crates-agent">
    Agent loop, tools, and provider adapters used by Runners.
  </Card>
  <Card title="Architecture" href="/architecture">
    App ↔ CLI ↔ relay process boundaries.
  </Card>
</CardGroup>

Next: run `lorca doctor` and confirm every line shows ✔ (or fix the ✘ lines it prints).

---

## 11. mobile reference

> Reference for the code under `mobile`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/11-mobile-reference.md
- Generated: 2026-09-26T20:19:59.943Z

### Source Files

- `mobile/modules/lorca-core/index.ts`
- `mobile/index.ts`
- `crates/cli/src/api.rs`

---
title: "mobile reference"
description: "Reference for the code under `mobile`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`mobile/` is the Expo (React Native, expo-router) iOS/Android Device app. It links `crates/mobile` (`lorca-mobile`) through the Expo module at `mobile/modules/lorca-core`, then speaks the same JSON API as the desktop CLI (`crates/cli/src/api.rs`) over in-process UniFFI calls. A phone is a Device (`os`: `ios` / `ipados` / `android`), never a Runner.

```text
mobile/index.ts  →  expo-router  →  engine.start()
                                       │
         mobile/modules/lorca-core  →  LorcaCore native  →  Core (UniFFI)
                                       │                      │
                                  onEvent / request      lorca::api::dispatch
                                       │                      │
                                  zustand store          relay sync loop
```

## Layout

:::files
mobile/
  index.ts                      # crypto polyfill + expo-router entry
  app.config.ts                 # Expo config (dev vs prod ids, plugins)
  app/                          # expo-router screens
  app/_layout.tsx               # engine.start(); paired vs pair screens
  modules/lorca-core/           # Expo native module + UniFFI build
  modules/lorca-core/index.ts   # start, request, onEvent, wake, setOpenChat
  modules/lorca-core/build.ts   # bun run core → ios/android native libs
  src/core/                     # engine, store, prefs, push, pairing, model
  src/ui/                       # screens and shared widgets
  targets/notify/               # iOS notification service extension
  plugins/                      # Expo config plugins
:::

| Path | Role |
| --- | --- |
| `crates/mobile` | UniFFI `Core`: `start`, `request`, `wake`, `push_key`, `push_open` |
| `mobile/modules/lorca-core` | Expo module `LorcaCore` + native UI view packages |
| `mobile/src/core/engine.ts` | App lifecycle, API verbs, event → store |
| `mobile/src/core/prefs.ts` | App-local prefs + core home directory |
| `mobile/app/_layout.tsx` | Calls `engine.start()`; gates paired vs pair screens |

## Exports: `lorca-core`

Package name: `lorca-core` (`mobile/modules/lorca-core/package.json`). Main: `mobile/modules/lorca-core/index.ts`.

### Core bridge

| Export | Signature | Behavior |
| --- | --- | --- |
| `start` | `(home: string, facts: HostFacts) => void` | Starts native `Core` once with home folder and host facts |
| `request` | `<T>(method: string, params?: unknown) => Promise<T>` | JSON API call; throws `Error(message)` on `{ error }` |
| `onEvent` | `(listener: (frame: Frame) => void) => () => void` | Subscribes to `{ event, data }` frames; returns unsubscribe |
| `wake` | `() => void` | Foreground sync nudge (skips relay backoff) |
| `setOpenChat` | `(chatId: string \| null) => void` | Android only: suppress pushes for the open chat |

```ts
export interface HostFacts {
  name: string;
  os: string;          // ios | ipados | android
  os_version: string;
  model: string;
}

export interface Frame {
  event: string;
  data: unknown;
}
```

`request` stringifies `params` (default `{}`), parses `{ result?, error?: { message } }`, and throws when `error` is present.

### Native UI views (sibling files)

These are **not** re-exported from `mobile/modules/lorca-core/index.ts`. Screens import them by file path under `mobile/modules/lorca-core/`:

| File | Export | Platforms |
| --- | --- | --- |
| `MarkdownView.tsx` | `MarkdownView`, `measureMarkdown` | iOS + Android |
| `ShimmerView.tsx` | `ShimmerView` | iOS + Android |
| `CompactWidthView.tsx` | `CompactWidthView` | iOS; Android falls back to `View` |
| `SoftScrollEdgeView.tsx` | `SoftScrollEdgeView` | iOS; Android falls back to `View` |

Registered native modules: `LorcaCoreModule`, `MarkdownViewModule`, `ShimmerViewModule` (both platforms); `CompactWidthViewModule` and `SoftScrollEdgeViewModule` (Apple only) — see `mobile/modules/lorca-core/expo-module.config.json`.

### UniFFI surface (`crates/mobile`)

| Symbol | Notes |
| --- | --- |
| `Core.start(home, name, os, os_version, model, listener)` | Loads `App` with `port: 0`, starts sync + event forwarder |
| `Core.request(method, params)` | Blocks; returns `{ "result" }` or `{ "error": { "message" } }` |
| `Core.wake()` | `app.wake_sync()` |
| `Core.push_key()` | Account push key bytes, or `None` until paired |
| `push_open(home, sealed)` | Decrypt push ciphertext without a running core (Android FCM path) |

Event lag: if the listener falls behind, the forwarder emits a fresh `snapshot` frame (same recovery as a lagging websocket client).

## Invocation

### Dev and build commands

| Command | What runs |
| --- | --- |
| `bun run mobile:dev` | `scripts/mobile.ts` — rebuild stale core/prebuild/pods/app, start Metro `:8081`, open booted simulator |
| `bun run mobile:phone` | Same loop with `--phone` (paired physical iPhone) |
| `bun run android` | `mobile` core Android build, then `expo run:android` |
| `bun run --cwd mobile core` | `mobile/modules/lorca-core/build.ts` (both platforms) |
| `bun run --cwd mobile core ios` | iOS xcframework + Swift bindings only |
| `bun run --cwd mobile core android` | Android jniLibs + Kotlin bindings only |

`mobile/modules/lorca-core/build.ts` defaults:

- Host dylib with `--features bindgen`, then UniFFI generate from `target/debug/liblorca_mobile.dylib`
- iOS: release `aarch64-apple-ios` + `aarch64-apple-ios-sim` → `ios/LorcaCore.xcframework`
- Android: `cargo ndk` for `arm64-v8a` + `x86_64` → `android/src/main/jniLibs`; Kotlin under `android/src/main/java`
- NDK: `ANDROID_NDK_HOME`, else latest under `ANDROID_HOME` / `~/Library/Android/sdk/ndk`
- Non-zero child exit → thrown `Error`; missing xcframework after iOS build → `"no xcframework"`

### App start

1. `mobile/index.ts` loads `react-native-get-random-values`, then `expo-router/entry`.
2. `mobile/app/_layout.tsx` mounts and calls `engine.start()` once.
3. `engine.start()` (`mobile/src/core/engine.ts`): subscribe `onEvent` → `core.start(coreHome(), hostFacts())` → `bootstrap` (`request("bootstrap")`) → push handlers; register pushes if paired.
4. Until `ready`, root layout returns `null`. Then `Stack.Protected` shows `(main)` when `paired`, else `pair`.

### Host facts and home

`hostFacts()` (`mobile/src/core/host.ts`):

| Field | Source |
| --- | --- |
| `os` | `ios` / `ipados` (tablet) / `android` |
| `os_version` | e.g. `iOS 18.0`, `Android 15` |
| `model` | `Device.modelName` (fallback `iPhone` / `Android`) |
| `name` | `Device.deviceName` or model |

`coreHome()` (`mobile/src/core/prefs.ts`): documents dir → `lorca-dev/core` when app id is `app.lorca.dev`, else `lorca/core`. App prefs live in sibling `prefs.json` (`dictation_lang`, `app_lang`).

### Engine API verbs (via `request`)

| Engine method | JSON method | Notes |
| --- | --- | --- |
| `bootstrap` (internal) | `bootstrap` | First snapshot; held events apply after |
| `sendMessage` | `chats.send` | attachments as `{ path, name, mime, … }` |
| `loadOlder` | `chats.messages` | `{ chat_id, before }` |
| `searchChats` | `chats.search` | `{ query, limit: 24 }` |
| `fetchFile` | `files.path` | |
| `createBot` / `updateBot` / look / avatar / runtime | `bots.create` / `bots.update` | |
| `createGroup` / rename / pin / delete / bots / owner | `chats.*` | |
| `setAutoReview` | `auto_review.set` | |
| `connectProvider` / `disconnectProvider` | `providers.connect_*` / `providers.disconnect` | ChatGPT/Grok open in-app browser |
| `answerPermission` | `chats.permission` | |
| `answerCommand` / `stopCommand` | `bash.stdin` / `bash.stop` | |
| `setRoutineEnabled` / `runRoutine` / `deleteRoutine` | `routines.*` | |
| `renameDevice` / `unpairDevice` / `unpair` | `device.rename` / `device.unpair` / `identity.forget` | |
| `pair` | `pair.accept` (+ `pair.abort` on AbortSignal) | |

Foreground: `core.wake()` (and `notify()`). Pull-to-refresh uses the same path.

## App variants

| | Production | Development |
| --- | --- | --- |
| Name | Lorca | Lorca Dev |
| App id | `app.lorca` | `app.lorca.dev` |
| Scheme | `lorca` | `lorca-dev` |
| Core folder | `lorca/core` | `lorca-dev/core` |
| App group | `group.app.lorca` | `group.app.lorca.dev` |

Set by `LORCA_MOBILE_VARIANT=development` or `EAS_BUILD_PROFILE=development` in `mobile/app.config.ts`. Dev loop forces `LORCA_MOBILE_VARIANT=development`.

## Events → store

| Event | Store effect |
| --- | --- |
| `snapshot` | `replaceSnapshot` |
| `roster.changed` | roster + drop removed chats |
| `message.added` / `updated` / `removed` | transcript |
| `chat.removed` | drop chat |
| `job.started` / `finished` / `thinking` / `retry` | working UI |
| `chat.usage` | usage |
| `relay.status` | `relayConnected`, `relayUpdateRequired`, `relayError`, `relayUrl` |
| `provider.auth` | open in-app browser |
| `identity.changed` | reset store if unpaired; else `paired: true` |
| `pair.posted` | pair progress → `waiting` (listener in `pair()`) |

During `bootstrap`, inbound events queue and apply after the snapshot returns.

## Pairing string (client-side check)

`parsePairingString` (`mobile/src/core/pairing.ts`) accepts `lorca://pair?relay=…&id=…&ek=…&n=…` (also tolerates text that contains `pair?`).

| Error | When |
| --- | --- |
| `That is not a Lorca pairing string` | Missing `lorca://pair?` / `pair?` |
| `Pairing string is missing a field` | Missing `relay`, `id`, `ek`, or `n` |

Core-side pair wait can reject with `Pairing cancelled` when `pair.abort` fires (AbortSignal on `engine.pair`). Wait timeout from the CLI pairing path: `Nobody joined within ten minutes`.

## Push

| Platform | Path |
| --- | --- |
| iOS | APNs token → `push.register` with `environment` `sandbox` or `production`; NSE `mobile/targets/notify` decrypts with app-group keychain account `push-key` |
| Android | FCM → `push.register` platform `fcm`; `PushService` calls `push_open(home, sealed)` |

`setOpenChat` / foreground flags suppress banners for the visible chat. Permission denied or missing Firebase/APNs → silent no-op (warn logged).

## Error cases

| Case | Signal | Handling |
| --- | --- | --- |
| Core not started | `"The Lorca core has not been started"` | Call `start` before `request` (iOS `NotStartedException` / Android `IllegalStateException`) |
| API error | `{ error: { message } }` → thrown `Error` | Engine methods reject / `.catch` + `console.warn` |
| Unknown method | `"unknown method {name}"` | From `api::dispatch` |
| Missing param | `"missing {key}"` / `"missing name"` | Param validation in API |
| Push register unpaired | `"No relay configured"` / `"Not paired"` | Logged in `registerForPushes` |
| Pair abort | `"Pairing cancelled"` | Expected on user cancel |
| Avatar not image | `"{name} is not an image"` | `bots.update` / create avatar path |
| Event apply failure | warn `applying {event}` | Held events continue applying |
| Prefs I/O | warn / empty object | Non-fatal |
| Provider browser cancel | `providers.auth.cancel` | On dismiss/cancel/error |
| Relay protocol refuse | `relayUpdateRequired` | Store flag; update app |
| Build script failure | non-zero exit / thrown Error | Dev loop leaves installed app unchanged |

`request` runs off the JS/UI thread (iOS global queue / Android `Dispatchers.IO`) so long waits such as `pair.accept` (up to about ten minutes in the core) do not block other calls.

## Tests

```bash
bun run --cwd mobile test
```

Covers pairing-string parse/reject, provider helpers, command-card visibility, attachment summaries, transcript preview/format helpers, and reading/push visibility behavior. Rust coverage for the UniFFI core lives in `crates/mobile/src/lib.rs` (`the_core_answers_the_api_and_forwards_events`).

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">Entry points, Device vs Runner, and high-value pages.</Card>
  <Card title="Architecture" href="/architecture">Components, boundaries, and relay data flow.</Card>
  <Card title="CLI reference" href="/cli-reference">Desktop CLI that shares the same JSON API.</Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">Library behind `api::dispatch` and the Device core.</Card>
  <Card title="HTTP API reference" href="/http-api-reference">Relay HTTP surface the phone syncs against.</Card>
</CardGroup>

Next: open `mobile/modules/lorca-core/index.ts` and skim the five exports (`start`, `request`, `onEvent`, `wake`, `setOpenChat`).

---

## 12. Data model reference

> Schemas, entities, fields, relationships, migrations, and validation rules.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/12-data-model-reference.md
- Generated: 2026-09-26T20:16:42.620Z

### Source Files

- `crates/cli/src/model.rs`
- `crates/relay/src/db.rs`
- `crates/agent/src/types.rs`
- `crates/cli/src/api.rs`

---
title: "Data model reference"
description: "Schemas, entities, fields, relationships, migrations, and validation rules."
---

Lorca stores plaintext domain objects on each Device (`crates/cli/src/model.rs`) and syncs them as encrypted blobs through the relay (`crates/relay/src/db.rs`). The relay SQL schema never sees bot text, credentials, or message bodies — only ciphertext, blob kinds, sequence numbers, and machine keys.

## Ownership map

```mermaid
erDiagram
  Identity ||--o{ Machine : registers
  Identity ||--o{ Blob : owns
  Identity ||--o{ Usage : tracks
  Blob }o--o| Machine : "sealed recipient"
  Blob }o--o| Group : "chat group_id"
  RosterBlob ||--o{ Bot : contains
  RosterBlob ||--o{ ChatMeta : contains
  RosterBlob ||--o{ Routine : contains
  ChatMeta ||--o{ Message : "local messages table"
  Bot }o--|| Device : "runner_id"
  Job }o--|| Bot : bot_id
  Job }o--|| ChatMeta : chat_id
```

| Layer | Path | What it holds |
| --- | --- | --- |
| Domain (plaintext) | `crates/cli/src/model.rs` | Devices, bots, chats, messages, jobs, credentials shapes |
| Local SQLite | `crates/cli/src/local_store.rs` | Decrypted roster + messages + outbox on each Device |
| Relay SQL | `crates/relay/src/db/{sqlite,postgres}.rs` | Identities, machines, opaque blobs, pairing, push tokens |
| Agent transcript | `crates/agent/src/types.rs` | LLM turn messages (Runner-local, not synced as-is) |

<Note>
Plaintext crosses the network only inside encrypted blobs. Field names in `model.rs` match what the macOS and mobile apps decode.
</Note>

## Constants and hard limits

| Constant | Value | Where |
| --- | --- | --- |
| `MAX_GROUP_BOTS` | `6` | `model.rs`, mobile `model.ts` |
| `MAX_BOT_HOPS` | `8` | bot-to-bot `message` jobs |
| `SNAPSHOT_MESSAGES` | `60` | newest messages in a bootstrap snapshot |
| `APP_TOOL_DETAIL_CHARS` | `400` | tool-row detail kept for apps |
| `APP_COMMAND_CHARS` | `8000` | shell command text on permission/command cards |
| `MAX_ATTACHMENT_BYTES` | `100 MiB` | Device accept limit (mobile) |
| `MAX_ATTACHMENTS` | `10` | per send (mobile) |
| Non-file blob max | `4 MiB` | relay `MAX_BLOB_BYTES` |
| File blob max | `100 MiB + 40` | relay envelope overhead |
| Blob page max | `8 MiB` | `GET /v1/blobs` / group pages |
| Sealed envelope TTL | `7 days` | relay hourly sweep |
| `quota_bytes` | CLI flag, `0` = unlimited | per-identity usage |

`relay_name(name)` maps slot/group ids to relay-safe names: 1–64 chars of `[A-Za-z0-9._-]`, else SHA-256 hex.

## Domain entities

### Device

Paired machine or phone. `os` of `macos` | `linux` | `windows` means Runner.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | Machine signing pubkey, base64url |
| `name`, `model`, `os`, `os_version` | string | Host facts |
| `box_pubkey` | string | Sealing target for envelopes |
| `plugins` | `PluginStatus[]` | Advertised setup state; secrets stay on Runner |
| `updated_at` | i64 | Unix seconds |

`PluginStatus.state`: `ready` | `needs_setup` | `needs_auth` | `connecting` | `error`.

### Bot

| Field | Type | Notes |
| --- | --- | --- |
| `id`, `name`, `description` | string | Description is the single behavioral field |
| `symbol_name`, `accent` | string | Avatar fallback |
| `avatar` | `Attachment?` | Image `file` blob |
| `runner_id` | string | Device that runs tools |
| `provider` | string | Default `"deepseek"` on create |
| `model`, `thinking` | string? | Provider defaults when absent |
| `legacy_instructions` | string | Wire name `instructions`; folded into `description` on read |
| `workdir` | string? | Default `<LORCA_HOME>/workspaces/<bot id>` |
| `created_at` | f64 | Unix seconds |

`thinking` values: `off` | `minimal` | `low` | `medium` | `high` | `xhigh` | `max` (same as `ThinkingLevel` in `lorca-agent`).

### Auto-review

Shared through the roster. Default: `is_enabled = true`, empty `rules`.

| Field | Type | Notes |
| --- | --- | --- |
| `AutoReviewRule.id` | string | |
| `text` | string | Plain-language rule |
| `behavior` | string | `allow` or `ask` (`ask` wins on conflict) |
| `tool` | string? | Exact `plugin_id/tool` for Always-allow |

### Chat and ChatMeta

`ChatMeta.kind`: `dm` | `group`.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | Also the relay group when hashed via `relay_name` |
| `title` | string? | Groups only; DMs named after bot |
| `bot_ids` | string[] | Group cap `MAX_GROUP_BOTS` |
| `owner_bot_id` | string? | Receives unaddressed messages |
| `is_pinned` | bool | |
| `created_at` | f64 | |

`Chat` flattens `ChatMeta` and adds Runner-local fields that are **not** synced: `unread_count`, `usage` (`ChatUsage`), `compactions` (`Compaction[]`).

### Message

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | `msg-<uuid>` when created locally |
| `chat_id` | string | |
| `author` | `Author` | tagged `kind`: `you` \| `bot` \| `system` |
| `body` | `Body` | tagged `kind` (below) |
| `state` | `MessageState` | `thinking` \| `streaming` \| `complete` \| `failed` |
| `created_at` | f64 | Unix seconds |
| `promoted_at` | f64? | Mid-turn user message becomes model-visible later |

#### Body kinds

| `kind` | Purpose | Key fields |
| --- | --- | --- |
| `text` | User/bot text | `text`, `attachments[]`, `mentions[]` (bot ids) |
| `tool` | Tool row | `name`, `summary`, `detail`, `is_running`, `call_id`, `arguments`, `result`, `run?` |
| `handoff` | Owner change | `from`, `to`, `reason` |
| `notice` | System marker | `text`, `routine_id?` |
| `permission` | Ask before action | `plugin_id`, `tool`, `summary`, `decision`, `rule?`, `command?` |

Permission `decision`: `pending` | `allowed` | `always` | `denied` | `expired`.

#### CommandRun (`bash` card)

`state`: `checking` | `asking` | `running` | `waiting` | `exited` | `failed` | `stopped` | `denied` | `expired` | `dismissed`.

`decision` (Auto-review answer): `allowed` | `always` | `denied` | `expired` | `dismissed`.

`Message::for_app()` strips tool `arguments`/`result`, truncates `detail` to 400 chars, and for computer/`bash` permission cards copies the command into `command` then nulls `arguments`.

### Attachment

| Field | Type | Notes |
| --- | --- | --- |
| `id`, `name`, `mime` | string | Bytes live as `file` blob + `~/.lorca/files/<id>` |
| `size` | u64 | |
| `width`, `height` | u32? | Images |

### Routine

Lives in the roster; Runner executes on schedule.

| Field | Type | Notes |
| --- | --- | --- |
| `id`, `bot_id`, `name`, `prompt` | string | |
| `schedule` | string | `every 30m` / `every 2h` / `every 1d` or 5-field cron |
| `is_enabled` | bool | |
| `enabled_at`, `created_at` | f64 | |
| `last_run_at` | f64? | |
| `last_outcome` | string? | `sent` \| `pass` \| `error` |
| `paused_reason` | string? | e.g. `away` |

### Credentials

`kind = credentials` blob under the account DEK (`crates/cli/src/credentials.rs`).

| Field | Type |
| --- | --- |
| `deepseek`, `anthropic`, `opencode`, `opencode_go` | `ApiKeyCredential?` (`api_key`, `base_url?`, `connected_at`) |
| `chatgpt`, `grok` | OAuth token blobs |
| `changed_at` | map kind → unix seconds; later change wins on merge |

Provider kinds: `deepseek`, `anthropic`, `opencode`, `opencode-go`, `chatgpt`, `grok`.

## Blob kinds and payloads

`KINDS` on the relay:

`roster` · `chat` · `job` · `job_cancel` · `job_result` · `request` · `response` · `machine` · `credentials` · `key` · `file`

**Sealed** (addressed to one machine; swept after 7 days if unconsumed): `job`, `job_cancel`, `job_result`, `request`, `response`.

| Kind | Payload type | Slot / group behavior |
| --- | --- | --- |
| `roster` | `RosterBlob` | Latest wins: bots, `ChatMeta[]`, routines, `auto_review` |
| `chat` | `ChatBlob` | Per-message slot; group = chat id |
| `machine` | `MachineBlob` | Device + live `turns[]` |
| `credentials` | `Credentials` | Account DEK; merge by `changed_at` |
| `file` | raw bytes | Object store; row may hold empty ciphertext + size |
| `job` | `Job` | Sealed to Runner box key |
| `job_result` | `JobResult` | Sealed to requester |
| `job_cancel` | `JobCancel` | `{ job_id }` |
| `request` / `response` | `Request` / `Response` | Memory read/write verbs |

### ChatBlob ops

```text
upsert  { message }           // insert/replace by message id
remove  { chat_id, message_id }
clear_unread { chat_id }
```

Tool rows without a `CommandRun` use `Slot::latest`; other upserts and removals use `first_and_latest` so the first seq keeps transcript place.

### Job

| Field | Notes |
| --- | --- |
| `kind` | `turn` \| `room_turn` \| `message` \| `routine` \| `command` |
| `trigger_message_id` | |
| `routine_id?`, `from_bot_id?` | |
| `hops` | capped by `MAX_BOT_HOPS` |
| `round`, `is_winding_down` | group exchange |
| `setup?` | marketplace first-turn `TemplateSetup` |

`JobResult.outcome`: `sent` | `pass` | `error`.

### Pairing envelopes

`PairRequest`: `machine_pubkey`, `box_pubkey`, `device`.  
`PairReply`: `identity_pubkey`, `content_pubkey`, `account_dek`, `relay_url`.

## Relay persistence schema

Backends share the same logical tables. Open path: `postgres://` / `postgresql://` → Postgres; otherwise SQLite file.

### Tables

| Table | Key columns |
| --- | --- |
| `identities` | `pubkey`, `content_pubkey`, `created_at` |
| `machines` | `machine_pubkey`, `identity_pubkey`, `box_pubkey`, `attestation`, `last_seen`, `created_at` |
| `revoked_machines` | `machine_pubkey`, `identity_pubkey`, `revoked_at` |
| `blobs` | `(identity_pubkey, id)`, `kind`, `recipient_machine_pubkey?`, `seq`, `ciphertext`, `size`, `slot?`, `group_id?`, `created_at` |
| `deleted_groups` | `(identity_pubkey, group_id)`, `deleted_at` |
| `sequences` | `identity_pubkey` → head `seq` |
| `usage` | `identity_pubkey` → `bytes` |
| `challenges` | `nonce`, `machine_pubkey`, `expires_at` |
| `pairings` | `nonce`, `identity_pubkey`, `request`/`reply` blobs, expiry |
| `push_tokens` | `machine_pubkey`, `platform`, `token`, `environment` (`sandbox`\|`production`) |
| `schema_version` | `version`, `applied_at` |

Postgres-only presence tables: `relay_instances`, `relay_sockets` (multi-process online state + `NOTIFY` for `Event`).

### Migrations

Both backends: `SCHEMA` is version 1; `MIGRATIONS: &[&str] = &[]` today. Rules in code:

1. Append-only steps (add table, nullable column, or index).
2. Never edit a shipped step.
3. SQLite re-runs `SCHEMA` (pragmas) every open; Postgres runs each step once under advisory lock `0x10ca_5c4e`, with deadlock retry.

Housekeeping: `tick` (expired challenges/pairings); `sweep` (stale sealed envelopes, old deleted-group marks, orphans in the file store, inactive identities).

## Device local store

`LocalStore` SQLite (`PRAGMA user_version = 1`) projects decrypted state for CLI and phone core:

| Table | Role |
| --- | --- |
| `metadata` | `auto_review_json`, `last_seq`, `machine_blob_hash`, `credentials_uploaded` |
| `devices` / `bots` / `chats` / `routines` | Ordered JSON rows |
| `messages` | Full `message_json` + denormalized author/body flags for search |
| `outbox` | Pending encrypted uploads; unique `slot_name` replaces in place |
| `chat_history` | `before_place` when older pages remain on relay |
| `device_turns` / `sent_jobs` | Live and cross-Runner wait state |
| `group_deletes` / `blob_deletes` / `applied_blobs` | Sync bookkeeping |

In-memory `State` mirrors these tables plus ephemeral `device_online` / `turns_online`.

## Agent transcript model

Runner-local types in `crates/agent/src/types.rs` (not the sync `Message` shape):

| Type | Role |
| --- | --- |
| `ContentPart` | `text` \| `image` |
| `AssistantPart` | `text`, `thinking`, `tool_call`, `server_block` |
| `LlmMessage` / `AgentMessage` | `user` \| `assistant` \| `tool_result` (+ `custom`) |
| `Usage` / `Cost` | Token and dollar totals |
| `StopReason` | `stop` \| `length` \| `tool_use` \| `error` \| `aborted` |
| `AgentEvent` | Loop lifecycle stream |

### Tool-argument validation

`lorca_agent::schema::validate_tool_arguments`:

1. Drop optional `null`s the model meant as “omit”.
2. Coerce common mistakes (`"true"` → bool, numeric strings → number/integer).
3. Validate with JSON Schema; failure returns an error string the model can read.
4. Uncompilable schema → arguments accepted as-is (tool author’s bug).

## Validation and lifecycle rules (API surface)

From `crates/cli/src/api.rs` and model helpers:

| Rule | Behavior |
| --- | --- |
| Group add bot | Only if `kind == group`, `< MAX_GROUP_BOTS`, id not already present |
| Group remove bot | Keep ≥ 1 bot; reassign `owner_bot_id` if owner removed |
| Set owner | Bot must already be in `bot_ids` |
| Avatar | Must be an image MIME; stored as `file` blob |
| Bot create | Always creates a DM chat in the same roster change |
| Search limit | Clamped `1..=50`, default `20` |
| Legacy `instructions` | Folded into `description` once via `normalize_description` |

```text
CommandRun open states:  checking → asking → running|waiting → terminal
Permission decision:     pending → allowed|always|denied|expired
```

## Sync kinds filter

Device poll kinds (`sync.rs`):

`roster,chat,machine,credentials,job,job_cancel,job_result,request,response`

First sync omits `chat` from the full log replay, then pages newest messages per roster chat.

## Related pages

<CardGroup>
  <Card title="Architecture" href="/architecture">
    Component boundaries and how blobs move between Devices and the relay.
  </Card>
  <Card title="HTTP API reference" href="/http-api-reference">
    Blob, file, machine, pairing, and sync routes that persist these rows.
  </Card>
  <Card title="crates/relay reference" href="/ref-crates-relay">
    Store trait, SQLite vs Postgres backends, sweep, and quotas.
  </Card>
  <Card title="crates/agent reference" href="/ref-crates-agent">
    Transcript types, thinking levels, and tool-argument validation.
  </Card>
</CardGroup>

Next: open `crates/cli/src/model.rs` and skim the `RosterBlob` / `ChatBlob` / `Job` section (about two minutes) before changing any wire field.

---

## 13. macos reference

> Reference for the code under `macos`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/13-macos-reference.md
- Generated: 2026-09-26T20:18:51.514Z

### Source Files

- `macos/Sources/Lorca/App/CLILauncher.swift`
- `macos/Sources/Lorca/App/AppDelegate.swift`
- `macos/Package.swift`
- `macos/Sources/Lorca/Model/Protocol.swift`
- `crates/cli/src/api.rs`
- `macos/Sources/Lorca/main.swift`

---
title: "macos reference"
description: "Reference for the code under `macos`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`macos/` is the AppKit SPM package that builds **Lorca** / **Lorca Dev**. The executable talks only to a local `lorca` CLI over `ws://127.0.0.1:<port>/ws`, probes that port on launch, and if nothing answers spawns `lorca serve` with readiness on stdout.

## Package surface

| Item | Value |
| --- | --- |
| Package name | `Lorca` (`macos/Package.swift`) |
| Platform | macOS 14+ |
| Executable target | `Lorca` → `Sources/Lorca` |
| Markdown FFI | binary `LorcaMarkdownFFI` + Swift target `LorcaMarkdown` |
| Updater | Sparkle 2.9+ (release builds) |
| Tests | `LorcaTests` under `Tests/Notifications` |

:::files
macos/
├── Package.swift
├── Resources/          # icns, en.lproj, zh-Hans.lproj
├── Sources/Lorca/
│   ├── main.swift      # NSApplication + AppDelegate
│   ├── App/            # launch, prefs, updater, menus
│   ├── Model/          # AppStore, CLIClient, Wire protocol
│   ├── Main/ Chat/ Sidebar/ Settings/ …
│   └── …
├── Libraries/          # LorcaMarkdownFFI.xcframework (build output)
└── Tests/
:::

**Bundle IDs** (from `scripts/app.ts`): production `app.lorca` → app name **Lorca**; debug `app.lorca.dev` → **Lorca Dev**.

## How the app starts

```mermaid
sequenceDiagram
  participant Main as main.swift
  participant AD as AppDelegate
  participant Store as AppStore
  participant Launch as CLILauncher
  participant Worker as CLILaunchWorker
  participant CLI as lorca serve
  participant WS as CLIClient

  Main->>AD: NSApplication.run
  AD->>Store: start()
  Store->>Launch: ensureRunning()
  Launch->>Worker: probe 127.0.0.1:port (1.5s)
  alt port already open
    Worker-->>Launch: ready(external: true)
  else no listener
    Worker->>CLI: serve --port --parent-pid --ready-stdout
    CLI-->>Worker: JSON line event=ready
    Worker-->>Launch: ready(external: false)
  end
  Launch-->>Store: onReady
  Store->>WS: connect ws://127.0.0.1:port/ws
  WS->>CLI: bootstrap
  CLI-->>Store: Snapshot
```

1. `main.swift` creates `AppDelegate`, sets activation policy `.regular`, runs `NSApplication`.
2. `AppDelegate.applicationDidFinishLaunching` builds the menu, observes `AppStore`, calls `store.start()`.
3. With a prior identity (`Preferences.hadIdentity`), the main window opens immediately; otherwise the app waits for the CLI answer before choosing main vs onboarding.
4. After the first successful (or failed) startup path leaves `isStarting`, notifications and Sparkle start once.

Quit path: `applicationWillTerminate` and SIGTERM both call `store.stop()` → disconnect websocket and terminate the child CLI.

## CLI process control

### Locate binary

`CLILaunchWorker.locateBinary` checks, in order:

1. `LORCA_CLI` env path  
2. `Contents/Resources/bin/lorca` in the app bundle  
3. `~/.cargo/bin/lorca`  
4. `/opt/homebrew/bin/lorca`  
5. `/usr/local/bin/lorca`  
6. each `PATH` entry + `/lorca`

### Spawn arguments

When the probe fails, the worker runs:

```bash
lorca serve --port <port> --parent-pid <app_pid> --ready-stdout
```

- stderr (and readiness stdout lines) append to `~/Library/Logs/<AppInfo.name>/cli.log`
- Ready signal: one JSON line with `"event":"ready"` and `"port"` matching the requested port
- Child exit triggers exponential restart: delay starts at **1s**, doubles up to **15s**

### Launcher status

| Status | Meaning |
| --- | --- |
| `idle` | Not started yet |
| `probing` | TCP probe in flight |
| `starting` | Child process spawning |
| `running(external:)` | Port ready; `true` if reused, `false` if app-spawned |
| `failed(String)` | User-visible failure message |

`offlineStatus` in the UI is `launcher.status.message`.

## Environment and defaults

### Ports and home

| Build | Bundle ID | Default port | Default `LORCA_HOME` | Suggested serve command |
| --- | --- | --- | --- | --- |
| Lorca | `app.lorca` | `4862` | `~/.lorca` | `lorca serve` |
| Lorca Dev | `app.lorca.dev` | `4863` | `~/.lorca-dev` | `lorca serve --home ~/.lorca-dev --port 4863` |

### Environment variables the app reads or sets

<ParamField body="LORCA_PORT" type="int">
Overrides `Preferences.cliPort` when `> 0`. Wins over UserDefaults.
</ParamField>

<ParamField body="LORCA_CLI" type="path">
Absolute path to the `lorca` binary; skips bundle/PATH search.
</ParamField>

<ParamField body="LORCA_HOME" type="path">
CLI data directory. If unset at spawn time, set to `AppInfo.defaultCLIHome`.
</ParamField>

<ParamField body="LORCA_DEFAULT_RELAY_URL" type="url">
Release builds set this to `https://relay.lorca.app` when unset. Dev builds leave it alone.
</ParamField>

<ParamField body="RUST_LOG" type="string">
Defaults to `lorca=info` for the child if unset.
</ParamField>

<ParamField body="LORCA_MOCK" type="1">
Skips the real CLI; `AppStore` uses seeded mock data and `ReplyEngine`.
</ParamField>

<ParamField body="LORCA_TRACE_STARTUP" type="1">
Writes launch milestones to `~/Library/Logs/<AppInfo.name>/startup.log`.
</ParamField>

Websocket URL the client always uses:

```text
ws://127.0.0.1:<Preferences.cliPort>/ws
```

Maximum WebSocket message size: **256 MiB** (snapshots carry full chat histories).

## Preferences (`UserDefaults`)

| Key | Default | Role |
| --- | --- | --- |
| `lorca.hadIdentity` | `false` | Open main window immediately on next launch |
| `lorca.cliPort` | build default (`4862` / `4863`) | Local CLI port; overridden by `LORCA_PORT` |
| `lorca.relayURL` | `""` | Settings › Advanced relay override |
| `lorca.selection` | nil | Persist selected chat (dev relaunch) |
| `lorca.showsInspector` | `true` | Inspector visibility |
| `lorca.sendOnReturn` | `true` | Composer Return sends |
| `lorca.showTimestamps` | `true` | Message timestamps |
| `lorca.dictationLanguage` | nil | Speech locale; nil = system |
| `AppleLanguages` (app domain) | nil | App UI language (`en`, `zh-Hans`, or system) |

`Preferences.reset()` clears only `hadIdentity` and `selection`.

Changing the CLI port in Settings updates `Preferences.cliPort` and reconnects through `AppStore.reconnect()` → launcher re-probe/spawn.

## WebSocket request surface

Frames: `{ "id": <int>, "method": "<name>", "params": {…} }`.  
Success: `{ "id", "result" }`. Error: `{ "id", "error": { "message" } }`.  
Push events: `{ "event": "<name>", "data": … }` (snake_case keys; Swift decodes with `convertFromSnakeCase`).

### Lifecycle / identity

| Method | Role |
| --- | --- |
| `hello` | Version, identity flags, relay status |
| `bootstrap` | Full `Wire.Snapshot` (first connect) |
| `identity.create` / `restore` / `forget` / `delete` | Account lifecycle |
| `pair.start` / `status` / `cancel` / `accept` / `abort` | Device pairing |
| `device.rename` / `device.unpair` | Device management |
| `config.set` | Set `relay_url` |
| `sync.wake` | Force relay sync |
| `ui.watching` | Chat currently on screen (`chat_id` or null) |

### Chats, bots, routines, plugins

| Group | Methods |
| --- | --- |
| Bots | `bots.create`, `bots.update`, `bots.delete`, `bots.memory`, `bots.memory.write` |
| Chats | `chats.create`, `chats.dm`, `chats.send`, `chats.stop`, `chats.compact`, `chats.delete`, `chats.rename`, `chats.pin`, `chats.add_bot`, `chats.remove_bot`, `chats.set_owner`, `chats.search`, `chats.messages`, `chats.mark_read`, `chats.permission` |
| Routines | `routines.create`, `routines.update`, `routines.delete`, `routines.run`, `routines.describe` |
| Marketplace / plugins | `marketplace`, `plugins.install`, `uninstall`, `set_variables`, `connect`, `detail` |
| Auto-review | `auto_review.set` |
| Shell cards | `bash.stdin`, `bash.stop` |
| Files | `files.path` |
| Providers | `providers.api_key`, `providers.connect_*`, `providers.auth.cancel`, `providers.disconnect` |

Unknown methods return `unknown method <name>` from `crates/cli/src/api.rs`.

### Events `AppStore` handles

`snapshot`, `roster.changed`, `message.added` / `message.updated` / `message.removed`, `chat.removed`, `job.started` / `job.finished` / `job.retry` / `job.thinking`, `chat.usage`, `relay.status`, `identity.changed`.

Bootstrap buffers events until the snapshot applies, then drains them so the UI never flashes empty roster state.

## Error cases

| Failure | Cause | What you see / do |
| --- | --- | --- |
| `missingBinary` | No bundled CLI and none on PATH/`LORCA_CLI` | “The lorca CLI is not bundled…” — install CLI or set `LORCA_CLI` |
| `launch(binary, reason)` | `Process.run` threw | “Could not start \<path\>: …” |
| `exited(code, log)` | Child terminated | “The CLI exited with code N. See cli.log.” — app retries with backoff |
| `startupClosed(log)` | stdout closed before ready JSON | “The CLI closed its startup channel…” — check `cli.log` |
| Probe timeout (1.5s) | Nothing on port | Spawn path above |
| `The Lorca CLI is not running` | Request while disconnected | Wait for reconnect or use Reconnect |
| `The CLI connection closed/dropped` | Socket died mid-request | Pending requests fail; client reconnects (0.4s → up to 5s) |
| `bootstrap failed` | Connected but snapshot request failed | Loading ends; offline UI with `offlineStatus` |
| Startup deadline | No snapshot in **2.5s** | `isStarting` clears; offline recovery controls |
| `relayUpdateRequired` | Relay rejects this protocol | One alert per launch: update via Sparkle when enabled |
| `unknown method` | Client/CLI version skew | Method string in error message |

<Warning>
On launcher `.starting` or `.failed`, `AppStore` disconnects the websocket so retries do not race an exiting child.
</Warning>

## Build, run, verify

| Command | Result |
| --- | --- |
| `bun run dev` | Rebuild CLI + Lorca Dev on Rust/Swift changes; relaunch via `open` |
| `bun run build` | Release `Lorca.app` via `scripts/app.ts` / `build.ts` |
| `bun run release-mac <version>` | Sign, notarize, publish Sparkle feed (see `docs/releasing-mac.md`) |
| `bun run test:mac-startup` | Compiles and runs launch-concurrency tests for `CLILaunchWorker` |
| `swift test` (in `macos/`) | Notification-focused unit tests |

Release updater: Sparkle feed `https://mac-releases.lorca.app/appcast.xml`, EdDSA key in Info.plist (`SUPublicEDKey`). **DEBUG** builds disable Sparkle (`Updater.isEnabled == false`).

<Check>
After a healthy launch: websocket connects, `bootstrap` returns a snapshot, `isConnected == true`, and chats or onboarding appear. With `LORCA_TRACE_STARTUP=1`, `startup.log` shows marks through `snapshot presented`.
</Check>

## Related pages

<CardGroup>
  <Card title="Architecture" href="/architecture">
    App ↔ CLI ↔ relay process boundaries and data flow.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    `lorca serve` flags, home directory layout, and websocket server details.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env vars and config keys shared across app and CLI.
  </Card>
  <Card title="Deployment" href="/deployment">
    Mac release, notarization, and Sparkle publishing.
  </Card>
</CardGroup>

Next: open `macos/Sources/Lorca/App/CLILauncher.swift` and confirm your port/`LORCA_HOME` match the Lorca vs Lorca Dev row above.

---

## 14. web reference

> Reference for the code under `web`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/14-web-reference.md
- Generated: 2026-09-26T20:19:37.652Z

### Source Files

- `web/content/docs/cli.mdx`
- `crates/cli/src/api.rs`
- `web/src/router.tsx`
- `web/src/routeTree.gen.ts`

---
title: "web reference"
description: "Reference for the code under `web`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`web/` is the Lorca marketing site and product docs app: a private Bun workspace package (`"name": "web"`) built with TanStack Start + Vite, file routes under `web/src/routes`, Fumadocs MDX under `web/content/docs`, and a Cloudflare Worker entry from `web/wrangler.jsonc`. Production host constant: `https://lorca.app`.

## Invoke

From the repo root (about one command each):

| Command | Effect |
| --- | --- |
| `bun run web` | `bun run --cwd web dev` → Vite on port **3000** |
| `bun run web:build` | Production build in `web/` |
| `bun run web:deploy` | `bun run build && wrangler deploy` in `web/` |

Inside `web/package.json`:

| Script | Command | Default |
| --- | --- | --- |
| `dev` | `vite dev --port 3000` | Port `3000` |
| `generate-routes` | `tsr generate` | Regenerates `src/routeTree.gen.ts` |
| `build` | `vite build` | Cloudflare SSR + assets |
| `preview` | `vite preview` | Local preview of build |
| `deploy` | `bun run build && wrangler deploy` | Worker name `lorca` |

Success signal for local work: `http://localhost:3000` serves `/`, `/docs`, and `/download`.

## Package layout

:::files
web/
  package.json          # scripts, #/* → ./src/*
  vite.config.ts        # fumadocsMdx, cloudflare SSR, tanstackStart, react, tailwind
  wrangler.jsonc        # Worker name lorca, nodejs_compat, observability
  tsr.config.json       # TanStack Router target: react
  content/docs/         # MDX product docs + meta.json / meta.zh.json
  public/               # install-cli.sh|.ps1, screens, favicon, _headers
  src/
    router.tsx          # getRouter()
    routeTree.gen.ts    # generated route tree (do not edit)
    lib/source.ts       # docs source, i18n, llms helpers
    components/docs/    # DocsPage loader + Fumadocs UI shell
    components/site/    # Home, Download, Nav, sections
    routes/             # file routes (en + zh_ pathless layout)
:::

Import alias: `"#/*": "./src/*"`.

## Router defaults

`getRouter()` in `web/src/router.tsx` creates the TanStack Router with:

| Option | Value |
| --- | --- |
| `routeTree` | Generated `routeTree` from `routeTree.gen.ts` |
| `scrollRestoration` | `true` |
| `scrollRestorationBehavior` | `'instant'` |
| `defaultPreload` | `'intent'` |
| `defaultPreloadStaleTime` | `0` |

SSR is registered with TanStack Start (`ssr: true` in the generated module augmentation).

## HTTP routes

English is the default locale (no `/en` prefix). Chinese lives under `/zh`. Pathless `zh_` route files keep Chinese pages out of the `/zh` landing layout.

| Path | Role |
| --- | --- |
| `/` | English landing (`Home`) |
| `/zh` | Chinese landing |
| `/download` | English download page |
| `/zh/download` | Chinese download page |
| `/docs/$` | English docs HTML (splat) |
| `/zh/docs/$` | Chinese docs HTML |
| `/docs/{$}.md` | English docs as Markdown |
| `/zh/docs/{$}.md` | Chinese docs as Markdown |
| `/llms.txt` | English docs index for LLMs |
| `/llms-full.txt` | English full Markdown dump |
| `/zh/llms.txt` | Chinese docs index |
| `/zh/llms-full.txt` | Chinese full Markdown dump |
| `/api/search` | Docs search (Orama via Fumadocs) |

Static public files (examples): `/install-cli.sh`, `/install-cli.ps1`, `/favicon.png`, `/screens/group.png`.

```mermaid
flowchart LR
  subgraph browser [Browser]
    Home["/ and /zh"]
    Dl["/download"]
    DocsUI["/docs/$"]
  end
  subgraph worker [Cloudflare Worker SSR]
    Source["lib/source.ts"]
    Search["/api/search"]
    Md["/docs/*.md and /llms*.txt"]
    MacFn["latestMacRelease server fn"]
  end
  subgraph content [Repo content]
    MDX["content/docs/*.mdx"]
    Public["public/install-cli.*"]
    Appcast["mac-releases.lorca.app/appcast.xml"]
  end
  Home --> Source
  DocsUI --> Source
  Source --> MDX
  Search --> Source
  Md --> Source
  Dl --> MacFn
  MacFn --> Appcast
  Public --> Dl
```

## Docs source API (`web/src/lib/source.ts`)

| Export | What it is |
| --- | --- |
| `docsRoute` | `'/docs'` |
| `docsI18n` | `defaultLanguage: 'en'`, `languages: ['en','zh']`, `hideLocale: 'default-locale'` |
| `docs` | `defineDocs({ dir: 'content/docs', docs: { async: true, postprocess: { includeProcessedMarkdown: true } } })` |
| `source` | Fumadocs `loader` over `docs.toFumadocsSource()`, Lucide icons plugin, `baseUrl: '/docs'` |
| `docsLlms` | `llms(source, …)` rendering `# {title} ({url})` + processed Markdown body |
| `markdownUrl(slugs, lang?)` | `/docs/bots.md` or `/zh/docs/bots.md`; empty slugs → `index.md` |
| `slugsOfMarkdownUrl(segments)` | Strips trailing `.md`; `index` → `[]` |

Sidebar order comes from `web/content/docs/meta.json` (English section titles) and `meta.zh.json` (Chinese). Pages: `index`, `get-started`, `bots`, `chats`, `group-chats`, `tools`, `memory`, `routines`, `plugins`, `auto-review`, `devices`, `providers`, `mobile`, `security`, `relay`, `cli`. A Chinese file is named `*.zh.mdx` beside the English `*.mdx`; missing translation falls back to English.

### Docs page loader

`loadDocsPage(lang, splat)` → server function `serverLoader`:

1. Resolve `source.getPage(slugs, lang)`.
2. Missing page → `notFound()`.
3. Return `{ path, url, slugs, title, description, markdownUrl, pageTree }`.
4. `docs.getPage(data.path)?.preload()` then render `DocsPage`.

`docsHead(lang, data)` sets title `{page} · {docs.title}`, description, canonical `SITE + data.url`, and `hreflang` alternates for `en` / `zh`.

Theme: system only (`forcedTheme: 'system'`, theme switch disabled). Language picker navigates to the same slugs under the other locale.

MDX components (`web/src/components/mdx.tsx`): Fumadocs defaults plus `Accordion` / `Accordions` / `Step` / `Steps`.

## Search API

:::endpoint GET /api/search Docs full-text search
Built with `createFromSource(source, { localeMap })` from `fumadocs-core/search/server`. The route handler forwards the incoming `Request` to `server.GET(request)`.

**Locale indexes**

| Locale | Config |
| --- | --- |
| `en` | `{ language: 'english' }` |
| `zh` | Mandarin tokenizer from `@orama/tokenizers/mandarin`; `search: { threshold: 0, tolerance: 0 }` |

Query string shape is whatever Fumadocs `createFromSource` accepts on the request URL; this package does not wrap or rename those parameters.
:::

## Download page and Mac release loader

`latestMacRelease` is a TanStack Start server function (`GET`):

| Behavior | Detail |
| --- | --- |
| Feed URL | `https://mac-releases.lorca.app/appcast.xml` |
| Timeout | `AbortSignal.timeout(5000)` |
| Parse | Newest Sparkle `<item>` by `sparkle:version`; enclosure `.zip` rewritten to `.dmg` |
| Success | `{ version, url, minimumSystemVersion, appleSilicon }` |
| Failure | Logs error, returns `null` (Mac button disabled; iOS / CLI still shown) |

Other download links (hardcoded in the download UI):

| Surface | Target |
| --- | --- |
| iOS | TestFlight `https://testflight.apple.com/join/WRR2R3y1` |
| CLI (Unix) | `curl -fsSL https://lorca.app/install-cli.sh \| sh` |
| CLI (Windows) | `irm https://lorca.app/install-cli.ps1 \| iex` |
| Windows / Linux desktop, Android | “Soon” placeholders |

## Install scripts (`web/public/`)

Served as UTF-8 plain text via `web/public/_headers`.

### `install-cli.sh` (macOS / Linux)

| Variable | Default / effect |
| --- | --- |
| `LORCA_VERSION` | Latest GitHub release; else `cli-v{version}` |
| `LORCA_INSTALL_DIR` | `$HOME/.local/bin` |
| `LORCA_NO_MODIFY_PATH` | `1` skips profile PATH edit |
| `LORCA_DOWNLOAD_URL` | Override releases root (default `https://github.com/egoist/lorca/releases`) |

Targets: `macos-aarch64`, `linux-aarch64`, `linux-x86_64`. Intel Mac without Rosetta → fatal. Windows shells redirected to the PowerShell script. Verifies `.sha256`, installs by rename so a running `lorca serve` keeps its binary, prints `lorca --version` on success.

### `install-cli.ps1` (Windows)

Same env vars (`$env:LORCA_*`). Target always `windows-x86_64` (Arm Windows runs the x64 build). Installs `lorca.exe`, unblocks the file, refuses to call `exit` so `iex` does not close the window.

## i18n

| Export | Values |
| --- | --- |
| `languages` | `'en'`, `'zh'` |
| `paths` | `en → '/'`, `zh → '/zh'` |
| `htmlLang` | `en`, `zh-Hans` |
| `languageOf(pathname)` | `/zh` or `/zh/…` → `zh`, else `en` |
| `i18nFor(lng)` | Per-language i18next instance; `fallbackLng: 'en'`; `initAsync: false` |

Root shell (`__root.tsx`) sets `<html lang=…>`, Inter font, theme-color meta for light/dark, and production-only analytics script `https://u.egoist.dev/script.js` when `import.meta.env.PROD`.

## Deploy and Worker config

`web/wrangler.jsonc`:

| Key | Value |
| --- | --- |
| `name` | `lorca` |
| `compatibility_date` | `2025-09-02` |
| `compatibility_flags` | `["nodejs_compat"]` |
| `main` | `@tanstack/react-start/server-entry` |
| `observability.enabled` | `true` |

Vite plugins (order): `fumadocsMdx`, TanStack devtools, `@cloudflare/vite-plugin` with `viteEnvironment: { name: 'ssr' }`, Tailwind, `tanstackStart`, React.

A CLI release does **not** require a site deploy. Shipping script or docs changes requires `bun run web:deploy`.

## Error cases

| Case | Behavior |
| --- | --- |
| Unknown docs slug | `notFound()` from loader / Markdown GET |
| `docs.getPage(path)` missing after load | `throw new Error(\`unknown page: ${path}\`)` |
| Appcast HTTP non-OK / empty / timeout / parse miss | `latestMacRelease` → `null`; Mac download disabled |
| Install: unsupported OS/CPU | Script exits / throws with message |
| Install: checksum mismatch | Fatal; ask to re-run installer |
| Install: archive missing binary | Fatal |
| Install: binary fails `--version` | Fatal |
| Install: unwritable install dir | Fatal; set `LORCA_INSTALL_DIR` |
| No `curl`/`wget` (sh) | Fatal |
| Invalid `LORCA_VERSION` | Fatal |

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">What Lorca exposes and where the site fits beside CLI, macOS, mobile, and relay.</Card>
  <Card title="CLI reference" href="/cli-reference">Commands and flags documented by the product docs under `/docs/cli`.</Card>
  <Card title="Deployment" href="/deployment">Build and `wrangler deploy` for the Worker that serves this package.</Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">The `lorca` binary the install scripts download and the desktop app launches.</Card>
</CardGroup>

Next: open `web/package.json` and run `bun run web`, then hit `http://localhost:3000/docs/cli`.

---

## 15. crates/provider-auth reference

> Reference for the code under `crates/provider-auth`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/15-crates-provider-auth-reference.md
- Generated: 2026-09-26T20:19:00.751Z

### Source Files

- `crates/provider-auth/src/lib.rs`
- `crates/cli/src/provider_auth.rs`

---
title: "crates/provider-auth reference"
description: "Reference for the code under `crates/provider-auth`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`lorca-provider-auth` (`crates/provider-auth`) owns ChatGPT and Grok OAuth token types plus PKCE loopback sign-in. Import it as `lorca_provider_auth::{chatgpt, grok}`. Devices run `login`; Runners call `refresh` before model calls. API-key providers live in `crates/cli/src/provider_auth.rs`, outside this crate.

:::files
crates/provider-auth/
  Cargo.toml          # package name: lorca-provider-auth
  src/
    lib.rs            # pub mod chatgpt; pub mod grok;
    chatgpt.rs        # ChatGPT PKCE on fixed port 1455
    grok.rs           # Grok PKCE on an ephemeral loopback port
:::

## What this crate exports

| Module | Primary types | Primary functions |
| --- | --- | --- |
| `chatgpt` | `ChatGptTokens`, `PkceFlow` | `login`, `refresh`, `exchange_code`, `wait_for_callback`, `jwt_claims` |
| `grok` | `GrokTokens`, `PkceFlow`, `Endpoints`, `Callback` | `login`, `refresh`, `revoke`, `exchange_code`, `jwt_claims` |

Both modules are public from `lib.rs`. There is no crate-level re-export of the token structs; callers use `lorca_provider_auth::chatgpt::ChatGptTokens` or `::grok::GrokTokens`.

### Token shapes

<ParamField body="ChatGptTokens.access_token" type="string" required>
Bearer access token from `auth.openai.com`.
</ParamField>
<ParamField body="ChatGptTokens.refresh_token" type="string" required>
Refresh token for `grant_type=refresh_token`.
</ParamField>
<ParamField body="ChatGptTokens.id_token" type="string | null">
Optional OIDC id token; used with the access token to recover `account_id` / `email`.
</ParamField>
<ParamField body="ChatGptTokens.account_id" type="string" required>
ChatGPT account id from JWT claim `https://api.openai.com/auth.chatgpt_account_id` (or `chatgpt_account_id`).
</ParamField>
<ParamField body="ChatGptTokens.email" type="string | null">
Email from JWT claims when present.
</ParamField>
<ParamField body="ChatGptTokens.expires_at" type="u64" required>
Unix seconds. `is_expired()` is true when `now + 60 >= expires_at`.
</ParamField>

<ParamField body="GrokTokens.access_token" type="string" required>
Bearer access token from `auth.x.ai`.
</ParamField>
<ParamField body="GrokTokens.refresh_token" type="string" required>
Refresh token; xAI may rotate it on refresh.
</ParamField>
<ParamField body="GrokTokens.id_token" type="string | null">
Optional id token.
</ParamField>
<ParamField body="GrokTokens.account_id" type="string | null">
`sub` from JWT or `/oauth2/userinfo` when available.
</ParamField>
<ParamField body="GrokTokens.email" type="string | null">
Email / preferred username / name from claims or userinfo.
</ParamField>
<ParamField body="GrokTokens.expires_at" type="u64" required>
Unix seconds. `is_expired()` is true when `now + 5 * 60 >= expires_at` (five-minute skew; tokens last about six hours).
</ParamField>

## How it is invoked

```mermaid
sequenceDiagram
  participant UI as Device UI / open_url
  participant CLI as cli provider_auth
  participant Auth as lorca-provider-auth
  participant IdP as auth.openai.com / auth.x.ai
  participant Agent as agent ChatGpt/Grok provider

  UI->>CLI: providers.connect_chatgpt / connect_grok
  CLI->>Auth: login(http, open_url, 5m)
  Auth->>Auth: bind loopback callback
  Auth->>UI: open authorize URL
  UI->>IdP: user consents
  IdP->>Auth: redirect with code
  Auth->>IdP: exchange_code
  Auth-->>CLI: ChatGptTokens / GrokTokens
  CLI->>CLI: update_credentials + sync blob
  Agent->>Auth: refresh when is_expired()
  Auth->>IdP: grant_type=refresh_token
  Agent->>Agent: store refreshed tokens
```

### From the CLI feature boundary

`crates/cli` enables this crate with Cargo feature `provider-auth` (`runner` includes it). `crates/mobile` links `lorca` with `features = ["provider-auth"]` and `default-features = false`.

`crates/cli/src/provider_auth.rs` wraps the crate:

| Function | Behavior |
| --- | --- |
| `connect_chatgpt(app, open_url)` | `chatgpt::login(&app.http, open_url, 5 minutes)` then stores under credentials kind `chatgpt` |
| `connect_grok(app, open_url)` | Builds `Endpoints` from `LORCA_GROK_ISSUER` or `Endpoints::xai()`, then `grok::login(..., 5 minutes)`, stores kind `grok` |
| `disconnect(app, "grok")` | Clears credentials and best-effort `grok::revoke` on a background task |

JSON API methods (feature-gated):

| Method | Params | Success result |
| --- | --- | --- |
| `providers.connect_chatgpt` | `{}` | `{ email, providers }` |
| `providers.connect_grok` | `{}` | `{ email, providers }` |
| `providers.auth.cancel` | `{}` | `null` (cancels in-flight OAuth) |
| `providers.disconnect` | `{ kind }` | `{ providers }` |

Runner builds open the system browser via `open::that`. Non-runner Devices emit `Event::ProviderAuth { kind, url }` so the phone/desktop UI can open an in-app browser while the core keeps the localhost listener alive.

Terminal entry:

```bash
lorca provider set chatgpt
lorca provider set grok
lorca provider remove grok
```

ChatGPT/Grok take no API key; the CLI prints `Finish the sign-in in the browser…` and dispatches the connect method (to a running `lorca serve` when present).

### From the agent crate

`crates/agent` always depends on `lorca-provider-auth`. Provider modules re-export the OAuth surface:

- `lorca_agent::providers::chatgpt` → `oauth` + `ChatGptTokens`
- `lorca_agent::providers::grok` → `oauth` + `GrokTokens`

Before each model call, `fresh_tokens()` loads stored tokens, skips refresh when not expired, otherwise calls `oauth::refresh`, handles a concurrent refresh that already rotated the refresh token, then `store`s the new tokens.

## Options and defaults

### ChatGPT (`chatgpt` module)

| Constant / option | Default |
| --- | --- |
| `CLIENT_ID` | `app_EMoamEEZ73f0CkXaXp7hrann` |
| `ISSUER` | `https://auth.openai.com` |
| `CALLBACK_PORT` | `1455` |
| Redirect URI | `http://localhost:1455/auth/callback` |
| `SCOPES` | `openid profile email offline_access` |
| PKCE | S256; 64-byte verifier, 32-byte state |
| Login timeout (CLI) | `5 * 60` seconds |
| Token `expires_in` fallback | `3600` seconds |
| Expiry skew | 60 seconds |

Authorize URL also sets `id_token_add_organizations=true` and `codex_cli_simplified_flow=true` (same shape as the Codex CLI flow).

### Grok (`grok` module)

| Constant / option | Default |
| --- | --- |
| `CLIENT_ID` | `b1a00492-073a-47ea-816f-4c329264a828` (public desktop client; no secret) |
| `ISSUER` | `https://auth.x.ai` |
| `SCOPES` | `openid profile email offline_access grok-cli:access api:access` |
| `REFERRER` | `lorca` |
| `ACCOUNTS_ORIGIN` | `https://accounts.x.ai` (CORS for the accounts page fetch) |
| Callback | `http://127.0.0.1:{ephemeral}/callback` (`TcpListener::bind(..., 0)`) |
| Login timeout (CLI) | `5 * 60` seconds |
| Token `expires_in` fallback | `6 * 3600` seconds |
| Expiry skew | 5 minutes |

<ParamField body="LORCA_GROK_ISSUER" type="string">
Optional env override. When set (non-empty), CLI connect/disconnect and agent providers use `Endpoints::at(issuer)` instead of `Endpoints::xai()`. Trailing slashes are stripped. Endpoints derived: `/oauth2/authorize`, `/oauth2/token`, `/oauth2/userinfo`, `/oauth2/revoke`.
</ParamField>

### `login` signatures

```rust
chatgpt::login(
    client: &reqwest::Client,
    open_url: impl FnOnce(&str) -> Result<(), String>,
    timeout: Duration,
) -> Result<ChatGptTokens, String>

grok::login(
    client: &reqwest::Client,
    endpoints: &Endpoints,
    open_url: impl FnOnce(&str) -> Result<(), String>,
    timeout: Duration,
) -> Result<GrokTokens, String>
```

Both bind the callback **before** calling `open_url`, so the browser redirect cannot race the listener. Dropping the login future aborts the waiter (`AbortOnDrop`) and releases the port.

Grok’s callback answers CORS preflight from `https://accounts.x.ai` (including `Access-Control-Allow-Private-Network`) so the accounts page can fetch the loopback URL cross-origin instead of falling back to a manual code paste.

## Error cases

All public fallible APIs return `Result<_, String>` with human-readable messages.

### Shared / login lifecycle

| Cause | Typical message |
| --- | --- |
| `open_url` fails | whatever the callback returns (e.g. `Cannot open the browser: …`, or CLI cancel path `Sign-in cancelled`) |
| Browser never returns | `Timed out waiting for the browser` |
| OAuth `error` / `error_description` query | `Sign-in was denied: {error}` |
| State mismatch | `Sign-in state mismatch` |
| Token HTTP failure | `{status}: {error_description\|error\|Token request rejected}` |
| Network to token endpoint | `Token request failed: …` / `Token refresh failed: …` |
| Unparseable token body | `Token response unreadable: …` |
| Missing `access_token` / `refresh_token` | `Token response has no access_token` / `… no refresh_token` |

### ChatGPT-specific

| Cause | Message |
| --- | --- |
| Port 1455 unavailable | `Cannot listen on port 1455: …` |
| JWT lacks ChatGPT account id | `Sign-in token carries no ChatGPT account id` |

### Grok-specific

| Cause | Message |
| --- | --- |
| Cannot bind ephemeral port | `Cannot listen for the sign-in callback: …` |
| Callback closed with no result | `The sign-in callback closed` |
| Callback missing `code` | `The callback carried no authorization code` |
| Forbidden / subscription text in token error | `{status}: {message}. Grok sign-in needs a SuperGrok or X Premium+ subscription.` |
| Revoke non-success | `Revoke answered {status}` (logged at debug on disconnect; local disconnect still proceeds) |

### CLI wrapper errors that sit above the crate

These come from `crates/cli/src/provider_auth.rs` / API dispatch, not from `lorca-provider-auth` itself:

- Unknown disconnect kind → `Unknown provider {kind}`
- Passing an API key to ChatGPT/Grok via CLI → `{kind} connects with a browser sign-in and takes no API key`

<Warning>
API-key connect paths (`connect_deepseek`, `connect_anthropic`, `connect_opencode`, `connect_opencode_go`) validate keys and write `ApiKeyCredential` values in the CLI crate. They do not call `lorca-provider-auth`.
</Warning>

## Storage and refresh contract

Tokens serialize into the account `Credentials` blob (`chatgpt` / `grok` fields) under the account DEK, synced as the encrypted `credentials` slot. Every Device shares one set; a Runner builds live providers from them.

Refresh behavior to keep in mind:

1. ChatGPT refresh posts `grant_type=refresh_token` + `client_id` to `{ISSUER}/oauth/token`.
2. Grok refresh posts the same grant to `{issuer}/oauth2/token`. If the response omits `refresh_token`, the previous refresh token is kept.
3. Agent `fresh_tokens` retries against the shared store when refresh fails because another holder already rotated the refresh token.
4. Grok disconnect best-effort revokes the refresh token at `/oauth2/revoke`; failure does not undo the local clear.

## Verify locally

About one command if the workspace builds:

```bash
cargo test -p lorca-provider-auth
```

Covered signals: authorize URL carries PKCE + scopes; ChatGPT cancel releases port 1455; Grok callback binds a free port, answers accounts.x.ai CORS, survives idle preconnects, and refresh keeps an old refresh token when none returns.

Next: open `crates/provider-auth/src/grok.rs` and skim `Callback::wait` if you are wiring a custom Device UI for Grok sign-in.

## Related pages

<CardGroup>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Feature flags, `provider_auth` wrappers, and `lorca provider` commands that call this crate.
  </Card>
  <Card title="crates/agent reference" href="/ref-crates-agent">
    ChatGPT/Grok providers that refresh tokens through `lorca-provider-auth` before model calls.
  </Card>
  <Card title="HTTP API reference" href="/http-api-reference">
    `providers.connect_*`, `providers.auth.cancel`, and `providers.disconnect` method shapes.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env overrides such as `LORCA_GROK_ISSUER` and credential storage paths.
  </Card>
</CardGroup>

---

## 16. Deployment and operations

> Build, containerize, deploy, configure runtime environments, and observe the running system.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/16-deployment-and-operations.md
- Generated: 2026-09-26T20:21:57.909Z

### Source Files

- `ARCHITECTURE.md`
- `crates/relay/Dockerfile`
- `docs/releasing-mac.md`
- `README.md`
- `docs/releasing-cli.md`

---
title: "Deployment and operations"
description: "Build, containerize, deploy, configure runtime environments, and observe the running system."
---

`lorca-relay` is the only always-on network service: build it from `crates/relay/Dockerfile`, run it behind HTTPS, point Devices at the URL with `LORCA_RELAY_URL` or Settings › Advanced › Relay URL. Everything else is a client release (CLI archives, Mac Sparkle feed, iOS TestFlight) or the marketing site Worker.

```mermaid
flowchart LR
  subgraph clients [Client releases]
    CLI["lorca CLI<br/>GitHub cli-v*"]
    MAC["Lorca.app<br/>Sparkle + R2"]
    IOS["iOS<br/>TestFlight"]
    WEB["web/<br/>Cloudflare Worker"]
  end
  subgraph runtime [Self-hosted relay]
    RELAY["lorca-relay<br/>axum :PORT"]
    DB[(SQLite volume<br/>or Postgres)]
    FILES[Local files dir<br/>or S3/R2]
  end
  CLI --> RELAY
  MAC --> RELAY
  IOS --> RELAY
  RELAY --> DB
  RELAY --> FILES
```

## Deployable surfaces

| Surface | Artifact | Typical host | Trigger |
| --- | --- | --- | --- |
| Relay | `lorca-relay` binary / Docker image | Railway, any Docker host | Image build + env |
| CLI | `lorca-cli-*-*.tar.gz` / `.zip` + `.sha256` | GitHub Releases (`cli-vX.Y.Z`) | `.github/workflows/release-cli.yml` |
| macOS app | `.dmg`, Sparkle `.zip`, `appcast.xml` | Cloudflare R2 (`mac-releases.lorca.app`) | `bun run release-mac` |
| iOS | App Store archive | App Store Connect / TestFlight | `bun run release-ios` |
| Website | TanStack Start Worker | Cloudflare (`wrangler deploy`) | `bun run web:deploy` |

Production Mac apps default the CLI to `https://relay.lorca.app` via `LORCA_DEFAULT_RELAY_URL` when unset. A Device with no relay still runs alone on that machine.

## Relay: build and run the container

Build from the **repo root** (Cargo workspace context). About 5–15 minutes cold; dependency layers stay cached until manifests change.

<Steps>
  <Step title="Build the image">
    ```bash
    docker build -f crates/relay/Dockerfile -t lorca-relay .
    ```
  </Step>
  <Step title="Run with a volume (SQLite)">
    ```bash
    docker run -p 8787:8787 \
      -e LORCA_RELAY_SECRET="$(openssl rand -hex 32)" \
      -v lorca-relay:/data \
      lorca-relay
    ```
    Working directory is `/data`. Defaults: `lorca-relay.db` and `lorca-relay.files` there.
  </Step>
  <Step title="Verify (under 30 seconds)">
    ```bash
    curl -s http://127.0.0.1:8787/v1/health
    ```
    Expected: `{"ok":true,"service":"lorca-relay","protocol":…}`. Root `/` returns `Lorca Relay is running...`.
  </Step>
</Steps>

Image behavior:

- Multi-stage: `cargo-chef` → release build of `-p lorca-relay` → `debian:trixie-slim`
- TLS roots are compiled in (`webpki-roots`); no CA bundle in the runtime image
- Listen address: `LORCA_RELAY_BIND`, else `[::]:${PORT:-8787}`
- SIGTERM closes sockets so Devices reconnect after a short backoff

Local binary (no Docker):

```bash
lorca-relay --bind 0.0.0.0:8787 --db lorca-relay.db
```

Put HTTPS and WebSocket upgrades in front (Caddy/nginx). Set `--trust-proxy` / `LORCA_RELAY_TRUST_PROXY=true` so rate limits see real client IPs.

Dev relay from the monorepo: `bun run relay` binds `0.0.0.0:8787` with DB under `temp/` and `--apns-topic app.lorca.dev`.

## Railway deploy

<Steps>
  <Step title="Create the service">
    Root Directory empty. Set Healthcheck Path to `/v1/health`. Watch Paths: `/crates/relay/**`, `/Cargo.toml`, `/Cargo.lock`.
  </Step>
  <Step title="Set required variables">
    | Variable | Value |
    | --- | --- |
    | `RAILWAY_DOCKERFILE_PATH` | `crates/relay/Dockerfile` |
    | `LORCA_RELAY_SECRET` | `openssl rand -hex 32` (stable across restarts) |
    | `LORCA_RELAY_TRUST_PROXY` | `true` |

    Leave `PORT` and `LORCA_RELAY_BIND` unset so Railway’s `$PORT` is used.
  </Step>
  <Step title="Pick storage">
    **SQLite:** volume at `/data`. One deploy at a time; old process stops before the new one starts (~few seconds of reconnect).

    **Postgres + bucket:** no volume; overlapping deploys and multiple replicas.
  </Step>
  <Step title="Attach a domain and point clients">
    First Device: Settings › Advanced › Relay URL, or `LORCA_RELAY_URL=https://<domain>`. Later Devices learn the URL from the pairing code.
  </Step>
</Steps>

### Postgres + object storage

| Variable | Value |
| --- | --- |
| `LORCA_RELAY_DB` | `${{Postgres.DATABASE_URL}}?sslmode=disable` |
| `LORCA_RELAY_S3_BUCKET` | Bucket name |
| `LORCA_RELAY_S3_ENDPOINT` | e.g. `https://<account>.r2.cloudflarestorage.com` |
| `LORCA_RELAY_S3_ACCESS_KEY` / `LORCA_RELAY_S3_SECRET_KEY` | Or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` |
| `LORCA_RELAY_S3_REGION` | Default `auto` |
| `LORCA_RELAY_S3_PREFIX` | Optional key prefix |

`sslmode=disable` is required for Railway’s private Postgres (self-signed cert vs webpki roots). All replicas must share the same `LORCA_RELAY_SECRET` and the same bucket. Objects use path-style URLs.

## Runtime configuration

Every flag has an env var (`lorca-relay --help`).

### Always set in production

<ParamField body="LORCA_RELAY_SECRET" type="string" required>
Signs bearer tokens. Unset → new secret every boot → every Device token invalid.
</ParamField>

<ParamField body="LORCA_RELAY_TRUST_PROXY" type="bool" default="false">
Read client IP from `X-Forwarded-For`. Required behind Railway/Caddy/nginx.
</ParamField>

### Storage and limits

| Env | Default | Role |
| --- | --- | --- |
| `LORCA_RELAY_DB` | `lorca-relay.db` | SQLite path or `postgres://…` |
| `LORCA_RELAY_FILES_DIR` | beside DB | Local attachment ciphertext (conflicts with S3) |
| `LORCA_RELAY_QUOTA_BYTES` | `5368709120` (5 GiB) | Per-identity ciphertext cap; `0` = unlimited |
| `LORCA_RELAY_CONCURRENT_UPLOADS` | `3` | Large uploads at once (~80 MB each while decoding); `0` = unlimited |
| `LORCA_RELAY_IP_PER_MINUTE` | `60` | Unauth routes (register/auth/pair) |
| `LORCA_RELAY_IDENTITY_PER_SECOND` | `50` | Per-identity; burst 10× |
| `LORCA_RELAY_MIN_PROTOCOL` | `0` | Older clients get `426` |
| `LORCA_RELAY_INACTIVE_DAYS` | `365` | Drop idle identities; `0` keeps all |

Attachment ceiling: **100 MiB** + 40-byte envelope. Non-file blobs: **4 MiB**.

### Push (optional)

| Env | Role |
| --- | --- |
| `LORCA_RELAY_APNS_KEY` | `.p8` text or file path |
| `LORCA_RELAY_APNS_KEY_ID` / `LORCA_RELAY_APNS_TEAM_ID` | Apple key + team |
| `LORCA_RELAY_APNS_TOPIC` | Default `app.lorca` |
| `LORCA_RELAY_FCM_SERVICE_ACCOUNT` | Firebase JSON text or path |

Without these keys the relay still syncs; phones get no pushes. First log line shows `push=apns`, `push=fcm`, or `push=apns+fcm`.

## Observe the running relay

| Check | How | Success |
| --- | --- | --- |
| Liveness | `GET /v1/health` | JSON `ok`, `service`, `protocol` |
| Banner | `GET /` | `Lorca Relay is running...` |
| Boot config | First log line | Names bind, db, files, push |
| Metrics | `GET /metrics` with bearer | Prometheus text when token set |
| Verbose rate limits | `RUST_LOG=info,lorca_relay=debug` | Logs limited requests |

<ParamField body="LORCA_RELAY_METRICS_TOKEN" type="string">
Enables `GET /metrics`. Unset → `404`. Scrape with `Authorization: Bearer <token>`. Multi-replica scrapes hit one instance; per-process counters carry `instance`; DB totals match across replicas.
</ParamField>

Metrics cover identities/machines, blob bytes by kind, open sockets, requests by route/status, rate-limit refusals, pushes, and sweep removals. Ciphertext content is never readable by the relay.

Housekeeping: minute tick + hourly/daily sweeps (stale envelopes, orphan objects, inactive identities).

## Client and site releases

### CLI (`cli-vX.Y.Z`)

1. Bump `[workspace.package]` version in root `Cargo.toml`, `cargo check -p lorca`, commit.
2. `gh workflow run release-cli.yml` or `git tag cli-vX.Y.Z && git push origin cli-vX.Y.Z`.
3. Publish the draft as **latest** so `install-cli.sh` / `install-cli.ps1` hit `releases/latest`.

Archives: macOS aarch64, Linux musl aarch64/x86_64, Windows x86_64 + `.sha256`. Install env: `LORCA_VERSION`, `LORCA_INSTALL_DIR` (default `~/.local/bin`), `LORCA_NO_MODIFY_PATH=1`, `LORCA_DOWNLOAD_URL`.

### macOS app (Sparkle)

One-time: Developer ID + `notarytool` profile `NOTARY`, Sparkle Ed25519 in login keychain, rclone remote `r2` → bucket `lorca-mac-releases`.

```bash
bun run release-mac 0.2.0          # build, notarize, appcast, upload
bun run release-mac --local        # notarized .dmg only, no publish
```

Version is root `package.json` `"version"`. Feed: `https://mac-releases.lorca.app/appcast.xml`. Staging under `dist/mac/`.

### iOS (TestFlight)

```bash
bun run release-ios                # archive + upload
bun run release-ios --local        # archive only
BUILD_NUMBER=42 bun run release-ios
```

Needs Xcode account, `cargo`, `pod`, etc. Marketing version from `mobile/app.config.ts`; default build number is local `YYYYMMDDHHmm`. Bundle id `app.lorca`. Appears in TestFlight after Apple processing (~30 minutes).

### Website

```bash
bun run web:deploy                 # vite build && wrangler deploy
```

Worker name `lorca` in `web/wrangler.jsonc` (`nodejs_compat`, observability on). Install scripts ship with the site; `_headers` serves them as UTF-8 text.

## Version lines to keep straight

| Product | Version source |
| --- | --- |
| CLI / crates | `[workspace.package]` in `Cargo.toml` |
| Mac app | `"version"` in root `package.json` |
| Phone marketing version | `mobile/app.config.ts` |

Prod vs dev installs use different ids (`app.lorca` vs `app.lorca.dev`), homes (`~/.lorca` vs `~/.lorca-dev`), and CLI ports (`4862` vs `4863`).

## Related pages

<CardGroup>
  <Card title="Configuration reference" href="/configuration-reference">
    Env vars, flags, and defaults for relay and clients.
  </Card>
  <Card title="crates/relay reference" href="/ref-crates-relay">
    Binary options, routes, storage backends, error cases.
  </Card>
  <Card title="Installation" href="/installation">
    Install CLI or app and first success signal.
  </Card>
  <Card title="Architecture" href="/architecture">
    Device/relay boundaries and sync data flow.
  </Card>
</CardGroup>

Next: `curl -s https://<your-relay>/v1/health` and confirm `ok` is `true`.

---

## 17. Testing

> How to run the test suites, what they cover, fixtures, and how continuous integration executes them.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/17-testing.md
- Generated: 2026-09-26T20:20:05.966Z

### Source Files

- `package.json`
- `Cargo.toml`
- `.github/workflows/release-cli.yml`
- `crates/cli/tests/provider_settings.rs`
- `macos/Package.swift`

---
title: "Testing"
description: "How to run the test suites, what they cover, fixtures, and how continuous integration executes them."
---

Run `cargo test` from the repo root for the Rust workspace (agent loop, crypto, SSE, CLI, relay, and related crates). That is the default local gate named in the root README.

```bash
cargo test
```

Success: every package reports `ok`, including the `lorca` binary integration tests under `crates/cli/tests/`.

## Suites at a glance

| Surface | Command | Runtime | What it proves |
| --- | --- | --- | --- |
| Rust workspace | `cargo test` | ~minutes on a cold cache | Unit + async tests across `crates/*`, plus CLI integration tests |
| Phone core | `bun run --cwd mobile test` | about 1 command | Pairing/model/format helpers and reading/engine behavior with native bridges mocked |
| Mac startup | `bun run test:mac-startup` | about 1 command | CLI launcher concurrency without loading AppKit |
| Mac SPM | `cd macos && swift test` | SPM `LorcaTests` | Notifications, transcript accessibility, markdown layout, running tasks |
| CLI release CI | tag `cli-v*` or `workflow_dispatch` | GitHub Actions | Cross-platform release builds + install-script verification (not `cargo test`) |

<Note>
There is one workflow under `.github/workflows/`: `release-cli.yml`. It does not run the unit/integration suites above. Local `cargo test` / Bun / Swift runs are the coverage for those.
</Note>

## Run Rust tests

<Steps>
  <Step title="Default workspace run">
    From the repo root:

    ```bash
    cargo test
    ```

    Workspace members: `lorca-agent`, `lorca-provider-auth`, `lorca-relay`, `lorca`, `lorca-mobile`, `lorca-markdown` (`Cargo.toml`). The `lorca` binary requires the `cli` feature (default on).
  </Step>
  <Step title="Package filters">
    ```bash
    cargo test -p lorca
    cargo test -p lorca-relay
    cargo test -p lorca-agent
    cargo test -p lorca-provider-auth
    cargo test -p lorca-markdown
    ```
  </Step>
  <Step title="Optional Postgres for relay DB">
    SQLite always runs (temp file). Postgres joins when this is set:

    ```bash
    export LORCA_RELAY_TEST_POSTGRES='postgres://postgres:lorca@127.0.0.1:55432/lorca'
    cargo test -p lorca-relay
    ```

    Identities are random so shared databases are safe across concurrent runs.
  </Step>
</Steps>

### CLI integration tests

Under `crates/cli/tests/`:

| File | Behavior under test |
| --- | --- |
| `provider_settings.rs` | `providers.api_key` returns only the requested key/base URL; secrets stay out of `snapshot()` and credential statuses; unknown kinds and missing `kind` error |
| `serve_ready.rs` | `lorca serve --ready-stdout` prints one JSON `{"event":"ready","port":…}` with logs off; WebSocket `/ws` answers `bootstrap`; a failed bind exits with empty stdout and no ready line |

`serve_ready.rs` is gated on `feature = "cli"` and spawns `CARGO_BIN_EXE_lorca` with a cleared env (`RUST_LOG=off`), temp `--home`, and `--port`.

### Relay coverage (high signal)

- **DB store** (`crates/relay/src/db/tests.rs`): slots (first/latest), refused puts, deleted groups, byte-budget pages, machines/challenges/revocation, pairing mailbox, presence/events, sweeps, orphans, inactive identities, usage recount, backward group paging.
- **HTTP routes** (`crates/relay/src/routes/tests.rs`): encrypted binary file round-trip and idempotent PUT; auth/metadata/quota/missing objects; envelope overhead and chunked body caps.
- **In-module unit tests** also sit in `hub`, `store`, `sweep`, `metrics`, `push`, and `main`.

Route tests stand up an in-process axum server on `127.0.0.1:0`, local SQLite + file store under a temp home, and tear both down on drop.

### Agent and provider unit tests

Most agent behavior is unit-tested inside `crates/agent` (retry, compaction, login shell, SSE/JSON, providers, harness, tools, agent loop). Provider-auth OAuth helpers are covered in `crates/provider-auth`.

## Live provider tests (ignored)

Three DeepSeek live tests are `#[ignore]` and need a real key:

```bash
DEEPSEEK_API_KEY=… cargo test -p lorca-agent live_deepseek -- --ignored --nocapture
DEEPSEEK_API_KEY=… cargo test -p lorca-agent live_deepseek_takes -- --ignored --nocapture
DEEPSEEK_API_KEY=… cargo test -p lorca-agent live_deepseek_summary -- --ignored --nocapture
```

| Filter | Location | Intent |
| --- | --- | --- |
| `live_deepseek_search_then_tool_call_replays` | Anthropic-compatible DeepSeek path | Search then tool call; seals/server blocks replay |
| `live_deepseek_takes_a_turns_four_cache_marks` | same | Cache-mark behavior across a turn |
| `live_deepseek_summary` | compaction | Live summary path |

Without `DEEPSEEK_API_KEY`, those bodies return early. They are not part of a plain `cargo test`.

## Run mobile tests

```bash
bun run --cwd mobile test
# equivalent: cd mobile && bun test src
```

| File | Coverage |
| --- | --- |
| `mobile/src/core/core.test.ts` | Pairing URL parse/reject; sent-message and command-card rules; provider kind/API-key/defaults/models; attachment summaries; dictation join; sidebar-style previews |
| `mobile/src/core/reading.test.ts` | Real engine/store with mocked RN/Expo/native core: read acknowledgements, background vs foreground unread, notifications, working-row states, snapshot/event ordering, running tasks |

Related check (not a test runner): `bun run --cwd mobile typecheck` (`tsc --noEmit`).

<Info>
Under Bun there is no native localization module, so i18n falls back to English the same way the phone core comments describe for these tests.
</Info>

## Run macOS tests

### Startup concurrency (no AppKit)

```bash
bun run test:mac-startup
```

`scripts/test-mac-startup.ts` compiles `macos/Tests/LaunchConcurrency.swift` with production `CLILaunchWorker.swift` and `StartupTrace.swift`, runs the binary against a temp directory, then deletes it. It checks launcher progress with the main thread blocked, fragmented readiness, early exit, and cancel without leaving a child.

### SPM `LorcaTests`

```bash
cd macos && swift test
```

`Package.swift` defines target `LorcaTests` at `Tests/Notifications`, depending on the `Lorca` executable target.

| File | Themes |
| --- | --- |
| `ChatNotificationTests.swift` | Failure vs recovered reply notifications; pending confirmation; suppress when read/visible |
| `ChatRowRunTests.swift` | Working row vs reply/status ordering; paging and removals |
| `TranscriptAccessibilityTests.swift` | Accessibility elements for on-screen rows, paging identity, selection/scroll |
| `MarkdownLayoutTests.swift` | Code-block padding, soft breaks, list indent, measured width |
| `RunningTaskTests.swift` | Elapsed time, status text, terminal-only running tasks |

## Fixtures and isolation patterns

```text
temp home / temp DB
  ├─ CLI tests:   $TMPDIR/lorca-provider-settings-<uuid>
  │               $TMPDIR/lorca-ready-<uuid>  (+ Drop cleanup)
  ├─ Relay routes: $TMPDIR/lorca-binary-<uuid>/{relay.db,files}
  ├─ Relay DB:     $TMPDIR/lorca-relay-test-<uuid>.db
  └─ Mac startup:  $TMPDIR/lorca-launch-tests-*  (mkdtemp, always removed)
```

Common patterns:

- **Temp home + Drop** clears directories even when a test fails.
- **Cleared process env** on `serve` children (`env_clear`, then `RUST_LOG=off`) so host credentials do not leak into the binary under test.
- **In-process servers** bind `127.0.0.1:0`; no fixed ports.
- **Mobile mocks** replace `react-native`, Expo modules, and `modules/lorca-core` so no account or provider is contacted.

## Continuous integration

Workflow: `.github/workflows/release-cli.yml` (`Release CLI`).

| Trigger | Effect |
| --- | --- |
| Push tag `cli-v*` | Build → install-script test → draft GitHub release |
| `workflow_dispatch` | Same path on the selected commit |

Jobs in order:

1. **`version`** — reads `lorca` version from `cargo metadata`; expects tag `cli-v<version>`; refuses tag/version/commit mismatches; skips work if that release is already published.
2. **`build`** (matrix) — `cargo build` / `cargo zigbuild --release --locked -p lorca` for `macos-aarch64`, `linux-aarch64` (musl), `linux-x86_64` (musl), `windows-x86_64` (gnu via zig).
3. **`test`** (matrix: `ubuntu-latest`, `macos-latest`, `windows-latest`) — serves built archives on `http://127.0.0.1:8000`, sets `LORCA_DOWNLOAD_URL`, runs install scripts twice (install then update), asserts `lorca --version` equals `lorca <version>`, and on Windows checks user PATH contains `\.local\bin`.
4. **`publish`** — drafts or refreshes GitHub release `cli-v<version>` with archives (does not auto-publish).

Concurrency group `release-cli` keeps one run at a time. `CARGO_TERM_COLOR=always`. No secrets required for drafting; the job token writes the release.

<Warning>
CI here validates **release artifacts and install scripts**, not the Rust/mobile/mac unit suites. Before tagging a CLI release, run `cargo test` locally when you changed crates.
</Warning>

## Surfaces without automated suites in-repo

| Area | Status |
| --- | --- |
| `web/` (TanStack Start site) | No `test` script or `*.test.*` / `*.spec.*` files in tree |
| Full AppKit UI | Exercised via SPM notification/transcript tests and manual `bun run dev`; startup concurrency is the automated launcher check |
| End-to-end phone ↔ relay ↔ Runner | Not a single checked-in E2E runner; relay/CLI/mobile cover pieces separately |

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `serve_ready` times out on ready line | Binary hung or logs interfered | Confirm default `cli` feature; tests already force `RUST_LOG=off` and `--ready-stdout` |
| Relay DB tests skip Postgres behavior | `LORCA_RELAY_TEST_POSTGRES` unset | Export a reachable `postgres://…` URL, then re-run `-p lorca-relay` |
| `live_deepseek_*` never runs | Ignored + no key | Pass `--ignored` and set `DEEPSEEK_API_KEY` |
| `test:mac-startup` compile fails | Missing `swiftc` / Xcode CLT | Install Xcode command-line tools; run on macOS |
| Release workflow “already published” | Tag release exists non-draft | Delete published release + tag before rebuilding, or bump `[workspace.package].version` |

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Prerequisites and first successful install before running suites.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `serve`, flags such as `--ready-stdout` / `--home` / `--port`, and exit behavior used by integration tests.
  </Card>
  <Card title="crates/relay reference" href="/ref-crates-relay">
    Store backends, routes, and the Postgres test env var surface.
  </Card>
  <Card title="Deployment and operations" href="/deployment">
    Release builds, install scripts, and how CI drafts CLI archives.
  </Card>
</CardGroup>

Next: run `cargo test` from the repo root and confirm the workspace finishes with all packages `ok`.

---

## 18. Changelog and migration

> Version history, breaking changes, and upgrade steps between versions.

- Page Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/pages/18-changelog-and-migration.md
- Generated: 2026-09-26T20:21:41.944Z

### Source Files

- `CHANGELOG.md`

---
title: "Changelog and migration"
description: "Version history, breaking changes, and upgrade steps between versions."
---

Lorca ships **three independent version lines**: Mac app (`package.json` → Sparkle), standalone CLI (`Cargo.toml` `[workspace.package]` → GitHub `cli-vX.Y.Z`), and mobile (`mobile/app.config.ts` → TestFlight / store builds). `CHANGELOG.md` feeds **Mac Sparkle release notes only**.

| Surface | Source of truth | Current tree | How users get updates |
| --- | --- | --- | --- |
| Mac app | root `package.json` `"version"` | `1.0.7` | Sparkle feed `https://mac-releases.lorca.app/appcast.xml` |
| CLI | `Cargo.toml` `[workspace.package].version` | `0.1.7` | Re-run install script, or pin `LORCA_VERSION` |
| Mobile | `mobile/app.config.ts` `version` | `1.0.0` | iOS: `bun run release-ios` → TestFlight |

<Warning>
`CHANGELOG.md` currently has `## [Unreleased]` and `## [0.1.0]` only. The Mac tree is at `1.0.7` with **no matching changelog section**, so `bun run release-mac` would ship that version without Sparkle notes until you add `## [1.0.7]`.
</Warning>

## Version map

```text
package.json "version"  ──► Info.plist CFBundle*  ──► Sparkle compares / updates
Cargo.toml version      ──► lorca --version       ──► GitHub release cli-vX.Y.Z
mobile/app.config.ts    ──► CFBundleShortVersion  ──► TestFlight (build number separate)
```

- Mac release replaces the **whole** `.app`, including the CLI under `Contents/Resources/bin`.
- Standalone CLI installs under `~/.local/bin` by default; a Mac-app update does **not** refresh that copy.
- CLI and Mac versions are **not** required to match.

## Changelog format (Mac)

`CHANGELOG.md` is Keep-a-Changelog-style. `scripts/changelog.ts` `extractReleaseNotes` reads a level-2 heading whose first token is the version (`## [1.0.7]`, `## 1.0.7`, `## v1.0.7`).

On release, `bun run release-mac` writes `Lorca-<version>.md` beside the Sparkle zip when a section exists; otherwise it logs a warning and continues.

### Add notes for the next Mac cut (~2 minutes)

<Steps>
  <Step title="Add the section">
    Put `## [X.Y.Z]` at the **top** of `CHANGELOG.md` (above older sections). Move finished bullets out of `## [Unreleased]`.
  </Step>
  <Step title="Release">
    Run `bun run release-mac X.Y.Z`. That bumps `package.json`, builds/signs/notarizes, regenerates `appcast.xml`, and uploads to R2.
  </Step>
  <Step title="Commit">
    Commit `package.json` and `CHANGELOG.md` so the tree matches what shipped.
  </Step>
</Steps>

After this, Sparkle’s update window shows that section’s body.

## Release notes

:::updates
@update Unreleased - Mac notes queued for the next Sparkle cut

- Command cards appear only while input or a long-running command needs you (Allow once / Always allow / Deny, or live output).
- Running-tasks toolbar control lists in-flight bot commands with Stop per command.
- Interactive commands (`sudo`, `ssh`, `[Y/n]`) no longer hang the turn; answers are never stored in chat. Quiet commands wait after 20s silence; sessions end after 30 minutes without output, when Lorca quits, or when the chat is deleted.
- Terminal color codes are stripped before the bot sees output (full file keeps them).
- ChatGPT, Grok, and OpenCode GPT/Grok chats stick to the prompt-cache server; Spent no longer double-counts cached input on those providers; Claude cache reuse improved across groups and heavy tool turns; one chat speaking no longer breaks another chat’s cache.

@update 0.1.0 - First Mac release

- Initial public Mac app release (`## [0.1.0]` in `CHANGELOG.md`).

@update CLI 0.1.7 - Latest tagged standalone CLI

- Workspace crate version `0.1.7`; GitHub tag `cli-v0.1.7`. Confirm with `lorca --version` → `lorca 0.1.7`.
:::

## Upgrade: Mac app (~1 minute)

Automatic checks are on (`SUEnableAutomaticChecks`). Sparkle verifies EdDSA signatures with `SUPublicEDKey` from Info.plist before install.

<Steps>
  <Step title="Check">
    Open Lorca → **Check for Updates…**, or Settings ▸ General ▸ Updates.
  </Step>
  <Step title="Install">
    Accept the update. Sparkle swaps the bundle (app + bundled CLI). Lorca stops the old CLI on quit and starts the new one on relaunch.
  </Step>
  <Step title="Verify">
    Confirm the version in Settings / About matches the feed (`Updater.currentVersion` reads `CFBundleShortVersionString`).
  </Step>
</Steps>

<Note>
Debug / `Lorca Dev` builds disable Sparkle (`Updater.isEnabled` is false under `DEBUG`). Old R2 archives stay published so far-behind installs can still full-download.
</Note>

## Upgrade: standalone CLI (~1 minute)

Re-run the installer. It downloads from `releases/latest` (or `LORCA_VERSION`), checks `.sha256`, and replaces the binary via rename so a running `lorca serve` keeps its open file.

<Tabs>
  <Tab title="macOS / Linux">
```sh
curl -fsSL https://lorca.app/install-cli.sh | sh
lorca --version
```
  </Tab>
  <Tab title="Windows (PowerShell)">
```powershell
irm https://lorca.app/install-cli.ps1 | iex
lorca --version
```
  </Tab>
  <Tab title="Pin a version">
```sh
LORCA_VERSION=0.1.7 curl -fsSL https://lorca.app/install-cli.sh | sh
```
  </Tab>
</Tabs>

| Variable | Default | Effect |
| --- | --- | --- |
| `LORCA_VERSION` | latest | Install that crate version (no `v` / `cli-v` prefix) |
| `LORCA_INSTALL_DIR` | `~/.local/bin` | Install location |
| `LORCA_NO_MODIFY_PATH` | unset | Skip PATH profile edits |
| `LORCA_DOWNLOAD_URL` | GitHub releases URL | Override download base |

<Check>
Success: installer prints `Installed lorca <version> at …/lorca`, and `lorca --version` matches.
</Check>

## Upgrade: iOS (~30 minutes after upload)

Marketing version is `version` in `mobile/app.config.ts` (`1.0.0`). Each upload needs a higher build number (`BUILD_NUMBER` or timestamp via `bun run release-ios`). Users get builds through TestFlight after App Store Connect processing.

## Storage and schema migration

### Device home (`~/.lorca` or `LORCA_HOME`)

`App::load` states the local storage layout has **no migration path**. On every launch it deletes superseded files if present:

- `state.json`
- `transcript.sqlite3` (+ `-wal` / `-shm`)

Current state lives in `lorca.sqlite3` (`PRAGMA user_version = 1`). Identity, credentials, chats, and plugins stay under the home directory across Mac/CLI binary upgrades.

### Hard wipe (destructive, ~1 minute)

```sh
bun run reset          # prompts; deletes ~/.lorca (or ~/.lorca-dev with --dev)
bun run reset -y       # skip confirm
bun run reset --relay  # also wipe local relay DB under temp/
```

Without the backup phrase, a reset identity is unrecoverable.

### Relay databases

SQLite and Postgres backends keep a `schema_version` table and an append-only `MIGRATIONS` list. Both lists are **empty** in the current tree (baseline schema only). Policy: steps only **add** tables, nullable columns, or indexes; never edit a shipped step. Postgres applies steps once (with deadlock retry) so old and new processes can run side by side during deploy.

## Cutting a release (maintainers)

### Mac app

1. Add `## [X.Y.Z]` to `CHANGELOG.md`.
2. `bun run release-mac X.Y.Z` (or `bun run release-mac --local` to notarize without upload).
3. Commit `package.json` + `CHANGELOG.md`.

Env knobs: `FORCE=1` replace published zip; `NO_HISTORY=1` skip delta archives; `R2_BUCKET` / `DOWNLOAD_URL_PREFIX` for the feed host.

### CLI

1. Set `[workspace.package].version` in `Cargo.toml`, run `cargo check -p lorca`, commit with `Cargo.lock`.
2. `gh workflow run release-cli.yml` or `git tag cli-vX.Y.Z && git push origin cli-vX.Y.Z`.
3. Publish the draft with **Set as the latest release** checked (`install-cli` uses `releases/latest`).

Tag must match crate version; a mismatched or already-published tag fails the workflow.

## Agent API: coming from pi

Porting TypeScript `pi-agent-core` code uses the name map in `docs/agent/coming-from-pi.md` (`Agent::new`, `prompt` / `continue_run`, `QueueMode`, `ToolExecutionMode`, channel events). Persistence and branching stay host-owned; Lorca does not ship pi’s session tree.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    First install for Mac, CLI, and mobile, plus recovery notes.
  </Card>
  <Card title="Deployment and operations" href="/deployment">
    Release pipelines, R2/Sparkle, CLI workflow, and relay ops.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Commands, flags, `lorca --version`, and serve defaults.
  </Card>
  <Card title="Data model reference" href="/data-model-reference">
    Local SQLite and relay `schema_version` shapes.
  </Card>
</CardGroup>

Next: open `CHANGELOG.md` and confirm whether your Mac version needs a new `## [X.Y.Z]` section before the next `bun run release-mac`.

---
