| author | |
| committer | |
| log | 9dd425b2234c062b4767583e42d4b2c1aabab15d |
| tree | 6955e51143b23e187de10466b7a76dcfabb3dcbb |
| parent | 50871eb5de641ffd41b1f9d0151668982d276393 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>51 files changed, 4098 insertions(+), 4016 deletions(-)
AGENTS.md+5-5| ... | ... | @@ -28,7 +28,7 @@ T3 Code has 3 key app surfaces: **web**, **desktop**, and **mobile**. |
| 28 | 28 | |
| 29 | 29 | **Desktop** is the main surface most users install first. It's a full Electron app that bundles the server runner as well. The desktop app can also be used as the host server, allowing remote connections from app.t3.codes or the mobile app. |
| 30 | 30 | |
| 31 | **Mobile** is a React native app for both iOS and Android. The mobile app allows for connecting to any T3 Code server to control work remotely. It is still in early access (Testflight), but it is pretty close to shipping globally. | |
| 31 | **Mobile** is a React Native app for both iOS and Android, available on the App Store and Google Play. The mobile app allows for connecting to any T3 Code server to control work remotely. | |
| 32 | 32 | |
| 33 | 33 | ## A note from Theo |
| 34 | 34 | |
| ... | ... | @@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste |
| 72 | 72 | - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow. |
| 73 | 73 | - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug. |
| 74 | 74 | - **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real. |
| 75 | - **Docs.** `docs/` mirrors this structure. Behavior changes that a user would notice belong in `docs/user/`; architecture changes in `docs/architecture/`; new vocabulary in `docs/reference/encyclopedia.md`. | |
| 75 | - **Docs.** `docs/` splits by audience. Behavior changes that a user would notice belong in `docs/user/` (shipped-product voice, no repo tooling or source paths); architecture and contributor changes in `docs/internals/`; runbooks in `docs/operations/`; new vocabulary in `docs/internals/glossary.md`. | |
| 76 | 76 | |
| 77 | 77 | ## Dev servers |
| 78 | 78 | |
| ... | ... | @@ -123,13 +123,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real |
| 123 | 123 | |
| 124 | 124 | Clients send typed WebSocket requests. The server turns them into _commands_, a pure _decider_ turns commands into persisted _events_, and a _projector_ derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Side effects run in queue-backed _reactors_ that emit _receipts_ when milestones land. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore. |
| 125 | 125 | |
| 126 | Full glossary with file links: `docs/reference/encyclopedia.md` | |
| 126 | Full glossary with file links: `docs/internals/glossary.md` | |
| 127 | 127 | |
| 128 | 128 | ## Where code lives |
| 129 | 129 | |
| 130 | - `apps/server` - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read `.repos/effect-smol/LLMS.md` and `docs/operations/effect-fn-checklist.md` before writing Effect code. | |
| 130 | - `apps/server` - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read `.repos/effect-smol/LLMS.md` before writing Effect code. | |
| 131 | 131 | - `apps/web` - React/Vite UI. `apps/desktop` wraps it, `apps/mobile` is React Native, `apps/marketing` is the site. |
| 132 | - `packages/contracts` - Effect/Schema contracts. Schema only, no runtime logic. | |
| 132 | - `packages/contracts` - Effect/Schema contracts plus small derived helpers. No heavy runtime logic. | |
| 133 | 133 | - `packages/shared` - shared runtime utils, subpath exports, no barrel. |
| 134 | 134 | - `packages/client-runtime` - client code shared by web and mobile. |
| 135 | 135 | - `.repos/` - vendored read-only references. Prefer their patterns over invented ones. Never edit or import from them. Sync with `vpr sync:repos` when bumping the matching dependency. |
README.md+14-11| ... | ... | @@ -17,13 +17,13 @@ We wanted something performant, remote-ready, and truly open. If we ever go the |
| 17 | 17 | > |
| 18 | 18 | > - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login` |
| 19 | 19 | > - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login` |
| 20 | > - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `cursor-agent login` | |
| 20 | > - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login` | |
| 21 | 21 | > - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login` |
| 22 | 22 | > - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login` |
| 23 | 23 | |
| 24 | 24 | ### Try it out (install-free) |
| 25 | 25 | |
| 26 | The easiest way to test T3 Code is to run the server in your terminal: | |
| 26 | The easiest way to test T3 Code is to run the server in your terminal (requires Node.js 22.16+, 23.11+, or 24.10+): | |
| 27 | 27 | |
| 28 | 28 | ```bash |
| 29 | 29 | npx t3@latest |
| ... | ... | @@ -61,17 +61,20 @@ We are very very early in this project. Expect bugs. |
| 61 | 61 | |
| 62 | 62 | We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be. |
| 63 | 63 | |
| 64 | There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs). | |
| 65 | ||
| 66 | 64 | ## Documentation |
| 67 | 65 | |
| 68 | - [Getting started](./docs/getting-started/quick-start.md) | |
| 69 | - [Remote access](./docs/user/remote-access.md) | |
| 70 | - [Keeping T3 Code in sync](./docs/user/server-updates.md) | |
| 71 | - [Architecture overview](./docs/architecture/overview.md) | |
| 72 | - [Provider guides](./docs/providers/codex.md) | |
| 73 | - [Operations](./docs/operations/ci.md) | |
| 74 | - [Reference](./docs/reference/encyclopedia.md) | |
| 66 | Full docs live in [docs/](./docs). There's no docs site yet. | |
| 67 | ||
| 68 | - [Install and first run](./docs/user/install.md) | |
| 69 | - [Permission modes](./docs/user/permission-modes.md) | |
| 70 | - [Keyboard shortcuts](./docs/user/keybindings.md) | |
| 71 | - [Remote access from a phone or another machine](./docs/user/remote-access.md) | |
| 72 | - [Keeping app and server in sync](./docs/user/updating.md) | |
| 73 | - [Source control integrations](./docs/user/source-control.md) | |
| 74 | - Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md) | |
| 75 | - Linux: [run T3 Code as a background service](./docs/user/background-service.md) | |
| 76 | ||
| 77 | Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md). | |
| 75 | 78 | |
| 76 | 79 | ## If you REALLY want to contribute still.... read this first |
| 77 | 80 |
docs/README.md+41-19| ... | ... | @@ -1,19 +1,41 @@ |
| 1 | # Documentation | |
| 2 | ||
| 3 | - [Getting started](./getting-started/quick-start.md) | |
| 4 | - Architecture | |
| 5 | - [Overview](./architecture/overview.md) | |
| 6 | - [Connection runtime](./architecture/connection-runtime.md) | |
| 7 | - [Remote environments](./architecture/remote.md) | |
| 8 | - [Server updates](./architecture/server-updates.md) | |
| 9 | - User guides | |
| 10 | - [Background service](./user/background-service.md) | |
| 11 | - [Remote access](./user/remote-access.md) | |
| 12 | - [Keeping T3 Code in sync](./user/server-updates.md) | |
| 13 | - [Keybindings](./user/keybindings.md) | |
| 14 | - [T3 Connect](./cloud/t3-connect-clerk.md) | |
| 15 | - [Integrations](./integrations/source-control-providers.md) | |
| 16 | - [Mobile](./mobile/app.md) | |
| 17 | - [Operations](./operations/ci.md) | |
| 18 | - [Providers](./providers/codex.md) | |
| 19 | - [Reference](./reference/encyclopedia.md) | |
| 1 | # T3 Code docs | |
| 2 | ||
| 3 | ## Using T3 Code | |
| 4 | ||
| 5 | - [Install and first run](./user/install.md) | |
| 6 | - [Permission modes](./user/permission-modes.md) | |
| 7 | - [Keyboard shortcuts](./user/keybindings.md) | |
| 8 | - [Remote access](./user/remote-access.md) | |
| 9 | - [Keeping app and server in sync](./user/updating.md) | |
| 10 | - [Source control integrations](./user/source-control.md) | |
| 11 | - [Background service (Linux)](./user/background-service.md) | |
| 12 | - Providers: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) | |
| 13 | ||
| 14 | Mobile app: [apps/mobile/README.md](../apps/mobile/README.md) | |
| 15 | ||
| 16 | --- | |
| 17 | ||
| 18 | ## Working on T3 Code | |
| 19 | ||
| 20 | Everything below is for maintainers. Setup lives in the [root README](../README.md); | |
| 21 | policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../AGENTS.md). | |
| 22 | ||
| 23 | - [Architecture overview](./internals/overview.md) | |
| 24 | - [Workspace layout](./internals/workspace-layout.md) | |
| 25 | - [Glossary](./internals/glossary.md) | |
| 26 | - [Scripts](./internals/scripts.md) | |
| 27 | - [Connection runtime](./internals/connection-runtime.md) | |
| 28 | - [Providers](./internals/providers.md) | |
| 29 | - [Remote environments](./internals/remote.md) | |
| 30 | - [Server updates](./internals/server-updates.md) | |
| 31 | - [Resource telemetry](./internals/resource-telemetry.md) | |
| 32 | - [Environment auth](./internals/environment-auth.md) | |
| 33 | - [T3 Connect](./internals/t3-connect.md) | |
| 34 | - [CI gates](./internals/ci.md) | |
| 35 | ||
| 36 | ### Runbooks | |
| 37 | ||
| 38 | - [Release](./operations/release.md) | |
| 39 | - [Observability](./operations/observability.md) | |
| 40 | - [Relay observability](./operations/relay-observability.md) | |
| 41 | - [Mobile app store screenshots](./operations/mobile-app-store-screenshots.md) |
docs/architecture/connection-runtime.md deleted-140| ... | ... | @@ -1,140 +0,0 @@ |
| 1 | # Connection Runtime | |
| 2 | ||
| 3 | The connection runtime is shared by web and mobile. It owns connectivity, | |
| 4 | authentication, retries, transport lifetime, cached environment data, and | |
| 5 | environment-scoped operations. | |
| 6 | ||
| 7 | Web and mobile mount this runtime once at the application root. There is no | |
| 8 | legacy connection owner or supported mixed mode. | |
| 9 | ||
| 10 | ## Ownership | |
| 11 | ||
| 12 | Each registered environment has one scoped Effect `Context` containing focused | |
| 13 | services: | |
| 14 | ||
| 15 | - `EnvironmentSupervisor` owns desired state, retry scheduling, and the active | |
| 16 | session scope. | |
| 17 | - `ConnectionBroker` prepares credentials and endpoints for primary, bearer, | |
| 18 | relay, and SSH targets. | |
| 19 | - `RpcSessionFactory` performs one transport attempt. It does not retry. | |
| 20 | - `EnvironmentRpc` exposes the active session without leaking the transport. | |
| 21 | - `EnvironmentProjectCommands` and `EnvironmentThreadCommands` construct | |
| 22 | orchestration commands, IDs, and timestamps. | |
| 23 | - `EnvironmentShell` and `EnvironmentThreads` own live subscriptions and cached | |
| 24 | snapshots. | |
| 25 | ||
| 26 | `EnvironmentServicesFactory` assembles that context, and `EnvironmentRegistry` | |
| 27 | owns its scope. There is no aggregate environment runtime facade. React | |
| 28 | components do not create connections, transports, retry loops, or RPC clients. | |
| 29 | ||
| 30 | ## Connection State | |
| 31 | ||
| 32 | The supervisor is the only retry owner. | |
| 33 | ||
| 34 | 1. A persisted or platform registration marks an environment as desired. | |
| 35 | 2. If the device is offline, the supervisor releases the active session and | |
| 36 | waits without consuming retry attempts. | |
| 37 | 3. When online, the supervisor asks the broker for one prepared connection and | |
| 38 | asks the session factory for one RPC session. | |
| 39 | 4. Transient failures retry forever with exponential backoff capped at 16 | |
| 40 | seconds. | |
| 41 | 5. Connectivity changes, application activation, credential changes, and | |
| 42 | explicit user retry interrupt the current wait and trigger a fresh attempt. | |
| 43 | Application activation also resets the backoff ladder. Mobile briefly probes | |
| 44 | a session after short interruptions and replaces it immediately after a | |
| 45 | meaningful background suspension. | |
| 46 | 6. Authentication or configuration failures remain blocked until an external | |
| 47 | wakeup changes the relevant input. | |
| 48 | 7. An involuntary session close keeps the registration and cache, then retries. | |
| 49 | 8. Explicit removal closes the session and deletes the registration, | |
| 50 | credentials, shell cache, and thread cache. | |
| 51 | ||
| 52 | The UI derives `available`, `offline`, `connecting`, `reconnecting`, | |
| 53 | `connected`, and `error` from supervisor state plus explicit data-sync state. | |
| 54 | It does not infer connection health from cached data or the existence of a | |
| 55 | transport object. An environment becomes `connected` after the socket opens and | |
| 56 | the initial config RPC succeeds, proving that the server is responsive. Shell | |
| 57 | and thread synchronization are independent data states. A healthy RPC | |
| 58 | transport with a failed shell subscription is shown as connected with a | |
| 59 | synchronization error, not as a reconnect that is not actually scheduled. | |
| 60 | ||
| 61 | ## Data Boundary | |
| 62 | ||
| 63 | Finite requests, durable subscriptions, and commands are separate APIs: | |
| 64 | ||
| 65 | - Query atoms revalidate when the RPC generation changes. | |
| 66 | - Subscription atoms switch to replacement sessions. | |
| 67 | - Expected subscription failures update domain sync state and wait for a | |
| 68 | replacement session; they do not take down a healthy transport. | |
| 69 | - Mutations resolve the current environment runtime at execution time. | |
| 70 | - Shell and thread snapshots are available while offline. | |
| 71 | - A connected transport may have `empty`, `cached`, `synchronizing`, `live`, or | |
| 72 | failed shell and thread data independently. | |
| 73 | - Cached shell and thread projections are never allowed to overwrite newer live | |
| 74 | data during a fast reconnect. | |
| 75 | - Domain atom factories route effects through the environment registry and | |
| 76 | resolve the current scoped service at execution time. | |
| 77 | - Web and mobile own their Atom runtimes, React hooks, and feature composition. | |
| 78 | ||
| 79 | The Promise bridge exists only at the React/Atom boundary. Runtime and business | |
| 80 | logic remain Effect-native. | |
| 81 | ||
| 82 | ## Platform Layers | |
| 83 | ||
| 84 | Web and mobile provide: | |
| 85 | ||
| 86 | - network status and network-change streams; | |
| 87 | - application lifecycle wakeups; | |
| 88 | - cloud session credentials; | |
| 89 | - device identity; | |
| 90 | - platform registrations; | |
| 91 | - persistent catalog, credential, shell, and thread stores; | |
| 92 | - HTTP, crypto, and telemetry layers. | |
| 93 | ||
| 94 | Platform layers adapt operating-system capabilities. They do not implement | |
| 95 | connection policy. | |
| 96 | ||
| 97 | ## Source Boundaries | |
| 98 | ||
| 99 | The public package subpaths mirror the runtime layers: | |
| 100 | ||
| 101 | - `connection/core` contains state, catalog, retry policy, and connectivity. | |
| 102 | - `connection/transport` contains brokerage, authorization, attempts, and RPC | |
| 103 | sessions. | |
| 104 | - `connection/platform` declares capabilities and persistence contracts. | |
| 105 | - `connection/services` contains environment-scoped data services. | |
| 106 | - `connection/application` assembles registries, discovery, and startup. | |
| 107 | - `connection/atoms` adapts shared services to application-owned Atom runtimes. | |
| 108 | - `connection/presentation` contains pure UI projections. | |
| 109 | ||
| 110 | Other reusable state lives in domain subpaths such as `shell`, `threads`, | |
| 111 | `terminal`, and `vcs`. Applications must import explicit package subpaths; the | |
| 112 | package intentionally has no root export. | |
| 113 | ||
| 114 | ## Application Boundary | |
| 115 | ||
| 116 | The application root mounts the shared connection application layer, creates | |
| 117 | its own Atom runtime, and selects the domain atom factories required by that | |
| 118 | platform. Web and mobile may expose different hooks and features without | |
| 119 | changing connection ownership. | |
| 120 | ||
| 121 | Application code must not construct `WsTransport`, RPC clients, retry loops, or | |
| 122 | raw orchestration commands. Persistence paths belong to the platform | |
| 123 | registration and cache stores, with explicit migration or invalidation policy. | |
| 124 | ||
| 125 | ## Verification | |
| 126 | ||
| 127 | Core state-machine tests use `@effect/vitest` and deterministic service layers. | |
| 128 | Required coverage includes: | |
| 129 | ||
| 130 | - offline startup and online wakeup; | |
| 131 | - forever retry with the 16-second cap; | |
| 132 | - explicit retry interrupting backoff; | |
| 133 | - authentication wakeups; | |
| 134 | - involuntary close and reconnect; | |
| 135 | - explicit removal clearing all owned state; | |
| 136 | - relay token reuse and refresh; | |
| 137 | - progressive relay discovery; | |
| 138 | - shell and thread cache hydration; | |
| 139 | - durable subscriptions switching sessions; | |
| 140 | - command metadata and idempotent queued-command metadata. |
docs/architecture/overview.md deleted-141| ... | ... | @@ -1,141 +0,0 @@ |
| 1 | # Architecture | |
| 2 | ||
| 3 | T3 Code runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app. | |
| 4 | ||
| 5 | ``` | |
| 6 | ┌─────────────────────────────────┐ | |
| 7 | │ Browser (React + Vite) │ | |
| 8 | │ wsTransport (state machine) │ | |
| 9 | │ Typed push decode at boundary │ | |
| 10 | └──────────┬──────────────────────┘ | |
| 11 | │ ws://localhost:3773 | |
| 12 | ┌──────────▼──────────────────────┐ | |
| 13 | │ apps/server (Node.js) │ | |
| 14 | │ WebSocket + HTTP static server │ | |
| 15 | │ ServerPushBus (ordered pushes) │ | |
| 16 | │ ServerReadiness (startup gate) │ | |
| 17 | │ OrchestrationEngine │ | |
| 18 | │ ProviderService │ | |
| 19 | │ CheckpointReactor │ | |
| 20 | │ RuntimeReceiptBus │ | |
| 21 | └──────────┬──────────────────────┘ | |
| 22 | │ JSON-RPC over stdio | |
| 23 | ┌──────────▼──────────────────────┐ | |
| 24 | │ codex app-server │ | |
| 25 | └─────────────────────────────────┘ | |
| 26 | ``` | |
| 27 | ||
| 28 | ## Components | |
| 29 | ||
| 30 | - **Browser app**: The React app renders session state, owns the client-side WebSocket transport, and treats typed push events as the boundary between server runtime details and UI state. | |
| 31 | ||
| 32 | - **Server**: `apps/server` is the main coordinator. It serves the web app, accepts WebSocket requests, waits for startup readiness before welcoming clients, and sends all outbound pushes through a single ordered push path. | |
| 33 | ||
| 34 | - **Provider runtime**: `codex app-server` does the actual provider/session work. The server talks to it over JSON-RPC on stdio and translates those runtime events into the app's orchestration model. | |
| 35 | ||
| 36 | - **Background workers**: Long-running async flows such as runtime ingestion, command reaction, and checkpoint processing run as queue-backed workers. This keeps work ordered, reduces timing races, and gives tests a deterministic way to wait for the system to go idle. | |
| 37 | ||
| 38 | - **Runtime signals**: The server emits lightweight typed receipts when important async milestones finish, such as checkpoint capture, diff finalization, or a turn becoming fully quiescent. Tests and orchestration code wait on these signals instead of polling internal state. | |
| 39 | ||
| 40 | - **Server updates**: A connected environment advertises whether its server can replace itself. When client and server versions differ, the browser selects an automatic, desktop-managed, or manual update path without changing connection ownership. See [Server Update Architecture](./server-updates.md). | |
| 41 | ||
| 42 | Related design: | |
| 43 | ||
| 44 | - [Resource telemetry architecture](./resource-telemetry.md) | |
| 45 | ||
| 46 | ## Event Lifecycle | |
| 47 | ||
| 48 | ### Startup and client connect | |
| 49 | ||
| 50 | ```mermaid | |
| 51 | sequenceDiagram | |
| 52 | participant Browser | |
| 53 | participant Transport as WsTransport | |
| 54 | participant Server as wsServer | |
| 55 | participant Layers as serverLayers | |
| 56 | participant Ready as ServerReadiness | |
| 57 | participant Push as ServerPushBus | |
| 58 | ||
| 59 | Browser->>Transport: Load app and open WebSocket | |
| 60 | Transport->>Server: Connect | |
| 61 | Server->>Layers: Start runtime services | |
| 62 | Server->>Ready: Wait for startup barriers | |
| 63 | Ready-->>Server: Ready | |
| 64 | Server->>Push: Publish server.welcome | |
| 65 | Push-->>Transport: Ordered welcome push | |
| 66 | Transport-->>Browser: Hydrate initial state | |
| 67 | ``` | |
| 68 | ||
| 69 | 1. The browser boots `WsTransport` and registers typed listeners in `wsNativeApi`. | |
| 70 | 2. The server accepts the connection in `wsServer` and brings up the runtime graph defined in `serverLayers`. | |
| 71 | 3. `ServerReadiness` waits until the key startup barriers are complete. | |
| 72 | 4. Once the server is ready, `wsServer` sends `server.welcome` from the contracts in `ws.ts` through `ServerPushBus`. | |
| 73 | 5. The browser receives that ordered push through `WsTransport`, and `wsNativeApi` uses it to seed local client state. | |
| 74 | ||
| 75 | ### User turn flow | |
| 76 | ||
| 77 | ```mermaid | |
| 78 | sequenceDiagram | |
| 79 | participant Browser | |
| 80 | participant Transport as WsTransport | |
| 81 | participant Server as wsServer | |
| 82 | participant Provider as ProviderService | |
| 83 | participant Codex as codex app-server | |
| 84 | participant Ingest as ProviderRuntimeIngestion | |
| 85 | participant Engine as OrchestrationEngine | |
| 86 | participant Push as ServerPushBus | |
| 87 | ||
| 88 | Browser->>Transport: Send user action | |
| 89 | Transport->>Server: Typed WebSocket request | |
| 90 | Server->>Provider: Route request | |
| 91 | Provider->>Codex: JSON-RPC over stdio | |
| 92 | Codex-->>Ingest: Provider runtime events | |
| 93 | Ingest->>Engine: Normalize into orchestration events | |
| 94 | Engine-->>Server: Domain events | |
| 95 | Server->>Push: Publish orchestration.domainEvent | |
| 96 | Push-->>Browser: Typed push | |
| 97 | ``` | |
| 98 | ||
| 99 | 1. A user action in the browser becomes a typed request through `WsTransport` and the browser API layer in `nativeApi`. | |
| 100 | 2. `wsServer` decodes that request using the shared WebSocket contracts in `ws.ts` and routes it to the right service. | |
| 101 | 3. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio. | |
| 102 | 4. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events. | |
| 103 | 5. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events. | |
| 104 | 6. `wsServer` pushes those updates to the browser through `ServerPushBus` on channels defined in [`orchestration.ts`][11]. | |
| 105 | ||
| 106 | ### Async completion flow | |
| 107 | ||
| 108 | ```mermaid | |
| 109 | sequenceDiagram | |
| 110 | participant Server as wsServer | |
| 111 | participant Worker as Queue-backed workers | |
| 112 | participant Cmd as ProviderCommandReactor | |
| 113 | participant Checkpoint as CheckpointReactor | |
| 114 | participant Receipt as RuntimeReceiptBus | |
| 115 | participant Push as ServerPushBus | |
| 116 | participant Browser | |
| 117 | ||
| 118 | Server->>Worker: Enqueue follow-up work | |
| 119 | Worker->>Cmd: Process provider commands | |
| 120 | Worker->>Checkpoint: Process checkpoint tasks | |
| 121 | Checkpoint->>Receipt: Publish completion receipt | |
| 122 | Cmd-->>Server: Produce orchestration changes | |
| 123 | Checkpoint-->>Server: Produce orchestration changes | |
| 124 | Server->>Push: Publish resulting state updates | |
| 125 | Push-->>Browser: User-visible push | |
| 126 | ``` | |
| 127 | ||
| 128 | 1. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14]. | |
| 129 | 2. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic. | |
| 130 | 3. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence. | |
| 131 | 4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers. | |
| 132 | 5. Any user-visible state changes produced by that async work still go back through `wsServer` and `ServerPushBus`. | |
| 133 | ||
| 134 | [8]: ../../apps/server/src/provider/Layers/ProviderService.ts | |
| 135 | [9]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | |
| 136 | [10]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | |
| 137 | [11]: ../../packages/contracts/src/orchestration.ts | |
| 138 | [13]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | |
| 139 | [14]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts | |
| 140 | [15]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts | |
| 141 | [16]: ../../packages/shared/src/DrainableWorker.ts |
docs/architecture/providers.md deleted-30| ... | ... | @@ -1,30 +0,0 @@ |
| 1 | # Provider architecture | |
| 2 | ||
| 3 | The web app communicates with the server via WebSocket using a simple JSON-RPC-style protocol: | |
| 4 | ||
| 5 | - **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }` | |
| 6 | - **Push events**: typed envelopes with `channel`, `sequence` (monotonic per connection), and channel-specific `data` | |
| 7 | ||
| 8 | Push channels: `server.welcome`, `server.configUpdated`, `terminal.event`, `orchestration.domainEvent`. Payloads are schema-validated at the transport boundary (`wsTransport.ts`). Decode failures produce structured `WsDecodeDiagnostic` with `code`, `reason`, and path info. | |
| 9 | ||
| 10 | Methods mirror the `NativeApi` interface defined in `@t3tools/contracts`: | |
| 11 | ||
| 12 | - `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn` | |
| 13 | - `providers.respondToRequest`, `providers.stopSession` | |
| 14 | - `shell.openInEditor`, `server.getConfig` | |
| 15 | ||
| 16 | Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. | |
| 17 | ||
| 18 | ## Client transport | |
| 19 | ||
| 20 | `wsTransport.ts` manages connection state: `connecting` → `open` → `reconnecting` → `closed` → `disposed`. Outbound requests are queued while disconnected and flushed on reconnect. Inbound pushes are decoded and validated at the boundary, then cached per channel. Subscribers can opt into `replayLatest` to receive the last push on subscribe. | |
| 21 | ||
| 22 | ## Server-side orchestration layers | |
| 23 | ||
| 24 | Provider runtime events flow through queue-based workers: | |
| 25 | ||
| 26 | 1. **ProviderRuntimeIngestion** — consumes provider runtime streams, emits orchestration commands | |
| 27 | 2. **ProviderCommandReactor** — reacts to orchestration intent events, dispatches provider calls | |
| 28 | 3. **CheckpointReactor** — captures git checkpoints on turn start/complete, publishes runtime receipts | |
| 29 | ||
| 30 | All three use `DrainableWorker` internally and expose `drain()` for deterministic test synchronization. |
docs/architecture/remote.md deleted-397| ... | ... | @@ -1,397 +0,0 @@ |
| 1 | # Remote Architecture | |
| 2 | ||
| 3 | This document describes the target architecture for first-class remote environments in T3 Code. | |
| 4 | ||
| 5 | It is intentionally architecture-first. It does not define a complete implementation plan or user-facing rollout checklist. The goal is to establish the core model so remote support can be added without another broad rewrite. | |
| 6 | ||
| 7 | ## Goals | |
| 8 | ||
| 9 | - Treat remote environments as first-class product primitives, not special cases. | |
| 10 | - Support multiple ways to reach the same environment. | |
| 11 | - Keep the T3 server as the execution boundary. | |
| 12 | - Let desktop, mobile, and web all share the same conceptual model. | |
| 13 | - Avoid introducing a local control plane unless product pressure proves it is necessary. | |
| 14 | ||
| 15 | ## Non-goals | |
| 16 | ||
| 17 | - Replacing the existing WebSocket server boundary with a custom transport protocol. | |
| 18 | - Making SSH the only remote story. | |
| 19 | - Syncing provider auth across machines. | |
| 20 | - Shipping every access method in the first iteration. | |
| 21 | ||
| 22 | ## High-level architecture | |
| 23 | ||
| 24 | T3 already has a clean runtime boundary: the client talks to a T3 server over HTTP/WebSocket, and the server owns orchestration, providers, terminals, git, and filesystem operations. | |
| 25 | ||
| 26 | Remote support should preserve that boundary. | |
| 27 | ||
| 28 | ```text | |
| 29 | ┌──────────────────────────────────────────────┐ | |
| 30 | │ Client (desktop / mobile / web) │ | |
| 31 | │ │ | |
| 32 | │ - known environments │ | |
| 33 | │ - connection manager │ | |
| 34 | │ - environment-aware routing │ | |
| 35 | └───────────────┬──────────────────────────────┘ | |
| 36 | │ | |
| 37 | │ resolves one access endpoint | |
| 38 | │ | |
| 39 | ┌───────────────▼──────────────────────────────┐ | |
| 40 | │ Access method │ | |
| 41 | │ │ | |
| 42 | │ - direct ws / wss │ | |
| 43 | │ - tunneled ws / wss │ | |
| 44 | │ - desktop-managed ssh bootstrap + forward │ | |
| 45 | └───────────────┬──────────────────────────────┘ | |
| 46 | │ | |
| 47 | │ connects to one T3 server | |
| 48 | │ | |
| 49 | ┌───────────────▼──────────────────────────────┐ | |
| 50 | │ Execution environment = one T3 server │ | |
| 51 | │ │ | |
| 52 | │ - environment identity │ | |
| 53 | │ - provider state │ | |
| 54 | │ - projects / threads / terminals │ | |
| 55 | │ - git / filesystem / process runtime │ | |
| 56 | └──────────────────────────────────────────────┘ | |
| 57 | ``` | |
| 58 | ||
| 59 | The important decision is that remoteness is expressed at the environment connection layer, not by splitting the T3 runtime itself. | |
| 60 | ||
| 61 | ## Domain model | |
| 62 | ||
| 63 | ### ExecutionEnvironment | |
| 64 | ||
| 65 | An `ExecutionEnvironment` is one running T3 server instance. | |
| 66 | ||
| 67 | It is the unit that owns: | |
| 68 | ||
| 69 | - provider availability and auth state | |
| 70 | - model availability | |
| 71 | - projects and threads | |
| 72 | - terminal processes | |
| 73 | - filesystem access | |
| 74 | - git operations | |
| 75 | - server settings | |
| 76 | ||
| 77 | It is identified by a stable `environmentId`. | |
| 78 | ||
| 79 | This is the shared cross-client primitive. Desktop, mobile, and web should all reason about the same concept here. | |
| 80 | ||
| 81 | ### KnownEnvironment | |
| 82 | ||
| 83 | A `KnownEnvironment` is a client-side saved entry for an environment the client knows how to reach. | |
| 84 | ||
| 85 | It is not server-authored. It is local to a device or client profile. | |
| 86 | ||
| 87 | Examples: | |
| 88 | ||
| 89 | - a saved LAN URL | |
| 90 | - a saved public `wss://` endpoint | |
| 91 | - a desktop-managed SSH host entry | |
| 92 | - a saved tunneled environment | |
| 93 | ||
| 94 | A known environment may or may not know the target `environmentId` before first successful connect. | |
| 95 | ||
| 96 | In the hosted web app, known environments are browser-local. A hosted pairing URL can create the saved entry, but it does not give the hosted app a server-side control plane or a copy of the session state. | |
| 97 | ||
| 98 | ### AccessEndpoint | |
| 99 | ||
| 100 | An `AccessEndpoint` is one concrete way to reach a known environment. | |
| 101 | ||
| 102 | This is the key abstraction that keeps SSH from taking over the model. | |
| 103 | ||
| 104 | A single environment may have many endpoints: | |
| 105 | ||
| 106 | - `wss://t3.example.com` | |
| 107 | - `ws://10.0.0.25:3773` | |
| 108 | - a tunneled relay URL | |
| 109 | - a desktop-managed SSH tunnel that resolves to a local forwarded WebSocket URL | |
| 110 | ||
| 111 | The environment stays the same. Only the access path changes. | |
| 112 | ||
| 113 | ### AdvertisedEndpoint | |
| 114 | ||
| 115 | An `AdvertisedEndpoint` is a server or desktop-authored candidate endpoint for an environment. It is how the backend tells the client which URLs may be useful for pairing and reconnecting. | |
| 116 | ||
| 117 | `AdvertisedEndpoint` is deliberately narrower than the full access model: | |
| 118 | ||
| 119 | - it describes a concrete HTTP and WebSocket base URL pair | |
| 120 | - it can mark the endpoint as default, available, or unavailable | |
| 121 | - it includes reachability hints such as loopback, LAN, private, public, or tunnel | |
| 122 | - it includes compatibility hints such as whether the endpoint can be used from the hosted HTTPS app | |
| 123 | ||
| 124 | Clients should treat advertised endpoints as hints, not as proof that a route works from the current device. The final connection attempt still decides whether the endpoint is reachable. | |
| 125 | ||
| 126 | The UI presents one default advertised endpoint in the network-access summary and keeps the rest behind an expandable advanced list. The default controls pairing QR codes and primary copy actions. Users can override it, but that override is a UI preference, not backend configuration. | |
| 127 | ||
| 128 | Persist the override by stable endpoint kind rather than raw URL whenever possible. For example, a LAN endpoint should be stored as the desktop LAN endpoint preference, not as `192.168.x.y`, because the address can change when the user switches networks. Provider endpoints should use provider-specific stable keys such as Tailscale IP or Tailscale MagicDNS HTTPS. Custom endpoints may fall back to their concrete identity. | |
| 129 | ||
| 130 | When no user default is saved, endpoint selection should prefer: | |
| 131 | ||
| 132 | 1. endpoints compatible with the hosted HTTPS app | |
| 133 | 2. explicitly default endpoints | |
| 134 | 3. non-loopback endpoints | |
| 135 | 4. loopback endpoints only for same-machine clients | |
| 136 | ||
| 137 | This keeps endpoint discovery centralized without making any one provider, such as Tailscale or a future tunnel service, part of the core environment model. | |
| 138 | ||
| 139 | ### Endpoint providers | |
| 140 | ||
| 141 | Endpoint providers are add-ons that contribute advertised endpoints for the current environment. | |
| 142 | ||
| 143 | The provider boundary is intentionally outside the core environment model: | |
| 144 | ||
| 145 | - core owns `ExecutionEnvironment`, saved environments, pairing, and connection lifecycle | |
| 146 | - providers discover or synthesize endpoints | |
| 147 | - providers return normalized `AdvertisedEndpoint` records | |
| 148 | - the UI and pairing logic select from those records without knowing provider-specific commands | |
| 149 | ||
| 150 | The first provider is Tailscale. It can discover Tailnet IP and MagicDNS addresses from the local machine and publish them as additional endpoint candidates. Future providers, such as a hosted tunnel service, should plug into the same shape rather than adding a separate remote environment path. | |
| 151 | ||
| 152 | Provider-specific confidence should remain a hint. A Tailscale endpoint still needs a successful browser or desktop connection before the client treats it as connected. | |
| 153 | ||
| 154 | ### Hosted pairing request | |
| 155 | ||
| 156 | A hosted pairing request is a bootstrap URL for the static web app, not a transport. | |
| 157 | ||
| 158 | Example: | |
| 159 | ||
| 160 | ```text | |
| 161 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | |
| 162 | ``` | |
| 163 | ||
| 164 | The hosted app reads the `host` parameter and pairing token, exchanges the token directly with that backend, then saves the resulting environment record in browser local storage. | |
| 165 | ||
| 166 | Important constraints: | |
| 167 | ||
| 168 | - the hosted app does not proxy HTTP or WebSocket traffic | |
| 169 | - the backend must still be reachable directly from the browser | |
| 170 | - HTTPS pages can only connect to HTTPS/WSS backends | |
| 171 | - HTTP LAN endpoints should keep using direct desktop or CLI pairing URLs | |
| 172 | - the token belongs in the URL hash so it is not sent to the hosted app origin | |
| 173 | ||
| 174 | ### RepositoryIdentity | |
| 175 | ||
| 176 | `RepositoryIdentity` remains a best-effort logical repo grouping mechanism across environments. | |
| 177 | ||
| 178 | It is not used for routing. It is only used for UI grouping and correlation between local and remote clones of the same repository. | |
| 179 | ||
| 180 | ### Workspace / Project | |
| 181 | ||
| 182 | The current `Project` model remains environment-local. | |
| 183 | ||
| 184 | That means: | |
| 185 | ||
| 186 | - a local clone and a remote clone are different projects | |
| 187 | - they may share a `RepositoryIdentity` | |
| 188 | - threads still bind to one project in one environment | |
| 189 | ||
| 190 | ## Access methods | |
| 191 | ||
| 192 | Access methods answer one question: | |
| 193 | ||
| 194 | How does the client speak WebSocket to a T3 server? | |
| 195 | ||
| 196 | They do not answer: | |
| 197 | ||
| 198 | - how the server got started | |
| 199 | - who manages the server process | |
| 200 | - whether the environment is local or remote | |
| 201 | ||
| 202 | ### 1. Direct WebSocket access | |
| 203 | ||
| 204 | Examples: | |
| 205 | ||
| 206 | - `ws://10.0.0.15:3773` | |
| 207 | - `wss://t3.example.com` | |
| 208 | ||
| 209 | This is the base model and should be the first-class default. | |
| 210 | ||
| 211 | Benefits: | |
| 212 | ||
| 213 | - works for desktop, mobile, and web | |
| 214 | - no client-specific process management required | |
| 215 | - best fit for hosted or self-managed remote T3 deployments | |
| 216 | ||
| 217 | Browser security rules are part of this access method. A hosted HTTPS web client can connect to `wss://` backends, but it cannot connect to plain `ws://` or `http://` LAN backends because that would be mixed content. | |
| 218 | ||
| 219 | ### 2. Tunneled WebSocket access | |
| 220 | ||
| 221 | Examples: | |
| 222 | ||
| 223 | - public relay URLs | |
| 224 | - private network relay URLs | |
| 225 | - local tunnel products such as pipenet | |
| 226 | ||
| 227 | This is still direct WebSocket access from the client's perspective. The difference is that the route is mediated by a tunnel or relay. | |
| 228 | ||
| 229 | For T3, tunnels are best modeled as another `AccessEndpoint`, not as a different kind of environment. | |
| 230 | ||
| 231 | This is especially useful when: | |
| 232 | ||
| 233 | - the host is behind NAT | |
| 234 | - inbound ports are unavailable | |
| 235 | - mobile must reach a desktop-hosted environment | |
| 236 | - a machine should be reachable without exposing raw LAN or public ports | |
| 237 | ||
| 238 | Tailscale-backed access sits here architecturally even though the current implementation is endpoint discovery rather than a T3-managed tunnel. It contributes private-network endpoints and lets the existing HTTP/WebSocket client path do the actual connection. | |
| 239 | ||
| 240 | ### 3. Desktop-managed SSH access | |
| 241 | ||
| 242 | SSH is an access and launch helper, not a separate environment type. | |
| 243 | ||
| 244 | The desktop main process can use SSH to: | |
| 245 | ||
| 246 | - reach a machine | |
| 247 | - probe it | |
| 248 | - launch or reuse a remote T3 server | |
| 249 | - establish a local port forward | |
| 250 | ||
| 251 | After that, the renderer should still connect using an ordinary WebSocket URL against the forwarded local port. | |
| 252 | ||
| 253 | This keeps the renderer transport model consistent with every other access method. | |
| 254 | ||
| 255 | The desktop main process owns the SSH bridge because it can spawn local SSH processes, manage askpass prompts, write temporary launch scripts, and clean up forwards. The renderer receives a saved environment record and connects through the forwarded URL; it should not need SSH-specific RPC paths for normal environment traffic. | |
| 256 | ||
| 257 | ## Launch methods | |
| 258 | ||
| 259 | Launch methods answer a different question: | |
| 260 | ||
| 261 | How does a T3 server come to exist on the target machine? | |
| 262 | ||
| 263 | Launch and access should stay separate in the design. | |
| 264 | ||
| 265 | ### 1. Pre-existing server | |
| 266 | ||
| 267 | The simplest launch method is no launch at all. | |
| 268 | ||
| 269 | The user or operator already runs T3 on the target machine, and the client connects through a direct or tunneled WebSocket endpoint. | |
| 270 | ||
| 271 | This should be the first remote mode shipped because it validates the environment model with minimal extra machinery. | |
| 272 | ||
| 273 | ### 2. Desktop-managed remote launch over SSH | |
| 274 | ||
| 275 | This is the main place where Zed is a useful reference. | |
| 276 | ||
| 277 | Useful ideas to borrow from Zed: | |
| 278 | ||
| 279 | - remote probing | |
| 280 | - platform detection | |
| 281 | - session directories with pid/log metadata | |
| 282 | - reconnect-friendly launcher behavior | |
| 283 | - desktop-owned connection UX | |
| 284 | ||
| 285 | What should be different in T3: | |
| 286 | ||
| 287 | - no custom stdio/socket proxy protocol between renderer and remote runtime | |
| 288 | - no attempt to make the remote runtime look like an editor transport | |
| 289 | - keep the final client-to-server connection as WebSocket | |
| 290 | ||
| 291 | The recommended T3 flow is: | |
| 292 | ||
| 293 | 1. Desktop connects over SSH. | |
| 294 | 2. Desktop probes the remote machine and verifies T3 availability. | |
| 295 | 3. Desktop launches or reuses a remote T3 server. | |
| 296 | 4. Desktop establishes local port forwarding. | |
| 297 | 5. Renderer connects to the forwarded WebSocket endpoint as a normal environment. | |
| 298 | ||
| 299 | The saved environment should remember that it was created by desktop SSH launch only for reconnect and lifecycle UX. That metadata should not change the server protocol or the environment identity model. | |
| 300 | ||
| 301 | Failure handling should be explicit: | |
| 302 | ||
| 303 | - SSH authentication failure should surface before any environment is saved | |
| 304 | - remote launch failure should include remote logs or the launcher command output when available | |
| 305 | - forwarded-port failure should leave the saved environment disconnected rather than falling back to an unrelated endpoint | |
| 306 | - reconnect should attempt to restore the SSH bridge before reconnecting the normal WebSocket client | |
| 307 | ||
| 308 | ### 3. Client-managed local publish | |
| 309 | ||
| 310 | This is the inverse of remote launch: a local T3 server is already running, and the client publishes it through a tunnel. | |
| 311 | ||
| 312 | This is useful for: | |
| 313 | ||
| 314 | - exposing a desktop-hosted environment to mobile | |
| 315 | - temporary remote access without changing router or firewall settings | |
| 316 | ||
| 317 | This is still a launch concern, not a new environment kind. | |
| 318 | ||
| 319 | ## Why access and launch must stay separate | |
| 320 | ||
| 321 | These concerns are easy to conflate, but separating them prevents architectural drift. | |
| 322 | ||
| 323 | Examples: | |
| 324 | ||
| 325 | - A manually hosted T3 server might be reached through direct `wss`. | |
| 326 | - The same server might also be reachable through a tunnel. | |
| 327 | - An SSH-managed server might be launched over SSH but then reached through forwarded WebSocket. | |
| 328 | - A local desktop server might be published through a tunnel for mobile. | |
| 329 | ||
| 330 | In all of those cases, the `ExecutionEnvironment` is the same kind of thing. | |
| 331 | ||
| 332 | Only the launch and access paths differ. | |
| 333 | ||
| 334 | ## Version Coordination | |
| 335 | ||
| 336 | Remote environments may stay online while web, desktop, or mobile clients move to a newer release. | |
| 337 | The environment descriptor therefore carries the running server version and may advertise a safe | |
| 338 | replacement path. The web and desktop UI use that information to show the appropriate action | |
| 339 | without making the connection transport responsible for process management. | |
| 340 | ||
| 341 | Published CLI servers on supported hosts can install and hand off to the client's exact version. A | |
| 342 | desktop-managed backend instead points the user to the desktop app on that machine, while older or | |
| 343 | unsupported servers fall back to a manual relaunch. The existing connection supervisor owns the | |
| 344 | disconnect and reconnect just as it would for any other involuntary socket close. | |
| 345 | ||
| 346 | See [Server Update Architecture](./server-updates.md) for capability detection, installation safety, | |
| 347 | and restart sequencing. | |
| 348 | ||
| 349 | ## Security model | |
| 350 | ||
| 351 | Remote support must assume that some environments will be reachable over untrusted networks. | |
| 352 | ||
| 353 | That means: | |
| 354 | ||
| 355 | - remote-capable environments should require explicit authentication | |
| 356 | - tunnel exposure should not rely on obscurity | |
| 357 | - client-saved endpoints should carry enough auth metadata to reconnect safely | |
| 358 | ||
| 359 | T3 already supports a WebSocket auth token on the server. That should become a first-class part of environment access rather than remaining an incidental query parameter convention. | |
| 360 | ||
| 361 | For publicly reachable environments, authenticated access should be treated as required. | |
| 362 | ||
| 363 | Hosted pairing should be treated as a client-side convenience only. The hosted app must not receive pairing tokens through query parameters, must not store pairing state server-side, and must not imply that an HTTP backend is safe or reachable from an HTTPS browser context. | |
| 364 | ||
| 365 | ## Relationship to Zed | |
| 366 | ||
| 367 | Zed is a useful reference implementation for managed remote launch and reconnect behavior. | |
| 368 | ||
| 369 | The relevant lessons are: | |
| 370 | ||
| 371 | - remote bootstrap should be explicit | |
| 372 | - reconnect should be first-class | |
| 373 | - connection UX belongs in the client shell | |
| 374 | - runtime ownership should stay clearly on the remote host | |
| 375 | ||
| 376 | The important mismatch is transport shape. | |
| 377 | ||
| 378 | Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime. | |
| 379 | ||
| 380 | T3 should not copy that part. | |
| 381 | ||
| 382 | T3 already has the right runtime boundary: | |
| 383 | ||
| 384 | - one T3 server per environment | |
| 385 | - ordinary HTTP/WebSocket between client and environment | |
| 386 | ||
| 387 | So T3 should borrow Zed's launch discipline, not its transport protocol. | |
| 388 | ||
| 389 | ## Recommended rollout | |
| 390 | ||
| 391 | 1. First-class known environments and access endpoints. | |
| 392 | 2. Direct `ws` / `wss` remote environments. | |
| 393 | 3. Authenticated tunnel-backed environments. | |
| 394 | 4. Desktop-managed SSH launch and forwarding. | |
| 395 | 5. Multi-environment UI improvements after the base runtime path is proven. | |
| 396 | ||
| 397 | This ordering keeps the architecture network-first and transport-agnostic while still leaving room for richer managed remote flows. |
docs/architecture/resource-telemetry.md deleted-391| ... | ... | @@ -1,391 +0,0 @@ |
| 1 | # Resource telemetry architecture | |
| 2 | ||
| 3 | Status: implemented | |
| 4 | ||
| 5 | ## Purpose | |
| 6 | ||
| 7 | Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset` | |
| 8 | subprocess probes with two persistent, direct data sources: | |
| 9 | ||
| 10 | 1. a standalone Rust resource-monitor executable that reads process counters | |
| 11 | through operating-system APIs via `sysinfo`; | |
| 12 | 2. Electron main-process APIs for Electron process metrics and host power state. | |
| 13 | ||
| 14 | The native monitor owns bounded in-memory history. The server only merges and | |
| 15 | summarizes that history when diagnostics requests it. Telemetry history is not | |
| 16 | persisted to disk or continuously copied into Node. | |
| 17 | ||
| 18 | ## Why a standalone executable | |
| 19 | ||
| 20 | The monitor is intentionally not a Node native addon. | |
| 21 | ||
| 22 | - No N-API, `ffi-rs`, or dynamic-library ABI is loaded into the server process. | |
| 23 | - A monitor crash cannot corrupt the Node runtime. | |
| 24 | - The server can supervise, restart, version-check, and measure the monitor as a | |
| 25 | normal child process. | |
| 26 | - The same protocol works for the desktop app and the published CLI. | |
| 27 | - Packaging is a single platform executable instead of an addon toolchain plus | |
| 28 | Node/Electron ABI matrix. | |
| 29 | ||
| 30 | The cost is one persistent child process and NDJSON serialization. That is a | |
| 31 | better failure boundary than repeatedly spawning shell utilities or loading | |
| 32 | native code into Node. | |
| 33 | ||
| 34 | ## Runtime topology | |
| 35 | ||
| 36 | ### Desktop | |
| 37 | ||
| 38 | ```text | |
| 39 | Electron main | |
| 40 | ├─ powerMonitor | |
| 41 | ├─ app.getAppMetrics() while diagnostics is open | |
| 42 | ├─ inherited fd 4, telemetry NDJSON ─────────────┐ | |
| 43 | └─ inherited fd 5, demand-control NDJSON ◀──────┤ | |
| 44 | ▼ | |
| 45 | Node server ── stdin/stdout NDJSON ── Rust resource monitor | |
| 46 | │ | |
| 47 | ├─ ResourceTelemetry Effect service | |
| 48 | ├─ background power policy projection | |
| 49 | └─ WebSocket RPC/subscription ── diagnostics UI | |
| 50 | ``` | |
| 51 | ||
| 52 | ### Web, headless, and remote server | |
| 53 | ||
| 54 | Electron telemetry is unavailable. The native monitor still runs beside the | |
| 55 | server and tracks the server process tree. Power fields degrade to `unknown` | |
| 56 | instead of invoking platform shell commands. | |
| 57 | ||
| 58 | ### WSL backend limitation | |
| 59 | ||
| 60 | Windows desktop packages currently ship the Windows resource-monitor executable. | |
| 61 | That executable cannot run inside the Linux WSL backend, so a WSL-only backend | |
| 62 | does not receive `resourceMonitorPath` and reports native process telemetry as | |
| 63 | unavailable. Electron host-power telemetry remains available over the inherited | |
| 64 | desktop pipe. Supporting native WSL process telemetry requires publishing a | |
| 65 | Linux sidecar for each supported architecture in the Windows artifact and | |
| 66 | converting its packaged path into the selected distro; the configuration | |
| 67 | deliberately does not pass the Windows `.exe` into WSL as a false fallback. | |
| 68 | ||
| 69 | ## Native monitor | |
| 70 | ||
| 71 | The executable lives in `native/resource-monitor`. | |
| 72 | ||
| 73 | It receives schema-compatible commands on stdin and emits one JSON object per | |
| 74 | line on stdout: | |
| 75 | ||
| 76 | - `configure` | |
| 77 | - `setExternalProcesses` | |
| 78 | - `setSampleInterval` | |
| 79 | - `setStreaming` | |
| 80 | - `sampleNow` | |
| 81 | - `readHistory` | |
| 82 | - `shutdown` | |
| 83 | - `hello` | |
| 84 | - `snapshot` | |
| 85 | - `historyChunk` | |
| 86 | - `error` | |
| 87 | ||
| 88 | The protocol version is defined by | |
| 89 | `RESOURCE_MONITOR_PROTOCOL_VERSION` in | |
| 90 | `packages/contracts/src/resourceTelemetry.ts`. | |
| 91 | ||
| 92 | ### Collection | |
| 93 | ||
| 94 | The monitor keeps one `sysinfo::System` instance and refreshes it at the | |
| 95 | power-adaptive interval selected by the server. It collects: | |
| 96 | ||
| 97 | - PID and parent PID; | |
| 98 | - process start time and run time; | |
| 99 | - process name and command line; | |
| 100 | - current and cumulative CPU usage; | |
| 101 | - resident and virtual memory; | |
| 102 | - cumulative process I/O counters. | |
| 103 | ||
| 104 | On Linux, task/thread enumeration is disabled. Command lines are loaded only | |
| 105 | when first needed. This avoids the expensive default behavior of walking every | |
| 106 | `/proc/<pid>/task/<tid>` directory on each refresh. | |
| 107 | ||
| 108 | ### Process-tree selection | |
| 109 | ||
| 110 | Each sample scans the accessible process table, builds the PID/PPID graph, and | |
| 111 | retains: | |
| 112 | ||
| 113 | - the server process; | |
| 114 | - every descendant of the server, including provider-spawned grandchildren such | |
| 115 | as shells, `node`, `tsgo`, language servers, and other tools; | |
| 116 | - Electron processes supplied as explicit external roots; | |
| 117 | - descendants of those Electron roots; | |
| 118 | - the resource monitor itself, because it is a server child. | |
| 119 | ||
| 120 | Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native | |
| 121 | start times are matched with a two-second tolerance because native start times | |
| 122 | can have coarser platform resolution. | |
| 123 | ||
| 124 | The process list is emitted in depth-first tree order so renderer collapse and | |
| 125 | expansion preserves complete subtrees. | |
| 126 | ||
| 127 | ### Native history and streaming | |
| 128 | ||
| 129 | Every native sample is appended to a one-hour in-memory ring bounded to 3,600 | |
| 130 | snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes. | |
| 131 | History stays in the sidecar until a `readHistory` request and is returned in | |
| 132 | bounded chunks. The first bound reached wins, so high process counts or large | |
| 133 | process names and command lines shorten the effective history window. | |
| 134 | ||
| 135 | Periodic snapshot streaming is disabled by default. The server enables it only | |
| 136 | while at least one diagnostics subscription is retained. `sampleNow` remains | |
| 137 | available for explicit refreshes and identity validation. | |
| 138 | ||
| 139 | The server adjusts native sampling without restarting the sidecar: | |
| 140 | ||
| 141 | - suspended, locked, low-power, or serious/critical thermal state: 15 seconds; | |
| 142 | - battery: 5 seconds; | |
| 143 | - normal AC: 1 second; | |
| 144 | - unknown or stale power: 5 seconds in the background and 1 second while live | |
| 145 | diagnostics is open. | |
| 146 | ||
| 147 | ### Sampling limits | |
| 148 | ||
| 149 | This is counter sampling, not syscall tracing. | |
| 150 | ||
| 151 | - A process that starts and exits entirely between samples may not be observed. | |
| 152 | - Cumulative CPU and I/O counters still provide accurate deltas for processes | |
| 153 | that survive across samples. | |
| 154 | - Exact file paths, individual write syscalls, ETW events, eBPF events, and | |
| 155 | Endpoint Security events are outside this implementation. | |
| 156 | ||
| 157 | Those deeper tracing systems can be added later as opt-in diagnostic modes | |
| 158 | without changing the public `ResourceTelemetry` model. | |
| 159 | ||
| 160 | ## I/O semantics | |
| 161 | ||
| 162 | The monitor preserves platform semantics instead of presenting all counters as | |
| 163 | equivalent: | |
| 164 | ||
| 165 | - Unix-like platforms report storage I/O counters exposed by `sysinfo`. | |
| 166 | - Windows reports all process I/O bytes, not only disk bytes. | |
| 167 | - Operating-system caches can prevent logical application reads or writes from | |
| 168 | appearing as physical storage bytes. | |
| 169 | ||
| 170 | The UI therefore labels these values as I/O reads and writes and exposes the | |
| 171 | per-process `ioSemantics` value. | |
| 172 | ||
| 173 | Group totals are observed deltas since telemetry startup. Per-process total | |
| 174 | columns are the operating system's cumulative counters for that process. | |
| 175 | ||
| 176 | ## Electron telemetry | |
| 177 | ||
| 178 | Electron main owns `DesktopTelemetryPublisher`. | |
| 179 | ||
| 180 | Power events trigger an immediate snapshot. While diagnostics is closed, the | |
| 181 | server sends the configured active and idle host-power intervals to Electron | |
| 182 | (30 seconds and 2 minutes in the balanced profile). During those heartbeats | |
| 183 | Electron reads: | |
| 184 | ||
| 185 | - `powerMonitor.isOnBatteryPower()`; | |
| 186 | - `powerMonitor.getSystemIdleTime()`; | |
| 187 | - `powerMonitor.getSystemIdleState()`; | |
| 188 | - `powerMonitor.getCurrentThermalState()`. | |
| 189 | ||
| 190 | `app.getAppMetrics()` is only called while diagnostics demand is active. Its | |
| 191 | live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while | |
| 192 | locked, suspended, or thermally constrained. | |
| 193 | ||
| 194 | It also listens for: | |
| 195 | ||
| 196 | - lock and unlock; | |
| 197 | - suspend and resume; | |
| 198 | - AC and battery transitions; | |
| 199 | - thermal-state changes; | |
| 200 | - CPU speed-limit changes. | |
| 201 | ||
| 202 | Suspension stays latched across event-driven snapshots. An explicit resume | |
| 203 | clears it immediately; a periodic heartbeat also clears a stale latch so a | |
| 204 | missed resume event cannot leave telemetry permanently constrained. | |
| 205 | ||
| 206 | Electron does not expose a cross-platform low-power-mode getter, so that field | |
| 207 | remains `unknown`. | |
| 208 | ||
| 209 | The desktop backend is spawned with: | |
| 210 | ||
| 211 | - fd 3 for the existing bootstrap payload; | |
| 212 | - fd 4 for Electron-to-server telemetry NDJSON; | |
| 213 | - fd 5 for server-to-Electron diagnostics-demand NDJSON. | |
| 214 | ||
| 215 | These are private Electron-main/server pipes. They do not use the renderer | |
| 216 | WebSocket and are recreated for every backend restart. | |
| 217 | ||
| 218 | ## Server Effect services | |
| 219 | ||
| 220 | The implementation is under `apps/server/src/resourceTelemetry`. | |
| 221 | ||
| 222 | ### `ResourceMonitorBinary` | |
| 223 | ||
| 224 | Resolves an executable from: | |
| 225 | ||
| 226 | 1. `T3CODE_RESOURCE_MONITOR_PATH`; | |
| 227 | 2. desktop bootstrap configuration; | |
| 228 | 3. bundled CLI resources; | |
| 229 | 4. local Cargo build outputs. | |
| 230 | ||
| 231 | Unsupported platforms, missing binaries, and non-executable binaries use | |
| 232 | schema-backed tagged errors with descriptive messages. | |
| 233 | ||
| 234 | ### `NativeTelemetryClient` | |
| 235 | ||
| 236 | Owns the resource-monitor process and protocol. | |
| 237 | ||
| 238 | - validates the hello/version handshake; | |
| 239 | - sends configuration and external process roots; | |
| 240 | - adapts the native interval from host power state; | |
| 241 | - enables streaming only for scoped live subscribers; | |
| 242 | - reads chunked native history on demand; | |
| 243 | - exposes `sampleNow`; | |
| 244 | - serializes commands; | |
| 245 | - supervises process exit and protocol failure; | |
| 246 | - restarts with bounded exponential backoff; | |
| 247 | - opens a circuit after repeated failures; | |
| 248 | - supports explicit retry; | |
| 249 | - publishes health changes immediately. | |
| 250 | ||
| 251 | Snapshot sequence numbers are scoped to a monitor generation. Server ingestion | |
| 252 | uses the monitor restart count as the generation key, so sequence reset after a | |
| 253 | restart cannot freeze telemetry. | |
| 254 | ||
| 255 | ### `DesktopTelemetryReceiver` | |
| 256 | ||
| 257 | Reads fd 4, decodes schema-validated messages, stores the latest Electron | |
| 258 | snapshot, and publishes desktop health. It writes diagnostics demand to fd 5 | |
| 259 | and gives the first sample a 90-second startup deadline. Once samples arrive, | |
| 260 | the stale deadline stays beyond the slower configured host-power interval with | |
| 261 | 30 seconds of scheduling grace, so intentional 2–10 minute idle polling does | |
| 262 | not oscillate the policy between constrained and unconstrained states. Decode | |
| 263 | errors, protocol mismatch, control-write failure, stream failure, stale input, | |
| 264 | and normal stream closure are represented explicitly. | |
| 265 | ||
| 266 | ### `ResourceTelemetry` | |
| 267 | ||
| 268 | Merges native and Electron data and owns public telemetry semantics. | |
| 269 | ||
| 270 | - calculates CPU and I/O rates from cumulative native counters; | |
| 271 | - preserves the last native rates during desktop-only updates; | |
| 272 | - classifies backend, Electron, and monitor processes; | |
| 273 | - computes process depth and child relationships; | |
| 274 | - tracks starts, exits, CPU time, and observed I/O; | |
| 275 | - projects power data; | |
| 276 | - acquires native streaming and Electron process metrics only for scoped live | |
| 277 | subscribers; | |
| 278 | - queries and replays native history only when requested; | |
| 279 | - validates `(pid, startTimeMs)` before process signaling; | |
| 280 | - updates history health even when no further native sample arrives. | |
| 281 | ||
| 282 | Electron and monitor processes are visible but are not valid targets for the | |
| 283 | existing process-signal RPC. | |
| 284 | ||
| 285 | ### History projection | |
| 286 | ||
| 287 | `ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native | |
| 288 | snapshots to derive rates, lifecycle counters, buckets, and process summaries. | |
| 289 | Current Electron process metrics are intentionally excluded from historical | |
| 290 | replay so they cannot overwrite older native CPU or memory samples. | |
| 291 | ||
| 292 | ### `ResourceAttribution` | |
| 293 | ||
| 294 | Tracks known logical application I/O separately from OS counters. Current | |
| 295 | integration points record successful writes for: | |
| 296 | ||
| 297 | - provider native and canonical event logs; | |
| 298 | - the local server trace sink. | |
| 299 | ||
| 300 | Entries contain component, operation, logical bytes, count, and elapsed time. | |
| 301 | Future persistence paths should call `ResourceAttribution.record` rather than | |
| 302 | adding diagnostics-specific counters. | |
| 303 | ||
| 304 | ## Background policy integration | |
| 305 | ||
| 306 | `HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host | |
| 307 | power does not retain live resource diagnostics or invoke shell probes. | |
| 308 | ||
| 309 | The monitor updates its latest timestamp on every Electron sample but only | |
| 310 | publishes semantic state changes. Increasing idle seconds alone does not cause a | |
| 311 | background-policy broadcast every second. | |
| 312 | ||
| 313 | ## Public API and UI | |
| 314 | ||
| 315 | The WebSocket RPC surface provides: | |
| 316 | ||
| 317 | - current snapshot; | |
| 318 | - bounded history; | |
| 319 | - explicit monitor retry; | |
| 320 | - a live snapshot subscription. | |
| 321 | ||
| 322 | The diagnostics page displays: | |
| 323 | ||
| 324 | - aggregate CPU, memory, I/O, and process counts; | |
| 325 | - backend, Electron, and monitor overhead groups; | |
| 326 | - power and thermal state; | |
| 327 | - collector health and restart information; | |
| 328 | - CPU and I/O history; | |
| 329 | - a collapsible live process tree; | |
| 330 | - safe process signaling for backend descendants; | |
| 331 | - instrumented logical application I/O. | |
| 332 | ||
| 333 | Legacy process diagnostics RPCs are projected from the same service so they no | |
| 334 | longer start recurring process-table commands. | |
| 335 | ||
| 336 | ## Packaging | |
| 337 | ||
| 338 | Desktop artifact builds compile the Rust target, stage it as | |
| 339 | `resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the | |
| 340 | backend bootstrap. | |
| 341 | ||
| 342 | CLI release jobs upload each active platform monitor artifact and copy it into: | |
| 343 | ||
| 344 | ```text | |
| 345 | apps/server/dist/resource-monitor/<platform>-<arch>/ | |
| 346 | ``` | |
| 347 | ||
| 348 | The published server package already includes `dist`, so those executables ship | |
| 349 | with the CLI. Missing platform artifacts degrade native telemetry to | |
| 350 | `unavailable`; the server continues running. | |
| 351 | ||
| 352 | ## Resource and failure behavior | |
| 353 | ||
| 354 | Steady state uses: | |
| 355 | ||
| 356 | - one native process; | |
| 357 | - power-adaptive native counter sampling with no periodic Node snapshot stream; | |
| 358 | - event-driven Electron power updates plus profile-driven heartbeats (30–60 | |
| 359 | seconds while active and 2–10 minutes while idle); | |
| 360 | - no `app.getAppMetrics()` calls while diagnostics is closed; | |
| 361 | - no telemetry database; | |
| 362 | - no recurring shell probes; | |
| 363 | - bounded PubSub queues and native ring history. | |
| 364 | ||
| 365 | The diagnostics page exposes the monitor's own process resource usage and | |
| 366 | collection duration so the observer's cost is measurable. | |
| 367 | ||
| 368 | Failures are isolated: | |
| 369 | ||
| 370 | - native failure does not stop the server; | |
| 371 | - Electron telemetry loss does not stop native telemetry; | |
| 372 | - schema/version errors are visible in health; | |
| 373 | - repeated native failures stop automatic restart churn until explicit retry; | |
| 374 | - server and desktop shutdown close their respective streams and child process | |
| 375 | scopes. | |
| 376 | ||
| 377 | ## Future integration points | |
| 378 | ||
| 379 | High-value follow-up work can use the existing service boundaries: | |
| 380 | ||
| 381 | - opt-in file-path attribution through platform-specific tracing; | |
| 382 | - process lifecycle events to reduce the chance of missing very short-lived | |
| 383 | children; | |
| 384 | - additional `ResourceAttribution` instrumentation for databases, checkpoints, | |
| 385 | caches, and file synchronization; | |
| 386 | - exported diagnostic bundles; | |
| 387 | - adaptive sample intervals based on diagnostics visibility and active work. | |
| 388 | ||
| 389 | These additions should preserve the current rules: direct platform APIs, | |
| 390 | schema-validated boundaries, explicit metric semantics, bounded retention, and | |
| 391 | no mandatory telemetry persistence. |
docs/architecture/runtime-modes.md deleted-6| ... | ... | @@ -1,6 +0,0 @@ |
| 1 | # Runtime modes | |
| 2 | ||
| 3 | T3 Code has a global runtime mode switch in the chat toolbar: | |
| 4 | ||
| 5 | - **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. | |
| 6 | - **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. |
docs/architecture/server-updates.md deleted-131| ... | ... | @@ -1,131 +0,0 @@ |
| 1 | # Server Update Architecture | |
| 2 | ||
| 3 | T3 Code can update a connected server to the exact version of the client that detected version | |
| 4 | drift. This path exists primarily for remote environments, where the user may not have a terminal | |
| 5 | open on the server machine. | |
| 6 | ||
| 7 | The feature has three boundaries: | |
| 8 | ||
| 9 | - the server advertises whether and how it can be replaced; | |
| 10 | - the client chooses the matching user action; | |
| 11 | - the server installs and verifies the replacement before handing off the process. | |
| 12 | ||
| 13 | ## Detection and Presentation | |
| 14 | ||
| 15 | `ExecutionEnvironmentDescriptor` includes the server version and an optional | |
| 16 | `capabilities.serverSelfUpdate` value. Progress-capable servers also advertise | |
| 17 | `capabilities.serverSelfUpdateProgress`. The client compares the server version with `APP_VERSION` | |
| 18 | after loading server config. | |
| 19 | ||
| 20 | The optional capability is intentionally backward compatible. An older server does not know about | |
| 21 | the field, so a missing value means the client must offer a manual relaunch instead of sending an | |
| 22 | unknown RPC. | |
| 23 | ||
| 24 | The shared `ServerUpdateAction` is rendered in both user-facing version-drift surfaces: | |
| 25 | ||
| 26 | - the conversation banner in `ChatView`; | |
| 27 | - primary and saved environment rows in **Settings** → **Connections**. | |
| 28 | ||
| 29 | Both surfaces target the client's exact version. When the reconnected server reports that version, | |
| 30 | the mismatch and action disappear. | |
| 31 | ||
| 32 | The operation state lives in `packages/client-runtime`, keyed by environment. Both web surfaces read | |
| 33 | the same `downloading`, `installing`, or `resuming` state, so route changes do not own or cancel the | |
| 34 | operation. | |
| 35 | ||
| 36 | ## Capability Selection | |
| 37 | ||
| 38 | The server resolves its capability once at startup and publishes it in the environment descriptor. | |
| 39 | ||
| 40 | | Advertised value | Process shape | Client behavior | | |
| 41 | | ----------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | |
| 42 | | `boot-service` | Linux server running under the T3-managed systemd user service | Call the update RPC; the service unit is replaced and restarted. | | |
| 43 | | `respawn` | Published npm CLI running in the foreground on macOS or Linux | Call the update RPC; the process hands off to a detached replacement. | | |
| 44 | | `desktop-managed` | Backend supervised by the desktop app | Tell the user to update the desktop app on the server machine. | | |
| 45 | | absent | Older server, development checkout, Windows foreground process, or an unrecognized supervisor | Offer the exact manual relaunch command. | | |
| 46 | ||
| 47 | Desktop ownership takes precedence over process-shape detection. A desktop-managed backend must | |
| 48 | never spawn a second CLI server beside the app-owned process. Likewise, a process launched by an | |
| 49 | unrecognized systemd unit does not claim the foreground respawn path because its supervisor could | |
| 50 | bring the old version back. | |
| 51 | ||
| 52 | ## Update Flow | |
| 53 | ||
| 54 | ```mermaid | |
| 55 | flowchart TD | |
| 56 | A[Client detects different versions] --> B{Advertised update path} | |
| 57 | B -->|desktop-managed| C[Update desktop app on server machine] | |
| 58 | B -->|missing| D[Copy exact manual relaunch command] | |
| 59 | B -->|boot-service or respawn| E{Progress capability} | |
| 60 | E -->|present| F[server.updateServerWithProgress] | |
| 61 | E -->|missing| G[server.updateServer fallback] | |
| 62 | F --> H[Download exact t3 version] | |
| 63 | G --> H | |
| 64 | H --> I[Install and run version preflight] | |
| 65 | I -->|fails| J[Remove failed runtime and keep current server] | |
| 66 | I -->|passes| K{Handoff method} | |
| 67 | K -->|boot-service| L[Rewrite and restart T3 systemd unit] | |
| 68 | K -->|respawn| M[Start delayed replacement and exit current process] | |
| 69 | L --> N[Reconnect with fresh backoff] | |
| 70 | M --> N | |
| 71 | N --> O[Replacement publishes ready at target version] | |
| 72 | ``` | |
| 73 | ||
| 74 | Both update RPCs require the environment's `orchestration:operate` authorization scope. Their | |
| 75 | payload accepts only an exact npm version, including an exact prerelease version; dist-tags such as | |
| 76 | `latest` and `nightly` are rejected. The unary `server.updateServer` method remains available so a | |
| 77 | new client can still repair skew with an older server. | |
| 78 | ||
| 79 | The update service permits one update at a time. It installs `t3@<version>` under | |
| 80 | `<baseDir>/runtime/versions/<version>` and writes an install-complete sentinel only after npm exits | |
| 81 | successfully. Boot-service setup and self-update share the same process-wide installation lock, so | |
| 82 | they cannot mutate a pinned runtime concurrently. | |
| 83 | ||
| 84 | Before any restart, the current Node executable runs the replacement with `--version`. A failed | |
| 85 | install, failed preflight, or wrong reported version leaves the current server running. A failed | |
| 86 | preflight also removes the candidate runtime so retrying the same version performs a clean install. | |
| 87 | ||
| 88 | ## Host Service Lifecycle | |
| 89 | ||
| 90 | The systemd user service is a host lifecycle concern, not a T3 Connect resource. The standalone | |
| 91 | `t3 service install`, `uninstall`, `update`, and `status` commands own it. Install and update both | |
| 92 | reconcile the unit through `BootService`; running `npx t3@latest service update` therefore pins and | |
| 93 | activates the latest CLI release without requiring a connected client. | |
| 94 | ||
| 95 | The `t3 connect` onboarding flow may offer service installation, but it calls the same reconciliation | |
| 96 | operation as `t3 service install`. Connect logout only disables cloud access and clears its | |
| 97 | authorization; it does not uninstall the host service. | |
| 98 | ||
| 99 | ## Process Handoff | |
| 100 | ||
| 101 | For `boot-service`, the server atomically rewrites the T3-managed user unit to point at the verified | |
| 102 | runtime and reloads systemd. It acknowledges the handoff, then restarts the unit after the same | |
| 103 | short grace period used by foreground respawn. A rejected deferred restart restores the previous | |
| 104 | unit and is logged by the still-running process. | |
| 105 | ||
| 106 | For `respawn`, the server starts a detached, delayed replacement that replays the original CLI | |
| 107 | arguments. It then acknowledges the request and schedules the current process to exit. The delays | |
| 108 | give the acknowledgement time to cross direct or relayed connections before the socket closes. | |
| 109 | ||
| 110 | Progress-capable servers emit `downloading` before installing the pinned runtime and `installing` | |
| 111 | before preflight and handoff. A terminal stream event acknowledges that restart is scheduled. The | |
| 112 | client then enters `resuming`, waits for the replacement lifecycle stream to publish `ready` with | |
| 113 | the target version, and only then completes the operation. It watches for the intentional | |
| 114 | disconnect's first backoff state and requests one fresh retry, which clears historical backoff debt | |
| 115 | without adding a separate reconnect loop. | |
| 116 | ||
| 117 | ## Release Invariant | |
| 118 | ||
| 119 | The exact client version must exist as the `t3` npm package before a client carrying that version is | |
| 120 | published. The release workflow therefore makes the GitHub release depend on CLI publication, and | |
| 121 | the hosted web deployment depends on that release. See [Release Checklist](../operations/release.md#server-self-update-release-invariant). | |
| 122 | ||
| 123 | ## Source Map | |
| 124 | ||
| 125 | - Capability contract: `packages/contracts/src/environment.ts` | |
| 126 | - Update RPC contract: `packages/contracts/src/server.ts` and `packages/contracts/src/rpc.ts` | |
| 127 | - Capability detection and handoff: `apps/server/src/cloud/selfUpdate.ts` | |
| 128 | - Host service commands: `apps/server/src/cli/service.ts` | |
| 129 | - Pinned runtime installation: `apps/server/src/cloud/pinnedRuntime.ts` | |
| 130 | - Client version comparison: `apps/web/src/versionSkew.ts` | |
| 131 | - Shared update action: `apps/web/src/components/ServerUpdateAction.tsx` |
docs/cloud/environment-auth.md deleted-113| ... | ... | @@ -1,113 +0,0 @@ |
| 1 | # Environment Authentication Profile | |
| 2 | ||
| 3 | The environment server and the relay use separate credentials, issuers, and trust | |
| 4 | boundaries. They intentionally use a similar OAuth-shaped model so that permission | |
| 5 | checks and token exchange behavior can be audited against established concepts. | |
| 6 | ||
| 7 | ## Authorization Model | |
| 8 | ||
| 9 | Environment authorization is capability-based. A session carries zero or more | |
| 10 | OAuth-style scope strings: | |
| 11 | ||
| 12 | | Scope | Permission | | |
| 13 | | ----------------------- | ------------------------------------------------------------------------ | | |
| 14 | | `orchestration:read` | Read snapshots, status, events, configuration, and filesystem/VCS state. | | |
| 15 | | `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state. | | |
| 16 | | `terminal:operate` | Create, attach, input, resize, clear, restart, and terminate terminals. | | |
| 17 | | `review:write` | Read review diff previews used to compose review feedback. | | |
| 18 | | `access:read` | Inspect pairing links and client sessions. | | |
| 19 | | `access:write` | Create or revoke pairing links and client sessions. | | |
| 20 | | `relay:read` | Inspect managed relay connectivity. | | |
| 21 | | `relay:write` | Link, configure, or unlink managed relay connectivity. | | |
| 22 | ||
| 23 | Ordinary pairing links grant the four client-operation scopes and read access to | |
| 24 | managed relay connectivity: | |
| 25 | `orchestration:read orchestration:operate terminal:operate review:write relay:read`. | |
| 26 | The desktop bootstrap credential and command-line administrative bootstrap | |
| 27 | credentials additionally grant `access:read access:write relay:write`. | |
| 28 | ||
| 29 | ## Authentication Flows | |
| 30 | ||
| 31 | ### Browser Session | |
| 32 | ||
| 33 | `POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a | |
| 34 | browser session cookie. The cookie is an HTTP transport adapter for the same | |
| 35 | scoped session model; the response never exposes the session secret to browser | |
| 36 | JavaScript. | |
| 37 | ||
| 38 | ### Bearer Access Token | |
| 39 | ||
| 40 | Non-browser clients use `POST /oauth/token` with an | |
| 41 | `application/x-www-form-urlencoded` body: | |
| 42 | ||
| 43 | ```text | |
| 44 | grant_type=urn:ietf:params:oauth:grant-type:token-exchange | |
| 45 | subject_token=<bootstrap credential> | |
| 46 | subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap | |
| 47 | requested_token_type=urn:ietf:params:oauth:token-type:access_token | |
| 48 | scope=orchestration:read orchestration:operate terminal:operate review:write relay:read | |
| 49 | ``` | |
| 50 | ||
| 51 | Clients may additionally submit `client_label`, `client_device_type`, and | |
| 52 | `client_os` extension parameters so the authorized-clients UI can identify the | |
| 53 | device that established the session. These are presentation hints only; the | |
| 54 | environment derives transport metadata such as IP address and user agent from | |
| 55 | the request and does not use these fields for authorization. | |
| 56 | ||
| 57 | The response has the token-exchange shape: | |
| 58 | ||
| 59 | ```json | |
| 60 | { | |
| 61 | "access_token": "<opaque session token>", | |
| 62 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", | |
| 63 | "token_type": "Bearer", | |
| 64 | "expires_in": 3600, | |
| 65 | "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" | |
| 66 | } | |
| 67 | ``` | |
| 68 | ||
| 69 | Requested scopes must be a subset of the one-time bootstrap credential grant. | |
| 70 | An ordinary paired client therefore cannot exchange its grant for | |
| 71 | `access:read`, `access:write`, or `relay:write`. | |
| 72 | ||
| 73 | ### WebSocket Ticket | |
| 74 | ||
| 75 | `POST /api/auth/websocket-ticket` accepts any authenticated session and returns | |
| 76 | a short-lived, single-purpose WebSocket ticket. This keeps bearer tokens and | |
| 77 | browser cookies out of WebSocket URLs while allowing the socket handshake to | |
| 78 | authenticate. The ticket carries its session's scopes; each RPC method then | |
| 79 | enforces `orchestration:read`, `orchestration:operate`, `terminal:operate`, | |
| 80 | `review:write`, or `access:read` as appropriate. Review feedback submission | |
| 81 | currently dispatches an orchestration operation, so clients performing it also | |
| 82 | need `orchestration:operate`. Creating a ticket is not | |
| 83 | authorization to call every RPC method. | |
| 84 | ||
| 85 | ## Standards Alignment | |
| 86 | ||
| 87 | - Bearer access tokens are used through the `Authorization: Bearer` scheme from | |
| 88 | RFC 6750. | |
| 89 | - The token endpoint profiles the request and response vocabulary from OAuth 2.0 | |
| 90 | Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`, | |
| 91 | `access_token`, `issued_token_type`, and `token_type`. | |
| 92 | - Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited, | |
| 93 | unordered capabilities with subset checking during exchange. | |
| 94 | ||
| 95 | This is intentionally not a general-purpose OAuth authorization server. The | |
| 96 | environment bootstrap token type is private, the bootstrap cookie and WebSocket | |
| 97 | connection-token routes are product-specific adapters, and the API returns its | |
| 98 | typed `HttpApi` errors rather than implementing every OAuth error response | |
| 99 | surface. | |
| 100 | ||
| 101 | ## Upgrade Behavior | |
| 102 | ||
| 103 | Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth | |
| 104 | records to scoped records. It deletes existing pairing links and sessions while | |
| 105 | leaving non-authentication environment state unchanged. Upgraded clients must | |
| 106 | pair again; old `owner` or `client` credentials are never silently mapped to new | |
| 107 | capabilities. | |
| 108 | ||
| 109 | ## Relay Boundary | |
| 110 | ||
| 111 | Relay-managed tunnels use their own tokens and keys. The relay can reuse scope | |
| 112 | parsing and token-exchange conventions, but an environment access token is not a | |
| 113 | relay token and cannot be presented to the relay. |
docs/cloud/t3-code-connect-auth-flow.html deleted-1160| ... | ... | @@ -1,1160 +0,0 @@ |
| 1 | <!doctype html> | |
| 2 | <html lang="en"> | |
| 3 | <head> | |
| 4 | <meta charset="utf-8" /> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 6 | <title>T3 Connect Architecture</title> | |
| 7 | <style> | |
| 8 | :root { | |
| 9 | color-scheme: light; | |
| 10 | --ink: #171717; | |
| 11 | --muted: #62646b; | |
| 12 | --hairline: #d8d8dd; | |
| 13 | --panel: #ffffff; | |
| 14 | --paper: #f5f5f2; | |
| 15 | --blue: #1457d9; | |
| 16 | --green: #1d7d4d; | |
| 17 | --amber: #a65f00; | |
| 18 | --red: #b8242c; | |
| 19 | --violet: #6150b8; | |
| 20 | --shadow: 0 18px 55px rgba(25, 25, 28, 0.08); | |
| 21 | } | |
| 22 | ||
| 23 | * { | |
| 24 | box-sizing: border-box; | |
| 25 | } | |
| 26 | ||
| 27 | body { | |
| 28 | margin: 0; | |
| 29 | background: | |
| 30 | linear-gradient(90deg, rgba(0, 0, 0, 0.035) 1px, transparent 1px), | |
| 31 | linear-gradient(rgba(0, 0, 0, 0.035) 1px, transparent 1px), var(--paper); | |
| 32 | background-size: 34px 34px; | |
| 33 | color: var(--ink); | |
| 34 | font-family: | |
| 35 | ui-sans-serif, | |
| 36 | -apple-system, | |
| 37 | BlinkMacSystemFont, | |
| 38 | "Segoe UI", | |
| 39 | sans-serif; | |
| 40 | line-height: 1.48; | |
| 41 | } | |
| 42 | ||
| 43 | .shell { | |
| 44 | width: min(1540px, calc(100vw - 40px)); | |
| 45 | margin: 0 auto; | |
| 46 | padding: 38px 0 72px; | |
| 47 | } | |
| 48 | ||
| 49 | header { | |
| 50 | border-bottom: 2px solid var(--ink); | |
| 51 | padding-bottom: 24px; | |
| 52 | margin-bottom: 28px; | |
| 53 | } | |
| 54 | ||
| 55 | .eyebrow { | |
| 56 | color: var(--blue); | |
| 57 | font-size: 12px; | |
| 58 | font-weight: 800; | |
| 59 | letter-spacing: 0.08em; | |
| 60 | text-transform: uppercase; | |
| 61 | } | |
| 62 | ||
| 63 | h1 { | |
| 64 | max-width: 980px; | |
| 65 | margin: 8px 0 12px; | |
| 66 | font-size: clamp(34px, 5vw, 66px); | |
| 67 | line-height: 0.98; | |
| 68 | } | |
| 69 | ||
| 70 | h2 { | |
| 71 | margin: 0 0 14px; | |
| 72 | font-size: 26px; | |
| 73 | line-height: 1.08; | |
| 74 | } | |
| 75 | ||
| 76 | h3 { | |
| 77 | margin: 22px 0 8px; | |
| 78 | font-size: 17px; | |
| 79 | } | |
| 80 | ||
| 81 | p { | |
| 82 | margin: 0 0 12px; | |
| 83 | } | |
| 84 | ||
| 85 | code, | |
| 86 | .mono { | |
| 87 | font-family: | |
| 88 | ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| 89 | } | |
| 90 | ||
| 91 | code { | |
| 92 | border-radius: 4px; | |
| 93 | background: #f0f0ed; | |
| 94 | padding: 1px 4px; | |
| 95 | font-size: 0.92em; | |
| 96 | } | |
| 97 | ||
| 98 | .lede { | |
| 99 | max-width: 1000px; | |
| 100 | margin: 0; | |
| 101 | color: var(--muted); | |
| 102 | font-size: 18px; | |
| 103 | } | |
| 104 | ||
| 105 | .grid { | |
| 106 | display: grid; | |
| 107 | grid-template-columns: repeat(12, 1fr); | |
| 108 | gap: 18px; | |
| 109 | } | |
| 110 | ||
| 111 | .panel { | |
| 112 | grid-column: span 12; | |
| 113 | border: 1px solid var(--hairline); | |
| 114 | border-radius: 10px; | |
| 115 | background: var(--panel); | |
| 116 | box-shadow: var(--shadow); | |
| 117 | padding: 22px; | |
| 118 | } | |
| 119 | ||
| 120 | .half { | |
| 121 | grid-column: span 6; | |
| 122 | } | |
| 123 | ||
| 124 | .callout { | |
| 125 | border-left: 5px solid var(--blue); | |
| 126 | background: #eef4ff; | |
| 127 | padding: 14px 16px; | |
| 128 | margin-top: 14px; | |
| 129 | } | |
| 130 | ||
| 131 | .callout.warning { | |
| 132 | border-left-color: var(--amber); | |
| 133 | background: #fff6e6; | |
| 134 | } | |
| 135 | ||
| 136 | .badge-row { | |
| 137 | display: flex; | |
| 138 | flex-wrap: wrap; | |
| 139 | gap: 8px; | |
| 140 | margin-top: 14px; | |
| 141 | } | |
| 142 | ||
| 143 | .badge { | |
| 144 | border: 1px solid var(--hairline); | |
| 145 | border-radius: 999px; | |
| 146 | background: #fff; | |
| 147 | padding: 5px 9px; | |
| 148 | font-size: 12px; | |
| 149 | font-weight: 700; | |
| 150 | } | |
| 151 | ||
| 152 | .badge.user { | |
| 153 | border-color: rgba(20, 87, 217, 0.3); | |
| 154 | color: var(--blue); | |
| 155 | } | |
| 156 | ||
| 157 | .badge.env { | |
| 158 | border-color: rgba(29, 125, 77, 0.3); | |
| 159 | color: var(--green); | |
| 160 | } | |
| 161 | ||
| 162 | .badge.local { | |
| 163 | border-color: rgba(166, 95, 0, 0.35); | |
| 164 | color: var(--amber); | |
| 165 | } | |
| 166 | ||
| 167 | .diagram { | |
| 168 | overflow-x: auto; | |
| 169 | margin: 12px 0 0; | |
| 170 | border: 1px solid var(--hairline); | |
| 171 | border-radius: 8px; | |
| 172 | background: #fbfbf8; | |
| 173 | color: var(--ink); | |
| 174 | padding: 18px; | |
| 175 | } | |
| 176 | ||
| 177 | .diagram svg { | |
| 178 | display: block; | |
| 179 | width: auto !important; | |
| 180 | min-width: 100%; | |
| 181 | max-width: none !important; | |
| 182 | height: auto; | |
| 183 | margin: 0 auto; | |
| 184 | } | |
| 185 | ||
| 186 | table { | |
| 187 | width: 100%; | |
| 188 | border-collapse: collapse; | |
| 189 | font-size: 14px; | |
| 190 | } | |
| 191 | ||
| 192 | th, | |
| 193 | td { | |
| 194 | border-bottom: 1px solid var(--hairline); | |
| 195 | padding: 11px 9px; | |
| 196 | text-align: left; | |
| 197 | vertical-align: top; | |
| 198 | } | |
| 199 | ||
| 200 | th { | |
| 201 | color: var(--muted); | |
| 202 | font-size: 12px; | |
| 203 | letter-spacing: 0.05em; | |
| 204 | text-transform: uppercase; | |
| 205 | } | |
| 206 | ||
| 207 | ul { | |
| 208 | margin: 8px 0 0; | |
| 209 | padding-left: 20px; | |
| 210 | } | |
| 211 | ||
| 212 | li + li { | |
| 213 | margin-top: 6px; | |
| 214 | } | |
| 215 | ||
| 216 | .flow { | |
| 217 | display: grid; | |
| 218 | gap: 10px; | |
| 219 | margin-top: 12px; | |
| 220 | } | |
| 221 | ||
| 222 | .step { | |
| 223 | border: 1px solid var(--hairline); | |
| 224 | border-left: 5px solid var(--green); | |
| 225 | border-radius: 8px; | |
| 226 | background: #fff; | |
| 227 | padding: 12px 14px; | |
| 228 | } | |
| 229 | ||
| 230 | .step strong { | |
| 231 | display: block; | |
| 232 | margin-bottom: 3px; | |
| 233 | } | |
| 234 | ||
| 235 | .source-list a { | |
| 236 | color: var(--blue); | |
| 237 | font-weight: 700; | |
| 238 | } | |
| 239 | ||
| 240 | @media (max-width: 860px) { | |
| 241 | .half { | |
| 242 | grid-column: span 12; | |
| 243 | } | |
| 244 | ||
| 245 | .shell { | |
| 246 | width: min(100vw - 24px, 1540px); | |
| 247 | padding-top: 24px; | |
| 248 | } | |
| 249 | } | |
| 250 | </style> | |
| 251 | </head> | |
| 252 | <body> | |
| 253 | <main class="shell"> | |
| 254 | <header> | |
| 255 | <div class="eyebrow">Implemented architecture</div> | |
| 256 | <h1>T3 Connect Control Plane and Managed Endpoint Flow</h1> | |
| 257 | <p class="lede"> | |
| 258 | T3 Connect links a locally authorized T3 environment to a signed-in cloud user, provisions | |
| 259 | a managed HTTPS/WSS endpoint for that environment, and brokers proof-bound remote | |
| 260 | connections. The local environment remains the authority that issues environment access | |
| 261 | credentials. The hosted relay stores links, reconciles managed endpoint resources, | |
| 262 | validates signed proofs, and delivers agent-activity notifications. | |
| 263 | </p> | |
| 264 | <div class="badge-row"> | |
| 265 | <span class="badge user">Client -> Relay: Clerk bearer or relay DPoP</span> | |
| 266 | <span class="badge env">Relay -> Environment: relay-signed health and mint proofs</span> | |
| 267 | <span class="badge env" | |
| 268 | >Environment -> Relay: environment bearer plus signed activity proof</span | |
| 269 | > | |
| 270 | <span class="badge local">Client -> Environment: environment access token plus DPoP</span> | |
| 271 | </div> | |
| 272 | </header> | |
| 273 | ||
| 274 | <section class="grid" aria-label="Architecture summary"> | |
| 275 | <article class="panel half"> | |
| 276 | <h2>Security Invariant</h2> | |
| 277 | <p> | |
| 278 | A cloud identity is not an environment login. Remote access requires all of the | |
| 279 | following: | |
| 280 | </p> | |
| 281 | <ul> | |
| 282 | <li>a Clerk-authenticated T3 Connect user;</li> | |
| 283 | <li>an active relay link for that user and environment;</li> | |
| 284 | <li>a DPoP proof key held by the remote client;</li> | |
| 285 | <li>a relay-signed mint request accepted by the linked local environment; and</li> | |
| 286 | <li>an environment-issued credential bound to the remote client's DPoP thumbprint.</li> | |
| 287 | </ul> | |
| 288 | <div class="callout"> | |
| 289 | The relay cannot mint an environment access token by itself. It can only ask the local | |
| 290 | environment to mint a short-lived bootstrap credential for an authorized cloud user. | |
| 291 | </div> | |
| 292 | <p>During an honest connection flow, the relay cannot act as the remote client:</p> | |
| 293 | <ul> | |
| 294 | <li> | |
| 295 | Normal environment API traffic goes directly from the remote client through the | |
| 296 | managed endpoint. The relay does not receive the resulting environment access token. | |
| 297 | </li> | |
| 298 | <li> | |
| 299 | The one-time bootstrap credential and resulting environment access token are bound to | |
| 300 | the remote client's DPoP public-key thumbprint. Redeeming or using them requires | |
| 301 | request proofs signed by the corresponding private key, which stays on the client. | |
| 302 | </li> | |
| 303 | <li> | |
| 304 | Relay-signed health and mint proofs are short-lived, audience-bound, scoped, and | |
| 305 | replay-guarded. They are accepted only by the narrow cloud endpoints, not as normal | |
| 306 | environment API credentials. | |
| 307 | </li> | |
| 308 | <li> | |
| 309 | Environment health and mint responses are signed by the linked environment and bound | |
| 310 | to the request nonce, so a different process behind the tunnel cannot impersonate the | |
| 311 | linked environment. | |
| 312 | </li> | |
| 313 | </ul> | |
| 314 | <div class="callout warning"> | |
| 315 | The hosted relay remains a trusted mint broker: it holds the cloud-mint signing key. | |
| 316 | DPoP prevents credential reuse by the relay or tunnel transport during an honest flow, | |
| 317 | but it does not make compromise of the relay signing authority harmless. | |
| 318 | </div> | |
| 319 | </article> | |
| 320 | ||
| 321 | <article class="panel half"> | |
| 322 | <h2>Product Boundaries</h2> | |
| 323 | <p> | |
| 324 | The generic local connector is called the <strong>relay client</strong>. The current | |
| 325 | implementation layer uses <code>cloudflared</code>, but that is an internal transport | |
| 326 | detail rather than the product contract. | |
| 327 | </p> | |
| 328 | <ul> | |
| 329 | <li> | |
| 330 | The relay Worker owns cloud links, managed endpoint allocations, and notifications. | |
| 331 | </li> | |
| 332 | <li> | |
| 333 | The environment server owns local sessions, environment keys, and access tokens. | |
| 334 | </li> | |
| 335 | <li>The relay client exposes only the environment's loopback HTTP server.</li> | |
| 336 | <li> | |
| 337 | Web and mobile clients connect directly to the managed environment endpoint after | |
| 338 | bootstrap. | |
| 339 | </li> | |
| 340 | </ul> | |
| 341 | </article> | |
| 342 | </section> | |
| 343 | ||
| 344 | <section class="panel" aria-label="Topology"> | |
| 345 | <h2>Current Topology</h2> | |
| 346 | <pre class="diagram mermaid"> | |
| 347 | flowchart LR | |
| 348 | subgraph Clients["User-facing clients"] | |
| 349 | Web["Desktop / hosted web UI"] | |
| 350 | Mobile["Mobile app"] | |
| 351 | CLI["Headless CLI"] | |
| 352 | end | |
| 353 | ||
| 354 | Clerk["Clerk<br/>user identity and OAuth"] | |
| 355 | ||
| 356 | subgraph Relay["T3 Code Relay on Cloudflare"] | |
| 357 | Worker["Relay Worker<br/>HTTP API"] | |
| 358 | Queue["APNs delivery queue<br/>with dead-letter queue"] | |
| 359 | Hyperdrive["Hyperdrive"] | |
| 360 | CF["Cloudflare tunnel + DNS APIs"] | |
| 361 | end | |
| 362 | ||
| 363 | DB["PlanetScale Postgres<br/>prod database + stage branches"] | |
| 364 | APNs["Apple Push Notification service"] | |
| 365 | Traces["Axiom OTLP traces"] | |
| 366 | ||
| 367 | subgraph Local["Linked T3 environment"] | |
| 368 | Env["Environment server<br/>local auth authority"] | |
| 369 | RelayClient["Relay client<br/>cloudflared implementation"] | |
| 370 | Secrets["Server secret store<br/>keys + relay config"] | |
| 371 | end | |
| 372 | ||
| 373 | Endpoint["Managed HTTPS/WSS endpoint"] | |
| 374 | ||
| 375 | Web -->|"Clerk bearer / relay DPoP"| Worker | |
| 376 | Mobile -->|"Clerk bearer / relay DPoP"| Worker | |
| 377 | CLI -->|"Clerk OAuth bearer"| Worker | |
| 378 | Web --> Clerk | |
| 379 | Mobile --> Clerk | |
| 380 | CLI -->|"PKCE authorization code flow"| Clerk | |
| 381 | Worker --> Hyperdrive --> DB | |
| 382 | Worker --> Traces | |
| 383 | Worker --> CF | |
| 384 | Worker --> Queue --> APNs | |
| 385 | Worker --> Endpoint | |
| 386 | Endpoint --> RelayClient -->|"http://127.0.0.1:port"| Env | |
| 387 | Env --> Secrets | |
| 388 | Web -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | |
| 389 | Mobile -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | |
| 390 | Env -->|"environment bearer + signed activity proof"| Worker | |
| 391 | </pre> | |
| 392 | </section> | |
| 393 | ||
| 394 | <section class="panel" aria-label="Authentication matrix"> | |
| 395 | <h2>Authentication and Transport Matrix</h2> | |
| 396 | <table> | |
| 397 | <thead> | |
| 398 | <tr> | |
| 399 | <th>Boundary</th> | |
| 400 | <th>Transport</th> | |
| 401 | <th>Authentication</th> | |
| 402 | <th>Purpose</th> | |
| 403 | </tr> | |
| 404 | </thead> | |
| 405 | <tbody> | |
| 406 | <tr> | |
| 407 | <td>Web or mobile -> relay link management</td> | |
| 408 | <td>HTTPS</td> | |
| 409 | <td>Clerk session-template bearer token</td> | |
| 410 | <td>Create a link challenge, submit an environment proof, list links, or unlink.</td> | |
| 411 | </tr> | |
| 412 | <tr> | |
| 413 | <td>CLI -> relay link management</td> | |
| 414 | <td>HTTPS</td> | |
| 415 | <td>Clerk OAuth access token obtained with PKCE</td> | |
| 416 | <td>Reconcile the desired headless cloud link when the server starts.</td> | |
| 417 | </tr> | |
| 418 | <tr> | |
| 419 | <td>Web or mobile -> relay protected client endpoints</td> | |
| 420 | <td>HTTPS</td> | |
| 421 | <td>Relay-issued DPoP token plus per-request DPoP proof</td> | |
| 422 | <td> | |
| 423 | Check environment status, request a remote connection, and register mobile devices. | |
| 424 | </td> | |
| 425 | </tr> | |
| 426 | <tr> | |
| 427 | <td>Relay -> managed environment endpoint</td> | |
| 428 | <td>HTTPS over the managed tunnel</td> | |
| 429 | <td>Short-lived relay-signed JWT request proof</td> | |
| 430 | <td> | |
| 431 | Request a signed health response or ask the local environment to mint a credential. | |
| 432 | </td> | |
| 433 | </tr> | |
| 434 | <tr> | |
| 435 | <td>Environment -> relay activity publication</td> | |
| 436 | <td>HTTPS</td> | |
| 437 | <td>Relay-issued environment bearer credential plus environment-signed JWT proof</td> | |
| 438 | <td> | |
| 439 | Publish redacted agent-activity state for push notifications and Live Activities. | |
| 440 | </td> | |
| 441 | </tr> | |
| 442 | <tr> | |
| 443 | <td>Remote client -> environment</td> | |
| 444 | <td>HTTPS and WSS over the managed tunnel</td> | |
| 445 | <td>Environment-issued access token bound to the client DPoP key</td> | |
| 446 | <td>Use the normal environment HTTP APIs and request a one-time WebSocket ticket.</td> | |
| 447 | </tr> | |
| 448 | </tbody> | |
| 449 | </table> | |
| 450 | </section> | |
| 451 | ||
| 452 | <section class="panel" aria-label="Credential ownership"> | |
| 453 | <h2>Credential Ownership</h2> | |
| 454 | <table> | |
| 455 | <thead> | |
| 456 | <tr> | |
| 457 | <th>Credential</th> | |
| 458 | <th>Created by</th> | |
| 459 | <th>Stored by</th> | |
| 460 | <th>Use</th> | |
| 461 | </tr> | |
| 462 | </thead> | |
| 463 | <tbody> | |
| 464 | <tr> | |
| 465 | <td>Clerk session-template JWT</td> | |
| 466 | <td>Clerk</td> | |
| 467 | <td>Web or mobile client</td> | |
| 468 | <td> | |
| 469 | Authenticate cloud-user relay requests and bootstrap relay DPoP token exchange. | |
| 470 | </td> | |
| 471 | </tr> | |
| 472 | <tr> | |
| 473 | <td>CLI OAuth access and refresh token</td> | |
| 474 | <td>Clerk OAuth</td> | |
| 475 | <td>Local server secret store</td> | |
| 476 | <td>Authorize headless CLI link reconciliation.</td> | |
| 477 | </tr> | |
| 478 | <tr> | |
| 479 | <td>Environment Ed25519 key pair</td> | |
| 480 | <td>Local environment server</td> | |
| 481 | <td>Local server secret store</td> | |
| 482 | <td> | |
| 483 | Sign link proofs, health responses, mint responses, and activity publication proofs. | |
| 484 | </td> | |
| 485 | </tr> | |
| 486 | <tr> | |
| 487 | <td>Relay cloud-mint key pair</td> | |
| 488 | <td>Relay deployment</td> | |
| 489 | <td>Private key in relay Worker config; public key installed into the environment</td> | |
| 490 | <td>Sign relay-to-environment health and mint requests.</td> | |
| 491 | </tr> | |
| 492 | <tr> | |
| 493 | <td>Relay environment bearer credential</td> | |
| 494 | <td>Relay</td> | |
| 495 | <td>Hashed in relay Postgres; plaintext in local server secret store</td> | |
| 496 | <td>Authenticate environment-to-relay agent-activity publication.</td> | |
| 497 | </tr> | |
| 498 | <tr> | |
| 499 | <td>Relay DPoP access token</td> | |
| 500 | <td>Relay</td> | |
| 501 | <td>Remote client memory cache</td> | |
| 502 | <td> | |
| 503 | Authorize status, connect, and mobile registration endpoints with proof of | |
| 504 | possession. | |
| 505 | </td> | |
| 506 | </tr> | |
| 507 | <tr> | |
| 508 | <td>Environment bootstrap credential</td> | |
| 509 | <td>Local environment server</td> | |
| 510 | <td>Passed relay -> remote client</td> | |
| 511 | <td>One-time exchange at the environment <code>/oauth/token</code> endpoint.</td> | |
| 512 | </tr> | |
| 513 | <tr> | |
| 514 | <td>Environment access token</td> | |
| 515 | <td>Local environment server</td> | |
| 516 | <td>Remote client</td> | |
| 517 | <td>Authorize environment HTTP operations and WebSocket ticket issuance.</td> | |
| 518 | </tr> | |
| 519 | <tr> | |
| 520 | <td>WebSocket ticket</td> | |
| 521 | <td>Local environment server</td> | |
| 522 | <td>Remote client until consumed</td> | |
| 523 | <td>Short-lived one-time credential for the WSS upgrade.</td> | |
| 524 | </tr> | |
| 525 | <tr> | |
| 526 | <td>Relay client connector token</td> | |
| 527 | <td>Cloudflare tunnel API</td> | |
| 528 | <td>Local server secret store</td> | |
| 529 | <td> | |
| 530 | Start the local relay client. It is passed through <code>TUNNEL_TOKEN</code>, not a | |
| 531 | shell argument. | |
| 532 | </td> | |
| 533 | </tr> | |
| 534 | </tbody> | |
| 535 | </table> | |
| 536 | </section> | |
| 537 | ||
| 538 | <section class="grid" aria-label="Core flows"> | |
| 539 | <article class="panel"> | |
| 540 | <h2>Flow 1: Link From the Desktop Web UI</h2> | |
| 541 | <pre class="diagram mermaid"> | |
| 542 | sequenceDiagram | |
| 543 | participant U as Signed-in user | |
| 544 | participant C as Desktop web UI | |
| 545 | participant E as Local environment | |
| 546 | participant R as Relay Worker | |
| 547 | participant CF as Cloudflare APIs | |
| 548 | participant RC as Local relay client | |
| 549 | ||
| 550 | U->>C: Enable T3 Connect for a locally paired environment | |
| 551 | C->>E: Check relay client availability | |
| 552 | alt relay client is missing | |
| 553 | C->>U: Prompt before download and install | |
| 554 | U->>C: Confirm | |
| 555 | C->>E: Stream relay-client install RPC | |
| 556 | E-->>C: Progress stages | |
| 557 | end | |
| 558 | C->>R: POST /v1/client/environment-link-challenges (Clerk bearer) | |
| 559 | R-->>C: Short-lived challenge | |
| 560 | C->>E: POST /api/connect/link-proof (local relay:write bearer) | |
| 561 | E->>E: Authenticate, reject forwarded authority, get or create key pair, validate loopback origin | |
| 562 | E-->>C: Environment-signed link proof | |
| 563 | C->>R: POST /v1/client/environment-links (Clerk bearer + proof) | |
| 564 | R->>R: Verify challenge, proof, capabilities, nonce, and loopback origin | |
| 565 | R->>CF: Reconcile tunnel, ingress, and CNAME | |
| 566 | R->>R: Upsert user/environment link and issue environment bearer | |
| 567 | R-->>C: Managed endpoint, relay config, and connector token | |
| 568 | C->>E: POST /api/connect/relay-config (local relay:write bearer) | |
| 569 | E->>RC: Start relay client with TUNNEL_TOKEN | |
| 570 | </pre> | |
| 571 | <div class="callout"> | |
| 572 | The local link-proof endpoint requires <code>relay:write</code>, rejects forwarded | |
| 573 | authority headers, and accepts only an exact loopback origin. Environment keypair | |
| 574 | persistence is atomic across concurrent link attempts. | |
| 575 | </div> | |
| 576 | <p> | |
| 577 | A locally paired mobile client uses the same relay challenge, local proof, relay link, | |
| 578 | and local relay-config endpoints. The desktop web UI is the surface that checks relay | |
| 579 | client availability and offers the managed install dialog. | |
| 580 | </p> | |
| 581 | </article> | |
| 582 | ||
| 583 | <article class="panel"> | |
| 584 | <h2>Flow 2: Headless CLI Link</h2> | |
| 585 | <pre class="diagram mermaid"> | |
| 586 | sequenceDiagram | |
| 587 | participant U as Operator | |
| 588 | participant CLI as t3 connect CLI | |
| 589 | participant Clerk as Clerk OAuth | |
| 590 | participant S as Environment server on next start | |
| 591 | participant R as Relay Worker | |
| 592 | participant RC as Local relay client | |
| 593 | ||
| 594 | U->>CLI: t3 connect link | |
| 595 | CLI->>CLI: Resolve relay client | |
| 596 | alt relay client is missing | |
| 597 | CLI->>U: Confirm managed relay-client install | |
| 598 | U->>CLI: Confirm | |
| 599 | CLI->>CLI: Install with terminal progress updates | |
| 600 | end | |
| 601 | CLI->>Clerk: Browser PKCE authorization-code flow | |
| 602 | Clerk-->>CLI: OAuth access and refresh tokens | |
| 603 | CLI->>CLI: Persist desired cloud-link state | |
| 604 | U->>S: Start T3 environment server | |
| 605 | S->>R: Create challenge and submit local signed proof | |
| 606 | R-->>S: Managed endpoint config and connector token | |
| 607 | S->>RC: Start relay client | |
| 608 | </pre> | |
| 609 | <p> | |
| 610 | <code>t3 connect login</code> stores authorization without enabling exposure. | |
| 611 | <code>t3 connect unlink</code> disables exposure while retaining authorization. | |
| 612 | <code>t3 connect logout</code> also removes the stored CLI authorization. | |
| 613 | </p> | |
| 614 | </article> | |
| 615 | ||
| 616 | <article class="panel"> | |
| 617 | <h2>Flow 3: Remote Connect Bootstrap</h2> | |
| 618 | <pre class="diagram mermaid"> | |
| 619 | sequenceDiagram | |
| 620 | participant C as Web or mobile client | |
| 621 | participant Clerk as Clerk | |
| 622 | participant R as Relay Worker | |
| 623 | participant E as Managed environment endpoint | |
| 624 | ||
| 625 | C->>Clerk: Get Clerk session-template JWT | |
| 626 | C->>R: POST /v1/client/dpop-token (Clerk JWT + DPoP proof) | |
| 627 | R-->>C: Relay DPoP access token | |
| 628 | C->>R: POST /v1/environments/:id/connect (relay DPoP) | |
| 629 | R->>E: POST /api/t3-connect/mint-credential (relay-signed proof) | |
| 630 | E->>E: Verify relay signer, linked user, scope, lifetime, cnf, and replay guards | |
| 631 | E-->>R: Bootstrap credential + environment-signed proof | |
| 632 | R->>R: Verify environment signature, nonce, endpoint, and DPoP binding | |
| 633 | R-->>C: Managed endpoint + bootstrap credential | |
| 634 | C->>E: POST /oauth/token (bootstrap credential + DPoP proof) | |
| 635 | E-->>C: Environment DPoP-bound access token | |
| 636 | C->>E: POST /api/auth/websocket-ticket | |
| 637 | E-->>C: One-time WebSocket ticket | |
| 638 | C->>E: WSS /ws with ticket | |
| 639 | </pre> | |
| 640 | <div class="callout"> | |
| 641 | Relay-to-environment control requests disable redirects, use only a reconciled managed | |
| 642 | endpoint allocation, and require a signed environment response before the relay returns | |
| 643 | data to the client. | |
| 644 | </div> | |
| 645 | </article> | |
| 646 | ||
| 647 | <article class="panel"> | |
| 648 | <h2>Flow 4: Agent Activity Notifications</h2> | |
| 649 | <pre class="diagram mermaid"> | |
| 650 | sequenceDiagram | |
| 651 | participant E as Local environment | |
| 652 | participant R as Relay Worker | |
| 653 | participant DB as Relay Postgres | |
| 654 | participant Q as APNs queue | |
| 655 | participant APNs as Apple Push Notification service | |
| 656 | participant M as Mobile app | |
| 657 | ||
| 658 | E->>E: Project publishable thread state and redact failure detail | |
| 659 | E->>R: POST /v1/environments/:environmentId/threads/:threadId/agent-activity | |
| 660 | Note over E,R: Environment bearer credential + environment-signed per-state proof | |
| 661 | R->>R: Verify credential, signature, scope, expiry, and nonce | |
| 662 | R->>DB: Store current activity row | |
| 663 | R->>Q: Enqueue push or Live Activity delivery jobs | |
| 664 | Q->>APNs: Deliver signed APNs request | |
| 665 | APNs-->>M: Push notification or Live Activity update | |
| 666 | </pre> | |
| 667 | <p> | |
| 668 | The local server publishes a deliberately narrow projection. Failed runs use a fixed | |
| 669 | redacted summary, and detail strings are capped before publication. | |
| 670 | </p> | |
| 671 | </article> | |
| 672 | </section> | |
| 673 | ||
| 674 | <section class="grid" aria-label="Managed endpoint lifecycle"> | |
| 675 | <article class="panel half"> | |
| 676 | <h2>Managed Endpoint Reconciliation</h2> | |
| 677 | <p> | |
| 678 | Managed endpoint resources are keyed by <code>(userId, environmentId)</code>. The relay | |
| 679 | reserves a stable hostname and tunnel name before provisioning, then checkpoints the | |
| 680 | tunnel ID, DNS record ID, and ready state in Postgres. | |
| 681 | </p> | |
| 682 | <div class="flow"> | |
| 683 | <div class="step"> | |
| 684 | <strong>1. Reserve deterministic allocation</strong> | |
| 685 | Hostname and tunnel name derive from relay stage, cloud user, and environment ID. | |
| 686 | </div> | |
| 687 | <div class="step"> | |
| 688 | <strong>2. Reuse or create tunnel</strong> | |
| 689 | Existing named tunnels are reused. Tunnel ingress is rewritten to the validated | |
| 690 | loopback origin. | |
| 691 | </div> | |
| 692 | <div class="step"> | |
| 693 | <strong>3. Reconcile DNS</strong> | |
| 694 | Existing CNAMEs are updated and duplicate records are removed. Create conflicts are | |
| 695 | recovered by listing and reconciling the winning record. | |
| 696 | </div> | |
| 697 | <div class="step"> | |
| 698 | <strong>4. Mark ready</strong> | |
| 699 | Connect and status requests use only ready allocations whose hostname still matches | |
| 700 | the relay-managed namespace. | |
| 701 | </div> | |
| 702 | </div> | |
| 703 | </article> | |
| 704 | ||
| 705 | <article class="panel half"> | |
| 706 | <h2>Unlink Cleanup</h2> | |
| 707 | <p> | |
| 708 | Relay unlink removes the user's managed endpoint allocation before revoking the | |
| 709 | user/environment link: | |
| 710 | </p> | |
| 711 | <ul> | |
| 712 | <li>delete the managed DNS record if present;</li> | |
| 713 | <li>delete the Cloudflare tunnel if present;</li> | |
| 714 | <li>remove the allocation row;</li> | |
| 715 | <li>revoke the user's environment link; and</li> | |
| 716 | <li> | |
| 717 | revoke the environment publication credential only after the final active link for | |
| 718 | that environment key disappears. | |
| 719 | </li> | |
| 720 | </ul> | |
| 721 | <p> | |
| 722 | Local unlink stops the relay client and removes the locally persisted relay URL, issuer, | |
| 723 | linked user, environment publication credential, cloud-mint public key, connector | |
| 724 | config, and agent-activity preference. | |
| 725 | </p> | |
| 726 | </article> | |
| 727 | </section> | |
| 728 | ||
| 729 | <section class="panel" aria-label="Cloudflare tunnel operational profile"> | |
| 730 | <h2>Cloudflare Tunnel Operational Profile</h2> | |
| 731 | <p> | |
| 732 | Managed tunnels are private application backends, not general-purpose public hosting. The | |
| 733 | relay provisions one named Cloudflare tunnel and one DNS record per active | |
| 734 | <code>(userId, environmentId)</code> allocation. | |
| 735 | </p> | |
| 736 | <ul> | |
| 737 | <li> | |
| 738 | <strong>Lifecycle:</strong> the tunnel is created when an environment is linked, reused | |
| 739 | across retry-safe reconciliation, kept for the lifetime of that link, and deleted on | |
| 740 | unlink. If teardown fails, the allocation checkpoint remains so cleanup can be retried. | |
| 741 | </li> | |
| 742 | <li> | |
| 743 | <strong>Origin:</strong> tunnel ingress is restricted to the linked environment server's | |
| 744 | validated loopback HTTP origin. Arbitrary upstream hosts and raw TCP origins are | |
| 745 | rejected. | |
| 746 | </li> | |
| 747 | <li> | |
| 748 | <strong>Protocols:</strong> managed endpoints carry HTTPS request/response traffic and | |
| 749 | WSS connections only. Remote clients use the normal environment HTTP APIs and | |
| 750 | authenticated WebSocket RPC transport through the tunnel. | |
| 751 | </li> | |
| 752 | <li> | |
| 753 | <strong>Relay-generated traffic:</strong> the hosted relay sends sparse HTTPS health | |
| 754 | checks and credential-mint requests during status checks and remote connection | |
| 755 | bootstrap. It does not proxy steady-state remote sessions. | |
| 756 | </li> | |
| 757 | <li> | |
| 758 | <strong>Traffic shape:</strong> expected load is interactive and user-driven. Active | |
| 759 | remote sessions may keep WebSockets open and exchange terminal, agent, and UI updates; | |
| 760 | inactive linked environments should carry little or no tunnel traffic. | |
| 761 | </li> | |
| 762 | <li> | |
| 763 | <strong>Capacity planning:</strong> the project is pre-launch, so there is not yet a | |
| 764 | production bandwidth baseline. Tunnel count grows with active linked user/environment | |
| 765 | allocations, while bandwidth and concurrent WebSockets grow with active remote use. | |
| 766 | </li> | |
| 767 | </ul> | |
| 768 | </section> | |
| 769 | ||
| 770 | <section class="panel" aria-label="SSRF and tunnel hardening"> | |
| 771 | <h2>SSRF and Tunnel Hardening</h2> | |
| 772 | <table> | |
| 773 | <thead> | |
| 774 | <tr> | |
| 775 | <th>Risk</th> | |
| 776 | <th>Implemented control</th> | |
| 777 | </tr> | |
| 778 | </thead> | |
| 779 | <tbody> | |
| 780 | <tr> | |
| 781 | <td>A client asks the relay to tunnel an arbitrary host.</td> | |
| 782 | <td> | |
| 783 | The environment and relay both accept managed tunnel origins only for loopback hosts | |
| 784 | and valid ports. | |
| 785 | </td> | |
| 786 | </tr> | |
| 787 | <tr> | |
| 788 | <td>A forwarded request tricks local origin validation.</td> | |
| 789 | <td> | |
| 790 | The local link-proof handler requires an exact loopback request URL and rejects | |
| 791 | forwarded host or protocol headers. | |
| 792 | </td> | |
| 793 | </tr> | |
| 794 | <tr> | |
| 795 | <td>A stored endpoint is replaced with an arbitrary external URL.</td> | |
| 796 | <td> | |
| 797 | Connect and status resolve a ready allocation from Postgres and require a hostname | |
| 798 | under the configured managed endpoint zone. | |
| 799 | </td> | |
| 800 | </tr> | |
| 801 | <tr> | |
| 802 | <td>A managed endpoint redirects relay egress elsewhere.</td> | |
| 803 | <td> | |
| 804 | Relay-to-environment status and mint requests set redirect handling to | |
| 805 | <code>manual</code>. | |
| 806 | </td> | |
| 807 | </tr> | |
| 808 | <tr> | |
| 809 | <td>A process behind the tunnel returns attacker-controlled data.</td> | |
| 810 | <td> | |
| 811 | The relay verifies environment-signed health and mint responses against the linked | |
| 812 | environment public key, expected nonce, and request binding. | |
| 813 | </td> | |
| 814 | </tr> | |
| 815 | <tr> | |
| 816 | <td>A stolen remote access token is replayed.</td> | |
| 817 | <td> | |
| 818 | Relay and environment access tokens are DPoP-bound; per-request proofs and replay | |
| 819 | guards are required. | |
| 820 | </td> | |
| 821 | </tr> | |
| 822 | <tr> | |
| 823 | <td>The relay client token leaks through process listings or shell parsing.</td> | |
| 824 | <td> | |
| 825 | The relay client is spawned without a shell and receives its connector token in | |
| 826 | <code>TUNNEL_TOKEN</code>. | |
| 827 | </td> | |
| 828 | </tr> | |
| 829 | </tbody> | |
| 830 | </table> | |
| 831 | <div class="callout warning"> | |
| 832 | Managed tunnel hostnames live under the configured tunnel DNS zone. Serving them from a | |
| 833 | dedicated registrable domain with a Public Suffix List entry remains an operational | |
| 834 | isolation requirement before broad untrusted use. | |
| 835 | </div> | |
| 836 | </section> | |
| 837 | ||
| 838 | <section class="panel" aria-label="Relay HTTP API"> | |
| 839 | <h2>Relay HTTP API</h2> | |
| 840 | <p> | |
| 841 | The relay publishes an OpenAPI document at <code>/openapi.json</code>, interactive API | |
| 842 | docs at <code>/docs</code>, and redirects <code>/</code> to the docs. | |
| 843 | </p> | |
| 844 | <table> | |
| 845 | <thead> | |
| 846 | <tr> | |
| 847 | <th>Endpoint</th> | |
| 848 | <th>Authentication</th> | |
| 849 | <th>Purpose</th> | |
| 850 | </tr> | |
| 851 | </thead> | |
| 852 | <tbody> | |
| 853 | <tr> | |
| 854 | <td><code>GET /health</code></td> | |
| 855 | <td>None</td> | |
| 856 | <td>Relay health check.</td> | |
| 857 | </tr> | |
| 858 | <tr> | |
| 859 | <td><code>GET /.well-known/oauth-authorization-server</code></td> | |
| 860 | <td>None</td> | |
| 861 | <td>Relay OAuth token-exchange discovery metadata.</td> | |
| 862 | </tr> | |
| 863 | <tr> | |
| 864 | <td><code>GET /.well-known/oauth-protected-resource</code></td> | |
| 865 | <td>None</td> | |
| 866 | <td>Relay DPoP protected-resource discovery metadata.</td> | |
| 867 | </tr> | |
| 868 | <tr> | |
| 869 | <td><code>GET /v1/environments</code></td> | |
| 870 | <td>Clerk bearer</td> | |
| 871 | <td>List environments linked to the signed-in user.</td> | |
| 872 | </tr> | |
| 873 | <tr> | |
| 874 | <td><code>POST /v1/client/environment-link-challenges</code></td> | |
| 875 | <td>Clerk bearer</td> | |
| 876 | <td>Create a short-lived user-bound environment link challenge.</td> | |
| 877 | </tr> | |
| 878 | <tr> | |
| 879 | <td><code>POST /v1/client/environment-links</code></td> | |
| 880 | <td>Clerk bearer</td> | |
| 881 | <td> | |
| 882 | Verify a local environment proof, reconcile a managed endpoint, and upsert the user | |
| 883 | link. | |
| 884 | </td> | |
| 885 | </tr> | |
| 886 | <tr> | |
| 887 | <td><code>DELETE /v1/client/environment-links/:environmentId</code></td> | |
| 888 | <td>Clerk bearer</td> | |
| 889 | <td>Remove managed endpoint resources and revoke the user's link.</td> | |
| 890 | </tr> | |
| 891 | <tr> | |
| 892 | <td><code>POST /v1/client/dpop-token</code></td> | |
| 893 | <td>Clerk token in token-exchange payload plus DPoP proof</td> | |
| 894 | <td>Issue a relay DPoP access token for the requested supported scopes.</td> | |
| 895 | </tr> | |
| 896 | <tr> | |
| 897 | <td><code>POST /v1/environments/:environmentId/status</code></td> | |
| 898 | <td>Relay DPoP</td> | |
| 899 | <td>Request and validate a signed environment health response.</td> | |
| 900 | </tr> | |
| 901 | <tr> | |
| 902 | <td><code>POST /v1/environments/:environmentId/connect</code></td> | |
| 903 | <td>Relay DPoP</td> | |
| 904 | <td>Request and validate an environment bootstrap credential.</td> | |
| 905 | </tr> | |
| 906 | <tr> | |
| 907 | <td><code>POST /v1/mobile/devices</code></td> | |
| 908 | <td>Relay DPoP</td> | |
| 909 | <td>Register or update a mobile device for notifications.</td> | |
| 910 | </tr> | |
| 911 | <tr> | |
| 912 | <td><code>POST /v1/mobile/live-activities</code></td> | |
| 913 | <td>Relay DPoP</td> | |
| 914 | <td>Register a Live Activity push token.</td> | |
| 915 | </tr> | |
| 916 | <tr> | |
| 917 | <td><code>DELETE /v1/mobile/devices/:deviceId</code></td> | |
| 918 | <td>Relay DPoP</td> | |
| 919 | <td>Unregister a mobile device.</td> | |
| 920 | </tr> | |
| 921 | <tr> | |
| 922 | <td> | |
| 923 | <code>POST /v1/environments/:environmentId/threads/:threadId/agent-activity</code> | |
| 924 | </td> | |
| 925 | <td>Environment bearer plus signed publish proof</td> | |
| 926 | <td>Publish redacted current agent activity for notification delivery.</td> | |
| 927 | </tr> | |
| 928 | </tbody> | |
| 929 | </table> | |
| 930 | </section> | |
| 931 | ||
| 932 | <section class="panel" aria-label="Environment HTTP API"> | |
| 933 | <h2>Environment Cloud HTTP API</h2> | |
| 934 | <table> | |
| 935 | <thead> | |
| 936 | <tr> | |
| 937 | <th>Endpoint</th> | |
| 938 | <th>Authentication</th> | |
| 939 | <th>Purpose</th> | |
| 940 | </tr> | |
| 941 | </thead> | |
| 942 | <tbody> | |
| 943 | <tr> | |
| 944 | <td><code>POST /api/connect/link-proof</code></td> | |
| 945 | <td>Local environment token with <code>relay:write</code></td> | |
| 946 | <td>Validate local origin and sign an environment link proof.</td> | |
| 947 | </tr> | |
| 948 | <tr> | |
| 949 | <td><code>POST /api/connect/relay-config</code></td> | |
| 950 | <td>Local environment token with <code>relay:write</code></td> | |
| 951 | <td>Persist linked relay configuration and start the relay client.</td> | |
| 952 | </tr> | |
| 953 | <tr> | |
| 954 | <td><code>GET /api/connect/link-state</code></td> | |
| 955 | <td>Local environment token with <code>relay:read</code></td> | |
| 956 | <td>Read the environment's current T3 Connect link state.</td> | |
| 957 | </tr> | |
| 958 | <tr> | |
| 959 | <td><code>POST /api/connect/preferences</code></td> | |
| 960 | <td>Local environment token with <code>relay:write</code></td> | |
| 961 | <td>Enable or disable agent-activity publication.</td> | |
| 962 | </tr> | |
| 963 | <tr> | |
| 964 | <td><code>POST /api/connect/unlink</code></td> | |
| 965 | <td>Local environment token with <code>relay:write</code></td> | |
| 966 | <td>Stop the relay client and clear local T3 Connect configuration.</td> | |
| 967 | </tr> | |
| 968 | <tr> | |
| 969 | <td><code>POST /api/t3-connect/health</code></td> | |
| 970 | <td>Relay-signed proof</td> | |
| 971 | <td>Return a signed environment health response.</td> | |
| 972 | </tr> | |
| 973 | <tr> | |
| 974 | <td><code>POST /api/t3-connect/mint-credential</code></td> | |
| 975 | <td>Relay-signed proof</td> | |
| 976 | <td>Mint a DPoP-bound one-time bootstrap credential for a remote client.</td> | |
| 977 | </tr> | |
| 978 | <tr> | |
| 979 | <td><code>POST /oauth/token</code></td> | |
| 980 | <td>Bootstrap credential plus DPoP proof</td> | |
| 981 | <td>Exchange a bootstrap credential for an environment access token.</td> | |
| 982 | </tr> | |
| 983 | <tr> | |
| 984 | <td><code>POST /api/auth/websocket-ticket</code></td> | |
| 985 | <td>Environment access token</td> | |
| 986 | <td>Issue a one-time ticket for the WSS upgrade.</td> | |
| 987 | </tr> | |
| 988 | </tbody> | |
| 989 | </table> | |
| 990 | </section> | |
| 991 | ||
| 992 | <section class="grid" aria-label="Runtime and infrastructure"> | |
| 993 | <article class="panel half"> | |
| 994 | <h2>Relay Client Installation</h2> | |
| 995 | <p> | |
| 996 | Desktop web UI availability checks and installs are local WebSocket RPC methods: | |
| 997 | <code>cloud.getRelayClientStatus</code> and streaming | |
| 998 | <code>cloud.installRelayClient</code>. The install stream emits the current stage: | |
| 999 | checking, waiting for lock, downloading, verifying, installing, validating, and | |
| 1000 | activating. | |
| 1001 | </p> | |
| 1002 | <p> | |
| 1003 | The web UI asks for confirmation before installation and displays a custom progress | |
| 1004 | dialog. The CLI invokes the same local relay-client service directly and reports the | |
| 1005 | same progress stages in the terminal. | |
| 1006 | </p> | |
| 1007 | </article> | |
| 1008 | ||
| 1009 | <article class="panel half"> | |
| 1010 | <h2>Relay Infrastructure</h2> | |
| 1011 | <ul> | |
| 1012 | <li>Cloudflare Worker for the hosted relay HTTP API.</li> | |
| 1013 | <li>PlanetScale Postgres database named <code>t3coderelay</code>.</li> | |
| 1014 | <li> | |
| 1015 | Production uses the shared database; non-production stages use database branches. | |
| 1016 | </li> | |
| 1017 | <li> | |
| 1018 | Cloudflare Hyperdrive with caching disabled and a constrained origin connection limit. | |
| 1019 | </li> | |
| 1020 | <li>Cloudflare tunnel and DNS bindings for managed endpoint reconciliation.</li> | |
| 1021 | <li>Cloudflare APNs delivery queue plus dead-letter queue.</li> | |
| 1022 | <li>A five-minute cron job that prunes expired DPoP replay rows.</li> | |
| 1023 | <li>Axiom OTLP trace dataset, scoped ingest token, and recent-spans view per stage.</li> | |
| 1024 | </ul> | |
| 1025 | </article> | |
| 1026 | </section> | |
| 1027 | ||
| 1028 | <section class="panel" aria-label="Deployment configuration"> | |
| 1029 | <h2>Deployment Configuration</h2> | |
| 1030 | <table> | |
| 1031 | <thead> | |
| 1032 | <tr> | |
| 1033 | <th>Boundary</th> | |
| 1034 | <th>Configuration</th> | |
| 1035 | </tr> | |
| 1036 | </thead> | |
| 1037 | <tbody> | |
| 1038 | <tr> | |
| 1039 | <td>Relay infrastructure</td> | |
| 1040 | <td> | |
| 1041 | <code>RELAY_API_ZONE_NAME</code>, <code>RELAY_TUNNEL_ZONE_NAME</code>, optional | |
| 1042 | <code>RELAY_DOMAIN</code>, <code>CLERK_PUBLISHABLE_KEY</code>, | |
| 1043 | <code>CLERK_SECRET_KEY</code>, <code>CLERK_JWT_AUDIENCE</code>, and APNs | |
| 1044 | credentials. | |
| 1045 | </td> | |
| 1046 | </tr> | |
| 1047 | <tr> | |
| 1048 | <td>Source-built desktop/server cloud features</td> | |
| 1049 | <td> | |
| 1050 | Optional <code>T3CODE_RELAY_URL</code>, <code>T3CODE_CLERK_PUBLISHABLE_KEY</code>, | |
| 1051 | and <code>T3CODE_CLERK_CLI_OAUTH_CLIENT_ID</code>. Release builds inject public | |
| 1052 | values at build time. | |
| 1053 | </td> | |
| 1054 | </tr> | |
| 1055 | <tr> | |
| 1056 | <td>Web and mobile cloud clients</td> | |
| 1057 | <td> | |
| 1058 | Public Clerk publishable key, Clerk JWT template name, and relay URL. Relay URLs | |
| 1059 | must normalize to an absolute HTTPS origin without credentials, query, fragment, or | |
| 1060 | non-root path. | |
| 1061 | </td> | |
| 1062 | </tr> | |
| 1063 | </tbody> | |
| 1064 | </table> | |
| 1065 | </section> | |
| 1066 | ||
| 1067 | <section class="panel" aria-label="Implementation checklist"> | |
| 1068 | <h2>Implementation Checklist</h2> | |
| 1069 | <ul> | |
| 1070 | <li> | |
| 1071 | Keep the relay cloud-mint private key hosted-only; install only its public key into | |
| 1072 | environments. | |
| 1073 | </li> | |
| 1074 | <li> | |
| 1075 | Keep environment signing private keys local and persist keypair creation atomically. | |
| 1076 | </li> | |
| 1077 | <li> | |
| 1078 | Require a local <code>relay:write</code> session before linking or changing relay | |
| 1079 | config. | |
| 1080 | </li> | |
| 1081 | <li>Validate relay URLs as secure absolute HTTPS origins before sending credentials.</li> | |
| 1082 | <li>Require loopback-only tunnel origins on both sides of the link-proof boundary.</li> | |
| 1083 | <li> | |
| 1084 | Resolve managed endpoint egress from ready allocation rows, not client-supplied URLs. | |
| 1085 | </li> | |
| 1086 | <li>Disable redirects on relay-to-environment requests.</li> | |
| 1087 | <li>Validate environment-signed response proofs before consuming tunneled responses.</li> | |
| 1088 | <li>Keep tunnel and DNS provisioning retry-safe and deprovision them on unlink.</li> | |
| 1089 | <li> | |
| 1090 | Revoke shared environment publication credentials only after the final active link | |
| 1091 | disappears. | |
| 1092 | </li> | |
| 1093 | <li>Redact and cap local agent failure details before publishing them externally.</li> | |
| 1094 | </ul> | |
| 1095 | </section> | |
| 1096 | ||
| 1097 | <section class="panel" aria-label="Standards references"> | |
| 1098 | <h2>Standards References</h2> | |
| 1099 | <ul class="source-list"> | |
| 1100 | <li> | |
| 1101 | <a href="https://www.rfc-editor.org/rfc/rfc8252">RFC 8252</a>: browser-based OAuth for | |
| 1102 | native apps, used by the CLI PKCE authorization flow. | |
| 1103 | </li> | |
| 1104 | <li> | |
| 1105 | <a href="https://www.rfc-editor.org/rfc/rfc8693">RFC 8693</a>: OAuth token exchange, | |
| 1106 | used for Clerk-token to relay-DPoP-token exchange. | |
| 1107 | </li> | |
| 1108 | <li> | |
| 1109 | <a href="https://www.rfc-editor.org/rfc/rfc9449">RFC 9449</a>: DPoP proof of possession, | |
| 1110 | used for relay and environment access tokens. | |
| 1111 | </li> | |
| 1112 | </ul> | |
| 1113 | </section> | |
| 1114 | </main> | |
| 1115 | <script type="module"> | |
| 1116 | import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"; | |
| 1117 | ||
| 1118 | mermaid.initialize({ | |
| 1119 | startOnLoad: false, | |
| 1120 | securityLevel: "strict", | |
| 1121 | theme: "base", | |
| 1122 | themeVariables: { | |
| 1123 | fontFamily: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | |
| 1124 | primaryColor: "#eef4ff", | |
| 1125 | primaryBorderColor: "#1457d9", | |
| 1126 | primaryTextColor: "#171717", | |
| 1127 | lineColor: "#62646b", | |
| 1128 | signalColor: "#171717", | |
| 1129 | signalTextColor: "#171717", | |
| 1130 | noteBkgColor: "#fff6e6", | |
| 1131 | noteTextColor: "#171717", | |
| 1132 | actorBkg: "#fbfbf8", | |
| 1133 | actorBorder: "#62646b", | |
| 1134 | actorTextColor: "#171717", | |
| 1135 | }, | |
| 1136 | flowchart: { | |
| 1137 | htmlLabels: true, | |
| 1138 | curve: "basis", | |
| 1139 | padding: 18, | |
| 1140 | }, | |
| 1141 | sequence: { | |
| 1142 | showSequenceNumbers: true, | |
| 1143 | mirrorActors: false, | |
| 1144 | rightAngles: false, | |
| 1145 | }, | |
| 1146 | }); | |
| 1147 | ||
| 1148 | await mermaid.run({ querySelector: ".diagram.mermaid" }); | |
| 1149 | ||
| 1150 | for (const svg of document.querySelectorAll(".diagram.mermaid svg")) { | |
| 1151 | const viewBox = svg.getAttribute("viewBox"); | |
| 1152 | const width = viewBox?.split(/\s+/).map(Number)[2]; | |
| 1153 | if (Number.isFinite(width) && width > 0) { | |
| 1154 | svg.style.setProperty("width", `${Math.ceil(width)}px`, "important"); | |
| 1155 | svg.style.setProperty("max-width", "none", "important"); | |
| 1156 | } | |
| 1157 | } | |
| 1158 | </script> | |
| 1159 | </body> | |
| 1160 | </html> |
docs/cloud/t3-connect-clerk.md deleted-243| ... | ... | @@ -1,243 +0,0 @@ |
| 1 | # T3 Connect Clerk Setup | |
| 2 | ||
| 3 | T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay accepts | |
| 4 | Clerk JWTs only when they are generated from the `t3-relay` template with the shared | |
| 5 | `t3-code-relay` audience. | |
| 6 | ||
| 7 | ## Application Keys | |
| 8 | ||
| 9 | T3 Connect is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env` | |
| 10 | or `.env.local` file: | |
| 11 | ||
| 12 | ```dotenv | |
| 13 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | |
| 14 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | |
| 15 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | |
| 16 | T3CODE_RELAY_URL=https://relay.example.com | |
| 17 | ``` | |
| 18 | ||
| 19 | The shared client loader projects these canonical values into framework-specific `VITE_*` and | |
| 20 | `EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new | |
| 21 | client configuration should use the canonical names. | |
| 22 | ||
| 23 | Configuration precedence is: | |
| 24 | ||
| 25 | 1. Process or CI environment variables. | |
| 26 | 2. Repository-root `.env.local`. | |
| 27 | 3. Repository-root `.env`. | |
| 28 | ||
| 29 | The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public | |
| 30 | identifiers, not secrets. | |
| 31 | Web, desktop, mobile, and bundled server builds statically inject the values they consume during | |
| 32 | their build step. A built artifact does not need an environment file at runtime. CI release builds | |
| 33 | should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, | |
| 34 | `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and | |
| 35 | production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS | |
| 36 | environment. | |
| 37 | ||
| 38 | When any client-facing public value is absent, cloud UI is omitted. When the CLI public values are | |
| 39 | absent, the `t3 connect` CLI command group is omitted. The bundled server still accepts runtime | |
| 40 | overrides for self-hosted or operator-managed | |
| 41 | deployments. | |
| 42 | ||
| 43 | For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay | |
| 44 | deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, | |
| 45 | `CLERK_PUBLISHABLE_KEY`, and `CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in | |
| 46 | deployment defaults. | |
| 47 | `vp run --filter t3code-relay deploy` invokes Alchemy from the relay directory, so Alchemy loads | |
| 48 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` | |
| 49 | with the deployed HTTPS relay URL. The relay still requires | |
| 50 | `CLERK_SECRET_KEY` as an Alchemy secret. Never put `CLERK_SECRET_KEY` in a client application | |
| 51 | environment or commit it to the repository. | |
| 52 | ||
| 53 | The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference | |
| 54 | that database and provision isolated PlanetScale branches, so deploy `prod` before creating a | |
| 55 | personal developer stage. | |
| 56 | ||
| 57 | ## Headless CLI OAuth Application | |
| 58 | ||
| 59 | The `t3 connect` commands authorize a headless environment with a separate Clerk OAuth application. | |
| 60 | This uses an OAuth public client with PKCE, so the CLI stores no client secret. | |
| 61 | ||
| 62 | In **Clerk Dashboard > OAuth applications**: | |
| 63 | ||
| 64 | 1. Create an OAuth application for the T3 CLI. | |
| 65 | 2. Enable the **Public** option so authorization-code exchange uses PKCE. | |
| 66 | 3. Add `http://127.0.0.1:34338/callback` as an allowed redirect URI. | |
| 67 | 4. Enable the `openid`, `profile`, and `email` scopes. | |
| 68 | 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build | |
| 69 | environment to the generated public client ID. | |
| 70 | ||
| 71 | The CLI derives Clerk's frontend API URL from the publishable key and calls Clerk's | |
| 72 | `/oauth/authorize` and `/oauth/token` endpoints directly. The relay is not involved in the OAuth | |
| 73 | handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link. | |
| 74 | ||
| 75 | The CLI supports these headless operations: | |
| 76 | ||
| 77 | ```sh | |
| 78 | t3 connect login | |
| 79 | t3 connect link | |
| 80 | t3 connect status | |
| 81 | t3 connect unlink | |
| 82 | t3 connect logout | |
| 83 | t3 serve | |
| 84 | ``` | |
| 85 | ||
| 86 | `t3 connect login` opens the Clerk authorization flow and stores the CLI credential without enabling | |
| 87 | cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed, | |
| 88 | authorizes when needed, and records durable intent to expose the environment. It works without a | |
| 89 | running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the | |
| 90 | managed tunnel. `t3 connect unlink` records disabled intent immediately, stops a reachable running | |
| 91 | connector, and attempts to revoke the relay-side environment record. It retains the stored CLI | |
| 92 | authorization so `t3 connect link` can re-enable exposure without another browser flow. `t3 connect | |
| 93 | logout` performs the same cleanup and removes the stored CLI authorization. | |
| 94 | ||
| 95 | The background service has an independent lifecycle. Connect setup may offer to install it, but | |
| 96 | logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`. | |
| 97 | ||
| 98 | The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH, | |
| 99 | forward that port before running `t3 connect login` or `t3 connect link`: | |
| 100 | ||
| 101 | ```sh | |
| 102 | ssh -L 34338:127.0.0.1:34338 <host> | |
| 103 | ``` | |
| 104 | ||
| 105 | A relay-hosted callback broker can remove this port-forward requirement later without changing the | |
| 106 | stored PKCE token model. | |
| 107 | ||
| 108 | ## JWT Template | |
| 109 | ||
| 110 | In **Clerk Dashboard > JWT templates**, create a template with: | |
| 111 | ||
| 112 | | Setting | Value | | |
| 113 | | ------- | ---------------------------- | | |
| 114 | | Name | `t3-relay` | | |
| 115 | | Claims | `{ "aud": "t3-code-relay" }` | | |
| 116 | ||
| 117 | Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set | |
| 118 | `CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and | |
| 119 | `CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value | |
| 120 | is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still | |
| 121 | selects the concrete relay deployment, but changing that URL does not require a JWT template change. | |
| 122 | ||
| 123 | ## Desktop OAuth Redirect Allowlist | |
| 124 | ||
| 125 | The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme. | |
| 126 | In **Clerk Dashboard > Native applications**, enable the Native API and add these entries under the | |
| 127 | mobile SSO redirect allowlist: | |
| 128 | ||
| 129 | ```text | |
| 130 | t3code-dev://app/ | |
| 131 | t3code://app/ | |
| 132 | ``` | |
| 133 | ||
| 134 | Local desktop development uses `t3code-dev://app`, while packaged builds use `t3code://app`. Add the | |
| 135 | matching origin to each Clerk instance's Backend API `allowed_origins` array as well. The development | |
| 136 | Clerk instance should only need `t3code-dev://app`; the production Clerk instance should only need | |
| 137 | `t3code://app`. `@clerk/electron` owns the native request adapter, encrypted Clerk token persistence, | |
| 138 | external-browser OAuth transport, and callback delivery for initial sign-in and linked-account flows. | |
| 139 | ||
| 140 | There is currently no Dashboard UI for `allowed_origins`. Preserve any existing entries and update | |
| 141 | the instance through the Backend API: | |
| 142 | ||
| 143 | ```sh | |
| 144 | curl -X PATCH https://api.clerk.com/v1/instance \ | |
| 145 | -H "Content-Type: application/json" \ | |
| 146 | -H "Authorization: Bearer $CLERK_SECRET_KEY" \ | |
| 147 | -d '{"allowed_origins":["t3code://app"]}' | |
| 148 | ``` | |
| 149 | ||
| 150 | Never put `CLERK_SECRET_KEY` in the desktop app, a client-facing environment file, or a build | |
| 151 | artifact. | |
| 152 | ||
| 153 | ## Desktop Passkeys | |
| 154 | ||
| 155 | The production macOS bundle ID is `com.t3tools.t3code`. To enable native passkeys: | |
| 156 | ||
| 157 | 1. Create an explicit macOS App ID for `com.t3tools.t3code` in the Apple Developer portal and enable | |
| 158 | **Associated Domains**. | |
| 159 | 2. Create a compatible macOS provisioning profile for that App ID and the certificate used to sign | |
| 160 | the distributed app. | |
| 161 | 3. In Clerk's Native API settings, add an iOS app with the same Apple Team ID and bundle ID. This is | |
| 162 | also the configuration point for Electron/macOS passkeys. | |
| 163 | 4. Confirm Clerk serves `https://<frontend-api>/.well-known/apple-app-site-association` and that | |
| 164 | `webcredentials.apps` contains `<TEAM_ID>.com.t3tools.t3code`. | |
| 165 | 5. Set the local or CI signing configuration described below. | |
| 166 | ||
| 167 | For a local signed build, add these values to `.env.local` or export them before invoking the | |
| 168 | desktop artifact command: | |
| 169 | ||
| 170 | ```dotenv | |
| 171 | T3CODE_APPLE_TEAM_ID=ABC1234567 | |
| 172 | T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile | |
| 173 | # Optional: comma-separated override when Clerk's RP ID differs from the Frontend API hostname. | |
| 174 | T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com | |
| 175 | ``` | |
| 176 | ||
| 177 | When `T3CODE_CLERK_PASSKEY_RP_DOMAINS` is absent, the build derives the RP domain from | |
| 178 | `T3CODE_CLERK_PUBLISHABLE_KEY`. Signed macOS builds fail early if the Team ID, provisioning profile, | |
| 179 | or RP-domain configuration is missing. The generated main-app entitlements include every configured | |
| 180 | `webcredentials:<domain>` entry; helper apps keep Electron's minimal default entitlements. | |
| 181 | ||
| 182 | The normal `dev:desktop` launcher is unsigned and cannot complete macOS passkey ceremonies. For | |
| 183 | renderer HMR, build and install a signed app first, run the renderer dev server, then launch the | |
| 184 | installed app executable with `VITE_DEV_SERVER_URL` and `T3CODE_PORT` set. Rebuild the signed app | |
| 185 | after native dependency, main-process, preload, entitlement, provisioning, or signing changes; | |
| 186 | renderer-only changes can reuse the installed app. | |
| 187 | ||
| 188 | For the default development ports, run `pnpm dev:web` in one terminal and launch the installed | |
| 189 | binary from another: | |
| 190 | ||
| 191 | ```sh | |
| 192 | VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \ | |
| 193 | T3CODE_PORT=13773 \ | |
| 194 | "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)" | |
| 195 | ``` | |
| 196 | ||
| 197 | After changing Associated Domains, bump the build version before rebuilding; macOS may otherwise | |
| 198 | reuse stale Shared Web Credentials metadata for the same app/version pair. | |
| 199 | ||
| 200 | Verify the installed bundle before testing: | |
| 201 | ||
| 202 | ```sh | |
| 203 | codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app" | |
| 204 | codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app" | |
| 205 | ``` | |
| 206 | ||
| 207 | The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth | |
| 208 | flow uses a custom redirect URI, add that exact URI to the same allowlist. | |
| 209 | ||
| 210 | ## Enable Waitlist Access | |
| 211 | ||
| 212 | For a private beta where people should request access, use **Clerk Dashboard > Waitlist**: | |
| 213 | ||
| 214 | 1. Toggle on **Enable waitlist** and save. | |
| 215 | 2. Review requests on the same page and select **Invite** or **Deny**. | |
| 216 | ||
| 217 | Approved signed-in users manage T3 Connect under **Connections**. The web and desktop sidebars do | |
| 218 | not expose a dedicated account or waitlist control. Signed-out users reach Clerk's waitlist and | |
| 219 | sign-in flow contextually from the T3 Connect controls on the Connections page. | |
| 220 | ||
| 221 | On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the | |
| 222 | Settings form sheet. It submits enrollment through Clerk's `useWaitlist()` flow because the prebuilt | |
| 223 | `<Waitlist />` component is web-only in the Expo SDK. Approved users can use **Sign in** from that | |
| 224 | screen. | |
| 225 | ||
| 226 | ## Alternative: Known-User Allowlist | |
| 227 | ||
| 228 | For a closed beta where all permitted users are known in advance, use an allowlist instead of a | |
| 229 | request-and-approval waitlist: | |
| 230 | ||
| 231 | To restrict the beta to permitted email addresses or domains: | |
| 232 | ||
| 233 | 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email | |
| 234 | domain. | |
| 235 | 2. Enable the allowlist and save. | |
| 236 | 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or | |
| 237 | manually created without a waitlist request flow. | |
| 238 | ||
| 239 | Do not enable an empty allowlist: it blocks all new sign-ups. | |
| 240 | ||
| 241 | Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud | |
| 242 | access. To remove an already-created user's access, ban that user in Clerk so their active | |
| 243 | sessions are ended and future sign-ins are rejected. |
docs/getting-started/codex-prerequisites.md deleted-5| ... | ... | @@ -1,5 +0,0 @@ |
| 1 | # Codex prerequisites | |
| 2 | ||
| 3 | - Install Codex CLI so `codex` is on your PATH. | |
| 4 | - Authenticate Codex before running T3 Code (for example via API key or ChatGPT auth supported by Codex). | |
| 5 | - T3 Code starts the server via `codex app-server` per session. |
docs/getting-started/quick-start.md deleted-22| ... | ... | @@ -1,22 +0,0 @@ |
| 1 | # Quick start | |
| 2 | ||
| 3 | ```bash | |
| 4 | # Development (with hot reload) | |
| 5 | bun run dev | |
| 6 | ||
| 7 | # Desktop development | |
| 8 | bun run dev:desktop | |
| 9 | ||
| 10 | # Desktop development on an isolated port set | |
| 11 | T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop | |
| 12 | ||
| 13 | # Production | |
| 14 | bun run build | |
| 15 | bun run start | |
| 16 | ||
| 17 | # Build a shareable macOS .dmg (arm64 by default) | |
| 18 | bun run dist:desktop:dmg | |
| 19 | ||
| 20 | # Or from any project directory after publishing: | |
| 21 | npx t3 | |
| 22 | ``` |
docs/integrations/source-control-providers.md deleted-127| ... | ... | @@ -1,127 +0,0 @@ |
| 1 | # Source Control Integrations | |
| 2 | ||
| 3 | T3 Code connects directly to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving your editor. Work stays in flow—no more jumping between browser tabs and terminal windows. | |
| 4 | ||
| 5 | ## Supported Providers | |
| 6 | ||
| 7 | T3 Code works with the platforms your team already uses: | |
| 8 | ||
| 9 | - **GitHub** – Pull requests, repository creation, and clone integration | |
| 10 | - **GitLab** – Merge requests, repository publishing, and hosted clones | |
| 11 | - **Bitbucket** – Pull request workflows (via API token authentication) | |
| 12 | - **Azure DevOps** – Pull request support for Microsoft-hosted repositories | |
| 13 | ||
| 14 | ## What You Can Do | |
| 15 | ||
| 16 | ### Start Projects from Anywhere | |
| 17 | ||
| 18 | **Clone repositories directly** | |
| 19 | ||
| 20 | - Open the Command Palette (`Cmd/Ctrl + K`) → **Add Project** | |
| 21 | - Choose **GitHub repository**, **GitLab repository**, **Bitbucket repository**, **Azure DevOps repository**, or paste any **Git URL** | |
| 22 | - Enter the repository path (`owner/repo`, `group/project`, `workspace/repository`, or `project/repository`) or a full Git URL, pick a destination, and start coding | |
| 23 | ||
| 24 | **Publish local projects to the cloud** | |
| 25 | ||
| 26 | - Have a local Git repository without a remote? | |
| 27 | - Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push—all in one flow | |
| 28 | - Perfect for turning a weekend prototype into a real project | |
| 29 | ||
| 30 | ### Manage Code Reviews Without Context Switching | |
| 31 | ||
| 32 | **Create pull requests while you work** | |
| 33 | ||
| 34 | - Push a branch and create a pull request from the Git panel | |
| 35 | - T3 Code can suggest titles and descriptions based on your commits | |
| 36 | - Supports GitHub Pull Requests, GitLab Merge Requests, and Bitbucket Pull Requests | |
| 37 | ||
| 38 | **Stay on top of open reviews** | |
| 39 | ||
| 40 | - See if your current branch already has an open PR/MR | |
| 41 | - Open the review directly in your browser with one click | |
| 42 | - Check out a teammate's branch to review code locally | |
| 43 | ||
| 44 | ### Know Your Setup at a Glance | |
| 45 | ||
| 46 | The **Source Control settings** page shows you exactly what's connected: | |
| 47 | ||
| 48 | - ✅ Which providers are authenticated and ready | |
| 49 | - ⚠️ What's missing and how to fix it | |
| 50 | - 👤 Which account is signed in (when available) | |
| 51 | ||
| 52 | Run a quick **Rescan** after setting up a new machine or changing credentials. | |
| 53 | ||
| 54 | ## Getting Started | |
| 55 | ||
| 56 | ### For GitHub (Recommended for most users) | |
| 57 | ||
| 58 | 1. Install the GitHub CLI on the machine running T3 Code: | |
| 59 | ```bash | |
| 60 | brew install gh | |
| 61 | ``` | |
| 62 | 2. Sign in: | |
| 63 | ```bash | |
| 64 | gh auth login | |
| 65 | ``` | |
| 66 | 3. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated | |
| 67 | ||
| 68 | That's it—you can now clone, publish, and create pull requests. | |
| 69 | ||
| 70 | ### For GitLab | |
| 71 | ||
| 72 | 1. Install the GitLab CLI: | |
| 73 | ```bash | |
| 74 | brew install glab | |
| 75 | ``` | |
| 76 | 2. Authenticate: | |
| 77 | ```bash | |
| 78 | glab auth login | |
| 79 | ``` | |
| 80 | 3. Check **Settings → Source Control** to confirm the connection | |
| 81 | ||
| 82 | ### For Bitbucket | |
| 83 | ||
| 84 | Bitbucket uses API tokens instead of a CLI tool: | |
| 85 | ||
| 86 | 1. Create an API token in your Atlassian account with read/write access to pull requests and repositories | |
| 87 | 2. Add these environment variables to the environment running T3 Code: | |
| 88 | ```bash | |
| 89 | export T3CODE_BITBUCKET_EMAIL="you@example.com" | |
| 90 | export T3CODE_BITBUCKET_API_TOKEN="your-token" | |
| 91 | ``` | |
| 92 | 3. Restart T3 Code and verify the connection in **Source Control settings** | |
| 93 | ||
| 94 | ### For Azure DevOps | |
| 95 | ||
| 96 | 1. Install Azure CLI: | |
| 97 | ```bash | |
| 98 | brew install azure-cli | |
| 99 | ``` | |
| 100 | 2. Add the DevOps extension: | |
| 101 | ```bash | |
| 102 | az extension add --name azure-devops | |
| 103 | ``` | |
| 104 | 3. Sign in: | |
| 105 | ```bash | |
| 106 | az login | |
| 107 | ``` | |
| 108 | ||
| 109 | --- | |
| 110 | ||
| 111 | ## Requirements & Troubleshooting | |
| 112 | ||
| 113 | **Git is required** – T3 Code uses Git for all local operations. Ensure `git` is installed on your server. | |
| 114 | ||
| 115 | **Server-side setup** – Authentication happens on the machine running T3 Code (the server), not your local browser. If you're using a hosted or team instance, your administrator may have already configured providers. | |
| 116 | ||
| 117 | **Common issues:** | |
| 118 | ||
| 119 | - **Provider shows "Not authenticated"** – Run the login command for that provider (e.g., `gh auth login`) in a terminal on the server, then rescan in Settings | |
| 120 | - **Bitbucket not connecting** – Double-check your environment variables are set in the correct shell profile and the server was restarted | |
| 121 | - **Can't push to a remote** – Verify your Git remote URL matches the provider you've authenticated with (SSH vs HTTPS remotes may need different credentials) | |
| 122 | ||
| 123 | **Need more help?** Check your provider's CLI documentation: | |
| 124 | ||
| 125 | - [GitHub CLI](https://cli.github.com/) | |
| 126 | - [GitLab CLI](https://gitlab.com/gitlab-org/cli) | |
| 127 | - [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) |
docs/internals/ci.md created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | # CI quality gates | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs four jobs on pull requests and | |
| 6 | pushes to `main`: | |
| 7 | ||
| 8 | - **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options), | |
| 9 | then `vpr typecheck` for the workspace type check. The same job | |
| 10 | builds the desktop pipeline (`vp run build:desktop`) and verifies the preload bundle exists and | |
| 11 | still exports its expected symbols. | |
| 12 | - **Test**: `vp run test` across the workspace. | |
| 13 | - **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping | |
| 14 | `scripts/mobile-native-static-check.ts`. | |
| 15 | - **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so | |
| 16 | release breakage surfaces on PRs rather than at tag time. | |
| 17 | ||
| 18 | `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) | |
| 19 | desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. It auto-enables | |
| 20 | signing only when platform credentials are present. macOS passkey builds additionally require | |
| 21 | `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. | |
| 22 | Without the core signing credentials, it still releases unsigned artifacts. | |
| 23 | ||
| 24 | See [Release Checklist](../operations/release.md) for the full release/signing setup checklist. |
docs/internals/connection-runtime.md created+183| ... | ... | @@ -0,0 +1,183 @@ |
| 1 | # Connection Runtime | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | The connection runtime is shared by web and mobile. It owns connectivity, | |
| 6 | authentication, retries, transport lifetime, cached environment data, and | |
| 7 | environment-scoped operations. | |
| 8 | ||
| 9 | Web and mobile mount this runtime once at the application root and compose it | |
| 10 | identically: `apps/web/src/connection/runtime.ts` and | |
| 11 | `apps/mobile/src/connection/runtime.ts` differ only in the platform layer they | |
| 12 | supply. There is no legacy connection owner or supported mixed mode. | |
| 13 | ||
| 14 | ## Composition | |
| 15 | ||
| 16 | [`connection/layer.ts`][layer] assembles the runtime: | |
| 17 | ||
| 18 | - `ConnectionResolver` ([resolver.ts][resolver]) resolves a catalog entry into a | |
| 19 | prepared, authenticated endpoint for primary, bearer, relay, or SSH targets. | |
| 20 | - `ConnectionDriver` ([driver.ts][driver]) prepares through the resolver, opens | |
| 21 | one RPC session, and reports `preparing`, `opening`, and `synchronizing`. | |
| 22 | - `RpcSessionFactory` ([rpc/session.ts][session]) performs one transport | |
| 23 | attempt. It does not retry. `RpcSession` is the interface it returns, | |
| 24 | exposing `client`, `initialConfig`, `ready`, `probe`, and `closed`. | |
| 25 | - `EnvironmentRegistry` ([registry.ts][registry]) owns the catalog and the | |
| 26 | per-environment scopes. | |
| 27 | - `ConnectionOnboarding` and `RelayEnvironmentDiscovery` sit alongside the | |
| 28 | registry. Startup calls `EnvironmentRegistry.start` and streams platform | |
| 29 | registrations into `reconcilePlatform`. | |
| 30 | ||
| 31 | The registry creates one environment-scoped supervisor per environment. | |
| 32 | `acquireSupervisor` serializes access per environment, reuses an existing | |
| 33 | supervisor when the catalog entry is unchanged, and closes and recreates the | |
| 34 | scope when it changed. `createServiceScope` builds an `EnvironmentSupervisor` | |
| 35 | bound to a closeable scope and connects it; `run` and `runStream` execute caller | |
| 36 | effects with that supervisor provided. | |
| 37 | ||
| 38 | `EnvironmentSupervisor` owns desired state, retry scheduling, and the active | |
| 39 | session scope. React components do not create connections, transports, retry | |
| 40 | loops, or RPC clients. | |
| 41 | ||
| 42 | ## Connection State | |
| 43 | ||
| 44 | The supervisor is the only retry owner. | |
| 45 | ||
| 46 | 1. A persisted or platform registration marks an environment as desired. | |
| 47 | 2. If the device is offline, the supervisor releases the active session and | |
| 48 | waits for a signal without consuming retry attempts or running a timer. | |
| 49 | 3. When online, it asks the driver for one prepared connection and one RPC | |
| 50 | session. | |
| 51 | 4. Transient failures retry forever with exponential backoff capped at 16 | |
| 52 | seconds (`RETRY_DELAYS_MS`). A connection stable for 30 seconds resets | |
| 53 | accumulated backoff. | |
| 54 | 5. Authentication or configuration failures remain blocked until an external | |
| 55 | wakeup changes the relevant input. | |
| 56 | 6. An involuntary session close keeps the registration and cache, then retries. | |
| 57 | 7. Explicit removal closes the session and deletes the registration, | |
| 58 | credentials, shell cache, and thread cache. | |
| 59 | ||
| 60 | ### Wakeups | |
| 61 | ||
| 62 | Wakeup handling differs by phase, in [supervisor.ts][supervisor]: | |
| 63 | ||
| 64 | - During establishment, `waitForEstablishmentInterrupt` consumes and **ignores** | |
| 65 | plain application activation. Restarting an in-flight attempt because the app | |
| 66 | came to the foreground would only delay it. The exception is | |
| 67 | `application-active-reconnect`, which mobile emits after a meaningful | |
| 68 | background suspension; it interrupts establishment and resets the retry | |
| 69 | ladder, because the OS may have silently killed the socket underneath the | |
| 70 | attempt. | |
| 71 | - Credential changes interrupt establishment only for relay targets, where a new | |
| 72 | credential changes what is being established. | |
| 73 | - Explicit disconnect, explicit retry, and going offline interrupt establishment | |
| 74 | in every case. | |
| 75 | - While waiting out backoff, application activation resets the retry ladder so a | |
| 76 | foregrounded app reconnects immediately instead of serving the remaining | |
| 77 | delay. | |
| 78 | - Once connected, `monitorConnectedLease` handles plain activation by probing | |
| 79 | the existing session (`lease.session.probe`, with a shorter timeout for | |
| 80 | mobile's `application-active-probe`) rather than reconnecting; a healthy | |
| 81 | session survives foregrounding. `application-active-reconnect` skips the probe | |
| 82 | and replaces the lease outright. | |
| 83 | ||
| 84 | The UI derives `available`, `offline`, `connecting`, `reconnecting`, | |
| 85 | `connected`, and `error` from supervisor state plus explicit data-sync state. | |
| 86 | It does not infer connection health from cached data or the existence of a | |
| 87 | transport object. An environment becomes `connected` after the socket opens and | |
| 88 | the initial config RPC succeeds, proving that the server is responsive. Shell | |
| 89 | and thread synchronization are independent data states. A healthy RPC transport | |
| 90 | with a failed shell subscription is shown as connected with a synchronization | |
| 91 | error, not as a reconnect that is not actually scheduled. | |
| 92 | ||
| 93 | ## Data Boundary | |
| 94 | ||
| 95 | Finite requests, durable subscriptions, and commands are separate APIs: | |
| 96 | ||
| 97 | - Query atoms revalidate when the RPC generation changes. | |
| 98 | - Subscription atoms switch to replacement sessions. | |
| 99 | - Subscription failure handling in [rpc/client.ts][client] distinguishes two | |
| 100 | cases. A transport failure (`isTransportFailure`: every failure is an RPC | |
| 101 | client error) ends the inner subscription without resubscribing, so the outer | |
| 102 | stream waits for the supervisor to supply a replacement session. A handled | |
| 103 | domain failure runs `onExpectedFailure` and, when | |
| 104 | `retryExpectedFailureAfter` is set, sleeps and resubscribes on the **same** | |
| 105 | session. A healthy transport is never torn down for a domain failure. | |
| 106 | - Mutations resolve the current environment runtime at execution time. | |
| 107 | - Shell and thread snapshots are available while offline. | |
| 108 | - Sync status is explicit and independent per domain. Shell status is `empty`, | |
| 109 | `cached`, `synchronizing`, or `live`, with a separate `error` field; there is | |
| 110 | no `failed` status. Thread status adds `deleted`. | |
| 111 | - Cached shell and thread projections are never allowed to overwrite newer live | |
| 112 | data during a fast reconnect. | |
| 113 | - Domain atom factories route effects through the environment registry and | |
| 114 | resolve the current scoped service at execution time. Project and thread | |
| 115 | commands are Atom factories under `src/state` | |
| 116 | (`createProjectEnvironmentAtoms`, `createThreadEnvironmentAtoms`), as are the | |
| 117 | shell and thread state factories (`createEnvironmentShellAtoms`, | |
| 118 | `createEnvironmentThreadStateAtoms`). | |
| 119 | - Web and mobile own their Atom runtimes, React hooks, and feature composition. | |
| 120 | ||
| 121 | The Promise bridge exists only at the React/Atom boundary. Runtime and business | |
| 122 | logic remain Effect-native. | |
| 123 | ||
| 124 | ## Platform Layers | |
| 125 | ||
| 126 | Web and mobile provide: | |
| 127 | ||
| 128 | - network status and network-change streams; | |
| 129 | - application lifecycle wakeups; | |
| 130 | - cloud session credentials; | |
| 131 | - device identity; | |
| 132 | - platform registrations; | |
| 133 | - persistent catalog, credential, shell, and thread stores; | |
| 134 | - HTTP, crypto, and telemetry layers. | |
| 135 | ||
| 136 | Platform layers adapt operating-system capabilities. They do not implement | |
| 137 | connection policy. `EnvironmentOwnedDataCleanup` is part of this contract: on | |
| 138 | removal the registry clears its cache and calls the platform implementation, so | |
| 139 | web clears composer drafts and mobile clears drafts plus the thread outbox. | |
| 140 | ||
| 141 | ## Source Boundaries | |
| 142 | ||
| 143 | Applications must import explicit package subpaths; the package intentionally | |
| 144 | has no root export. The subpaths are documented in | |
| 145 | [packages/client-runtime/README.md](../../packages/client-runtime/README.md), | |
| 146 | with the `exports` map in that package's `package.json` as the authoritative | |
| 147 | list. Files that are not exported are implementation details. | |
| 148 | ||
| 149 | ## Application Boundary | |
| 150 | ||
| 151 | The application root mounts the shared connection layer, creates its own Atom | |
| 152 | runtime, and selects the domain atom factories required by that platform. Web | |
| 153 | and mobile may expose different hooks and features without changing connection | |
| 154 | ownership. | |
| 155 | ||
| 156 | Application code must not construct RPC clients, retry loops, or raw | |
| 157 | orchestration commands. Persistence paths belong to the platform registration | |
| 158 | and cache stores, with explicit migration or invalidation policy. | |
| 159 | ||
| 160 | ## Verification | |
| 161 | ||
| 162 | Core state-machine tests use `@effect/vitest` and deterministic service layers. | |
| 163 | Required coverage includes: | |
| 164 | ||
| 165 | - offline startup and online wakeup; | |
| 166 | - forever retry with the 16-second cap; | |
| 167 | - explicit retry interrupting backoff; | |
| 168 | - authentication wakeups; | |
| 169 | - involuntary close and reconnect; | |
| 170 | - explicit removal clearing all owned state; | |
| 171 | - relay token reuse and refresh; | |
| 172 | - progressive relay discovery; | |
| 173 | - shell and thread cache hydration; | |
| 174 | - durable subscriptions switching sessions; | |
| 175 | - command metadata and idempotent queued-command metadata. | |
| 176 | ||
| 177 | [layer]: ../../packages/client-runtime/src/connection/layer.ts | |
| 178 | [resolver]: ../../packages/client-runtime/src/connection/resolver.ts | |
| 179 | [driver]: ../../packages/client-runtime/src/connection/driver.ts | |
| 180 | [registry]: ../../packages/client-runtime/src/connection/registry.ts | |
| 181 | [supervisor]: ../../packages/client-runtime/src/connection/supervisor.ts | |
| 182 | [session]: ../../packages/client-runtime/src/rpc/session.ts | |
| 183 | [client]: ../../packages/client-runtime/src/rpc/client.ts |
docs/internals/environment-auth.md created+141| ... | ... | @@ -0,0 +1,141 @@ |
| 1 | # Environment Authentication Profile | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | The environment server and the relay use separate credentials, issuers, and trust | |
| 6 | boundaries. They intentionally use a similar OAuth-shaped model so that permission | |
| 7 | checks and token exchange behavior can be audited against established concepts. | |
| 8 | ||
| 9 | ## Authorization Model | |
| 10 | ||
| 11 | Environment authorization is capability-based. A session carries zero or more | |
| 12 | OAuth-style scope strings: | |
| 13 | ||
| 14 | | Scope | Permission | | |
| 15 | | ----------------------- | ------------------------------------------------------------------------ | | |
| 16 | | `orchestration:read` | Read snapshots, status, events, configuration, and filesystem/VCS state. | | |
| 17 | | `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state. | | |
| 18 | | `terminal:operate` | Create, attach, input, resize, clear, restart, and terminate terminals. | | |
| 19 | | `review:write` | Read review diff previews used to compose review feedback. | | |
| 20 | | `access:read` | Inspect pairing links and client sessions. | | |
| 21 | | `access:write` | Create or revoke pairing links and client sessions. | | |
| 22 | | `relay:read` | Inspect managed relay connectivity. | | |
| 23 | | `relay:write` | Link, configure, or unlink managed relay connectivity. | | |
| 24 | ||
| 25 | Ordinary pairing links grant the four client-operation scopes and read access to | |
| 26 | managed relay connectivity: | |
| 27 | `orchestration:read orchestration:operate terminal:operate review:write relay:read`. | |
| 28 | The desktop bootstrap credential and command-line administrative bootstrap | |
| 29 | credentials additionally grant `access:read access:write relay:write`. | |
| 30 | ||
| 31 | ## Authentication Flows | |
| 32 | ||
| 33 | ### Browser Session | |
| 34 | ||
| 35 | `POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a | |
| 36 | browser session cookie. The cookie is an HTTP transport adapter for the same | |
| 37 | scoped session model; the response never exposes the session secret to browser | |
| 38 | JavaScript. | |
| 39 | ||
| 40 | ### Bearer Access Token | |
| 41 | ||
| 42 | Non-browser clients use `POST /oauth/token` with an | |
| 43 | `application/x-www-form-urlencoded` body: | |
| 44 | ||
| 45 | ```text | |
| 46 | grant_type=urn:ietf:params:oauth:grant-type:token-exchange | |
| 47 | subject_token=<bootstrap credential> | |
| 48 | subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap | |
| 49 | requested_token_type=urn:ietf:params:oauth:token-type:access_token | |
| 50 | scope=orchestration:read orchestration:operate terminal:operate review:write relay:read | |
| 51 | ``` | |
| 52 | ||
| 53 | Clients may additionally submit `client_label`, `client_device_type`, and | |
| 54 | `client_os` extension parameters so the authorized-clients UI can identify the | |
| 55 | device that established the session. These are presentation hints only; the | |
| 56 | environment derives transport metadata such as IP address and user agent from | |
| 57 | the request and does not use these fields for authorization. | |
| 58 | ||
| 59 | The response has the token-exchange shape: | |
| 60 | ||
| 61 | ```json | |
| 62 | { | |
| 63 | "access_token": "<opaque session token>", | |
| 64 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", | |
| 65 | "token_type": "Bearer", | |
| 66 | "expires_in": 2592000, | |
| 67 | "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" | |
| 68 | } | |
| 69 | ``` | |
| 70 | ||
| 71 | Sessions issued from a plain bearer exchange use the store's | |
| 72 | `DEFAULT_SESSION_TTL` of 30 days. The shorter one-hour `expires_in: 3600` applies | |
| 73 | only to DPoP-bound exchanges, where the token is additionally constrained by a | |
| 74 | proof key. See `SessionStore.ts` and `EnvironmentAuth.ts`. | |
| 75 | ||
| 76 | Requested scopes must be a subset of the one-time bootstrap credential grant. | |
| 77 | An ordinary paired client therefore cannot exchange its grant for | |
| 78 | `access:read`, `access:write`, or `relay:write`. | |
| 79 | ||
| 80 | ### DPoP-Bound Access Token | |
| 81 | ||
| 82 | The same `/oauth/token` exchange supports proof-of-possession tokens. A client | |
| 83 | that sends a `DPoP` header has its proof verified by `verifyRequestDpopProof`; | |
| 84 | the resulting JWK thumbprint is stored on the session, which is then issued with | |
| 85 | method `dpop-access-token` and a one-hour TTL instead of the bearer default. An | |
| 86 | invalid proof gets a DPoP challenge header and a credential error rather than a | |
| 87 | bearer token. | |
| 88 | ||
| 89 | `dpop-access-token` is advertised alongside `browser-session-cookie` and | |
| 90 | `bearer-access-token` in the descriptor's `sessionMethods` | |
| 91 | (`EnvironmentAuthPolicy.ts`), so clients can discover support rather than | |
| 92 | assume it. Relay-brokered clients use this mode so that a leaked token cannot be | |
| 93 | replayed without the corresponding key. | |
| 94 | ||
| 95 | ### WebSocket Ticket | |
| 96 | ||
| 97 | `POST /api/auth/websocket-ticket` accepts any authenticated session and returns | |
| 98 | a short-lived, single-purpose WebSocket ticket, issued through | |
| 99 | `EnvironmentAuth.issueWebSocketTicket` with a five-minute default TTL. The | |
| 100 | client presents its bearer or DPoP credential in headers to get the ticket, then | |
| 101 | appends only that ticket to the socket URL as `wsTicket`. This keeps long-lived | |
| 102 | tokens and browser cookies out of WebSocket URLs while letting the handshake | |
| 103 | authenticate. | |
| 104 | ||
| 105 | The ticket carries its session's scopes; each RPC method then enforces | |
| 106 | `orchestration:read`, `orchestration:operate`, `terminal:operate`, | |
| 107 | `review:write`, `relay:write`, or `access:read` as appropriate, through | |
| 108 | `RPC_REQUIRED_SCOPES` in `apps/server/src/auth/RpcAuthorization.ts`. Review feedback submission currently dispatches | |
| 109 | an orchestration operation, so clients performing it also need | |
| 110 | `orchestration:operate`. Creating a ticket is not authorization to call every | |
| 111 | RPC method. | |
| 112 | ||
| 113 | ## Standards Alignment | |
| 114 | ||
| 115 | - Bearer access tokens are used through the `Authorization: Bearer` scheme from | |
| 116 | RFC 6750. | |
| 117 | - The token endpoint profiles the request and response vocabulary from OAuth 2.0 | |
| 118 | Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`, | |
| 119 | `access_token`, `issued_token_type`, and `token_type`. | |
| 120 | - Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited, | |
| 121 | unordered capabilities with subset checking during exchange. | |
| 122 | ||
| 123 | This is intentionally not a general-purpose OAuth authorization server. The | |
| 124 | environment bootstrap token type is private, the bootstrap cookie and WebSocket | |
| 125 | connection-token routes are product-specific adapters, and the API returns its | |
| 126 | typed `HttpApi` errors rather than implementing every OAuth error response | |
| 127 | surface. | |
| 128 | ||
| 129 | ## Upgrade Behavior | |
| 130 | ||
| 131 | Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth | |
| 132 | records to scoped records. It deletes existing pairing links and sessions while | |
| 133 | leaving non-authentication environment state unchanged. Upgraded clients must | |
| 134 | pair again; old `owner` or `client` credentials are never silently mapped to new | |
| 135 | capabilities. | |
| 136 | ||
| 137 | ## Relay Boundary | |
| 138 | ||
| 139 | Relay-managed tunnels use their own tokens and keys. The relay can reuse scope | |
| 140 | parsing and token-exchange conventions, but an environment access token is not a | |
| 141 | relay token and cannot be presented to the relay. |
docs/internals/glossary.md created+181| ... | ... | @@ -0,0 +1,181 @@ |
| 1 | # Glossary | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | This is a living glossary for T3 Code. It explains what common terms mean in this codebase. | |
| 6 | ||
| 7 | ## Table of contents | |
| 8 | ||
| 9 | - [Project and workspace](#project-and-workspace) | |
| 10 | - [Thread timeline](#thread-timeline) | |
| 11 | - [Orchestration](#orchestration) | |
| 12 | - [Provider runtime](#provider-runtime) | |
| 13 | - [Checkpointing](#checkpointing) | |
| 14 | ||
| 15 | ## Concepts | |
| 16 | ||
| 17 | ### Project and workspace | |
| 18 | ||
| 19 | #### Project | |
| 20 | ||
| 21 | The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot` and a title. It does not contain threads: `OrchestrationProject` and `OrchestrationThread` are separate arrays on the read model, and a project can have zero threads. See [workspace-layout.md][2]. | |
| 22 | ||
| 23 | #### Workspace root | |
| 24 | ||
| 25 | The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2]. | |
| 26 | ||
| 27 | #### Worktree | |
| 28 | ||
| 29 | A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live behind the VCS driver contract in `apps/server/src/vcs/VcsDriver.ts`, implemented by [GitVcsDriverCore.ts][3]. | |
| 30 | ||
| 31 | ### Thread timeline | |
| 32 | ||
| 33 | #### Thread | |
| 34 | ||
| 35 | The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4]. | |
| 36 | ||
| 37 | #### Turn | |
| 38 | ||
| 39 | A single user-to-assistant work cycle inside a thread. It starts with user input and ends when the session leaves `running` status, which [projector.ts][4] treats as the authoritative completion signal (`settledTurnStateForSessionStatus`). Checkpoint and diff work may settle afterward without changing when the turn ended. See [the contracts][1] and [ProviderRuntimeIngestion.ts][5]. | |
| 40 | ||
| 41 | #### Activity | |
| 42 | ||
| 43 | A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4]. | |
| 44 | ||
| 45 | ### Orchestration | |
| 46 | ||
| 47 | Orchestration is the server-side domain layer that turns runtime activity into stable app state. The main entry point is [OrchestrationEngine.ts][7], with core logic in [decider.ts][8] and [projector.ts][4]. | |
| 48 | ||
| 49 | #### Aggregate | |
| 50 | ||
| 51 | The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8]. | |
| 52 | ||
| 53 | #### Command | |
| 54 | ||
| 55 | A typed request to change domain state. In [the contracts][1], commands are validated in [commandInvariants.ts][9] and turned into events by [decider.ts][8]. | |
| 56 | Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`. | |
| 57 | ||
| 58 | #### Domain Event | |
| 59 | ||
| 60 | A persisted fact that something already happened. In [the contracts][1], events are the source of truth, and [projector.ts][4] shows how they are applied. | |
| 61 | Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`. | |
| 62 | ||
| 63 | #### Decider | |
| 64 | ||
| 65 | The pure orchestration logic that turns commands plus current state into events. The core implementation is in [decider.ts][8], with preconditions in [commandInvariants.ts][9]. | |
| 66 | ||
| 67 | #### Projection | |
| 68 | ||
| 69 | A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10]. | |
| 70 | ||
| 71 | #### Projector | |
| 72 | ||
| 73 | The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11]. | |
| 74 | ||
| 75 | #### Read model | |
| 76 | ||
| 77 | The current materialized view of orchestration state. In [the contracts][1], it holds projects, threads, messages, activities, checkpoints, and session state. See [ProjectionSnapshotQuery.ts][10] and [OrchestrationEngine.ts][7]. | |
| 78 | ||
| 79 | #### Reactor | |
| 80 | ||
| 81 | A side-effecting service that handles follow-up work after events or runtime signals. Examples include [CheckpointReactor.ts][6], [ProviderCommandReactor.ts][12], and [ProviderRuntimeIngestion.ts][5]. | |
| 82 | ||
| 83 | #### Receipt | |
| 84 | ||
| 85 | A typed signal emitted when an async milestone completes, such as `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, or `turn.processing.quiesced`. Receipts are a test-only mechanism: the production `RuntimeReceiptBusLive` publish is a no-op and only the test layer is PubSub-backed. Do not build production behavior on them. See [RuntimeReceiptBus.ts][13] and [CheckpointReactor.ts][6]. | |
| 86 | ||
| 87 | #### Quiesced | |
| 88 | ||
| 89 | "Quiesced" means a turn has gone quiet and stable: follow-up work such as [CheckpointReactor.ts][6] has settled. It appears in [the receipt schema][13], so in practice it is something tests wait on rather than a production signal. | |
| 90 | ||
| 91 | ### Provider runtime | |
| 92 | ||
| 93 | The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [providers.md][16]. | |
| 94 | ||
| 95 | #### Provider | |
| 96 | ||
| 97 | The backend agent runtime that actually performs work. Five drivers ship built in: Codex, Claude, Cursor, Grok, and OpenCode. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17] as a representative adapter. | |
| 98 | ||
| 99 | #### Session | |
| 100 | ||
| 101 | The live provider-backed runtime attached to a thread. Session shape is in [the orchestration contracts][1], and lifecycle is managed in [ProviderService.ts][14]. | |
| 102 | ||
| 103 | #### Runtime mode | |
| 104 | ||
| 105 | The safety/access mode for a thread or session. [The contracts][1] define four values: `approval-required`, `auto-accept-edits`, `auto`, and `full-access`. See [permission modes][18]. | |
| 106 | ||
| 107 | #### Interaction mode | |
| 108 | ||
| 109 | The agent interaction style for a thread. In [the contracts][1], the values are `default` and `plan`. | |
| 110 | ||
| 111 | #### Assistant delivery mode | |
| 112 | ||
| 113 | Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` accumulates text. Buffered delivery is not held until the turn completes: it spills once accumulated text would exceed 24,000 characters, and flushes at approval and user-input boundaries. See [ProviderRuntimeIngestion.ts][5]. | |
| 114 | ||
| 115 | #### Snapshot | |
| 116 | ||
| 117 | A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19]. | |
| 118 | ||
| 119 | ### Checkpointing | |
| 120 | ||
| 121 | Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6]. | |
| 122 | ||
| 123 | #### Checkpoint | |
| 124 | ||
| 125 | A saved snapshot of a thread workspace at a particular turn. In practice it is a hidden Git ref in [CheckpointStore.ts][19] plus a projected summary from [ProjectionCheckpoints.ts][21]. Capture and lifecycle work happen in [CheckpointReactor.ts][6]. | |
| 126 | ||
| 127 | #### Checkpoint ref | |
| 128 | ||
| 129 | The durable identifier for a filesystem checkpoint, stored as a Git ref. It is typed in [the contracts][1], constructed in [Utils.ts][22], and used by [CheckpointStore.ts][19]. | |
| 130 | ||
| 131 | #### Checkpoint baseline | |
| 132 | ||
| 133 | The starting checkpoint for diffing a thread timeline. This flow is surfaced through [RuntimeReceiptBus.ts][13], coordinated in [CheckpointReactor.ts][6], and supported by [Utils.ts][22]. | |
| 134 | ||
| 135 | #### Checkpoint diff | |
| 136 | ||
| 137 | The patch difference between two checkpoints. Query logic lives in [CheckpointDiffQuery.ts][20], diff parsing lives in [Diffs.ts][23], and finalization is coordinated by [CheckpointReactor.ts][6]. | |
| 138 | ||
| 139 | #### Turn diff | |
| 140 | ||
| 141 | The file patch and changed-file summary for one turn. It is usually computed in [CheckpointDiffQuery.ts][20], represented in [the contracts][1], and recorded into thread state by [projector.ts][4]. | |
| 142 | ||
| 143 | ## Practical Shortcuts | |
| 144 | ||
| 145 | - If you see `requested`, think "intent recorded". | |
| 146 | - If you see `completed`, think "result applied". | |
| 147 | - If you see `receipt`, think "async milestone signal, for tests". | |
| 148 | - If you see `checkpoint`, think "workspace snapshot for diff/restore". | |
| 149 | - If you see `quiesced`, think "all relevant follow-up work has gone idle". | |
| 150 | ||
| 151 | ## Related Docs | |
| 152 | ||
| 153 | - [Architecture overview][24] | |
| 154 | - [Provider architecture][16] | |
| 155 | - [Permission modes][18] | |
| 156 | - [Workspace layout][2] | |
| 157 | ||
| 158 | [1]: ../../packages/contracts/src/orchestration.ts | |
| 159 | [2]: ./workspace-layout.md | |
| 160 | [3]: ../../apps/server/src/vcs/GitVcsDriverCore.ts | |
| 161 | [4]: ../../apps/server/src/orchestration/projector.ts | |
| 162 | [5]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | |
| 163 | [6]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts | |
| 164 | [7]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | |
| 165 | [8]: ../../apps/server/src/orchestration/decider.ts | |
| 166 | [9]: ../../apps/server/src/orchestration/commandInvariants.ts | |
| 167 | [10]: ../../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts | |
| 168 | [11]: ../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts | |
| 169 | [12]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | |
| 170 | [13]: ../../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts | |
| 171 | [14]: ../../apps/server/src/provider/Layers/ProviderService.ts | |
| 172 | [15]: ../../apps/server/src/provider/Services/ProviderAdapter.ts | |
| 173 | [16]: ./providers.md | |
| 174 | [17]: ../../apps/server/src/provider/Layers/CodexAdapter.ts | |
| 175 | [18]: ../user/permission-modes.md | |
| 176 | [19]: ../../apps/server/src/checkpointing/CheckpointStore.ts | |
| 177 | [20]: ../../apps/server/src/checkpointing/CheckpointDiffQuery.ts | |
| 178 | [21]: ../../apps/server/src/persistence/Services/ProjectionCheckpoints.ts | |
| 179 | [22]: ../../apps/server/src/checkpointing/Utils.ts | |
| 180 | [23]: ../../apps/server/src/checkpointing/Diffs.ts | |
| 181 | [24]: ./overview.md |
docs/internals/overview.md created+152| ... | ... | @@ -0,0 +1,152 @@ |
| 1 | # Architecture | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | T3 Code is a server runtime that owns agent sessions, workspaces, and version control, plus clients | |
| 6 | (web, desktop, mobile) that talk to it over one authenticated Effect RPC WebSocket. The server is the | |
| 7 | execution boundary: every provider process, terminal, git operation, and filesystem read happens | |
| 8 | there, never in the client. | |
| 9 | ||
| 10 | ``` | |
| 11 | ┌────────────────────────────────────────────────┐ | |
| 12 | │ Clients: apps/web, apps/desktop, apps/mobile │ | |
| 13 | │ shared runtime: packages/client-runtime │ | |
| 14 | │ connection supervisor, RPC session, Atom state│ | |
| 15 | └──────────────────┬─────────────────────────────┘ | |
| 16 | │ Effect RPC over WebSocket (/ws) | |
| 17 | │ contract: packages/contracts | |
| 18 | ┌──────────────────▼─────────────────────────────┐ | |
| 19 | │ apps/server │ | |
| 20 | │ orchestration engine (event-sourced) │ | |
| 21 | │ provider driver registry (5 built-in drivers) │ | |
| 22 | │ checkpointing, VCS, terminals, filesystem │ | |
| 23 | └──────────────────┬─────────────────────────────┘ | |
| 24 | │ per-driver transport | |
| 25 | ┌──────────────────▼─────────────────────────────┐ | |
| 26 | │ Agent CLIs: Codex, Claude, Cursor, Grok, │ | |
| 27 | │ OpenCode │ | |
| 28 | └────────────────────────────────────────────────┘ | |
| 29 | ``` | |
| 30 | ||
| 31 | ## The RPC boundary | |
| 32 | ||
| 33 | The client/server contract is an Effect RPC group, not a hand-rolled push protocol. [`rpc.ts`][rpc] | |
| 34 | declares `WS_METHODS` and assembles `WsRpcGroup`; each member is either unary or a server stream | |
| 35 | (`stream: true`). Streaming members such as `orchestration.subscribeShell`, | |
| 36 | `orchestration.subscribeThread`, `subscribeServerConfig`, and `terminal.attach` replace what used to | |
| 37 | be a broadcast push bus: a client subscribes to what it needs and the server pushes only on that | |
| 38 | subscription. | |
| 39 | ||
| 40 | [`ws.ts`][ws] serves the group. `websocketRpcRouteLayer` mounts `GET /ws`, authenticates the upgrade | |
| 41 | through `EnvironmentAuth.authenticateWebSocketUpgrade`, then hands the socket to | |
| 42 | `RpcServer.toHttpEffectWebsocket`. Authorization is per method: `RPC_REQUIRED_SCOPE` maps each method | |
| 43 | to a scope, and `authorizeEffect`/`authorizeStream` enforce it. Holding a valid socket is not | |
| 44 | authorization to call everything on it. See [environment-auth.md](./environment-auth.md). | |
| 45 | ||
| 46 | On the client, [`session.ts`][session] opens the socket and builds the typed client. | |
| 47 | `RpcSessionFactory` is the service; a session exposes `client`, `initialConfig`, `ready`, `probe`, | |
| 48 | and `closed`. It performs one attempt and does not retry. Retry, backoff, and offline policy belong | |
| 49 | to the connection supervisor. | |
| 50 | ||
| 51 | ## Shared client runtime | |
| 52 | ||
| 53 | `packages/client-runtime` holds every non-visual client concern: connection lifecycle, | |
| 54 | authentication, RPC, cached environment data, and domain state as Atom factories. Web and mobile | |
| 55 | compose it the same way (`apps/web/src/connection/runtime.ts` and | |
| 56 | `apps/mobile/src/connection/runtime.ts` mirror each other, differing only in platform-specific | |
| 57 | background-activity layers) and differ beyond that only in the platform layer they supply and the | |
| 58 | UI they build on top. React components never construct transports, retry loops, | |
| 59 | or RPC clients. See [connection-runtime.md](./connection-runtime.md). | |
| 60 | ||
| 61 | ## Orchestration is event-sourced | |
| 62 | ||
| 63 | The server does not mutate app state directly. Clients dispatch typed commands; the engine turns them | |
| 64 | into persisted events; projections derive the read model. | |
| 65 | ||
| 66 | [`OrchestrationEngine.ts`][engine] serializes this. `dispatch` offers a `CommandEnvelope` onto | |
| 67 | `commandQueue` and awaits its result; a single worker fiber takes envelopes one at a time, so command | |
| 68 | processing is totally ordered. For each envelope `processEnvelope`: | |
| 69 | ||
| 70 | 1. checks the durable command receipt, making retries idempotent; | |
| 71 | 2. runs `decideOrchestrationCommand` ([`decider.ts`][decider]) to produce events from command plus | |
| 72 | current state, pure and side-effect free; | |
| 73 | 3. inside one SQL transaction, appends events to the event store, applies them to the in-memory read | |
| 74 | model via [`projector.ts`][projector], projects them into persisted tables, and writes the | |
| 75 | accepted receipt; | |
| 76 | 4. after commit, swaps in the new read model and publishes committed events to subscribers. | |
| 77 | ||
| 78 | Because persistence and projection share a transaction, the read model cannot durably disagree with | |
| 79 | the event log. On dispatch failure the engine rereads persisted events past the starting sequence and | |
| 80 | reconciles. | |
| 81 | ||
| 82 | Command and event names live in [`orchestration.ts`][contracts]. Some commands are client | |
| 83 | dispatchable (`thread.create`, `thread.turn.start`, `thread.approval.respond`); others are internal | |
| 84 | and produced only by server-side reactors (`thread.message.assistant.delta`, | |
| 85 | `thread.turn.diff.complete`). | |
| 86 | ||
| 87 | A turn is complete when its session leaves `running` status, projected by | |
| 88 | `settledTurnStateForSessionStatus` in [`projector.ts`][projector]. Checkpoint work settling later | |
| 89 | does not define turn end. | |
| 90 | ||
| 91 | ## Drainable workers | |
| 92 | ||
| 93 | Follow-up work runs asynchronously in queue-backed workers built on [`DrainableWorker`][worker]: | |
| 94 | [`ProviderRuntimeIngestion`][ingest] normalizes provider runtime streams into orchestration commands, | |
| 95 | [`ProviderCommandReactor`][cmd] dispatches provider calls in response to intent events, and | |
| 96 | [`CheckpointReactor`][checkpoint] captures and reverts workspace checkpoints. | |
| 97 | ||
| 98 | `DrainableWorker` pairs a transactional queue with a transactional count of outstanding items. | |
| 99 | `enqueue` atomically offers and increments; processing always decrements. `drain` retries until the | |
| 100 | count reaches zero, so a test can await "queue empty and current item finished" instead of sleeping. | |
| 101 | Each of the three services exposes `drain` for exactly this. | |
| 102 | ||
| 103 | Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in | |
| 104 | [`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not | |
| 105 | build production behavior on receipts. | |
| 106 | ||
| 107 | ## Provider drivers | |
| 108 | ||
| 109 | Five drivers ship built in, registered in [`builtInDrivers.ts`][drivers] as `BUILT_IN_DRIVERS`: | |
| 110 | Codex, Claude, Cursor, Grok, and OpenCode. A driver declares its kind and config schema and creates a | |
| 111 | scoped adapter; `ProviderInstanceRegistry` owns live instances and `ProviderAdapterRegistry` resolves | |
| 112 | an instance to its adapter, so `ProviderService` routes session and turn operations without knowing | |
| 113 | which agent is behind them. See [providers.md](./providers.md). | |
| 114 | ||
| 115 | ## Checkpointing | |
| 116 | ||
| 117 | Each turn is bracketed by workspace checkpoints so diffs and reverts are exact. `CheckpointStore` | |
| 118 | captures state as hidden Git refs through the VCS driver's checkpoint operations; | |
| 119 | `CheckpointDiffQuery` answers turn and full-thread diff requests; `CheckpointReactor` coordinates | |
| 120 | baseline capture, completed-turn capture, diff projection, and reverting both the workspace and the | |
| 121 | provider conversation. The storage contract is `VcsCheckpointOps` in | |
| 122 | [`VcsDriver.ts`](../../apps/server/src/vcs/VcsDriver.ts), implemented for Git in the same directory. | |
| 123 | ||
| 124 | ## Startup | |
| 125 | ||
| 126 | [`serverRuntimeStartup.ts`][startup] runs a fixed lifecycle: start keybindings, settings, and | |
| 127 | reactors; publish welcome; signal command readiness (logged as `Accepting commands`); wait for the | |
| 128 | HTTP listener via `markHttpListening`; publish ready; fork the heartbeat; then either print headless | |
| 129 | output or open the browser. Command readiness precedes the listener, so a socket that opens can | |
| 130 | already dispatch. | |
| 131 | ||
| 132 | ## Related | |
| 133 | ||
| 134 | - [Workspace layout](./workspace-layout.md), [Glossary](./glossary.md) | |
| 135 | - [Remote environments](./remote.md), [Server updates](./server-updates.md) | |
| 136 | - [Resource telemetry](./resource-telemetry.md) | |
| 137 | - [Scripts](./scripts.md), [CI gates](./ci.md) | |
| 138 | ||
| 139 | [rpc]: ../../packages/contracts/src/rpc.ts | |
| 140 | [contracts]: ../../packages/contracts/src/orchestration.ts | |
| 141 | [ws]: ../../apps/server/src/ws.ts | |
| 142 | [session]: ../../packages/client-runtime/src/rpc/session.ts | |
| 143 | [startup]: ../../apps/server/src/serverRuntimeStartup.ts | |
| 144 | [engine]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | |
| 145 | [decider]: ../../apps/server/src/orchestration/decider.ts | |
| 146 | [projector]: ../../apps/server/src/orchestration/projector.ts | |
| 147 | [worker]: ../../packages/shared/src/DrainableWorker.ts | |
| 148 | [ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | |
| 149 | [cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | |
| 150 | [checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts | |
| 151 | [receipts]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts | |
| 152 | [drivers]: ../../apps/server/src/provider/builtInDrivers.ts |
docs/internals/providers.md created+92| ... | ... | @@ -0,0 +1,92 @@ |
| 1 | # Provider architecture | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | A provider is the agent runtime that does the actual work. T3 Code supports several, and the | |
| 6 | orchestration layer does not know which one is behind a thread. | |
| 7 | ||
| 8 | ## Built-in drivers | |
| 9 | ||
| 10 | [`builtInDrivers.ts`][drivers] exports `BUILT_IN_DRIVERS` with five entries: | |
| 11 | ||
| 12 | | Driver kind | Driver source | | |
| 13 | | ------------- | --------------------------------------- | | |
| 14 | | `codex` | [`Drivers/CodexDriver.ts`][codex] | | |
| 15 | | `claudeAgent` | [`Drivers/ClaudeDriver.ts`][claude] | | |
| 16 | | `cursor` | [`Drivers/CursorDriver.ts`][cursor] | | |
| 17 | | `grok` | [`Drivers/GrokDriver.ts`][grok] | | |
| 18 | | `opencode` | [`Drivers/OpenCodeDriver.ts`][opencode] | | |
| 19 | ||
| 20 | Each driver declares its `driverKind`, a `configSchema`, and a `create` function that builds an | |
| 21 | adapter in a child scope. Adapter implementations live beside them in | |
| 22 | `apps/server/src/provider/Layers/` (`CodexAdapter.ts`, `ClaudeAdapter.ts`, and so on) and conform to | |
| 23 | [`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's | |
| 24 | transport, config, and event shapes are mapped. | |
| 25 | ||
| 26 | ## Registry and routing | |
| 27 | ||
| 28 | Two registries separate configuration from live processes: | |
| 29 | ||
| 30 | - [`ProviderInstanceRegistry`][instances] keys configured instances by `ProviderInstanceId`. Creating | |
| 31 | one looks up the driver by `driverKind`, decodes `entry.config` with that driver's schema, opens a | |
| 32 | child scope, and calls `driver.create`. | |
| 33 | - [`ProviderAdapterRegistry`][registry] resolves an instance ID to its live adapter via | |
| 34 | `getByInstance`. | |
| 35 | ||
| 36 | [`ProviderService`][service] sits on top. It combines the adapter registry with the provider session | |
| 37 | directory to route session and turn operations for a thread, so callers name a thread, not an agent. | |
| 38 | ||
| 39 | Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No | |
| 40 | orchestration, contract, or client change is required for the common case. | |
| 41 | ||
| 42 | ## How provider work is requested | |
| 43 | ||
| 44 | Clients never call a provider directly. They dispatch orchestration commands over the RPC method | |
| 45 | `orchestration.dispatchCommand`, defined with the rest of the orchestration surface in | |
| 46 | [`orchestration.ts`][contracts]. The client-dispatchable provider-facing commands are | |
| 47 | `thread.turn.start`, `thread.turn.interrupt`, `thread.approval.respond`, | |
| 48 | `thread.user-input.respond`, `thread.checkpoint.revert`, and `thread.session.stop`, plus the mode | |
| 49 | setters `thread.runtime-mode.set` and `thread.interaction-mode.set`. | |
| 50 | ||
| 51 | The engine persists an event for the command, and a server-side reactor performs the provider call. | |
| 52 | Provider output comes back as internal commands such as `thread.message.assistant.delta` and | |
| 53 | `thread.session.set`, which clients observe through `orchestration.subscribeThread`. See | |
| 54 | [overview.md](./overview.md) for the command/event loop. | |
| 55 | ||
| 56 | ## Server-side workers | |
| 57 | ||
| 58 | Provider work flows through three queue-backed workers. All three are built with | |
| 59 | `makeDrainableWorker` from [`DrainableWorker.ts`][worker] and expose `drain` for deterministic test | |
| 60 | synchronization. | |
| 61 | ||
| 62 | 1. [`ProviderRuntimeIngestion`][ingest] consumes provider runtime streams and emits orchestration | |
| 63 | commands. | |
| 64 | 2. [`ProviderCommandReactor`][cmd] reacts to orchestration intent events and dispatches provider | |
| 65 | calls. | |
| 66 | 3. [`CheckpointReactor`][checkpoint] captures workspace checkpoints on turn start and completion, and | |
| 67 | performs reverts. | |
| 68 | ||
| 69 | ### Buffered assistant delivery | |
| 70 | ||
| 71 | A thread in `buffered` assistant delivery mode accumulates assistant text instead of streaming each | |
| 72 | delta. The buffer is not held until turn completion. In [`ProviderRuntimeIngestion`][ingest], | |
| 73 | `MAX_BUFFERED_ASSISTANT_CHARS` is 24,000: the append that would exceed it invalidates the buffer and | |
| 74 | spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries, | |
| 75 | when a request opens (approval) or user input is requested, via | |
| 76 | `flushBufferedAssistantMessagesForTurn`. | |
| 77 | ||
| 78 | [drivers]: ../../apps/server/src/provider/builtInDrivers.ts | |
| 79 | [codex]: ../../apps/server/src/provider/Drivers/CodexDriver.ts | |
| 80 | [claude]: ../../apps/server/src/provider/Drivers/ClaudeDriver.ts | |
| 81 | [cursor]: ../../apps/server/src/provider/Drivers/CursorDriver.ts | |
| 82 | [grok]: ../../apps/server/src/provider/Drivers/GrokDriver.ts | |
| 83 | [opencode]: ../../apps/server/src/provider/Drivers/OpenCodeDriver.ts | |
| 84 | [adapter]: ../../apps/server/src/provider/Services/ProviderAdapter.ts | |
| 85 | [instances]: ../../apps/server/src/provider/Services/ProviderInstanceRegistry.ts | |
| 86 | [registry]: ../../apps/server/src/provider/Services/ProviderAdapterRegistry.ts | |
| 87 | [service]: ../../apps/server/src/provider/Layers/ProviderService.ts | |
| 88 | [contracts]: ../../packages/contracts/src/orchestration.ts | |
| 89 | [worker]: ../../packages/shared/src/DrainableWorker.ts | |
| 90 | [ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | |
| 91 | [cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | |
| 92 | [checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts |
docs/internals/remote.md created+235| ... | ... | @@ -0,0 +1,235 @@ |
| 1 | # Remote Architecture | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | Remote environments are shipped, not planned. Direct, bearer-paired, relay-tunneled, Tailscale, and | |
| 6 | desktop-managed SSH access all exist today. This document describes the model they share and where | |
| 7 | each piece lives. For the user-facing setup guide see | |
| 8 | [remote access](../user/remote-access.md). | |
| 9 | ||
| 10 | ## The model | |
| 11 | ||
| 12 | T3 has one runtime boundary: a client talks to a T3 server over HTTP and WebSocket, and the server | |
| 13 | owns orchestration, providers, terminals, git, and filesystem operations. Remoteness is expressed at | |
| 14 | the connection layer, never by splitting the runtime. | |
| 15 | ||
| 16 | ```text | |
| 17 | ┌──────────────────────────────────────────────┐ | |
| 18 | │ Client (desktop / mobile / web) │ | |
| 19 | │ known environments, connection supervisor │ | |
| 20 | └───────────────┬──────────────────────────────┘ | |
| 21 | │ resolves one access endpoint | |
| 22 | ┌───────────────▼──────────────────────────────┐ | |
| 23 | │ Access method │ | |
| 24 | │ direct ws/wss, relay tunnel, │ | |
| 25 | │ Tailscale serve, desktop-managed ssh │ | |
| 26 | └───────────────┬──────────────────────────────┘ | |
| 27 | │ connects to one T3 server | |
| 28 | ┌───────────────▼──────────────────────────────┐ | |
| 29 | │ Execution environment = one T3 server │ | |
| 30 | │ identity, providers, projects/threads, │ | |
| 31 | │ terminals, git, filesystem │ | |
| 32 | └──────────────────────────────────────────────┘ | |
| 33 | ``` | |
| 34 | ||
| 35 | ### ExecutionEnvironment | |
| 36 | ||
| 37 | One running T3 server instance. It owns provider availability and auth, model availability, projects | |
| 38 | and threads, terminal processes, filesystem access, git operations, and server settings. | |
| 39 | ||
| 40 | It is identified by a stable `environmentId`, persisted by the server at `<stateDir>/environment-id` | |
| 41 | and generated on first start (`apps/server/src/environment/ServerEnvironment.ts`). Desktop, mobile, | |
| 42 | and web all reason about the same concept. | |
| 43 | ||
| 44 | ### Known environments and connection targets | |
| 45 | ||
| 46 | A saved client-side entry for an environment the client knows how to reach. It is not | |
| 47 | server-authored; it is local to a device or client profile. In the hosted web app these entries are | |
| 48 | browser-local. A hosted pairing URL can create one, but it does not give the hosted app a server-side | |
| 49 | control plane or a copy of session state. | |
| 50 | ||
| 51 | [`connection/model.ts`][model] defines four target tags, which are the real access taxonomy: | |
| 52 | ||
| 53 | | Target | Used for | | |
| 54 | | ------------------------- | ------------------------------------------------------------------------ | | |
| 55 | | `PrimaryConnectionTarget` | The platform-managed local server (desktop backend, CLI-served web app). | | |
| 56 | | `BearerConnectionTarget` | Any manually paired endpoint reached over direct HTTP/WebSocket. | | |
| 57 | | `RelayConnectionTarget` | Managed T3 Connect relay tunnels. | | |
| 58 | | `SshConnectionTarget` | Desktop-managed SSH environments. | | |
| 59 | ||
| 60 | Bearer, relay, and SSH are persisted; primary is platform-managed. Note that Tailscale is not a | |
| 61 | separate target kind. A Tailscale URL is paired through the ordinary bearer path in | |
| 62 | [`onboarding.ts`][onboarding] (`preparePairingRegistration`), which accepts either a pairing URL or a | |
| 63 | host plus pairing code. Tailscale is an endpoint provider and transport, not a distinct runtime | |
| 64 | concept. | |
| 65 | ||
| 66 | ### AdvertisedEndpoint | |
| 67 | ||
| 68 | A server- or desktop-authored candidate endpoint for an environment: a concrete HTTP and WebSocket | |
| 69 | base URL pair, a default/available/unavailable marker, reachability hints (loopback, LAN, private, | |
| 70 | public, tunnel), and compatibility hints such as whether the hosted HTTPS app can use it. | |
| 71 | ||
| 72 | Clients treat advertised endpoints as hints, not proof that a route works from the current device. | |
| 73 | The connection attempt decides. | |
| 74 | ||
| 75 | The UI shows one default endpoint in the network-access summary and keeps the rest behind an advanced | |
| 76 | list. `selectPairingEndpoint` in | |
| 77 | [`ConnectionsSettings.tsx`](../../apps/web/src/components/settings/ConnectionsSettings.tsx) excludes | |
| 78 | unavailable endpoints and then picks, in order: | |
| 79 | ||
| 80 | 1. the saved `defaultEndpointKey` override; | |
| 81 | 2. the first endpoint marked `isDefault`; | |
| 82 | 3. the first endpoint whose reachability is not `loopback`; | |
| 83 | 4. the first endpoint compatible with the hosted HTTPS app; | |
| 84 | 5. otherwise nothing. | |
| 85 | ||
| 86 | There is no unconditional loopback fallback. A loopback endpoint only wins through an explicit saved | |
| 87 | override or `isDefault`. Persist the override by stable endpoint kind rather than raw URL where | |
| 88 | possible, since LAN addresses change with networks; Tailscale endpoints use provider-specific stable | |
| 89 | keys (`tailscale-ip:`, `tailscale-magicdns:`). | |
| 90 | ||
| 91 | ### Endpoint providers | |
| 92 | ||
| 93 | Endpoint providers contribute advertised endpoints without becoming part of the core environment | |
| 94 | model: core owns environments, pairing, and connection lifecycle, and providers return normalized | |
| 95 | `AdvertisedEndpoint` records. | |
| 96 | ||
| 97 | Tailscale is the first provider, and T3 manages more than discovery. When `tailscaleServeEnabled` is | |
| 98 | set, the server acquires a Tailscale serve mapping for its actual listening port at startup with | |
| 99 | `ensureTailscaleServe` and releases it with `disableTailscaleServe` on scope close | |
| 100 | (`apps/server/src/server.ts`, using [`@t3tools/tailscale`](../../packages/tailscale/src/tailscale.ts)). | |
| 101 | Endpoint identifiers are synthesized in `apps/desktop/src/backend/tailscaleEndpointProvider.ts` with | |
| 102 | `private-network` reachability. | |
| 103 | ||
| 104 | ### Hosted pairing request | |
| 105 | ||
| 106 | A hosted pairing request is a bootstrap URL for the static web app, not a transport: | |
| 107 | ||
| 108 | ```text | |
| 109 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | |
| 110 | ``` | |
| 111 | ||
| 112 | The hosted app reads `host`, takes the token from the URL hash, exchanges it directly with that | |
| 113 | backend, strips the token from browser history, and saves the environment record locally. Helpers | |
| 114 | live in [`shared/remote.ts`](../../packages/shared/src/remote.ts) (`setPairingTokenOnUrl`, | |
| 115 | `getPairingTokenFromUrl`, `stripPairingTokenFromUrl`) and `apps/web/src/hostedPairing.ts`. | |
| 116 | ||
| 117 | Constraints: | |
| 118 | ||
| 119 | - the hosted app does not proxy HTTP or WebSocket traffic; | |
| 120 | - the backend must be directly reachable from the browser; | |
| 121 | - HTTPS pages can only reach HTTPS/WSS backends; | |
| 122 | - HTTP LAN endpoints keep using direct desktop or CLI pairing URLs; | |
| 123 | - the token belongs in the hash so it is never sent to the hosted app origin. | |
| 124 | ||
| 125 | ### RepositoryIdentity and Project | |
| 126 | ||
| 127 | `RepositoryIdentity` is a best-effort logical repo grouping across environments, used for UI grouping | |
| 128 | and correlation only, never for routing. `Project` remains environment-local: a local clone and a | |
| 129 | remote clone are different projects that may share a `RepositoryIdentity`, and threads bind to one | |
| 130 | project in one environment. | |
| 131 | ||
| 132 | ## Access methods | |
| 133 | ||
| 134 | Access answers one question: how does the client speak WebSocket to a T3 server? It does not answer | |
| 135 | how the server got started or who manages the process. | |
| 136 | ||
| 137 | ### Direct WebSocket access | |
| 138 | ||
| 139 | `wss://t3.example.com` or `ws://10.0.0.15:3773`, paired as a bearer target. This is the base model. | |
| 140 | It works for desktop, mobile, and web with no client-side process management. Browser security rules | |
| 141 | are part of it: a hosted HTTPS client cannot connect to plain `ws://` or `http://` LAN backends. | |
| 142 | ||
| 143 | ### Relay-tunneled access | |
| 144 | ||
| 145 | Managed T3 Connect relay tunnels use `RelayConnectionTarget` and are the answer when the host is | |
| 146 | behind NAT, inbound ports are unavailable, or mobile must reach a desktop-hosted environment. From | |
| 147 | the client's perspective this is still an ordinary WebSocket connection; the route is mediated. The | |
| 148 | relay Worker only brokers credentials and a managed endpoint; application traffic then flows over | |
| 149 | the provisioned Cloudflare tunnel hostname for the life of the connection, not through the relay | |
| 150 | Worker itself. See [t3-connect.md](./t3-connect.md). | |
| 151 | ||
| 152 | ### Tailscale access | |
| 153 | ||
| 154 | A T3-managed `tailscale serve` mapping exposes the server on the tailnet over HTTPS, and the | |
| 155 | resulting private-network endpoints are advertised for pairing. Connection then follows the ordinary | |
| 156 | bearer path. | |
| 157 | ||
| 158 | ### Desktop-managed SSH access | |
| 159 | ||
| 160 | SSH is an access and launch helper, not a separate environment type. `DesktopSshEnvironment` | |
| 161 | ([apps/desktop/src/ssh/DesktopSshEnvironment.ts][sshenv]) exposes `discoverHosts`, | |
| 162 | `ensureEnvironment`, and `disconnectEnvironment`. It discovers targets from SSH config and known | |
| 163 | hosts, owns password/askpass prompts, and delegates lifecycle to `SshEnvironmentManager` in | |
| 164 | [packages/ssh/src/tunnel.ts][sshtunnel], which resolves the target, launches or reuses the remote T3 | |
| 165 | server, opens a local tunnel, checks HTTP readiness, optionally issues a remote pairing token, and | |
| 166 | returns local HTTP/WS endpoints. Disconnect closes the tunnel and stops the remote server if the | |
| 167 | launcher started it; a server that was already running (marked `external`) is left running. | |
| 168 | ||
| 169 | The desktop main process owns this because it can spawn SSH, manage prompts, write launch scripts, | |
| 170 | and clean up forwards. The renderer connects through the forwarded URL like any other environment and | |
| 171 | needs no SSH-specific RPC path. | |
| 172 | ||
| 173 | Failure handling is explicit: SSH auth failure surfaces before an environment is saved, remote launch | |
| 174 | failure includes launcher output where available, forwarded-port failure leaves the environment | |
| 175 | disconnected rather than falling back to an unrelated endpoint, and reconnect restores the SSH bridge | |
| 176 | before reconnecting the WebSocket client. | |
| 177 | ||
| 178 | ## Launch methods | |
| 179 | ||
| 180 | Launch answers a different question: how does a T3 server come to exist on the target machine? Keep | |
| 181 | it separate from access. | |
| 182 | ||
| 183 | - **Pre-existing server.** The operator already runs T3 and the client connects directly or through a | |
| 184 | tunnel. | |
| 185 | - **Desktop-managed remote launch over SSH.** Desktop probes the machine, launches or reuses a remote | |
| 186 | server, forwards a port, and the renderer connects normally. The saved environment records that it | |
| 187 | came from SSH launch for reconnect and lifecycle UX only; that metadata never changes the protocol | |
| 188 | or the identity model. | |
| 189 | - **Client-managed local publish.** A local server is published through the relay with | |
| 190 | `t3 connect link`, exposing a desktop-hosted environment to mobile without router or firewall | |
| 191 | changes. | |
| 192 | ||
| 193 | The same `ExecutionEnvironment` can be reached several of these ways. Only the launch and access | |
| 194 | paths differ. | |
| 195 | ||
| 196 | ## Security model | |
| 197 | ||
| 198 | Some environments are reachable over untrusted networks, so remote-capable environments require | |
| 199 | explicit authentication, tunnel exposure never relies on obscurity, and saved endpoints carry enough | |
| 200 | auth metadata to reconnect safely. | |
| 201 | ||
| 202 | WebSocket authentication is a dedicated short-lived ticket, not a token in a query string. The client | |
| 203 | presents its long-lived bearer or DPoP credential in HTTP headers to | |
| 204 | `POST /api/auth/websocket-ticket` ([authorization/remote.ts][authremote]), and appends only the | |
| 205 | returned ticket as `wsTicket` on the socket URL. The server issues it through | |
| 206 | `EnvironmentAuth.issueWebSocketTicket`; tickets are tagged `kind: "websocket"` and default to a | |
| 207 | five-minute TTL (`DEFAULT_WEBSOCKET_TOKEN_TTL` in `apps/server/src/auth/SessionStore.ts`). The | |
| 208 | handshake verifies the ticket, and each RPC method still enforces its own scope. See | |
| 209 | [environment-auth.md](./environment-auth.md). | |
| 210 | ||
| 211 | Hosted pairing is a client-side convenience only. The hosted app must not receive pairing tokens | |
| 212 | through query parameters, must not store pairing state server-side, and must not imply that an HTTP | |
| 213 | backend is reachable from an HTTPS browser context. | |
| 214 | ||
| 215 | ## Version coordination | |
| 216 | ||
| 217 | Remote environments stay online while clients move to newer releases. The environment descriptor | |
| 218 | carries the running server version and may advertise a safe replacement path, so the UI can show the | |
| 219 | right action without making the transport responsible for process management. The connection | |
| 220 | supervisor owns the resulting disconnect and reconnect like any other involuntary close. See | |
| 221 | [server-updates.md](./server-updates.md). | |
| 222 | ||
| 223 | ## Future work | |
| 224 | ||
| 225 | These remain unbuilt and are listed to keep the model honest: | |
| 226 | ||
| 227 | - third-party tunnel products as additional endpoint providers; | |
| 228 | - a relay-hosted OAuth callback broker (see [t3-connect.md](./t3-connect.md)); | |
| 229 | - richer multi-environment UI beyond the current connections list. | |
| 230 | ||
| 231 | [model]: ../../packages/client-runtime/src/connection/model.ts | |
| 232 | [onboarding]: ../../packages/client-runtime/src/connection/onboarding.ts | |
| 233 | [authremote]: ../../packages/client-runtime/src/authorization/remote.ts | |
| 234 | [sshenv]: ../../apps/desktop/src/ssh/DesktopSshEnvironment.ts | |
| 235 | [sshtunnel]: ../../packages/ssh/src/tunnel.ts |
docs/internals/resource-telemetry.md created+393| ... | ... | @@ -0,0 +1,393 @@ |
| 1 | # Resource telemetry architecture | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | Status: implemented | |
| 6 | ||
| 7 | ## Purpose | |
| 8 | ||
| 9 | Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset` | |
| 10 | subprocess probes with two persistent, direct data sources: | |
| 11 | ||
| 12 | 1. a standalone Rust resource-monitor executable that reads process counters | |
| 13 | through operating-system APIs via `sysinfo`; | |
| 14 | 2. Electron main-process APIs for Electron process metrics and host power state. | |
| 15 | ||
| 16 | The native monitor owns bounded in-memory history. The server only merges and | |
| 17 | summarizes that history when diagnostics requests it. Telemetry history is not | |
| 18 | persisted to disk or continuously copied into Node. | |
| 19 | ||
| 20 | ## Why a standalone executable | |
| 21 | ||
| 22 | The monitor is intentionally not a Node native addon. | |
| 23 | ||
| 24 | - No N-API, `ffi-rs`, or dynamic-library ABI is loaded into the server process. | |
| 25 | - A monitor crash cannot corrupt the Node runtime. | |
| 26 | - The server can supervise, restart, version-check, and measure the monitor as a | |
| 27 | normal child process. | |
| 28 | - The same protocol works for the desktop app and the published CLI. | |
| 29 | - Packaging is a single platform executable instead of an addon toolchain plus | |
| 30 | Node/Electron ABI matrix. | |
| 31 | ||
| 32 | The cost is one persistent child process and NDJSON serialization. That is a | |
| 33 | better failure boundary than repeatedly spawning shell utilities or loading | |
| 34 | native code into Node. | |
| 35 | ||
| 36 | ## Runtime topology | |
| 37 | ||
| 38 | ### Desktop | |
| 39 | ||
| 40 | ```text | |
| 41 | Electron main | |
| 42 | ├─ powerMonitor | |
| 43 | ├─ app.getAppMetrics() while diagnostics is open | |
| 44 | ├─ inherited fd 4, telemetry NDJSON ─────────────┐ | |
| 45 | └─ inherited fd 5, demand-control NDJSON ◀──────┤ | |
| 46 | ▼ | |
| 47 | Node server ── stdin/stdout NDJSON ── Rust resource monitor | |
| 48 | │ | |
| 49 | ├─ ResourceTelemetry Effect service | |
| 50 | ├─ background power policy projection | |
| 51 | └─ WebSocket RPC/subscription ── diagnostics UI | |
| 52 | ``` | |
| 53 | ||
| 54 | ### Web, headless, and remote server | |
| 55 | ||
| 56 | Electron telemetry is unavailable. The native monitor still runs beside the | |
| 57 | server and tracks the server process tree. Power fields degrade to `unknown` | |
| 58 | instead of invoking platform shell commands. | |
| 59 | ||
| 60 | ### WSL backend limitation | |
| 61 | ||
| 62 | Windows desktop packages currently ship the Windows resource-monitor executable. | |
| 63 | That executable cannot run inside the Linux WSL backend, so a WSL-only backend | |
| 64 | does not receive `resourceMonitorPath` and reports native process telemetry as | |
| 65 | unavailable. Electron host-power telemetry remains available over the inherited | |
| 66 | desktop pipe. Supporting native WSL process telemetry requires publishing a | |
| 67 | Linux sidecar for each supported architecture in the Windows artifact and | |
| 68 | converting its packaged path into the selected distro; the configuration | |
| 69 | deliberately does not pass the Windows `.exe` into WSL as a false fallback. | |
| 70 | ||
| 71 | ## Native monitor | |
| 72 | ||
| 73 | The executable lives in `native/resource-monitor`. | |
| 74 | ||
| 75 | It receives schema-compatible commands on stdin and emits one JSON object per | |
| 76 | line on stdout: | |
| 77 | ||
| 78 | - `configure` | |
| 79 | - `setExternalProcesses` | |
| 80 | - `setSampleInterval` | |
| 81 | - `setStreaming` | |
| 82 | - `sampleNow` | |
| 83 | - `readHistory` | |
| 84 | - `shutdown` | |
| 85 | - `hello` | |
| 86 | - `snapshot` | |
| 87 | - `historyChunk` | |
| 88 | - `error` | |
| 89 | ||
| 90 | The protocol version is defined by | |
| 91 | `RESOURCE_MONITOR_PROTOCOL_VERSION` in | |
| 92 | `packages/contracts/src/resourceTelemetry.ts`. | |
| 93 | ||
| 94 | ### Collection | |
| 95 | ||
| 96 | The monitor keeps one `sysinfo::System` instance and refreshes it at the | |
| 97 | power-adaptive interval selected by the server. It collects: | |
| 98 | ||
| 99 | - PID and parent PID; | |
| 100 | - process start time and run time; | |
| 101 | - process name and command line; | |
| 102 | - current and cumulative CPU usage; | |
| 103 | - resident and virtual memory; | |
| 104 | - cumulative process I/O counters. | |
| 105 | ||
| 106 | On Linux, task/thread enumeration is disabled. Command lines are loaded only | |
| 107 | when first needed. This avoids the expensive default behavior of walking every | |
| 108 | `/proc/<pid>/task/<tid>` directory on each refresh. | |
| 109 | ||
| 110 | ### Process-tree selection | |
| 111 | ||
| 112 | Each sample scans the accessible process table, builds the PID/PPID graph, and | |
| 113 | retains: | |
| 114 | ||
| 115 | - the server process; | |
| 116 | - every descendant of the server, including provider-spawned grandchildren such | |
| 117 | as shells, `node`, `tsgo`, language servers, and other tools; | |
| 118 | - Electron processes supplied as explicit external roots; | |
| 119 | - descendants of those Electron roots; | |
| 120 | - the resource monitor itself, because it is a server child. | |
| 121 | ||
| 122 | Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native | |
| 123 | start times are matched with a two-second tolerance because native start times | |
| 124 | can have coarser platform resolution. | |
| 125 | ||
| 126 | The process list is emitted in depth-first tree order so renderer collapse and | |
| 127 | expansion preserves complete subtrees. | |
| 128 | ||
| 129 | ### Native history and streaming | |
| 130 | ||
| 131 | Every native sample is appended to a one-hour in-memory ring bounded to 3,600 | |
| 132 | snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes. | |
| 133 | History stays in the sidecar until a `readHistory` request and is returned in | |
| 134 | bounded chunks. The first bound reached wins, so high process counts or large | |
| 135 | process names and command lines shorten the effective history window. | |
| 136 | ||
| 137 | Periodic snapshot streaming is disabled by default. The server enables it only | |
| 138 | while at least one diagnostics subscription is retained. `sampleNow` remains | |
| 139 | available for explicit refreshes and identity validation. | |
| 140 | ||
| 141 | The server adjusts native sampling without restarting the sidecar: | |
| 142 | ||
| 143 | - suspended, locked, low-power, or serious/critical thermal state: 15 seconds; | |
| 144 | - battery: 5 seconds; | |
| 145 | - normal AC: 1 second; | |
| 146 | - unknown or stale power: 5 seconds in the background and 1 second while live | |
| 147 | diagnostics is open. | |
| 148 | ||
| 149 | ### Sampling limits | |
| 150 | ||
| 151 | This is counter sampling, not syscall tracing. | |
| 152 | ||
| 153 | - A process that starts and exits entirely between samples may not be observed. | |
| 154 | - Cumulative CPU and I/O counters still provide accurate deltas for processes | |
| 155 | that survive across samples. | |
| 156 | - Exact file paths, individual write syscalls, ETW events, eBPF events, and | |
| 157 | Endpoint Security events are outside this implementation. | |
| 158 | ||
| 159 | Those deeper tracing systems can be added later as opt-in diagnostic modes | |
| 160 | without changing the public `ResourceTelemetry` model. | |
| 161 | ||
| 162 | ## I/O semantics | |
| 163 | ||
| 164 | The monitor preserves platform semantics instead of presenting all counters as | |
| 165 | equivalent: | |
| 166 | ||
| 167 | - Unix-like platforms report storage I/O counters exposed by `sysinfo`. | |
| 168 | - Windows reports all process I/O bytes, not only disk bytes. | |
| 169 | - Operating-system caches can prevent logical application reads or writes from | |
| 170 | appearing as physical storage bytes. | |
| 171 | ||
| 172 | The UI therefore labels these values as I/O reads and writes and exposes the | |
| 173 | per-process `ioSemantics` value. | |
| 174 | ||
| 175 | Group totals are observed deltas since telemetry startup. Per-process total | |
| 176 | columns are the operating system's cumulative counters for that process. | |
| 177 | ||
| 178 | ## Electron telemetry | |
| 179 | ||
| 180 | Electron main owns `DesktopTelemetryPublisher`. | |
| 181 | ||
| 182 | Power events trigger an immediate snapshot. While diagnostics is closed, the | |
| 183 | server sends the configured active and idle host-power intervals to Electron | |
| 184 | (30 seconds and 2 minutes in the balanced profile). During those heartbeats | |
| 185 | Electron reads: | |
| 186 | ||
| 187 | - `powerMonitor.isOnBatteryPower()`; | |
| 188 | - `powerMonitor.getSystemIdleTime()`; | |
| 189 | - `powerMonitor.getSystemIdleState()`; | |
| 190 | - `powerMonitor.getCurrentThermalState()`. | |
| 191 | ||
| 192 | `app.getAppMetrics()` is only called while diagnostics demand is active. Its | |
| 193 | live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while | |
| 194 | locked, suspended, or thermally constrained. | |
| 195 | ||
| 196 | It also listens for: | |
| 197 | ||
| 198 | - lock and unlock; | |
| 199 | - suspend and resume; | |
| 200 | - AC and battery transitions; | |
| 201 | - thermal-state changes; | |
| 202 | - CPU speed-limit changes. | |
| 203 | ||
| 204 | Suspension stays latched across event-driven snapshots. An explicit resume | |
| 205 | clears it immediately; a periodic heartbeat also clears a stale latch so a | |
| 206 | missed resume event cannot leave telemetry permanently constrained. | |
| 207 | ||
| 208 | Electron does not expose a cross-platform low-power-mode getter, so that field | |
| 209 | remains `unknown`. | |
| 210 | ||
| 211 | The desktop backend is spawned with: | |
| 212 | ||
| 213 | - fd 3 for the existing bootstrap payload; | |
| 214 | - fd 4 for Electron-to-server telemetry NDJSON; | |
| 215 | - fd 5 for server-to-Electron diagnostics-demand NDJSON. | |
| 216 | ||
| 217 | These are private Electron-main/server pipes. They do not use the renderer | |
| 218 | WebSocket and are recreated for every backend restart. | |
| 219 | ||
| 220 | ## Server Effect services | |
| 221 | ||
| 222 | The implementation is under `apps/server/src/resourceTelemetry`. | |
| 223 | ||
| 224 | ### `ResourceMonitorBinary` | |
| 225 | ||
| 226 | Resolves an executable from: | |
| 227 | ||
| 228 | 1. `T3CODE_RESOURCE_MONITOR_PATH`; | |
| 229 | 2. desktop bootstrap configuration; | |
| 230 | 3. bundled CLI resources; | |
| 231 | 4. local Cargo build outputs. | |
| 232 | ||
| 233 | Unsupported platforms, missing binaries, and non-executable binaries use | |
| 234 | schema-backed tagged errors with descriptive messages. | |
| 235 | ||
| 236 | ### `NativeTelemetryClient` | |
| 237 | ||
| 238 | Owns the resource-monitor process and protocol. | |
| 239 | ||
| 240 | - validates the hello/version handshake; | |
| 241 | - sends configuration and external process roots; | |
| 242 | - adapts the native interval from host power state; | |
| 243 | - enables streaming only for scoped live subscribers; | |
| 244 | - reads chunked native history on demand; | |
| 245 | - exposes `sampleNow`; | |
| 246 | - serializes commands; | |
| 247 | - supervises process exit and protocol failure; | |
| 248 | - restarts with bounded exponential backoff; | |
| 249 | - opens a circuit after repeated failures; | |
| 250 | - supports explicit retry; | |
| 251 | - publishes health changes immediately. | |
| 252 | ||
| 253 | Snapshot sequence numbers are scoped to a monitor generation. Server ingestion | |
| 254 | uses the monitor restart count as the generation key, so sequence reset after a | |
| 255 | restart cannot freeze telemetry. | |
| 256 | ||
| 257 | ### `DesktopTelemetryReceiver` | |
| 258 | ||
| 259 | Reads fd 4, decodes schema-validated messages, stores the latest Electron | |
| 260 | snapshot, and publishes desktop health. It writes diagnostics demand to fd 5 | |
| 261 | and gives the first sample a 90-second startup deadline. Once samples arrive, | |
| 262 | the stale deadline stays beyond the slower configured host-power interval with | |
| 263 | 30 seconds of scheduling grace, so intentional 2–10 minute idle polling does | |
| 264 | not oscillate the policy between constrained and unconstrained states. Decode | |
| 265 | errors, protocol mismatch, control-write failure, stream failure, stale input, | |
| 266 | and normal stream closure are represented explicitly. | |
| 267 | ||
| 268 | ### `ResourceTelemetry` | |
| 269 | ||
| 270 | Merges native and Electron data and owns public telemetry semantics. | |
| 271 | ||
| 272 | - calculates CPU and I/O rates from cumulative native counters; | |
| 273 | - preserves the last native rates during desktop-only updates; | |
| 274 | - classifies backend, Electron, and monitor processes; | |
| 275 | - computes process depth and child relationships; | |
| 276 | - tracks starts, exits, CPU time, and observed I/O; | |
| 277 | - projects power data; | |
| 278 | - acquires native streaming and Electron process metrics only for scoped live | |
| 279 | subscribers; | |
| 280 | - queries and replays native history only when requested; | |
| 281 | - validates `(pid, startTimeMs)` before process signaling; | |
| 282 | - updates history health even when no further native sample arrives. | |
| 283 | ||
| 284 | Electron and monitor processes are visible but are not valid targets for the | |
| 285 | existing process-signal RPC. | |
| 286 | ||
| 287 | ### History projection | |
| 288 | ||
| 289 | `ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native | |
| 290 | snapshots to derive rates, lifecycle counters, buckets, and process summaries. | |
| 291 | Current Electron process metrics are intentionally excluded from historical | |
| 292 | replay so they cannot overwrite older native CPU or memory samples. | |
| 293 | ||
| 294 | ### `ResourceAttribution` | |
| 295 | ||
| 296 | Tracks known logical application I/O separately from OS counters. Current | |
| 297 | integration points record successful writes for: | |
| 298 | ||
| 299 | - provider native and canonical event logs; | |
| 300 | - the local server trace sink. | |
| 301 | ||
| 302 | Entries contain component, operation, logical bytes, count, and elapsed time. | |
| 303 | Future persistence paths should call `ResourceAttribution.record` rather than | |
| 304 | adding diagnostics-specific counters. | |
| 305 | ||
| 306 | ## Background policy integration | |
| 307 | ||
| 308 | `HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host | |
| 309 | power does not retain live resource diagnostics or invoke shell probes. | |
| 310 | ||
| 311 | The monitor updates its latest timestamp on every Electron sample but only | |
| 312 | publishes semantic state changes. Increasing idle seconds alone does not cause a | |
| 313 | background-policy broadcast every second. | |
| 314 | ||
| 315 | ## Public API and UI | |
| 316 | ||
| 317 | The WebSocket RPC surface provides: | |
| 318 | ||
| 319 | - current snapshot; | |
| 320 | - bounded history; | |
| 321 | - explicit monitor retry; | |
| 322 | - a live snapshot subscription. | |
| 323 | ||
| 324 | The diagnostics page displays: | |
| 325 | ||
| 326 | - aggregate CPU, memory, I/O, and process counts; | |
| 327 | - backend, Electron, and monitor overhead groups; | |
| 328 | - power and thermal state; | |
| 329 | - collector health and restart information; | |
| 330 | - CPU and I/O history; | |
| 331 | - a collapsible live process tree; | |
| 332 | - safe process signaling for backend descendants; | |
| 333 | - instrumented logical application I/O. | |
| 334 | ||
| 335 | Legacy process diagnostics RPCs are projected from the same service so they no | |
| 336 | longer start recurring process-table commands. | |
| 337 | ||
| 338 | ## Packaging | |
| 339 | ||
| 340 | Desktop artifact builds compile the Rust target, stage it as | |
| 341 | `resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the | |
| 342 | backend bootstrap. | |
| 343 | ||
| 344 | CLI release jobs upload each active platform monitor artifact and copy it into: | |
| 345 | ||
| 346 | ```text | |
| 347 | apps/server/dist/resource-monitor/<platform>-<arch>/ | |
| 348 | ``` | |
| 349 | ||
| 350 | The published server package already includes `dist`, so those executables ship | |
| 351 | with the CLI. Missing platform artifacts degrade native telemetry to | |
| 352 | `unavailable`; the server continues running. | |
| 353 | ||
| 354 | ## Resource and failure behavior | |
| 355 | ||
| 356 | Steady state uses: | |
| 357 | ||
| 358 | - one native process; | |
| 359 | - power-adaptive native counter sampling with no periodic Node snapshot stream; | |
| 360 | - event-driven Electron power updates plus profile-driven heartbeats (30–60 | |
| 361 | seconds while active and 2–10 minutes while idle); | |
| 362 | - no `app.getAppMetrics()` calls while diagnostics is closed; | |
| 363 | - no telemetry database; | |
| 364 | - no recurring shell probes; | |
| 365 | - bounded PubSub queues and native ring history. | |
| 366 | ||
| 367 | The diagnostics page exposes the monitor's own process resource usage and | |
| 368 | collection duration so the observer's cost is measurable. | |
| 369 | ||
| 370 | Failures are isolated: | |
| 371 | ||
| 372 | - native failure does not stop the server; | |
| 373 | - Electron telemetry loss does not stop native telemetry; | |
| 374 | - schema/version errors are visible in health; | |
| 375 | - repeated native failures stop automatic restart churn until explicit retry; | |
| 376 | - server and desktop shutdown close their respective streams and child process | |
| 377 | scopes. | |
| 378 | ||
| 379 | ## Future integration points | |
| 380 | ||
| 381 | High-value follow-up work can use the existing service boundaries: | |
| 382 | ||
| 383 | - opt-in file-path attribution through platform-specific tracing; | |
| 384 | - process lifecycle events to reduce the chance of missing very short-lived | |
| 385 | children; | |
| 386 | - additional `ResourceAttribution` instrumentation for databases, checkpoints, | |
| 387 | caches, and file synchronization; | |
| 388 | - exported diagnostic bundles; | |
| 389 | - adaptive sample intervals based on diagnostics visibility and active work. | |
| 390 | ||
| 391 | These additions should preserve the current rules: direct platform APIs, | |
| 392 | schema-validated boundaries, explicit metric semantics, bounded retention, and | |
| 393 | no mandatory telemetry persistence. |
docs/internals/scripts.md created+119| ... | ... | @@ -0,0 +1,119 @@ |
| 1 | # Scripts | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | ## First checkout | |
| 6 | ||
| 7 | T3 Code uses [Vite+](https://viteplus.dev/guide/). Install the global `vp` command, install | |
| 8 | dependencies, then start the dev stack: | |
| 9 | ||
| 10 | ```bash | |
| 11 | curl -fsSL https://vite.plus | bash # Windows: irm https://vite.plus/ps1 | iex | |
| 12 | vp i | |
| 13 | vp run dev | |
| 14 | ``` | |
| 15 | ||
| 16 | Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to | |
| 17 | Node otherwise, and nothing in contributor setup needs it. | |
| 18 | ||
| 19 | `vp run dev` prints a one-time pairing URL. Open it so the first browser navigation is | |
| 20 | authenticated. | |
| 21 | ||
| 22 | ## Dev | |
| 23 | ||
| 24 | - `vp run dev`: Starts contracts, server, and web in watch mode. | |
| 25 | - `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The | |
| 26 | startup pairing URL is built against the shared origin, and the mapping is removed on exit. | |
| 27 | - `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes | |
| 28 | `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has | |
| 29 | no effect; use `--browser`. | |
| 30 | - `vp run dev:server`: Starts just the server. It runs on Node (`node --watch src/bin.ts`), so | |
| 31 | without Bun present it selects `NodePtyAdapter` and `NodeHttpServer`. | |
| 32 | - `vp run dev:web`: Starts just the Vite dev server for the web app. | |
| 33 | - `vp run dev:desktop`: Starts the Electron shell against the dev server. | |
| 34 | - `vp run dev:marketing`: Starts the Astro marketing site. | |
| 35 | - Pass dev-runner flags directly after the root task name, for example: | |
| 36 | `vp run dev --home-dir /tmp/t3code-dev` | |
| 37 | ||
| 38 | ### Dev state directories | |
| 39 | ||
| 40 | - Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even | |
| 41 | when `T3CODE_HOME` is set, storing state in `<worktree>/.t3/userdata`. Pass `--home-dir <path>` to | |
| 42 | choose another isolated directory explicitly. Submodules are not worktrees and keep the normal | |
| 43 | precedence. | |
| 44 | - From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state | |
| 45 | separate from `~/.t3/userdata`. An explicit `--home-dir <path>` stores state under | |
| 46 | `<path>/userdata`; the base directory remains available for caches, worktrees, and other shared | |
| 47 | data. | |
| 48 | ||
| 49 | ## Build, check, test | |
| 50 | ||
| 51 | - `vp run build`: Fans out over `apps/*`, `packages/*`, `oxlint-plugin-t3code`, and `scripts`. | |
| 52 | Workspaces that define a build task run one: desktop, marketing, server (which depends on web), and | |
| 53 | web. Shared packages are consumed and bundled transitively rather than built separately. | |
| 54 | - `vp run build:desktop`: Builds the desktop pipeline (desktop plus server). | |
| 55 | - `vp run start`: Runs the production server (serves the built web app as static files). | |
| 56 | - `vp check`: Vite+ format, lint, and type checks. This repo sets `typeCheck: false` in its lint | |
| 57 | options, so workspace type checking runs separately. | |
| 58 | - `vp run typecheck`: Strict TypeScript checks for all packages. | |
| 59 | - `vp run test`: Runs workspace tests. | |
| 60 | - `vp run lint:mobile`: Mobile native static analysis (`scripts/mobile-native-static-check.ts`). | |
| 61 | - `node apps/server/scripts/t3-sqlite-state.ts <query|exec> --base-dir <path> ...`: Inspects or seeds | |
| 62 | an isolated T3 SQLite database; writes create a private backup first. | |
| 63 | ||
| 64 | ## Desktop artifacts | |
| 65 | ||
| 66 | - `vp run dist:desktop:artifact --platform <mac|linux|win> --target <target> --arch <arch>`: Builds a desktop artifact for a specific platform/target/arch. | |
| 67 | - `vp run dist:desktop:dmg`: Builds a shareable macOS `.dmg` into `./release`. Architecture defaults | |
| 68 | to the host, so this produces an arm64 DMG on Apple Silicon. Use `dist:desktop:dmg:arm64` or | |
| 69 | `dist:desktop:dmg:x64`, or pass `--arch <arm64|x64|universal>`, to force one. | |
| 70 | - `vp run dist:desktop:linux`: Builds a Linux AppImage into `./release`. | |
| 71 | - `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64` | |
| 72 | variants exist. | |
| 73 | ||
| 74 | ### Desktop `.dmg` packaging notes | |
| 75 | ||
| 76 | - Default build is unsigned/not notarized for local sharing. | |
| 77 | - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. | |
| 78 | - Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a | |
| 79 | `127.0.0.1` document URL, and not an explicit `index.html` path). | |
| 80 | - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an | |
| 81 | auth token for WebSocket/API traffic. | |
| 82 | - Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first | |
| 83 | launch. | |
| 84 | - To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg --keep-stage` | |
| 85 | - To allow code-signing/notarization when configured in CI/secrets, add: `--signed`. | |
| 86 | - Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and | |
| 87 | `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from | |
| 88 | `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it. | |
| 89 | - Windows `--signed` uses Azure Trusted Signing and expects: | |
| 90 | `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`, | |
| 91 | `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`. | |
| 92 | - Azure authentication env vars are also required (for example service principal with secret): | |
| 93 | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. | |
| 94 | ||
| 95 | ## Browser development | |
| 96 | ||
| 97 | `dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend | |
| 98 | from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the | |
| 99 | server, allowing the same bundle to work from localhost or a tailnet hostname. | |
| 100 | ||
| 101 | ## Running multiple dev instances | |
| 102 | ||
| 103 | Worktrees derive a preferred port offset from their path. | |
| 104 | ||
| 105 | - Default ports: server `13773`, web `5733` | |
| 106 | - Shifted ports: `base + offset` | |
| 107 | - Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop` | |
| 108 | ||
| 109 | Offset resolution, in order: | |
| 110 | ||
| 111 | 1. `T3CODE_PORT_OFFSET`, which must be a non-negative integer. Negative values are rejected. | |
| 112 | 2. `T3CODE_DEV_INSTANCE`. An all-digit value is used directly as the offset; any other non-empty | |
| 113 | value is hashed into one. | |
| 114 | 3. The worktree path hash. | |
| 115 | ||
| 116 | Collision scanning depends on the mode. `dev:web` scans only the web port and shifts only the web | |
| 117 | offset. `dev:server` scans only the server port. `dev` and `dev:desktop` scan both and shift them | |
| 118 | together as one shared offset. Explicit server or dev-URL overrides remove the corresponding port | |
| 119 | from the availability check. Treat the `[dev-runner]` output as authoritative. |
docs/internals/server-updates.md created+140| ... | ... | @@ -0,0 +1,140 @@ |
| 1 | # Server Update Architecture | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | T3 Code can update a connected server to the exact version of the client that detected version | |
| 6 | drift. This path exists primarily for remote environments, where the user may not have a terminal | |
| 7 | open on the server machine. | |
| 8 | ||
| 9 | The feature has three boundaries: | |
| 10 | ||
| 11 | - the server advertises whether and how it can be replaced; | |
| 12 | - the client chooses the matching user action; | |
| 13 | - the server installs and verifies the replacement before handing off the process. | |
| 14 | ||
| 15 | ## Detection and Presentation | |
| 16 | ||
| 17 | `ExecutionEnvironmentDescriptor` includes the server version and an optional | |
| 18 | `capabilities.serverSelfUpdate` value. Progress-capable servers also advertise | |
| 19 | `capabilities.serverSelfUpdateProgress`. The client compares the server version with `APP_VERSION` | |
| 20 | after loading server config. | |
| 21 | ||
| 22 | The optional capability is intentionally backward compatible. An older server does not know about | |
| 23 | the field, so a missing value means the client must offer a manual relaunch instead of sending an | |
| 24 | unknown RPC. | |
| 25 | ||
| 26 | The shared `ServerUpdateAction` is rendered in both user-facing version-drift surfaces: | |
| 27 | ||
| 28 | - the conversation banner in `ChatView`; | |
| 29 | - primary and saved environment rows in **Settings** → **Connections**. | |
| 30 | ||
| 31 | Both surfaces target the client's exact version. When the reconnected server reports that version, | |
| 32 | the mismatch and action disappear. | |
| 33 | ||
| 34 | The operation state lives in `packages/client-runtime`, keyed by environment. Both web surfaces read | |
| 35 | the same `downloading`, `installing`, or `resuming` state, so route changes do not own or cancel the | |
| 36 | operation. | |
| 37 | ||
| 38 | ## Capability Selection | |
| 39 | ||
| 40 | The server resolves its capability once at startup and publishes it in the environment descriptor. | |
| 41 | ||
| 42 | | Advertised value | Process shape | Client behavior | | |
| 43 | | ----------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | |
| 44 | | `boot-service` | Linux server running under the T3-managed systemd user service | Call the update RPC; the service unit is replaced and restarted. | | |
| 45 | | `respawn` | Published npm CLI running in the foreground on macOS or Linux | Call the update RPC; the process hands off to a detached replacement. | | |
| 46 | | `desktop-managed` | Backend supervised by the desktop app | Tell the user to update the desktop app on the server machine. | | |
| 47 | | absent | Older server, development checkout, Windows foreground process, or an unrecognized supervisor | Offer the exact manual relaunch command. | | |
| 48 | ||
| 49 | Desktop ownership takes precedence over process-shape detection. A desktop-managed backend must | |
| 50 | never spawn a second CLI server beside the app-owned process. Likewise, a process launched by an | |
| 51 | unrecognized systemd unit does not claim the foreground respawn path because its supervisor could | |
| 52 | bring the old version back. | |
| 53 | ||
| 54 | ## Update Flow | |
| 55 | ||
| 56 | ```mermaid | |
| 57 | flowchart TD | |
| 58 | A[Client detects different versions] --> B{Advertised update path} | |
| 59 | B -->|desktop-managed| C[Update desktop app on server machine] | |
| 60 | B -->|missing| D[Copy exact manual relaunch command] | |
| 61 | B -->|boot-service or respawn| E{Progress capability} | |
| 62 | E -->|present| F[server.updateServerWithProgress] | |
| 63 | E -->|missing| G[server.updateServer fallback] | |
| 64 | F --> H[Install exact t3 version in pinned runtime] | |
| 65 | G --> H | |
| 66 | H --> I[Run version preflight] | |
| 67 | I -->|bad code or version| J[Remove candidate runtime and keep current server] | |
| 68 | I -->|cannot run preflight| J2[Keep candidate and current server] | |
| 69 | I -->|passes| K{Handoff method} | |
| 70 | K -->|boot-service| L[Rewrite and restart T3 systemd unit] | |
| 71 | K -->|respawn| M[Start delayed replacement and exit current process] | |
| 72 | L --> N[Reconnect with fresh backoff] | |
| 73 | M --> N | |
| 74 | N --> O[Replacement publishes ready at target version] | |
| 75 | ``` | |
| 76 | ||
| 77 | Both update RPCs require the environment's `orchestration:operate` authorization scope. Their | |
| 78 | payload accepts only an exact npm version, including an exact prerelease version; dist-tags such as | |
| 79 | `latest` and `nightly` are rejected. The unary `server.updateServer` method remains available so a | |
| 80 | new client can still repair skew with an older server. | |
| 81 | ||
| 82 | The update service permits one update at a time. It installs `t3@<version>` under | |
| 83 | `<baseDir>/runtime/versions/<version>` and writes an install-complete sentinel only after npm exits | |
| 84 | successfully. Boot-service setup and self-update share the same process-wide installation lock, so | |
| 85 | they cannot mutate a pinned runtime concurrently. | |
| 86 | ||
| 87 | Before any restart, the current Node executable runs the replacement with `--version`. A failed | |
| 88 | install, failed preflight, or wrong reported version leaves the current server running. | |
| 89 | ||
| 90 | Candidate cleanup is narrower than "any failed preflight". The candidate runtime is removed only when | |
| 91 | the preflight process actually completes and reports a bad exit code or the wrong version: that is | |
| 92 | the case where a completed npm install produced an unusable tree, so retrying the same version must | |
| 93 | perform a clean install rather than reuse it. If the preflight cannot run at all, for example a spawn | |
| 94 | error or the `PREFLIGHT_TIMEOUT` elapsing, the update fails before reaching cleanup and the candidate | |
| 95 | directory is left in place. | |
| 96 | ||
| 97 | ## Host Service Lifecycle | |
| 98 | ||
| 99 | The systemd user service is a host lifecycle concern, not a T3 Connect resource. The standalone | |
| 100 | `t3 service install`, `uninstall`, `update`, and `status` commands own it. Install and update both | |
| 101 | reconcile the unit through `BootService`; running `npx t3@latest service update` therefore pins and | |
| 102 | activates the latest CLI release without requiring a connected client. | |
| 103 | ||
| 104 | The `t3 connect` onboarding flow may offer service installation, but it calls the same reconciliation | |
| 105 | operation as `t3 service install`. Connect logout only disables cloud access and clears its | |
| 106 | authorization; it does not uninstall the host service. | |
| 107 | ||
| 108 | ## Process Handoff | |
| 109 | ||
| 110 | For `boot-service`, the server atomically rewrites the T3-managed user unit to point at the verified | |
| 111 | runtime and reloads systemd. It acknowledges the handoff, then restarts the unit after the same | |
| 112 | short grace period used by foreground respawn. A rejected deferred restart restores the previous | |
| 113 | unit and is logged by the still-running process. | |
| 114 | ||
| 115 | For `respawn`, the server starts a detached, delayed replacement that replays the original CLI | |
| 116 | arguments. It then acknowledges the request and schedules the current process to exit. The delays | |
| 117 | give the acknowledgement time to cross direct or relayed connections before the socket closes. | |
| 118 | ||
| 119 | Progress-capable servers emit `downloading` before installing the pinned runtime and `installing` | |
| 120 | before preflight and handoff. A terminal stream event acknowledges that restart is scheduled. The | |
| 121 | client then enters `resuming`, waits for the replacement lifecycle stream to publish `ready` with | |
| 122 | the target version, and only then completes the operation. It watches for the intentional | |
| 123 | disconnect's first backoff state and requests one fresh retry, which clears historical backoff debt | |
| 124 | without adding a separate reconnect loop. | |
| 125 | ||
| 126 | ## Release Invariant | |
| 127 | ||
| 128 | The exact client version must exist as the `t3` npm package before a client carrying that version is | |
| 129 | published. The release workflow therefore makes the GitHub release depend on CLI publication, and | |
| 130 | the hosted web deployment depends on that release. See [Release Checklist](../operations/release.md#server-self-update-release-invariant). | |
| 131 | ||
| 132 | ## Source Map | |
| 133 | ||
| 134 | - Capability contract: `packages/contracts/src/environment.ts` | |
| 135 | - Update RPC contract: `packages/contracts/src/server.ts` and `packages/contracts/src/rpc.ts` | |
| 136 | - Capability detection and handoff: `apps/server/src/cloud/selfUpdate.ts` | |
| 137 | - Host service commands: `apps/server/src/cli/service.ts` | |
| 138 | - Pinned runtime installation: `apps/server/src/cloud/pinnedRuntime.ts` | |
| 139 | - Client version comparison: `apps/web/src/versionSkew.ts` | |
| 140 | - Shared update action: `apps/web/src/components/ServerUpdateAction.tsx` |
docs/internals/t3-code-connect-auth-flow.html created+1160| ... | ... | @@ -0,0 +1,1160 @@ |
| 1 | <!doctype html> | |
| 2 | <html lang="en"> | |
| 3 | <head> | |
| 4 | <meta charset="utf-8" /> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 6 | <title>T3 Connect Architecture</title> | |
| 7 | <style> | |
| 8 | :root { | |
| 9 | color-scheme: light; | |
| 10 | --ink: #171717; | |
| 11 | --muted: #62646b; | |
| 12 | --hairline: #d8d8dd; | |
| 13 | --panel: #ffffff; | |
| 14 | --paper: #f5f5f2; | |
| 15 | --blue: #1457d9; | |
| 16 | --green: #1d7d4d; | |
| 17 | --amber: #a65f00; | |
| 18 | --red: #b8242c; | |
| 19 | --violet: #6150b8; | |
| 20 | --shadow: 0 18px 55px rgba(25, 25, 28, 0.08); | |
| 21 | } | |
| 22 | ||
| 23 | * { | |
| 24 | box-sizing: border-box; | |
| 25 | } | |
| 26 | ||
| 27 | body { | |
| 28 | margin: 0; | |
| 29 | background: | |
| 30 | linear-gradient(90deg, rgba(0, 0, 0, 0.035) 1px, transparent 1px), | |
| 31 | linear-gradient(rgba(0, 0, 0, 0.035) 1px, transparent 1px), var(--paper); | |
| 32 | background-size: 34px 34px; | |
| 33 | color: var(--ink); | |
| 34 | font-family: | |
| 35 | ui-sans-serif, | |
| 36 | -apple-system, | |
| 37 | BlinkMacSystemFont, | |
| 38 | "Segoe UI", | |
| 39 | sans-serif; | |
| 40 | line-height: 1.48; | |
| 41 | } | |
| 42 | ||
| 43 | .shell { | |
| 44 | width: min(1540px, calc(100vw - 40px)); | |
| 45 | margin: 0 auto; | |
| 46 | padding: 38px 0 72px; | |
| 47 | } | |
| 48 | ||
| 49 | header { | |
| 50 | border-bottom: 2px solid var(--ink); | |
| 51 | padding-bottom: 24px; | |
| 52 | margin-bottom: 28px; | |
| 53 | } | |
| 54 | ||
| 55 | .eyebrow { | |
| 56 | color: var(--blue); | |
| 57 | font-size: 12px; | |
| 58 | font-weight: 800; | |
| 59 | letter-spacing: 0.08em; | |
| 60 | text-transform: uppercase; | |
| 61 | } | |
| 62 | ||
| 63 | h1 { | |
| 64 | max-width: 980px; | |
| 65 | margin: 8px 0 12px; | |
| 66 | font-size: clamp(34px, 5vw, 66px); | |
| 67 | line-height: 0.98; | |
| 68 | } | |
| 69 | ||
| 70 | h2 { | |
| 71 | margin: 0 0 14px; | |
| 72 | font-size: 26px; | |
| 73 | line-height: 1.08; | |
| 74 | } | |
| 75 | ||
| 76 | h3 { | |
| 77 | margin: 22px 0 8px; | |
| 78 | font-size: 17px; | |
| 79 | } | |
| 80 | ||
| 81 | p { | |
| 82 | margin: 0 0 12px; | |
| 83 | } | |
| 84 | ||
| 85 | code, | |
| 86 | .mono { | |
| 87 | font-family: | |
| 88 | ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| 89 | } | |
| 90 | ||
| 91 | code { | |
| 92 | border-radius: 4px; | |
| 93 | background: #f0f0ed; | |
| 94 | padding: 1px 4px; | |
| 95 | font-size: 0.92em; | |
| 96 | } | |
| 97 | ||
| 98 | .lede { | |
| 99 | max-width: 1000px; | |
| 100 | margin: 0; | |
| 101 | color: var(--muted); | |
| 102 | font-size: 18px; | |
| 103 | } | |
| 104 | ||
| 105 | .grid { | |
| 106 | display: grid; | |
| 107 | grid-template-columns: repeat(12, 1fr); | |
| 108 | gap: 18px; | |
| 109 | } | |
| 110 | ||
| 111 | .panel { | |
| 112 | grid-column: span 12; | |
| 113 | border: 1px solid var(--hairline); | |
| 114 | border-radius: 10px; | |
| 115 | background: var(--panel); | |
| 116 | box-shadow: var(--shadow); | |
| 117 | padding: 22px; | |
| 118 | } | |
| 119 | ||
| 120 | .half { | |
| 121 | grid-column: span 6; | |
| 122 | } | |
| 123 | ||
| 124 | .callout { | |
| 125 | border-left: 5px solid var(--blue); | |
| 126 | background: #eef4ff; | |
| 127 | padding: 14px 16px; | |
| 128 | margin-top: 14px; | |
| 129 | } | |
| 130 | ||
| 131 | .callout.warning { | |
| 132 | border-left-color: var(--amber); | |
| 133 | background: #fff6e6; | |
| 134 | } | |
| 135 | ||
| 136 | .badge-row { | |
| 137 | display: flex; | |
| 138 | flex-wrap: wrap; | |
| 139 | gap: 8px; | |
| 140 | margin-top: 14px; | |
| 141 | } | |
| 142 | ||
| 143 | .badge { | |
| 144 | border: 1px solid var(--hairline); | |
| 145 | border-radius: 999px; | |
| 146 | background: #fff; | |
| 147 | padding: 5px 9px; | |
| 148 | font-size: 12px; | |
| 149 | font-weight: 700; | |
| 150 | } | |
| 151 | ||
| 152 | .badge.user { | |
| 153 | border-color: rgba(20, 87, 217, 0.3); | |
| 154 | color: var(--blue); | |
| 155 | } | |
| 156 | ||
| 157 | .badge.env { | |
| 158 | border-color: rgba(29, 125, 77, 0.3); | |
| 159 | color: var(--green); | |
| 160 | } | |
| 161 | ||
| 162 | .badge.local { | |
| 163 | border-color: rgba(166, 95, 0, 0.35); | |
| 164 | color: var(--amber); | |
| 165 | } | |
| 166 | ||
| 167 | .diagram { | |
| 168 | overflow-x: auto; | |
| 169 | margin: 12px 0 0; | |
| 170 | border: 1px solid var(--hairline); | |
| 171 | border-radius: 8px; | |
| 172 | background: #fbfbf8; | |
| 173 | color: var(--ink); | |
| 174 | padding: 18px; | |
| 175 | } | |
| 176 | ||
| 177 | .diagram svg { | |
| 178 | display: block; | |
| 179 | width: auto !important; | |
| 180 | min-width: 100%; | |
| 181 | max-width: none !important; | |
| 182 | height: auto; | |
| 183 | margin: 0 auto; | |
| 184 | } | |
| 185 | ||
| 186 | table { | |
| 187 | width: 100%; | |
| 188 | border-collapse: collapse; | |
| 189 | font-size: 14px; | |
| 190 | } | |
| 191 | ||
| 192 | th, | |
| 193 | td { | |
| 194 | border-bottom: 1px solid var(--hairline); | |
| 195 | padding: 11px 9px; | |
| 196 | text-align: left; | |
| 197 | vertical-align: top; | |
| 198 | } | |
| 199 | ||
| 200 | th { | |
| 201 | color: var(--muted); | |
| 202 | font-size: 12px; | |
| 203 | letter-spacing: 0.05em; | |
| 204 | text-transform: uppercase; | |
| 205 | } | |
| 206 | ||
| 207 | ul { | |
| 208 | margin: 8px 0 0; | |
| 209 | padding-left: 20px; | |
| 210 | } | |
| 211 | ||
| 212 | li + li { | |
| 213 | margin-top: 6px; | |
| 214 | } | |
| 215 | ||
| 216 | .flow { | |
| 217 | display: grid; | |
| 218 | gap: 10px; | |
| 219 | margin-top: 12px; | |
| 220 | } | |
| 221 | ||
| 222 | .step { | |
| 223 | border: 1px solid var(--hairline); | |
| 224 | border-left: 5px solid var(--green); | |
| 225 | border-radius: 8px; | |
| 226 | background: #fff; | |
| 227 | padding: 12px 14px; | |
| 228 | } | |
| 229 | ||
| 230 | .step strong { | |
| 231 | display: block; | |
| 232 | margin-bottom: 3px; | |
| 233 | } | |
| 234 | ||
| 235 | .source-list a { | |
| 236 | color: var(--blue); | |
| 237 | font-weight: 700; | |
| 238 | } | |
| 239 | ||
| 240 | @media (max-width: 860px) { | |
| 241 | .half { | |
| 242 | grid-column: span 12; | |
| 243 | } | |
| 244 | ||
| 245 | .shell { | |
| 246 | width: min(100vw - 24px, 1540px); | |
| 247 | padding-top: 24px; | |
| 248 | } | |
| 249 | } | |
| 250 | </style> | |
| 251 | </head> | |
| 252 | <body> | |
| 253 | <main class="shell"> | |
| 254 | <header> | |
| 255 | <div class="eyebrow">Implemented architecture</div> | |
| 256 | <h1>T3 Connect Control Plane and Managed Endpoint Flow</h1> | |
| 257 | <p class="lede"> | |
| 258 | T3 Connect links a locally authorized T3 environment to a signed-in cloud user, provisions | |
| 259 | a managed HTTPS/WSS endpoint for that environment, and brokers proof-bound remote | |
| 260 | connections. The local environment remains the authority that issues environment access | |
| 261 | credentials. The hosted relay stores links, reconciles managed endpoint resources, | |
| 262 | validates signed proofs, and delivers agent-activity notifications. | |
| 263 | </p> | |
| 264 | <div class="badge-row"> | |
| 265 | <span class="badge user">Client -> Relay: Clerk bearer or relay DPoP</span> | |
| 266 | <span class="badge env">Relay -> Environment: relay-signed health and mint proofs</span> | |
| 267 | <span class="badge env" | |
| 268 | >Environment -> Relay: environment bearer plus signed activity proof</span | |
| 269 | > | |
| 270 | <span class="badge local">Client -> Environment: environment access token plus DPoP</span> | |
| 271 | </div> | |
| 272 | </header> | |
| 273 | ||
| 274 | <section class="grid" aria-label="Architecture summary"> | |
| 275 | <article class="panel half"> | |
| 276 | <h2>Security Invariant</h2> | |
| 277 | <p> | |
| 278 | A cloud identity is not an environment login. Remote access requires all of the | |
| 279 | following: | |
| 280 | </p> | |
| 281 | <ul> | |
| 282 | <li>a Clerk-authenticated T3 Connect user;</li> | |
| 283 | <li>an active relay link for that user and environment;</li> | |
| 284 | <li>a DPoP proof key held by the remote client;</li> | |
| 285 | <li>a relay-signed mint request accepted by the linked local environment; and</li> | |
| 286 | <li>an environment-issued credential bound to the remote client's DPoP thumbprint.</li> | |
| 287 | </ul> | |
| 288 | <div class="callout"> | |
| 289 | The relay cannot mint an environment access token by itself. It can only ask the local | |
| 290 | environment to mint a short-lived bootstrap credential for an authorized cloud user. | |
| 291 | </div> | |
| 292 | <p>During an honest connection flow, the relay cannot act as the remote client:</p> | |
| 293 | <ul> | |
| 294 | <li> | |
| 295 | Normal environment API traffic goes directly from the remote client through the | |
| 296 | managed endpoint. The relay does not receive the resulting environment access token. | |
| 297 | </li> | |
| 298 | <li> | |
| 299 | The one-time bootstrap credential and resulting environment access token are bound to | |
| 300 | the remote client's DPoP public-key thumbprint. Redeeming or using them requires | |
| 301 | request proofs signed by the corresponding private key, which stays on the client. | |
| 302 | </li> | |
| 303 | <li> | |
| 304 | Relay-signed health and mint proofs are short-lived, audience-bound, scoped, and | |
| 305 | replay-guarded. They are accepted only by the narrow cloud endpoints, not as normal | |
| 306 | environment API credentials. | |
| 307 | </li> | |
| 308 | <li> | |
| 309 | Environment health and mint responses are signed by the linked environment and bound | |
| 310 | to the request nonce, so a different process behind the tunnel cannot impersonate the | |
| 311 | linked environment. | |
| 312 | </li> | |
| 313 | </ul> | |
| 314 | <div class="callout warning"> | |
| 315 | The hosted relay remains a trusted mint broker: it holds the cloud-mint signing key. | |
| 316 | DPoP prevents credential reuse by the relay or tunnel transport during an honest flow, | |
| 317 | but it does not make compromise of the relay signing authority harmless. | |
| 318 | </div> | |
| 319 | </article> | |
| 320 | ||
| 321 | <article class="panel half"> | |
| 322 | <h2>Product Boundaries</h2> | |
| 323 | <p> | |
| 324 | The generic local connector is called the <strong>relay client</strong>. The current | |
| 325 | implementation layer uses <code>cloudflared</code>, but that is an internal transport | |
| 326 | detail rather than the product contract. | |
| 327 | </p> | |
| 328 | <ul> | |
| 329 | <li> | |
| 330 | The relay Worker owns cloud links, managed endpoint allocations, and notifications. | |
| 331 | </li> | |
| 332 | <li> | |
| 333 | The environment server owns local sessions, environment keys, and access tokens. | |
| 334 | </li> | |
| 335 | <li>The relay client exposes only the environment's loopback HTTP server.</li> | |
| 336 | <li> | |
| 337 | Web and mobile clients connect directly to the managed environment endpoint after | |
| 338 | bootstrap. | |
| 339 | </li> | |
| 340 | </ul> | |
| 341 | </article> | |
| 342 | </section> | |
| 343 | ||
| 344 | <section class="panel" aria-label="Topology"> | |
| 345 | <h2>Current Topology</h2> | |
| 346 | <pre class="diagram mermaid"> | |
| 347 | flowchart LR | |
| 348 | subgraph Clients["User-facing clients"] | |
| 349 | Web["Desktop / hosted web UI"] | |
| 350 | Mobile["Mobile app"] | |
| 351 | CLI["Headless CLI"] | |
| 352 | end | |
| 353 | ||
| 354 | Clerk["Clerk<br/>user identity and OAuth"] | |
| 355 | ||
| 356 | subgraph Relay["T3 Code Relay on Cloudflare"] | |
| 357 | Worker["Relay Worker<br/>HTTP API"] | |
| 358 | Queue["APNs delivery queue<br/>with dead-letter queue"] | |
| 359 | Hyperdrive["Hyperdrive"] | |
| 360 | CF["Cloudflare tunnel + DNS APIs"] | |
| 361 | end | |
| 362 | ||
| 363 | DB["PlanetScale Postgres<br/>prod database + stage branches"] | |
| 364 | APNs["Apple Push Notification service"] | |
| 365 | Traces["Axiom OTLP traces"] | |
| 366 | ||
| 367 | subgraph Local["Linked T3 environment"] | |
| 368 | Env["Environment server<br/>local auth authority"] | |
| 369 | RelayClient["Relay client<br/>cloudflared implementation"] | |
| 370 | Secrets["Server secret store<br/>keys + relay config"] | |
| 371 | end | |
| 372 | ||
| 373 | Endpoint["Managed HTTPS/WSS endpoint"] | |
| 374 | ||
| 375 | Web -->|"Clerk bearer / relay DPoP"| Worker | |
| 376 | Mobile -->|"Clerk bearer / relay DPoP"| Worker | |
| 377 | CLI -->|"Clerk OAuth bearer"| Worker | |
| 378 | Web --> Clerk | |
| 379 | Mobile --> Clerk | |
| 380 | CLI -->|"PKCE authorization code flow"| Clerk | |
| 381 | Worker --> Hyperdrive --> DB | |
| 382 | Worker --> Traces | |
| 383 | Worker --> CF | |
| 384 | Worker --> Queue --> APNs | |
| 385 | Worker --> Endpoint | |
| 386 | Endpoint --> RelayClient -->|"http://127.0.0.1:port"| Env | |
| 387 | Env --> Secrets | |
| 388 | Web -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | |
| 389 | Mobile -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | |
| 390 | Env -->|"environment bearer + signed activity proof"| Worker | |
| 391 | </pre> | |
| 392 | </section> | |
| 393 | ||
| 394 | <section class="panel" aria-label="Authentication matrix"> | |
| 395 | <h2>Authentication and Transport Matrix</h2> | |
| 396 | <table> | |
| 397 | <thead> | |
| 398 | <tr> | |
| 399 | <th>Boundary</th> | |
| 400 | <th>Transport</th> | |
| 401 | <th>Authentication</th> | |
| 402 | <th>Purpose</th> | |
| 403 | </tr> | |
| 404 | </thead> | |
| 405 | <tbody> | |
| 406 | <tr> | |
| 407 | <td>Web or mobile -> relay link management</td> | |
| 408 | <td>HTTPS</td> | |
| 409 | <td>Clerk session-template bearer token</td> | |
| 410 | <td>Create a link challenge, submit an environment proof, list links, or unlink.</td> | |
| 411 | </tr> | |
| 412 | <tr> | |
| 413 | <td>CLI -> relay link management</td> | |
| 414 | <td>HTTPS</td> | |
| 415 | <td>Clerk OAuth access token obtained with PKCE</td> | |
| 416 | <td>Reconcile the desired headless cloud link when the server starts.</td> | |
| 417 | </tr> | |
| 418 | <tr> | |
| 419 | <td>Web or mobile -> relay protected client endpoints</td> | |
| 420 | <td>HTTPS</td> | |
| 421 | <td>Relay-issued DPoP token plus per-request DPoP proof</td> | |
| 422 | <td> | |
| 423 | Check environment status, request a remote connection, and register mobile devices. | |
| 424 | </td> | |
| 425 | </tr> | |
| 426 | <tr> | |
| 427 | <td>Relay -> managed environment endpoint</td> | |
| 428 | <td>HTTPS over the managed tunnel</td> | |
| 429 | <td>Short-lived relay-signed JWT request proof</td> | |
| 430 | <td> | |
| 431 | Request a signed health response or ask the local environment to mint a credential. | |
| 432 | </td> | |
| 433 | </tr> | |
| 434 | <tr> | |
| 435 | <td>Environment -> relay activity publication</td> | |
| 436 | <td>HTTPS</td> | |
| 437 | <td>Relay-issued environment bearer credential plus environment-signed JWT proof</td> | |
| 438 | <td> | |
| 439 | Publish redacted agent-activity state for push notifications and Live Activities. | |
| 440 | </td> | |
| 441 | </tr> | |
| 442 | <tr> | |
| 443 | <td>Remote client -> environment</td> | |
| 444 | <td>HTTPS and WSS over the managed tunnel</td> | |
| 445 | <td>Environment-issued access token bound to the client DPoP key</td> | |
| 446 | <td>Use the normal environment HTTP APIs and request a one-time WebSocket ticket.</td> | |
| 447 | </tr> | |
| 448 | </tbody> | |
| 449 | </table> | |
| 450 | </section> | |
| 451 | ||
| 452 | <section class="panel" aria-label="Credential ownership"> | |
| 453 | <h2>Credential Ownership</h2> | |
| 454 | <table> | |
| 455 | <thead> | |
| 456 | <tr> | |
| 457 | <th>Credential</th> | |
| 458 | <th>Created by</th> | |
| 459 | <th>Stored by</th> | |
| 460 | <th>Use</th> | |
| 461 | </tr> | |
| 462 | </thead> | |
| 463 | <tbody> | |
| 464 | <tr> | |
| 465 | <td>Clerk session-template JWT</td> | |
| 466 | <td>Clerk</td> | |
| 467 | <td>Web or mobile client</td> | |
| 468 | <td> | |
| 469 | Authenticate cloud-user relay requests and bootstrap relay DPoP token exchange. | |
| 470 | </td> | |
| 471 | </tr> | |
| 472 | <tr> | |
| 473 | <td>CLI OAuth access and refresh token</td> | |
| 474 | <td>Clerk OAuth</td> | |
| 475 | <td>Local server secret store</td> | |
| 476 | <td>Authorize headless CLI link reconciliation.</td> | |
| 477 | </tr> | |
| 478 | <tr> | |
| 479 | <td>Environment Ed25519 key pair</td> | |
| 480 | <td>Local environment server</td> | |
| 481 | <td>Local server secret store</td> | |
| 482 | <td> | |
| 483 | Sign link proofs, health responses, mint responses, and activity publication proofs. | |
| 484 | </td> | |
| 485 | </tr> | |
| 486 | <tr> | |
| 487 | <td>Relay cloud-mint key pair</td> | |
| 488 | <td>Relay deployment</td> | |
| 489 | <td>Private key in relay Worker config; public key installed into the environment</td> | |
| 490 | <td>Sign relay-to-environment health and mint requests.</td> | |
| 491 | </tr> | |
| 492 | <tr> | |
| 493 | <td>Relay environment bearer credential</td> | |
| 494 | <td>Relay</td> | |
| 495 | <td>Hashed in relay Postgres; plaintext in local server secret store</td> | |
| 496 | <td>Authenticate environment-to-relay agent-activity publication.</td> | |
| 497 | </tr> | |
| 498 | <tr> | |
| 499 | <td>Relay DPoP access token</td> | |
| 500 | <td>Relay</td> | |
| 501 | <td>Remote client memory cache</td> | |
| 502 | <td> | |
| 503 | Authorize status, connect, and mobile registration endpoints with proof of | |
| 504 | possession. | |
| 505 | </td> | |
| 506 | </tr> | |
| 507 | <tr> | |
| 508 | <td>Environment bootstrap credential</td> | |
| 509 | <td>Local environment server</td> | |
| 510 | <td>Passed relay -> remote client</td> | |
| 511 | <td>One-time exchange at the environment <code>/oauth/token</code> endpoint.</td> | |
| 512 | </tr> | |
| 513 | <tr> | |
| 514 | <td>Environment access token</td> | |
| 515 | <td>Local environment server</td> | |
| 516 | <td>Remote client</td> | |
| 517 | <td>Authorize environment HTTP operations and WebSocket ticket issuance.</td> | |
| 518 | </tr> | |
| 519 | <tr> | |
| 520 | <td>WebSocket ticket</td> | |
| 521 | <td>Local environment server</td> | |
| 522 | <td>Remote client until consumed</td> | |
| 523 | <td>Short-lived one-time credential for the WSS upgrade.</td> | |
| 524 | </tr> | |
| 525 | <tr> | |
| 526 | <td>Relay client connector token</td> | |
| 527 | <td>Cloudflare tunnel API</td> | |
| 528 | <td>Local server secret store</td> | |
| 529 | <td> | |
| 530 | Start the local relay client. It is passed through <code>TUNNEL_TOKEN</code>, not a | |
| 531 | shell argument. | |
| 532 | </td> | |
| 533 | </tr> | |
| 534 | </tbody> | |
| 535 | </table> | |
| 536 | </section> | |
| 537 | ||
| 538 | <section class="grid" aria-label="Core flows"> | |
| 539 | <article class="panel"> | |
| 540 | <h2>Flow 1: Link From the Desktop Web UI</h2> | |
| 541 | <pre class="diagram mermaid"> | |
| 542 | sequenceDiagram | |
| 543 | participant U as Signed-in user | |
| 544 | participant C as Desktop web UI | |
| 545 | participant E as Local environment | |
| 546 | participant R as Relay Worker | |
| 547 | participant CF as Cloudflare APIs | |
| 548 | participant RC as Local relay client | |
| 549 | ||
| 550 | U->>C: Enable T3 Connect for a locally paired environment | |
| 551 | C->>E: Check relay client availability | |
| 552 | alt relay client is missing | |
| 553 | C->>U: Prompt before download and install | |
| 554 | U->>C: Confirm | |
| 555 | C->>E: Stream relay-client install RPC | |
| 556 | E-->>C: Progress stages | |
| 557 | end | |
| 558 | C->>R: POST /v1/client/environment-link-challenges (Clerk bearer) | |
| 559 | R-->>C: Short-lived challenge | |
| 560 | C->>E: POST /api/connect/link-proof (local relay:write bearer) | |
| 561 | E->>E: Authenticate, reject forwarded authority, get or create key pair, validate loopback origin | |
| 562 | E-->>C: Environment-signed link proof | |
| 563 | C->>R: POST /v1/client/environment-links (Clerk bearer + proof) | |
| 564 | R->>R: Verify challenge, proof, capabilities, nonce, and loopback origin | |
| 565 | R->>CF: Reconcile tunnel, ingress, and CNAME | |
| 566 | R->>R: Upsert user/environment link and issue environment bearer | |
| 567 | R-->>C: Managed endpoint, relay config, and connector token | |
| 568 | C->>E: POST /api/connect/relay-config (local relay:write bearer) | |
| 569 | E->>RC: Start relay client with TUNNEL_TOKEN | |
| 570 | </pre> | |
| 571 | <div class="callout"> | |
| 572 | The local link-proof endpoint requires <code>relay:write</code>, rejects forwarded | |
| 573 | authority headers, and accepts only an exact loopback origin. Environment keypair | |
| 574 | persistence is atomic across concurrent link attempts. | |
| 575 | </div> | |
| 576 | <p> | |
| 577 | A locally paired mobile client uses the same relay challenge, local proof, relay link, | |
| 578 | and local relay-config endpoints. The desktop web UI is the surface that checks relay | |
| 579 | client availability and offers the managed install dialog. | |
| 580 | </p> | |
| 581 | </article> | |
| 582 | ||
| 583 | <article class="panel"> | |
| 584 | <h2>Flow 2: Headless CLI Link</h2> | |
| 585 | <pre class="diagram mermaid"> | |
| 586 | sequenceDiagram | |
| 587 | participant U as Operator | |
| 588 | participant CLI as t3 connect CLI | |
| 589 | participant Clerk as Clerk OAuth | |
| 590 | participant S as Environment server on next start | |
| 591 | participant R as Relay Worker | |
| 592 | participant RC as Local relay client | |
| 593 | ||
| 594 | U->>CLI: t3 connect link | |
| 595 | CLI->>CLI: Resolve relay client | |
| 596 | alt relay client is missing | |
| 597 | CLI->>U: Confirm managed relay-client install | |
| 598 | U->>CLI: Confirm | |
| 599 | CLI->>CLI: Install with terminal progress updates | |
| 600 | end | |
| 601 | CLI->>Clerk: Browser PKCE authorization-code flow | |
| 602 | Clerk-->>CLI: OAuth access and refresh tokens | |
| 603 | CLI->>CLI: Persist desired cloud-link state | |
| 604 | U->>S: Start T3 environment server | |
| 605 | S->>R: Create challenge and submit local signed proof | |
| 606 | R-->>S: Managed endpoint config and connector token | |
| 607 | S->>RC: Start relay client | |
| 608 | </pre> | |
| 609 | <p> | |
| 610 | <code>t3 connect login</code> stores authorization without enabling exposure. | |
| 611 | <code>t3 connect unlink</code> disables exposure while retaining authorization. | |
| 612 | <code>t3 connect logout</code> also removes the stored CLI authorization. | |
| 613 | </p> | |
| 614 | </article> | |
| 615 | ||
| 616 | <article class="panel"> | |
| 617 | <h2>Flow 3: Remote Connect Bootstrap</h2> | |
| 618 | <pre class="diagram mermaid"> | |
| 619 | sequenceDiagram | |
| 620 | participant C as Web or mobile client | |
| 621 | participant Clerk as Clerk | |
| 622 | participant R as Relay Worker | |
| 623 | participant E as Managed environment endpoint | |
| 624 | ||
| 625 | C->>Clerk: Get Clerk session-template JWT | |
| 626 | C->>R: POST /v1/client/dpop-token (Clerk JWT + DPoP proof) | |
| 627 | R-->>C: Relay DPoP access token | |
| 628 | C->>R: POST /v1/environments/:id/connect (relay DPoP) | |
| 629 | R->>E: POST /api/t3-connect/mint-credential (relay-signed proof) | |
| 630 | E->>E: Verify relay signer, linked user, scope, lifetime, cnf, and replay guards | |
| 631 | E-->>R: Bootstrap credential + environment-signed proof | |
| 632 | R->>R: Verify environment signature, nonce, endpoint, and DPoP binding | |
| 633 | R-->>C: Managed endpoint + bootstrap credential | |
| 634 | C->>E: POST /oauth/token (bootstrap credential + DPoP proof) | |
| 635 | E-->>C: Environment DPoP-bound access token | |
| 636 | C->>E: POST /api/auth/websocket-ticket | |
| 637 | E-->>C: One-time WebSocket ticket | |
| 638 | C->>E: WSS /ws with ticket | |
| 639 | </pre> | |
| 640 | <div class="callout"> | |
| 641 | Relay-to-environment control requests disable redirects, use only a reconciled managed | |
| 642 | endpoint allocation, and require a signed environment response before the relay returns | |
| 643 | data to the client. | |
| 644 | </div> | |
| 645 | </article> | |
| 646 | ||
| 647 | <article class="panel"> | |
| 648 | <h2>Flow 4: Agent Activity Notifications</h2> | |
| 649 | <pre class="diagram mermaid"> | |
| 650 | sequenceDiagram | |
| 651 | participant E as Local environment | |
| 652 | participant R as Relay Worker | |
| 653 | participant DB as Relay Postgres | |
| 654 | participant Q as APNs queue | |
| 655 | participant APNs as Apple Push Notification service | |
| 656 | participant M as Mobile app | |
| 657 | ||
| 658 | E->>E: Project publishable thread state and redact failure detail | |
| 659 | E->>R: POST /v1/environments/:environmentId/threads/:threadId/agent-activity | |
| 660 | Note over E,R: Environment bearer credential + environment-signed per-state proof | |
| 661 | R->>R: Verify credential, signature, scope, expiry, and nonce | |
| 662 | R->>DB: Store current activity row | |
| 663 | R->>Q: Enqueue push or Live Activity delivery jobs | |
| 664 | Q->>APNs: Deliver signed APNs request | |
| 665 | APNs-->>M: Push notification or Live Activity update | |
| 666 | </pre> | |
| 667 | <p> | |
| 668 | The local server publishes a deliberately narrow projection. Failed runs use a fixed | |
| 669 | redacted summary, and detail strings are capped before publication. | |
| 670 | </p> | |
| 671 | </article> | |
| 672 | </section> | |
| 673 | ||
| 674 | <section class="grid" aria-label="Managed endpoint lifecycle"> | |
| 675 | <article class="panel half"> | |
| 676 | <h2>Managed Endpoint Reconciliation</h2> | |
| 677 | <p> | |
| 678 | Managed endpoint resources are keyed by <code>(userId, environmentId)</code>. The relay | |
| 679 | reserves a stable hostname and tunnel name before provisioning, then checkpoints the | |
| 680 | tunnel ID, DNS record ID, and ready state in Postgres. | |
| 681 | </p> | |
| 682 | <div class="flow"> | |
| 683 | <div class="step"> | |
| 684 | <strong>1. Reserve deterministic allocation</strong> | |
| 685 | Hostname and tunnel name derive from relay stage, cloud user, and environment ID. | |
| 686 | </div> | |
| 687 | <div class="step"> | |
| 688 | <strong>2. Reuse or create tunnel</strong> | |
| 689 | Existing named tunnels are reused. Tunnel ingress is rewritten to the validated | |
| 690 | loopback origin. | |
| 691 | </div> | |
| 692 | <div class="step"> | |
| 693 | <strong>3. Reconcile DNS</strong> | |
| 694 | Existing CNAMEs are updated and duplicate records are removed. Create conflicts are | |
| 695 | recovered by listing and reconciling the winning record. | |
| 696 | </div> | |
| 697 | <div class="step"> | |
| 698 | <strong>4. Mark ready</strong> | |
| 699 | Connect and status requests use only ready allocations whose hostname still matches | |
| 700 | the relay-managed namespace. | |
| 701 | </div> | |
| 702 | </div> | |
| 703 | </article> | |
| 704 | ||
| 705 | <article class="panel half"> | |
| 706 | <h2>Unlink Cleanup</h2> | |
| 707 | <p> | |
| 708 | Relay unlink removes the user's managed endpoint allocation before revoking the | |
| 709 | user/environment link: | |
| 710 | </p> | |
| 711 | <ul> | |
| 712 | <li>delete the managed DNS record if present;</li> | |
| 713 | <li>delete the Cloudflare tunnel if present;</li> | |
| 714 | <li>remove the allocation row;</li> | |
| 715 | <li>revoke the user's environment link; and</li> | |
| 716 | <li> | |
| 717 | revoke the environment publication credential only after the final active link for | |
| 718 | that environment key disappears. | |
| 719 | </li> | |
| 720 | </ul> | |
| 721 | <p> | |
| 722 | Local unlink stops the relay client and removes the locally persisted relay URL, issuer, | |
| 723 | linked user, environment publication credential, cloud-mint public key, connector | |
| 724 | config, and agent-activity preference. | |
| 725 | </p> | |
| 726 | </article> | |
| 727 | </section> | |
| 728 | ||
| 729 | <section class="panel" aria-label="Cloudflare tunnel operational profile"> | |
| 730 | <h2>Cloudflare Tunnel Operational Profile</h2> | |
| 731 | <p> | |
| 732 | Managed tunnels are private application backends, not general-purpose public hosting. The | |
| 733 | relay provisions one named Cloudflare tunnel and one DNS record per active | |
| 734 | <code>(userId, environmentId)</code> allocation. | |
| 735 | </p> | |
| 736 | <ul> | |
| 737 | <li> | |
| 738 | <strong>Lifecycle:</strong> the tunnel is created when an environment is linked, reused | |
| 739 | across retry-safe reconciliation, kept for the lifetime of that link, and deleted on | |
| 740 | unlink. If teardown fails, the allocation checkpoint remains so cleanup can be retried. | |
| 741 | </li> | |
| 742 | <li> | |
| 743 | <strong>Origin:</strong> tunnel ingress is restricted to the linked environment server's | |
| 744 | validated loopback HTTP origin. Arbitrary upstream hosts and raw TCP origins are | |
| 745 | rejected. | |
| 746 | </li> | |
| 747 | <li> | |
| 748 | <strong>Protocols:</strong> managed endpoints carry HTTPS request/response traffic and | |
| 749 | WSS connections only. Remote clients use the normal environment HTTP APIs and | |
| 750 | authenticated WebSocket RPC transport through the tunnel. | |
| 751 | </li> | |
| 752 | <li> | |
| 753 | <strong>Relay-generated traffic:</strong> the hosted relay sends sparse HTTPS health | |
| 754 | checks and credential-mint requests during status checks and remote connection | |
| 755 | bootstrap. It does not proxy steady-state remote sessions. | |
| 756 | </li> | |
| 757 | <li> | |
| 758 | <strong>Traffic shape:</strong> expected load is interactive and user-driven. Active | |
| 759 | remote sessions may keep WebSockets open and exchange terminal, agent, and UI updates; | |
| 760 | inactive linked environments should carry little or no tunnel traffic. | |
| 761 | </li> | |
| 762 | <li> | |
| 763 | <strong>Capacity planning:</strong> the project is pre-launch, so there is not yet a | |
| 764 | production bandwidth baseline. Tunnel count grows with active linked user/environment | |
| 765 | allocations, while bandwidth and concurrent WebSockets grow with active remote use. | |
| 766 | </li> | |
| 767 | </ul> | |
| 768 | </section> | |
| 769 | ||
| 770 | <section class="panel" aria-label="SSRF and tunnel hardening"> | |
| 771 | <h2>SSRF and Tunnel Hardening</h2> | |
| 772 | <table> | |
| 773 | <thead> | |
| 774 | <tr> | |
| 775 | <th>Risk</th> | |
| 776 | <th>Implemented control</th> | |
| 777 | </tr> | |
| 778 | </thead> | |
| 779 | <tbody> | |
| 780 | <tr> | |
| 781 | <td>A client asks the relay to tunnel an arbitrary host.</td> | |
| 782 | <td> | |
| 783 | The environment and relay both accept managed tunnel origins only for loopback hosts | |
| 784 | and valid ports. | |
| 785 | </td> | |
| 786 | </tr> | |
| 787 | <tr> | |
| 788 | <td>A forwarded request tricks local origin validation.</td> | |
| 789 | <td> | |
| 790 | The local link-proof handler requires an exact loopback request URL and rejects | |
| 791 | forwarded host or protocol headers. | |
| 792 | </td> | |
| 793 | </tr> | |
| 794 | <tr> | |
| 795 | <td>A stored endpoint is replaced with an arbitrary external URL.</td> | |
| 796 | <td> | |
| 797 | Connect and status resolve a ready allocation from Postgres and require a hostname | |
| 798 | under the configured managed endpoint zone. | |
| 799 | </td> | |
| 800 | </tr> | |
| 801 | <tr> | |
| 802 | <td>A managed endpoint redirects relay egress elsewhere.</td> | |
| 803 | <td> | |
| 804 | Relay-to-environment status and mint requests set redirect handling to | |
| 805 | <code>manual</code>. | |
| 806 | </td> | |
| 807 | </tr> | |
| 808 | <tr> | |
| 809 | <td>A process behind the tunnel returns attacker-controlled data.</td> | |
| 810 | <td> | |
| 811 | The relay verifies environment-signed health and mint responses against the linked | |
| 812 | environment public key, expected nonce, and request binding. | |
| 813 | </td> | |
| 814 | </tr> | |
| 815 | <tr> | |
| 816 | <td>A stolen remote access token is replayed.</td> | |
| 817 | <td> | |
| 818 | Relay and environment access tokens are DPoP-bound; per-request proofs and replay | |
| 819 | guards are required. | |
| 820 | </td> | |
| 821 | </tr> | |
| 822 | <tr> | |
| 823 | <td>The relay client token leaks through process listings or shell parsing.</td> | |
| 824 | <td> | |
| 825 | The relay client is spawned without a shell and receives its connector token in | |
| 826 | <code>TUNNEL_TOKEN</code>. | |
| 827 | </td> | |
| 828 | </tr> | |
| 829 | </tbody> | |
| 830 | </table> | |
| 831 | <div class="callout warning"> | |
| 832 | Managed tunnel hostnames live under the configured tunnel DNS zone. Serving them from a | |
| 833 | dedicated registrable domain with a Public Suffix List entry remains an operational | |
| 834 | isolation requirement before broad untrusted use. | |
| 835 | </div> | |
| 836 | </section> | |
| 837 | ||
| 838 | <section class="panel" aria-label="Relay HTTP API"> | |
| 839 | <h2>Relay HTTP API</h2> | |
| 840 | <p> | |
| 841 | The relay publishes an OpenAPI document at <code>/openapi.json</code>, interactive API | |
| 842 | docs at <code>/docs</code>, and redirects <code>/</code> to the docs. | |
| 843 | </p> | |
| 844 | <table> | |
| 845 | <thead> | |
| 846 | <tr> | |
| 847 | <th>Endpoint</th> | |
| 848 | <th>Authentication</th> | |
| 849 | <th>Purpose</th> | |
| 850 | </tr> | |
| 851 | </thead> | |
| 852 | <tbody> | |
| 853 | <tr> | |
| 854 | <td><code>GET /health</code></td> | |
| 855 | <td>None</td> | |
| 856 | <td>Relay health check.</td> | |
| 857 | </tr> | |
| 858 | <tr> | |
| 859 | <td><code>GET /.well-known/oauth-authorization-server</code></td> | |
| 860 | <td>None</td> | |
| 861 | <td>Relay OAuth token-exchange discovery metadata.</td> | |
| 862 | </tr> | |
| 863 | <tr> | |
| 864 | <td><code>GET /.well-known/oauth-protected-resource</code></td> | |
| 865 | <td>None</td> | |
| 866 | <td>Relay DPoP protected-resource discovery metadata.</td> | |
| 867 | </tr> | |
| 868 | <tr> | |
| 869 | <td><code>GET /v1/environments</code></td> | |
| 870 | <td>Clerk bearer</td> | |
| 871 | <td>List environments linked to the signed-in user.</td> | |
| 872 | </tr> | |
| 873 | <tr> | |
| 874 | <td><code>POST /v1/client/environment-link-challenges</code></td> | |
| 875 | <td>Clerk bearer</td> | |
| 876 | <td>Create a short-lived user-bound environment link challenge.</td> | |
| 877 | </tr> | |
| 878 | <tr> | |
| 879 | <td><code>POST /v1/client/environment-links</code></td> | |
| 880 | <td>Clerk bearer</td> | |
| 881 | <td> | |
| 882 | Verify a local environment proof, reconcile a managed endpoint, and upsert the user | |
| 883 | link. | |
| 884 | </td> | |
| 885 | </tr> | |
| 886 | <tr> | |
| 887 | <td><code>DELETE /v1/client/environment-links/:environmentId</code></td> | |
| 888 | <td>Clerk bearer</td> | |
| 889 | <td>Remove managed endpoint resources and revoke the user's link.</td> | |
| 890 | </tr> | |
| 891 | <tr> | |
| 892 | <td><code>POST /v1/client/dpop-token</code></td> | |
| 893 | <td>Clerk token in token-exchange payload plus DPoP proof</td> | |
| 894 | <td>Issue a relay DPoP access token for the requested supported scopes.</td> | |
| 895 | </tr> | |
| 896 | <tr> | |
| 897 | <td><code>POST /v1/environments/:environmentId/status</code></td> | |
| 898 | <td>Relay DPoP</td> | |
| 899 | <td>Request and validate a signed environment health response.</td> | |
| 900 | </tr> | |
| 901 | <tr> | |
| 902 | <td><code>POST /v1/environments/:environmentId/connect</code></td> | |
| 903 | <td>Relay DPoP</td> | |
| 904 | <td>Request and validate an environment bootstrap credential.</td> | |
| 905 | </tr> | |
| 906 | <tr> | |
| 907 | <td><code>POST /v1/mobile/devices</code></td> | |
| 908 | <td>Relay DPoP</td> | |
| 909 | <td>Register or update a mobile device for notifications.</td> | |
| 910 | </tr> | |
| 911 | <tr> | |
| 912 | <td><code>POST /v1/mobile/live-activities</code></td> | |
| 913 | <td>Relay DPoP</td> | |
| 914 | <td>Register a Live Activity push token.</td> | |
| 915 | </tr> | |
| 916 | <tr> | |
| 917 | <td><code>DELETE /v1/mobile/devices/:deviceId</code></td> | |
| 918 | <td>Relay DPoP</td> | |
| 919 | <td>Unregister a mobile device.</td> | |
| 920 | </tr> | |
| 921 | <tr> | |
| 922 | <td> | |
| 923 | <code>POST /v1/environments/:environmentId/threads/:threadId/agent-activity</code> | |
| 924 | </td> | |
| 925 | <td>Environment bearer plus signed publish proof</td> | |
| 926 | <td>Publish redacted current agent activity for notification delivery.</td> | |
| 927 | </tr> | |
| 928 | </tbody> | |
| 929 | </table> | |
| 930 | </section> | |
| 931 | ||
| 932 | <section class="panel" aria-label="Environment HTTP API"> | |
| 933 | <h2>Environment Cloud HTTP API</h2> | |
| 934 | <table> | |
| 935 | <thead> | |
| 936 | <tr> | |
| 937 | <th>Endpoint</th> | |
| 938 | <th>Authentication</th> | |
| 939 | <th>Purpose</th> | |
| 940 | </tr> | |
| 941 | </thead> | |
| 942 | <tbody> | |
| 943 | <tr> | |
| 944 | <td><code>POST /api/connect/link-proof</code></td> | |
| 945 | <td>Local environment token with <code>relay:write</code></td> | |
| 946 | <td>Validate local origin and sign an environment link proof.</td> | |
| 947 | </tr> | |
| 948 | <tr> | |
| 949 | <td><code>POST /api/connect/relay-config</code></td> | |
| 950 | <td>Local environment token with <code>relay:write</code></td> | |
| 951 | <td>Persist linked relay configuration and start the relay client.</td> | |
| 952 | </tr> | |
| 953 | <tr> | |
| 954 | <td><code>GET /api/connect/link-state</code></td> | |
| 955 | <td>Local environment token with <code>relay:read</code></td> | |
| 956 | <td>Read the environment's current T3 Connect link state.</td> | |
| 957 | </tr> | |
| 958 | <tr> | |
| 959 | <td><code>POST /api/connect/preferences</code></td> | |
| 960 | <td>Local environment token with <code>relay:write</code></td> | |
| 961 | <td>Enable or disable agent-activity publication.</td> | |
| 962 | </tr> | |
| 963 | <tr> | |
| 964 | <td><code>POST /api/connect/unlink</code></td> | |
| 965 | <td>Local environment token with <code>relay:write</code></td> | |
| 966 | <td>Stop the relay client and clear local T3 Connect configuration.</td> | |
| 967 | </tr> | |
| 968 | <tr> | |
| 969 | <td><code>POST /api/t3-connect/health</code></td> | |
| 970 | <td>Relay-signed proof</td> | |
| 971 | <td>Return a signed environment health response.</td> | |
| 972 | </tr> | |
| 973 | <tr> | |
| 974 | <td><code>POST /api/t3-connect/mint-credential</code></td> | |
| 975 | <td>Relay-signed proof</td> | |
| 976 | <td>Mint a DPoP-bound one-time bootstrap credential for a remote client.</td> | |
| 977 | </tr> | |
| 978 | <tr> | |
| 979 | <td><code>POST /oauth/token</code></td> | |
| 980 | <td>Bootstrap credential plus DPoP proof</td> | |
| 981 | <td>Exchange a bootstrap credential for an environment access token.</td> | |
| 982 | </tr> | |
| 983 | <tr> | |
| 984 | <td><code>POST /api/auth/websocket-ticket</code></td> | |
| 985 | <td>Environment access token</td> | |
| 986 | <td>Issue a one-time ticket for the WSS upgrade.</td> | |
| 987 | </tr> | |
| 988 | </tbody> | |
| 989 | </table> | |
| 990 | </section> | |
| 991 | ||
| 992 | <section class="grid" aria-label="Runtime and infrastructure"> | |
| 993 | <article class="panel half"> | |
| 994 | <h2>Relay Client Installation</h2> | |
| 995 | <p> | |
| 996 | Desktop web UI availability checks and installs are local WebSocket RPC methods: | |
| 997 | <code>cloud.getRelayClientStatus</code> and streaming | |
| 998 | <code>cloud.installRelayClient</code>. The install stream emits the current stage: | |
| 999 | checking, waiting for lock, downloading, verifying, installing, validating, and | |
| 1000 | activating. | |
| 1001 | </p> | |
| 1002 | <p> | |
| 1003 | The web UI asks for confirmation before installation and displays a custom progress | |
| 1004 | dialog. The CLI invokes the same local relay-client service directly and reports the | |
| 1005 | same progress stages in the terminal. | |
| 1006 | </p> | |
| 1007 | </article> | |
| 1008 | ||
| 1009 | <article class="panel half"> | |
| 1010 | <h2>Relay Infrastructure</h2> | |
| 1011 | <ul> | |
| 1012 | <li>Cloudflare Worker for the hosted relay HTTP API.</li> | |
| 1013 | <li>PlanetScale Postgres database named <code>t3coderelay</code>.</li> | |
| 1014 | <li> | |
| 1015 | Production uses the shared database; non-production stages use database branches. | |
| 1016 | </li> | |
| 1017 | <li> | |
| 1018 | Cloudflare Hyperdrive with caching disabled and a constrained origin connection limit. | |
| 1019 | </li> | |
| 1020 | <li>Cloudflare tunnel and DNS bindings for managed endpoint reconciliation.</li> | |
| 1021 | <li>Cloudflare APNs delivery queue plus dead-letter queue.</li> | |
| 1022 | <li>A five-minute cron job that prunes expired DPoP replay rows.</li> | |
| 1023 | <li>Axiom OTLP trace dataset, scoped ingest token, and recent-spans view per stage.</li> | |
| 1024 | </ul> | |
| 1025 | </article> | |
| 1026 | </section> | |
| 1027 | ||
| 1028 | <section class="panel" aria-label="Deployment configuration"> | |
| 1029 | <h2>Deployment Configuration</h2> | |
| 1030 | <table> | |
| 1031 | <thead> | |
| 1032 | <tr> | |
| 1033 | <th>Boundary</th> | |
| 1034 | <th>Configuration</th> | |
| 1035 | </tr> | |
| 1036 | </thead> | |
| 1037 | <tbody> | |
| 1038 | <tr> | |
| 1039 | <td>Relay infrastructure</td> | |
| 1040 | <td> | |
| 1041 | <code>RELAY_API_ZONE_NAME</code>, <code>RELAY_TUNNEL_ZONE_NAME</code>, optional | |
| 1042 | <code>RELAY_DOMAIN</code>, <code>CLERK_PUBLISHABLE_KEY</code>, | |
| 1043 | <code>CLERK_SECRET_KEY</code>, <code>CLERK_JWT_AUDIENCE</code>, and APNs | |
| 1044 | credentials. | |
| 1045 | </td> | |
| 1046 | </tr> | |
| 1047 | <tr> | |
| 1048 | <td>Source-built desktop/server cloud features</td> | |
| 1049 | <td> | |
| 1050 | Optional <code>T3CODE_RELAY_URL</code>, <code>T3CODE_CLERK_PUBLISHABLE_KEY</code>, | |
| 1051 | and <code>T3CODE_CLERK_CLI_OAUTH_CLIENT_ID</code>. Release builds inject public | |
| 1052 | values at build time. | |
| 1053 | </td> | |
| 1054 | </tr> | |
| 1055 | <tr> | |
| 1056 | <td>Web and mobile cloud clients</td> | |
| 1057 | <td> | |
| 1058 | Public Clerk publishable key, Clerk JWT template name, and relay URL. Relay URLs | |
| 1059 | must normalize to an absolute HTTPS origin without credentials, query, fragment, or | |
| 1060 | non-root path. | |
| 1061 | </td> | |
| 1062 | </tr> | |
| 1063 | </tbody> | |
| 1064 | </table> | |
| 1065 | </section> | |
| 1066 | ||
| 1067 | <section class="panel" aria-label="Implementation checklist"> | |
| 1068 | <h2>Implementation Checklist</h2> | |
| 1069 | <ul> | |
| 1070 | <li> | |
| 1071 | Keep the relay cloud-mint private key hosted-only; install only its public key into | |
| 1072 | environments. | |
| 1073 | </li> | |
| 1074 | <li> | |
| 1075 | Keep environment signing private keys local and persist keypair creation atomically. | |
| 1076 | </li> | |
| 1077 | <li> | |
| 1078 | Require a local <code>relay:write</code> session before linking or changing relay | |
| 1079 | config. | |
| 1080 | </li> | |
| 1081 | <li>Validate relay URLs as secure absolute HTTPS origins before sending credentials.</li> | |
| 1082 | <li>Require loopback-only tunnel origins on both sides of the link-proof boundary.</li> | |
| 1083 | <li> | |
| 1084 | Resolve managed endpoint egress from ready allocation rows, not client-supplied URLs. | |
| 1085 | </li> | |
| 1086 | <li>Disable redirects on relay-to-environment requests.</li> | |
| 1087 | <li>Validate environment-signed response proofs before consuming tunneled responses.</li> | |
| 1088 | <li>Keep tunnel and DNS provisioning retry-safe and deprovision them on unlink.</li> | |
| 1089 | <li> | |
| 1090 | Revoke shared environment publication credentials only after the final active link | |
| 1091 | disappears. | |
| 1092 | </li> | |
| 1093 | <li>Redact and cap local agent failure details before publishing them externally.</li> | |
| 1094 | </ul> | |
| 1095 | </section> | |
| 1096 | ||
| 1097 | <section class="panel" aria-label="Standards references"> | |
| 1098 | <h2>Standards References</h2> | |
| 1099 | <ul class="source-list"> | |
| 1100 | <li> | |
| 1101 | <a href="https://www.rfc-editor.org/rfc/rfc8252">RFC 8252</a>: browser-based OAuth for | |
| 1102 | native apps, used by the CLI PKCE authorization flow. | |
| 1103 | </li> | |
| 1104 | <li> | |
| 1105 | <a href="https://www.rfc-editor.org/rfc/rfc8693">RFC 8693</a>: OAuth token exchange, | |
| 1106 | used for Clerk-token to relay-DPoP-token exchange. | |
| 1107 | </li> | |
| 1108 | <li> | |
| 1109 | <a href="https://www.rfc-editor.org/rfc/rfc9449">RFC 9449</a>: DPoP proof of possession, | |
| 1110 | used for relay and environment access tokens. | |
| 1111 | </li> | |
| 1112 | </ul> | |
| 1113 | </section> | |
| 1114 | </main> | |
| 1115 | <script type="module"> | |
| 1116 | import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"; | |
| 1117 | ||
| 1118 | mermaid.initialize({ | |
| 1119 | startOnLoad: false, | |
| 1120 | securityLevel: "strict", | |
| 1121 | theme: "base", | |
| 1122 | themeVariables: { | |
| 1123 | fontFamily: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | |
| 1124 | primaryColor: "#eef4ff", | |
| 1125 | primaryBorderColor: "#1457d9", | |
| 1126 | primaryTextColor: "#171717", | |
| 1127 | lineColor: "#62646b", | |
| 1128 | signalColor: "#171717", | |
| 1129 | signalTextColor: "#171717", | |
| 1130 | noteBkgColor: "#fff6e6", | |
| 1131 | noteTextColor: "#171717", | |
| 1132 | actorBkg: "#fbfbf8", | |
| 1133 | actorBorder: "#62646b", | |
| 1134 | actorTextColor: "#171717", | |
| 1135 | }, | |
| 1136 | flowchart: { | |
| 1137 | htmlLabels: true, | |
| 1138 | curve: "basis", | |
| 1139 | padding: 18, | |
| 1140 | }, | |
| 1141 | sequence: { | |
| 1142 | showSequenceNumbers: true, | |
| 1143 | mirrorActors: false, | |
| 1144 | rightAngles: false, | |
| 1145 | }, | |
| 1146 | }); | |
| 1147 | ||
| 1148 | await mermaid.run({ querySelector: ".diagram.mermaid" }); | |
| 1149 | ||
| 1150 | for (const svg of document.querySelectorAll(".diagram.mermaid svg")) { | |
| 1151 | const viewBox = svg.getAttribute("viewBox"); | |
| 1152 | const width = viewBox?.split(/\s+/).map(Number)[2]; | |
| 1153 | if (Number.isFinite(width) && width > 0) { | |
| 1154 | svg.style.setProperty("width", `${Math.ceil(width)}px`, "important"); | |
| 1155 | svg.style.setProperty("max-width", "none", "important"); | |
| 1156 | } | |
| 1157 | } | |
| 1158 | </script> | |
| 1159 | </body> | |
| 1160 | </html> |
docs/internals/t3-connect.md created+257| ... | ... | @@ -0,0 +1,257 @@ |
| 1 | # T3 Connect | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay verifies | |
| 6 | two kinds of bearer credential: template JWTs generated from the `t3-relay` template with the shared | |
| 7 | `t3-code-relay` audience, and Clerk OAuth tokens issued to the CLI. `verifyRelayClientBearerToken` in | |
| 8 | `infra/relay/src/http/Api.ts` tries the template/session path first and falls back to OAuth | |
| 9 | verification (`acceptsToken: "oauth_token"`), so the CLI's OAuth credential works without a JWT | |
| 10 | template. | |
| 11 | ||
| 12 | For the wider system diagram, see | |
| 13 | [t3-code-connect-auth-flow.html](./t3-code-connect-auth-flow.html). | |
| 14 | ||
| 15 | ## Application Keys | |
| 16 | ||
| 17 | T3 Connect is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env` | |
| 18 | or `.env.local` file: | |
| 19 | ||
| 20 | ```dotenv | |
| 21 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | |
| 22 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | |
| 23 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | |
| 24 | T3CODE_RELAY_URL=https://relay.example.com | |
| 25 | ``` | |
| 26 | ||
| 27 | The shared client loader projects these canonical values into framework-specific `VITE_*` and | |
| 28 | `EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new | |
| 29 | client configuration should use the canonical names. | |
| 30 | ||
| 31 | Configuration precedence is: | |
| 32 | ||
| 33 | 1. Process or CI environment variables. | |
| 34 | 2. Repository-root `.env.local`. | |
| 35 | 3. Repository-root `.env`. | |
| 36 | ||
| 37 | The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public | |
| 38 | identifiers, not secrets. | |
| 39 | Web, desktop, mobile, and bundled server builds statically inject the values they consume during | |
| 40 | their build step. A built artifact does not need an environment file at runtime. CI release builds | |
| 41 | should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, | |
| 42 | `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and | |
| 43 | production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS | |
| 44 | environment. | |
| 45 | ||
| 46 | When any client-facing public value is absent, cloud UI is omitted. The `t3 connect` command group is | |
| 47 | always registered: when the CLI public values are absent, `makeCli` in `apps/server/src/bin.ts` | |
| 48 | registers a hidden fallback `connect` command that reports the missing configuration instead of | |
| 49 | silently vanishing from help. The bundled server still accepts runtime overrides for self-hosted or | |
| 50 | operator-managed deployments. | |
| 51 | ||
| 52 | For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay | |
| 53 | deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, | |
| 54 | `CLERK_PUBLISHABLE_KEY`, and `CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in | |
| 55 | deployment defaults. | |
| 56 | `vp run --filter t3code-relay deploy` invokes Alchemy from the relay directory, so Alchemy loads | |
| 57 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` | |
| 58 | with the deployed HTTPS relay URL. The relay still requires | |
| 59 | `CLERK_SECRET_KEY` as an Alchemy secret. Never put `CLERK_SECRET_KEY` in a client application | |
| 60 | environment or commit it to the repository. | |
| 61 | ||
| 62 | The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference | |
| 63 | that database and provision isolated PlanetScale branches, so deploy `prod` before creating a | |
| 64 | personal developer stage. | |
| 65 | ||
| 66 | ## Headless CLI OAuth Application | |
| 67 | ||
| 68 | The `t3 connect` commands authorize a headless environment with a separate Clerk OAuth application. | |
| 69 | This uses an OAuth public client with PKCE, so the CLI stores no client secret. | |
| 70 | ||
| 71 | In **Clerk Dashboard > OAuth applications**: | |
| 72 | ||
| 73 | 1. Create an OAuth application for the T3 CLI. | |
| 74 | 2. Enable the **Public** option so authorization-code exchange uses PKCE. | |
| 75 | 3. Add **both** allowed redirect URIs: | |
| 76 | - `http://127.0.0.1:34338/callback` for the loopback listener; | |
| 77 | - `https://app.t3.codes/connect/callback` for the hosted out-of-band flow. This is | |
| 78 | `connectCallbackUrl(DEFAULT_HOSTED_APP_URL)` from `packages/shared/src/connectAuth.ts`, so a | |
| 79 | custom `T3CODE_HOSTED_APP_URL` means `$T3CODE_HOSTED_APP_URL/connect/callback` instead. | |
| 80 | Omitting it breaks headless and SSH authorization. | |
| 81 | 4. Enable the `openid`, `profile`, and `email` scopes. | |
| 82 | 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build | |
| 83 | environment to the generated public client ID. | |
| 84 | ||
| 85 | The CLI derives Clerk's frontend API URL from the publishable key and calls Clerk's | |
| 86 | `/oauth/authorize` and `/oauth/token` endpoints directly. The relay is not involved in the OAuth | |
| 87 | handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link. | |
| 88 | ||
| 89 | The connect command group is: | |
| 90 | ||
| 91 | ```sh | |
| 92 | t3 connect # default: onboarding | |
| 93 | t3 connect login | |
| 94 | t3 connect link # --publish-only | |
| 95 | t3 connect status # --json | |
| 96 | t3 connect publish # --disable | |
| 97 | t3 connect unlink | |
| 98 | t3 connect logout | |
| 99 | ``` | |
| 100 | ||
| 101 | `t3 serve` is a separate top-level command, not a connect subcommand. | |
| 102 | ||
| 103 | `t3 connect login` opens the Clerk authorization flow and stores the CLI credential without enabling | |
| 104 | cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed, | |
| 105 | authorizes when needed, and records durable intent to expose the environment. It works without a | |
| 106 | running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the | |
| 107 | managed tunnel. `t3 connect unlink` records disabled intent immediately, stops a reachable running | |
| 108 | connector, and attempts to revoke the relay-side environment record. It retains the stored CLI | |
| 109 | authorization so `t3 connect link` can re-enable exposure without another browser flow. `t3 connect | |
| 110 | logout` performs the same cleanup and removes the stored CLI authorization. | |
| 111 | ||
| 112 | The background service has an independent lifecycle. Connect setup may offer to install it, but | |
| 113 | logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`. | |
| 114 | ||
| 115 | ### Headless and SSH authorization | |
| 116 | ||
| 117 | The loopback OAuth callback listener binds to port `34338`. That path only works when a browser on | |
| 118 | the same machine can reach it, so `authorizeCli` in `apps/server/src/cli/connect.ts` automatically | |
| 119 | selects the out-of-band flow when `--headless` is passed or when it detects SSH through | |
| 120 | `SSH_CONNECTION` or `SSH_TTY`. The out-of-band flow prints the hosted `/connect` authorization URL | |
| 121 | and accepts a pasted authorization code, so no port is involved. | |
| 122 | ||
| 123 | Port forwarding is therefore optional, not required. Forward the port only if you specifically want | |
| 124 | the loopback flow over SSH: | |
| 125 | ||
| 126 | ```sh | |
| 127 | ssh -L 34338:127.0.0.1:34338 <host> | |
| 128 | ``` | |
| 129 | ||
| 130 | ## JWT Template | |
| 131 | ||
| 132 | In **Clerk Dashboard > JWT templates**, create a template with: | |
| 133 | ||
| 134 | | Setting | Value | | |
| 135 | | ------- | ---------------------------- | | |
| 136 | | Name | `t3-relay` | | |
| 137 | | Claims | `{ "aud": "t3-code-relay" }` | | |
| 138 | ||
| 139 | Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set | |
| 140 | `CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and | |
| 141 | `CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value | |
| 142 | is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still | |
| 143 | selects the concrete relay deployment, but changing that URL does not require a JWT template change. | |
| 144 | ||
| 145 | ## Desktop OAuth Redirect Allowlist | |
| 146 | ||
| 147 | The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme. | |
| 148 | In **Clerk Dashboard > Native applications**, enable the Native API and add these entries under the | |
| 149 | mobile SSO redirect allowlist: | |
| 150 | ||
| 151 | ```text | |
| 152 | t3code-dev://app/ | |
| 153 | t3code://app/ | |
| 154 | ``` | |
| 155 | ||
| 156 | Local desktop development uses `t3code-dev://app`, while packaged builds use `t3code://app`. Add the | |
| 157 | matching origin to each Clerk instance's Backend API `allowed_origins` array as well. The development | |
| 158 | Clerk instance should only need `t3code-dev://app`; the production Clerk instance should only need | |
| 159 | `t3code://app`. `@clerk/electron` owns the native request adapter, encrypted Clerk token persistence, | |
| 160 | external-browser OAuth transport, and callback delivery for initial sign-in and linked-account flows. | |
| 161 | ||
| 162 | There is currently no Dashboard UI for `allowed_origins`. Preserve any existing entries and update | |
| 163 | the instance through the Backend API: | |
| 164 | ||
| 165 | ```sh | |
| 166 | curl -X PATCH https://api.clerk.com/v1/instance \ | |
| 167 | -H "Content-Type: application/json" \ | |
| 168 | -H "Authorization: Bearer $CLERK_SECRET_KEY" \ | |
| 169 | -d '{"allowed_origins":["t3code://app"]}' | |
| 170 | ``` | |
| 171 | ||
| 172 | Never put `CLERK_SECRET_KEY` in the desktop app, a client-facing environment file, or a build | |
| 173 | artifact. | |
| 174 | ||
| 175 | ## Desktop Passkeys | |
| 176 | ||
| 177 | The production macOS bundle ID is `com.t3tools.t3code`. To enable native passkeys: | |
| 178 | ||
| 179 | 1. Create an explicit macOS App ID for `com.t3tools.t3code` in the Apple Developer portal and enable | |
| 180 | **Associated Domains**. | |
| 181 | 2. Create a compatible macOS provisioning profile for that App ID and the certificate used to sign | |
| 182 | the distributed app. | |
| 183 | 3. In Clerk's Native API settings, add an iOS app with the same Apple Team ID and bundle ID. This is | |
| 184 | also the configuration point for Electron/macOS passkeys. | |
| 185 | 4. Confirm Clerk serves `https://<frontend-api>/.well-known/apple-app-site-association` and that | |
| 186 | `webcredentials.apps` contains `<TEAM_ID>.com.t3tools.t3code`. | |
| 187 | 5. Set the local or CI signing configuration described below. | |
| 188 | ||
| 189 | For a local signed build, add these values to `.env.local` or export them before invoking the | |
| 190 | desktop artifact command: | |
| 191 | ||
| 192 | ```dotenv | |
| 193 | T3CODE_APPLE_TEAM_ID=ABC1234567 | |
| 194 | T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile | |
| 195 | # Optional: comma-separated override when Clerk's RP ID differs from the Frontend API hostname. | |
| 196 | T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com | |
| 197 | ``` | |
| 198 | ||
| 199 | When `T3CODE_CLERK_PASSKEY_RP_DOMAINS` is absent, the build derives the RP domain from | |
| 200 | `T3CODE_CLERK_PUBLISHABLE_KEY`. Signed macOS builds fail early if the Team ID, provisioning profile, | |
| 201 | or RP-domain configuration is missing. The generated main-app entitlements include every configured | |
| 202 | `webcredentials:<domain>` entry; helper apps keep Electron's minimal default entitlements. | |
| 203 | ||
| 204 | The normal `dev:desktop` launcher is unsigned and cannot complete macOS passkey ceremonies. For | |
| 205 | renderer HMR, build and install a signed app first, run the renderer dev server, then launch the | |
| 206 | installed app executable with `VITE_DEV_SERVER_URL` and `T3CODE_PORT` set. Rebuild the signed app | |
| 207 | after native dependency, main-process, preload, entitlement, provisioning, or signing changes; | |
| 208 | renderer-only changes can reuse the installed app. | |
| 209 | ||
| 210 | For the default development ports, run `pnpm dev:web` in one terminal and launch the installed | |
| 211 | binary from another: | |
| 212 | ||
| 213 | ```sh | |
| 214 | VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \ | |
| 215 | T3CODE_PORT=13773 \ | |
| 216 | "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)" | |
| 217 | ``` | |
| 218 | ||
| 219 | After changing Associated Domains, bump the build version before rebuilding; macOS may otherwise | |
| 220 | reuse stale Shared Web Credentials metadata for the same app/version pair. | |
| 221 | ||
| 222 | Verify the installed bundle before testing: | |
| 223 | ||
| 224 | ```sh | |
| 225 | codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app" | |
| 226 | codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app" | |
| 227 | ``` | |
| 228 | ||
| 229 | The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth | |
| 230 | flow uses a custom redirect URI, add that exact URI to the same allowlist. | |
| 231 | ||
| 232 | ## Sign-in Surfaces | |
| 233 | ||
| 234 | Signed-in users manage T3 Connect under **Connections**. The settings sidebar also has dedicated | |
| 235 | controls, rendered by `SettingsSidebarNav.tsx`: `T3ConnectSidebarSignIn` in the footer shows a | |
| 236 | **Sign in to T3 Connect** button while signed out, and `T3ConnectSidebarAvatar` shows a Clerk | |
| 237 | `UserButton` account control while signed in. Both are gated on cloud public configuration. | |
| 238 | Desktop renders the same web bundle, so it has them too. The waitlist enrollment flow from the | |
| 239 | private beta was removed when Connect went GA; sign-up is open unless a Clerk restriction below is | |
| 240 | enabled. | |
| 241 | ||
| 242 | ## Restricting Sign-ups: Known-User Allowlist | |
| 243 | ||
| 244 | For a closed deployment where all permitted users are known in advance, restrict sign-up to | |
| 245 | permitted email addresses or domains: | |
| 246 | ||
| 247 | 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email | |
| 248 | domain. | |
| 249 | 2. Enable the allowlist and save. | |
| 250 | 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or | |
| 251 | manually created. | |
| 252 | ||
| 253 | Do not enable an empty allowlist: it blocks all new sign-ups. | |
| 254 | ||
| 255 | Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud | |
| 256 | access. To remove an already-created user's access, ban that user in Clerk so their active | |
| 257 | sessions are ended and future sign-ins are rejected. |
docs/internals/workspace-layout.md created+63| ... | ... | @@ -0,0 +1,63 @@ |
| 1 | # Workspace layout | |
| 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 5 | A pnpm workspace driven by [vite-plus](https://vite.plus) (`vp`). See [scripts.md](./scripts.md) for | |
| 6 | the task commands. | |
| 7 | ||
| 8 | ## apps | |
| 9 | ||
| 10 | - `apps/server` (`t3`): the execution runtime and the published CLI. Owns orchestration, provider | |
| 11 | drivers, checkpointing, VCS, terminals, filesystem access, auth, and the HTTP + WebSocket surface. | |
| 12 | Also serves the built web app. | |
| 13 | - `apps/web` (`@t3tools/web`): React + Vite UI. Consumes the shared client runtime and adds routing, | |
| 14 | components, and web-specific platform layers. | |
| 15 | - `apps/desktop` (`@t3tools/desktop`): Electron shell. Supervises a desktop-scoped `t3` backend, | |
| 16 | loads the web bundle over the `t3code://` protocol, and owns SSH-managed remote environments. | |
| 17 | - `apps/mobile` (`@t3tools/mobile`): Expo/React Native client. Same client runtime composition as | |
| 18 | web, different platform layer and UI. | |
| 19 | - `apps/marketing` (`@t3tools/marketing`): Astro marketing site. | |
| 20 | ||
| 21 | ## packages | |
| 22 | ||
| 23 | - `packages/contracts` (`@t3tools/contracts`): shared Effect Schema definitions. RPC group, | |
| 24 | orchestration commands/events/read model, auth scopes, environment descriptors, settings. | |
| 25 | - `packages/shared` (`@t3tools/shared`): framework-agnostic utilities used by server and clients | |
| 26 | (`DrainableWorker`, git and source-control helpers, relay auth and signing, DPoP, semver, logging, | |
| 27 | observability, and more). | |
| 28 | - `packages/client-runtime` (`@t3tools/client-runtime`): connection lifecycle, authorization, RPC | |
| 29 | session, environment registry, and Atom-based domain state shared by web and mobile. See its | |
| 30 | [README](../../packages/client-runtime/README.md). | |
| 31 | - `packages/ssh` (`@t3tools/ssh`): SSH config parsing, auth prompts, command execution, and the | |
| 32 | tunnel/environment manager behind desktop-managed SSH environments. | |
| 33 | - `packages/tailscale` (`@t3tools/tailscale`): Tailscale CLI wrapper, including the | |
| 34 | `ensureTailscaleServe` / `disableTailscaleServe` serve lifecycle the server drives. | |
| 35 | - `packages/effect-acp` (`effect-acp`): Effect client and agent implementation of the Agent Client | |
| 36 | Protocol, used by ACP-speaking provider drivers. | |
| 37 | - `packages/effect-codex-app-server` (`effect-codex-app-server`): Effect client for the | |
| 38 | `codex app-server` JSON-RPC protocol. | |
| 39 | ||
| 40 | ## infra | |
| 41 | ||
| 42 | - `infra/relay` (`t3code-relay`): the hosted T3 Connect relay, deployed with Alchemy. Handles | |
| 43 | environment discovery, cloud-side records, and mobile notifications. It is not in the hot path; | |
| 44 | after connect, client traffic goes directly to the environment. See | |
| 45 | [t3-connect.md](./t3-connect.md). | |
| 46 | ||
| 47 | ## Other top-level directories | |
| 48 | ||
| 49 | - `scripts/`: workspace tooling run through `vp run`. Dev runner, desktop artifact builds, release | |
| 50 | helpers, mobile static checks and showcase capture, update-manifest merging. | |
| 51 | - `assets/`: brand and app icon sources per channel (`dev`, `nightly`, `prod`). | |
| 52 | - `patches/`: pnpm patches for pinned upstream dependencies. | |
| 53 | - `oxlint-plugin-t3code/`: repo-specific lint rules. | |
| 54 | - `experiments/`: throwaway prototypes. Not part of the shipped build. | |
| 55 | - `docs/`: this documentation tree. | |
| 56 | ||
| 57 | ## Import conventions | |
| 58 | ||
| 59 | `@t3tools/shared` and `@t3tools/client-runtime` use explicit subpath exports with no barrel index and | |
| 60 | no root export. Import the narrow path (`@t3tools/shared/DrainableWorker`, | |
| 61 | `@t3tools/client-runtime/state/threads`) rather than the package root. Files that are not exported | |
| 62 | are implementation details. `@t3tools/contracts` does export a root alongside `./settings` and | |
| 63 | `./relay`. |
docs/operations/ci.md deleted-6| ... | ... | @@ -1,6 +0,0 @@ |
| 1 | # CI quality gates | |
| 2 | ||
| 3 | - `.github/workflows/ci.yml` runs `vp check` (lint + typecheck), `vpr typecheck`, and `vp run test` on pull requests and pushes to `main`. | |
| 4 | - `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. | |
| 5 | - The release workflow auto-enables signing only when platform credentials are present. macOS passkey builds additionally require `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. Without the core signing credentials, it still releases unsigned artifacts. | |
| 6 | - See [Release Checklist](./release.md) for the full release/signing setup checklist. |
docs/operations/effect-fn-checklist.md deleted-198| ... | ... | @@ -1,198 +0,0 @@ |
| 1 | # Effect.fn Refactor Checklist | |
| 2 | ||
| 3 | Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`. | |
| 4 | ||
| 5 | Refactor Method: | |
| 6 | ||
| 7 | ```ts | |
| 8 | // Old | |
| 9 | function old () { | |
| 10 | return Effect.gen(function* () { | |
| 11 | ... | |
| 12 | }); | |
| 13 | } | |
| 14 | ||
| 15 | const old2 = () => Effect.gen(function* () { | |
| 16 | ... | |
| 17 | }); | |
| 18 | ``` | |
| 19 | ||
| 20 | ```ts | |
| 21 | // New | |
| 22 | const new = Effect.fn('functionName')(function* () { | |
| 23 | ... | |
| 24 | }) | |
| 25 | ``` | |
| 26 | ||
| 27 | - Use `Effect.fn('name')(function* (input: Input): Effect.fn.Return<A, E, R> {})` to annotate the return type of the function if needed. | |
| 28 | ||
| 29 | - The 2nd argument works as a pipe, and it gets the effect and input as arguments: | |
| 30 | ||
| 31 | ```ts | |
| 32 | Effect.fn("name")( | |
| 33 | function* (input: Input): Effect.fn.Return<A, E, R> {}, | |
| 34 | (effect, input) => Effect.catch(effect, (reason) => Effect.logWarning("Err", { input, reason })), | |
| 35 | ); | |
| 36 | ``` | |
| 37 | ||
| 38 | ## Summary | |
| 39 | ||
| 40 | - Total non-test candidates: `322` | |
| 41 | ||
| 42 | ## Suggested Order | |
| 43 | ||
| 44 | - [ ] `apps/server/src/provider/Layers/ProviderService.ts` | |
| 45 | - [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts` | |
| 46 | - [x] `apps/server/src/provider/Layers/CodexAdapter.ts` | |
| 47 | - [x] `apps/server/src/git/Layers/GitCore.ts` | |
| 48 | - [x] `apps/server/src/git/Layers/GitManager.ts` | |
| 49 | - [x] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` | |
| 50 | - [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` | |
| 51 | - [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` | |
| 52 | - [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts` | |
| 53 | - [ ] `Everything else` | |
| 54 | ||
| 55 | ## Checklist | |
| 56 | ||
| 57 | ### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`) | |
| 58 | ||
| 59 | - [x] [buildUserMessageEffect](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L554) | |
| 60 | - [x] [makeClaudeAdapter](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L913) | |
| 61 | - [x] [startSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414) | |
| 62 | - [x] [sendTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887) | |
| 63 | - [x] [interruptTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975) | |
| 64 | - [x] [readThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984) | |
| 65 | - [x] [rollbackThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990) | |
| 66 | - [x] [stopSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L3039) | |
| 67 | - [x] Internal helpers and callback wrappers in this file | |
| 68 | ||
| 69 | ### `apps/server/src/git/Layers/GitCore.ts` (`58`) | |
| 70 | ||
| 71 | - [x] [makeGitCore](../../apps/server/src/git/Layers/GitCore.ts#L513) | |
| 72 | - [x] [handleTraceLine](../../apps/server/src/git/Layers/GitCore.ts#L324) | |
| 73 | - [x] [emitCompleteLines](../../apps/server/src/git/Layers/GitCore.ts#L455) | |
| 74 | - [x] [commit](../../apps/server/src/git/Layers/GitCore.ts#L1190) | |
| 75 | - [x] [pushCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1223) | |
| 76 | - [x] [pullCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1323) | |
| 77 | - [x] [checkoutBranch](../../apps/server/src/git/Layers/GitCore.ts#L1727) | |
| 78 | - [x] Service methods and callback wrappers in this file | |
| 79 | ||
| 80 | ### `apps/server/src/git/Layers/GitManager.ts` (`28`) | |
| 81 | ||
| 82 | - [x] [configurePullRequestHeadUpstream](../../apps/server/src/git/Layers/GitManager.ts#L387) | |
| 83 | - [x] [materializePullRequestHeadBranch](../../apps/server/src/git/Layers/GitManager.ts#L428) | |
| 84 | - [x] [findOpenPr](../../apps/server/src/git/Layers/GitManager.ts#L576) | |
| 85 | - [x] [findLatestPr](../../apps/server/src/git/Layers/GitManager.ts#L602) | |
| 86 | - [x] [runCommitStep](../../apps/server/src/git/Layers/GitManager.ts#L728) | |
| 87 | - [x] [runPrStep](../../apps/server/src/git/Layers/GitManager.ts#L842) | |
| 88 | - [x] [runFeatureBranchStep](../../apps/server/src/git/Layers/GitManager.ts#L1106) | |
| 89 | - [x] Remaining helpers and nested callback wrappers in this file | |
| 90 | ||
| 91 | ### `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (`25`) | |
| 92 | ||
| 93 | - [x] [runProjectorForEvent](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161) | |
| 94 | - [x] [applyProjectsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357) | |
| 95 | - [x] [applyThreadsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415) | |
| 96 | - [x] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250` | |
| 97 | - [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264` | |
| 98 | - [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305` | |
| 99 | - [x] Remaining apply helpers in this file | |
| 100 | ||
| 101 | ### `apps/server/src/provider/Layers/ProviderService.ts` (`24`) | |
| 102 | ||
| 103 | - [ ] [makeProviderService](../../apps/server/src/provider/Layers/ProviderService.ts#L134) | |
| 104 | - [ ] [recoverSessionForThread](../../apps/server/src/provider/Layers/ProviderService.ts#L196) | |
| 105 | - [ ] [resolveRoutableSession](../../apps/server/src/provider/Layers/ProviderService.ts#L255) | |
| 106 | - [ ] [startSession](../../apps/server/src/provider/Layers/ProviderService.ts#L284) | |
| 107 | - [ ] [sendTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L347) | |
| 108 | - [ ] [interruptTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L393) | |
| 109 | - [ ] [respondToRequest](../../apps/server/src/provider/Layers/ProviderService.ts#L411) | |
| 110 | - [ ] [respondToUserInput](../../apps/server/src/provider/Layers/ProviderService.ts#L430) | |
| 111 | - [ ] [stopSession](../../apps/server/src/provider/Layers/ProviderService.ts#L445) | |
| 112 | - [ ] [listSessions](../../apps/server/src/provider/Layers/ProviderService.ts#L466) | |
| 113 | - [ ] [rollbackConversation](../../apps/server/src/provider/Layers/ProviderService.ts#L516) | |
| 114 | - [ ] [runStopAll](../../apps/server/src/provider/Layers/ProviderService.ts#L538) | |
| 115 | ||
| 116 | ### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`) | |
| 117 | ||
| 118 | - [x] [finalizeAssistantMessage](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680) | |
| 119 | - [x] [upsertProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722) | |
| 120 | - [x] [finalizeBufferedProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761) | |
| 121 | - [x] [clearTurnStateForSession](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800) | |
| 122 | - [x] [processRuntimeEvent](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908) | |
| 123 | - [x] Nested callback wrappers in this file | |
| 124 | ||
| 125 | ### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`) | |
| 126 | ||
| 127 | - [x] [makeCodexAdapter](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1317) | |
| 128 | - [x] [sendTurn](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1399) | |
| 129 | - [x] [writeNativeEvent](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1546) | |
| 130 | - [x] [listener](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1555) | |
| 131 | - [x] Remaining nested callback wrappers in this file | |
| 132 | ||
| 133 | ### `apps/server/src/checkpointing/CheckpointStore.ts` (`10`) | |
| 134 | ||
| 135 | - [ ] [captureCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L123) | |
| 136 | - [ ] [restoreCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L137) | |
| 137 | - [ ] [diffCheckpoints](../../apps/server/src/checkpointing/CheckpointStore.ts#L144) | |
| 138 | - [ ] [deleteCheckpointRefs](../../apps/server/src/checkpointing/CheckpointStore.ts#L151) | |
| 139 | - [ ] Nested callback wrappers in this file | |
| 140 | ||
| 141 | ### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`) | |
| 142 | ||
| 143 | - [ ] [toLogMessage](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77) | |
| 144 | - [ ] [makeThreadWriter](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102) | |
| 145 | - [ ] [makeEventNdjsonLogger](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174) | |
| 146 | - [ ] [write](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231) | |
| 147 | - [ ] [close](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L247) | |
| 148 | - [ ] Flush and writer-resolution callback wrappers in this file | |
| 149 | ||
| 150 | ### `apps/server/scripts/cli.ts` (`8`) | |
| 151 | ||
| 152 | - [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L125) | |
| 153 | - [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L170) | |
| 154 | - [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L221) | |
| 155 | - [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L239) | |
| 156 | ||
| 157 | ### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`) | |
| 158 | ||
| 159 | - [ ] [processEnvelope](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64) | |
| 160 | - [ ] [dispatch](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218) | |
| 161 | - [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162) | |
| 162 | - [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L200) | |
| 163 | ||
| 164 | ### `apps/server/src/orchestration/projector.ts` (`5`) | |
| 165 | ||
| 166 | - [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L242) | |
| 167 | - [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L336) | |
| 168 | - [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L397) | |
| 169 | - [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L446) | |
| 170 | - [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L478) | |
| 171 | ||
| 172 | ### Smaller clusters | |
| 173 | ||
| 174 | - [ ] [packages/shared/src/DrainableWorker.ts](../../packages/shared/src/DrainableWorker.ts) (`4`) | |
| 175 | - [ ] [apps/server/src/wsServer/pushBus.ts](../../apps/server/src/wsServer/pushBus.ts) (`4`) | |
| 176 | - [ ] [apps/server/src/wsServer.ts](../../apps/server/src/wsServer.ts) (`4`) | |
| 177 | - [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](../../apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`) | |
| 178 | - [ ] [apps/server/src/persistence/Layers/Sqlite.ts](../../apps/server/src/persistence/Layers/Sqlite.ts) (`4`) | |
| 179 | - [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`) | |
| 180 | - [ ] [apps/server/src/main.ts](../../apps/server/src/main.ts) (`4`) | |
| 181 | - [ ] [apps/server/src/keybindings.ts](../../apps/server/src/keybindings.ts) (`4`) | |
| 182 | - [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](../../apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`) | |
| 183 | - [ ] [apps/server/src/serverLayers.ts](../../apps/server/src/serverLayers.ts) (`3`) | |
| 184 | - [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](../../apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`) | |
| 185 | - [ ] [apps/server/src/telemetry/Identify.ts](../../apps/server/src/telemetry/Identify.ts) (`2`) | |
| 186 | - [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](../../apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`) | |
| 187 | - [ ] [apps/server/src/provider/Layers/CodexProvider.ts](../../apps/server/src/provider/Layers/CodexProvider.ts) (`2`) | |
| 188 | - [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](../../apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`) | |
| 189 | - [ ] [apps/server/src/persistence/NodeSqliteClient.ts](../../apps/server/src/persistence/NodeSqliteClient.ts) (`2`) | |
| 190 | - [ ] [apps/server/src/persistence/Migrations.ts](../../apps/server/src/persistence/Migrations.ts) (`2`) | |
| 191 | - [ ] [apps/server/src/open.ts](../../apps/server/src/open.ts) (`2`) | |
| 192 | - [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](../../apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) | |
| 193 | - [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](../../apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`) | |
| 194 | - [ ] [apps/server/src/provider/makeManagedServerProvider.ts](../../apps/server/src/provider/makeManagedServerProvider.ts) (`1`) | |
| 195 | ||
| 196 | ``` | |
| 197 | ||
| 198 | ``` |
docs/operations/mobile-app-store-screenshots.md+48-26| ... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 | # Mobile app-store screenshot harness |
| 2 | 2 | |
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 3 | 5 | The screenshot harness runs the real mobile application against three disposable local T3 |
| 4 | 6 | environments. It creates an isolated base directory and server for each environment, real Git |
| 5 | 7 | projects with deterministic content, seeded orchestration projections, and persisted terminal |
| ... | ... | @@ -44,35 +46,44 @@ delay allows native terminal and Git review data to finish rendering. |
| 44 | 46 | A full capture regenerates the selected native project with Expo's clean development prebuild before |
| 45 | 47 | building it. Use --skip-build for repeated captures after the first build. |
| 46 | 48 | |
| 47 | The harness uses its own Metro port (8199 by default), so an ordinary mobile server or another | |
| 48 | worktree cannot accidentally provide the bundle being photographed. | |
| 49 | The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is | |
| 50 | shared across every checkout. The readiness check only verifies that the port is open; it does not | |
| 51 | verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore | |
| 52 | collide or attach to the wrong Metro process. | |
| 53 | ||
| 54 | Every configured device defaults to dark appearance, so plain `pnpm screenshots:mobile` produces | |
| 55 | 30 dark PNGs. Pass `--appearance light`, `--appearance dark`, or `--appearance both` to override the | |
| 56 | configured appearance; `both` produces 60 PNGs. | |
| 49 | 57 | |
| 50 | 58 | The default matrix is: |
| 51 | 59 | |
| 52 | | Output folder | Capture target | Upload dimensions | Store slot | | |
| 53 | | ------------------------------------- | ------------------------- | ----------------- | ----------------------------------------- | | |
| 54 | | `apple/iphone-6.9/{light,dark}/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | | |
| 55 | | `apple/iphone-6.5/{light,dark}/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | | |
| 56 | | `apple/ipad-13/{light,dark}/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | | |
| 57 | | `google-play/phone/{light,dark}/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | | |
| 58 | | `google-play/tablet-7/{light,dark}/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | | |
| 59 | | `google-play/tablet-10/{light,dark}/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | | |
| 60 | ||
| 61 | Each target captures thread, terminal, review, thread list, and environments, producing 30 PNG | |
| 62 | files for one appearance or 60 for both. Each appearance folder's five screenshots satisfy the configured Apple limit of 1–10, Google | |
| 60 | | Output folder | Capture target | Upload dimensions | Store slot | | |
| 61 | | ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- | | |
| 62 | | `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | | |
| 63 | | `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | | |
| 64 | | `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | | |
| 65 | | `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | | |
| 66 | | `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | | |
| 67 | | `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | | |
| 68 | ||
| 69 | Each target captures thread, terminal, review, thread list, and environments. Each appearance | |
| 70 | folder's five screenshots satisfy the configured Apple limit of 1–10, Google | |
| 63 | 71 | phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8. |
| 64 | 72 | |
| 65 | 73 | The generated tree is deliberately aligned with the store upload fields: |
| 66 | 74 | |
| 67 | 75 | artifacts/app-store/screenshots/ |
| 68 | 76 | ├── apple/ |
| 69 | │ ├── iphone-6.9/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 70 | │ ├── iphone-6.5/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 71 | │ └── ipad-13/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 77 | │ ├── iphone-6.9/dark/{thread,terminal,review,threads,environments}.png | |
| 78 | │ ├── iphone-6.5/dark/{thread,terminal,review,threads,environments}.png | |
| 79 | │ └── ipad-13/dark/{thread,terminal,review,threads,environments}.png | |
| 72 | 80 | └── google-play/ |
| 73 | ├── phone/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 74 | ├── tablet-7/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 75 | └── tablet-10/{light,dark}/{thread,terminal,review,threads,environments}.png | |
| 81 | ├── phone/dark/{thread,terminal,review,threads,environments}.png | |
| 82 | ├── tablet-7/dark/{thread,terminal,review,threads,environments}.png | |
| 83 | └── tablet-10/dark/{thread,terminal,review,threads,environments}.png | |
| 84 | ||
| 85 | A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance | |
| 86 | folders. | |
| 76 | 87 | |
| 77 | 88 | Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD |
| 78 | 89 | names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport. |
| ... | ... | @@ -85,10 +96,11 @@ runs iOS and Android concurrently: iPhone and iPad capture on a |
| 85 | 96 | 12-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a |
| 86 | 97 | 16-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator. |
| 87 | 98 | |
| 88 | Every job uploads its PNGs even when a later capture fails, which makes partial runs useful for | |
| 89 | diagnosis. Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow | |
| 90 | run's Artifacts section. Each job runs validation again immediately before upload. Artifacts are | |
| 91 | retained for 14 days. | |
| 99 | Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis. | |
| 100 | The separate validation step is success-gated: it runs before upload only when capture succeeds. If | |
| 101 | capture fails, the `always()` upload still publishes partial PNGs without re-validating them. | |
| 102 | Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's | |
| 103 | Artifacts section. Artifacts are retained for 14 days. | |
| 92 | 104 | |
| 93 | 105 | The workflow uses the same checked-in device and scene matrix as local capture. Android remains |
| 94 | 106 | ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the |
| ... | ... | @@ -111,11 +123,19 @@ Reuse the native build and retain the disposable environment: |
| 111 | 123 | |
| 112 | 124 | pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running |
| 113 | 125 | |
| 114 | Run Metro separately: | |
| 126 | By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate | |
| 127 | terminal, start it with the same showcase environment and explicit harness port: | |
| 128 | ||
| 129 | cd apps/mobile | |
| 130 | APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 pnpm exec expo start --dev-client --port 8199 | |
| 131 | ||
| 132 | Then run the capture from the repository root: | |
| 115 | 133 | |
| 116 | pnpm --filter @t3tools/mobile showcase | |
| 117 | 134 | pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9 |
| 118 | 135 | |
| 136 | `pnpm --filter @t3tools/mobile showcase` starts Expo on its normal port, so it is not compatible with | |
| 137 | the harness's `--skip-metro` mode. | |
| 138 | ||
| 119 | 139 | List the matrix and flags: |
| 120 | 140 | |
| 121 | 141 | pnpm screenshots:mobile --list |
| ... | ... | @@ -151,7 +171,9 @@ remote-first while the harness retains reliable loopback connections to its ephe |
| 151 | 171 | ## Local prerequisites |
| 152 | 172 | |
| 153 | 173 | - iOS: Xcode command-line tools, the configured simulator runtimes, and installed CocoaPods. |
| 154 | - Android: ANDROID_HOME (or the default macOS SDK path), adb, emulator, and the configured AVD. | |
| 174 | - Android: SDK resolution checks `ANDROID_HOME`, then `ANDROID_SDK_ROOT`, then defaults to | |
| 175 | `$HOME/Library/Android/sdk` on macOS or `$HOME/Android/Sdk` on other platforms. The resolved SDK | |
| 176 | must provide `adb` and `emulator`, and the configured AVD must exist. | |
| 155 | 177 | |
| 156 | 178 | The harness is the source of truth for upload dimensions; do not resize its output. If store rules |
| 157 | 179 | change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size, |
docs/operations/observability.md+41-20| ... | ... | @@ -1,39 +1,53 @@ |
| 1 | 1 | # Observability |
| 2 | 2 | |
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 3 | 5 | T3 Code has one server-side observability model: |
| 4 | 6 | |
| 5 | 7 | - pretty logs go to stdout for humans |
| 6 | 8 | - completed spans go to a local NDJSON trace file |
| 7 | 9 | - traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM |
| 8 | 10 | |
| 9 | The local trace file is the persisted source of truth. There is no separate persisted server log file anymore. | |
| 11 | The local trace file is the persisted source of truth for normal local launches. Those launches do not | |
| 12 | write a separate server log file, but SSH-managed launches also persist the remote process's | |
| 13 | stdout/stderr at `~/.t3/ssh-launch/<state>/server.log`. | |
| 10 | 14 | |
| 11 | 15 | ## Where To Find Things |
| 12 | 16 | |
| 13 | 17 | ### Logs |
| 14 | 18 | |
| 15 | Logs are human-facing only: | |
| 19 | Logs are human-facing: | |
| 16 | 20 | |
| 17 | 21 | - destination: stdout |
| 18 | 22 | - format: `Logger.consolePretty()` |
| 19 | - persistence: none | |
| 23 | - normal local persistence: none | |
| 24 | - SSH-managed launch persistence: `~/.t3/ssh-launch/<state>/server.log` | |
| 20 | 25 | |
| 21 | 26 | If you want a log message to show up in the trace file, emit it inside an active span with `Effect.log...`. `Logger.tracerLogger` will attach it as a span event. |
| 22 | 27 | |
| 23 | 28 | ### Traces |
| 24 | 29 | |
| 25 | Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`). | |
| 30 | Completed spans are written as NDJSON records to `serverTracePath`. The default depends on how the | |
| 31 | server starts: production and explicitly configured homes use | |
| 32 | `<home>/userdata/logs/server.trace.ndjson` (so `~/.t3/userdata/...` by default, or | |
| 33 | `/custom/path/userdata/...` with `--home-dir /custom/path`), a linked worktree dev run uses | |
| 34 | `<worktree>/.t3/userdata/logs/server.trace.ndjson`, and an implicit dev run outside a linked | |
| 35 | worktree uses `~/.t3/dev/logs/server.trace.ndjson`. | |
| 26 | 36 | |
| 27 | Important fields in each record: | |
| 37 | Important fields common to both record types: | |
| 28 | 38 | |
| 39 | - `type`: `effect-span` or `otlp-span` | |
| 29 | 40 | - `name`: span name |
| 30 | 41 | - `traceId`, `spanId`, `parentSpanId`: correlation |
| 31 | 42 | - `durationMs`: elapsed time |
| 32 | 43 | - `attributes`: structured context |
| 33 | 44 | - `events`: embedded logs and custom events |
| 34 | - `exit`: `Success`, `Failure`, or `Interrupted` | |
| 35 | 45 | |
| 36 | The schema lives in `apps/server/src/observability/TraceRecord.ts`. | |
| 46 | `effect-span` records also contain `exit` with `Success`, `Failure`, or `Interrupted`. `otlp-span` | |
| 47 | records instead carry OTLP resource, scope, and optional status fields. | |
| 48 | ||
| 49 | The `TraceRecord`, `EffectTraceRecord`, and `OtlpTraceRecord` schemas live in | |
| 50 | `packages/shared/src/observability.ts`. | |
| 37 | 51 | |
| 38 | 52 | ### Metrics |
| 39 | 53 | |
| ... | ... | @@ -165,30 +179,35 @@ The backend reads observability config at process start. If you change OTLP env |
| 165 | 179 | |
| 166 | 180 | The trace file is the fastest way to inspect raw span data. |
| 167 | 181 | |
| 168 | Resolve the production or explicitly configured trace file once. Runtime state lives under the | |
| 169 | base directory's `userdata` folder: | |
| 182 | Resolve the path for the launch mode once. Production and explicitly configured homes store runtime | |
| 183 | state under the base directory's `userdata` folder: | |
| 170 | 184 | |
| 171 | 185 | ```bash |
| 172 | 186 | TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson" |
| 173 | 187 | ``` |
| 174 | 188 | |
| 175 | Tail it: | |
| 189 | A dev server started from a linked worktree defaults to that worktree's local home: | |
| 176 | 190 | |
| 177 | 191 | ```bash |
| 178 | tail -f "$TRACE_FILE" | |
| 192 | TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson" | |
| 179 | 193 | ``` |
| 180 | 194 | |
| 181 | For an implicit monorepo dev server, use: | |
| 195 | Only an implicit dev run outside a linked worktree uses the shared dev directory: | |
| 182 | 196 | |
| 183 | 197 | ```bash |
| 184 | 198 | TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson" |
| 199 | ``` | |
| 200 | ||
| 201 | Tail the selected file: | |
| 202 | ||
| 203 | ```bash | |
| 185 | 204 | tail -f "$TRACE_FILE" |
| 186 | 205 | ``` |
| 187 | 206 | |
| 188 | 207 | Show failed spans: |
| 189 | 208 | |
| 190 | 209 | ```bash |
| 191 | jq -c 'select(.exit._tag != "Success") | { | |
| 210 | jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | { | |
| 192 | 211 | name, |
| 193 | 212 | durationMs, |
| 194 | 213 | exit, |
| ... | ... | @@ -281,10 +300,12 @@ Recommended flow in Grafana: |
| 281 | 300 | Good first searches: |
| 282 | 301 | |
| 283 | 302 | - service name such as `t3-local`, `t3-dev`, or `t3-desktop` |
| 284 | - span names like `sql.execute`, `git.runCommand`, `provider.sendTurn` | |
| 303 | - span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status` | |
| 304 | - Git spans whose `git.operation` attribute identifies the operation | |
| 285 | 305 | - orchestration spans with attributes like `orchestration.command_type` |
| 286 | 306 | |
| 287 | Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful. | |
| 307 | Once you know traces are arriving, narrower TraceQL queries for names such as `sendTurn` or Git | |
| 308 | operation names become useful. | |
| 288 | 309 | |
| 289 | 310 | ### Use Metrics To See Systemic Problems |
| 290 | 311 | |
| ... | ... | @@ -297,7 +318,6 @@ Good metric families to watch: |
| 297 | 318 | - `t3_orchestration_command_ack_duration` |
| 298 | 319 | - `t3_provider_turn_duration` |
| 299 | 320 | - `t3_git_command_duration` |
| 300 | - `t3_db_query_duration` | |
| 301 | 321 | |
| 302 | 322 | Counters tell you volume and failure rate: |
| 303 | 323 | |
| ... | ... | @@ -305,7 +325,6 @@ Counters tell you volume and failure rate: |
| 305 | 325 | - `t3_orchestration_commands_total` |
| 306 | 326 | - `t3_provider_turns_total` |
| 307 | 327 | - `t3_git_commands_total` |
| 308 | - `t3_db_queries_total` | |
| 309 | 328 | |
| 310 | 329 | Use metrics when the question is: |
| 311 | 330 | |
| ... | ... | @@ -339,7 +358,7 @@ If you need those later, add client-side instrumentation or a dedicated server f |
| 339 | 358 | ### "Why did this request fail?" |
| 340 | 359 | |
| 341 | 360 | 1. Start with the local NDJSON file. |
| 342 | 2. Find spans where `exit._tag != "Success"`. | |
| 361 | 2. Find `effect-span` records where `exit._tag != "Success"`. | |
| 343 | 362 | 3. Group by `traceId`. |
| 344 | 363 | 4. Inspect sibling spans and span events. |
| 345 | 364 | 5. If needed, move to Tempo for the full trace tree. |
| ... | ... | @@ -522,6 +541,8 @@ Current high-value span and metric boundaries include: |
| 522 | 541 | |
| 523 | 542 | ### Current Constraints |
| 524 | 543 | |
| 525 | - logs outside spans are not persisted | |
| 544 | - logs outside spans are not persisted in the trace file; SSH-managed launch stdout/stderr is still | |
| 545 | captured in its launcher log | |
| 526 | 546 | - metrics are not snapshotted locally |
| 527 | - the old `serverLogPath` still exists in config for compatibility, but the trace file is the persisted artifact that matters | |
| 547 | - the old `serverLogPath` still exists in config for compatibility, but the trace file is the primary | |
| 548 | structured persisted artifact |
docs/operations/relay-observability.md+22-11| ... | ... | @@ -1,9 +1,14 @@ |
| 1 | 1 | # Relay observability |
| 2 | 2 | |
| 3 | The relay Alchemy stack owns a focused Axiom trace setup: | |
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | 4 | |
| 5 | - `t3-code-relay-traces-prod`, an OpenTelemetry trace dataset for Worker requests | |
| 6 | - `t3-code-relay-otel-ingest-prod`, a dataset-scoped ingest token bound to the Worker | |
| 5 | The relay Alchemy stack owns a shared Axiom trace setup: | |
| 6 | ||
| 7 | - `t3-code-relay-traces-prod`, the OpenTelemetry trace dataset shared by the Worker, mobile app, and | |
| 8 | first-party relay clients | |
| 9 | - `t3-code-relay-otel-ingest-prod`, the dataset-scoped Worker ingest token | |
| 10 | - `t3-code-mobile-otel-ingest-prod`, the dataset-scoped mobile ingest token | |
| 11 | - `t3-code-relay-client-otel-ingest-prod`, the dataset-scoped first-party relay-client ingest token | |
| 7 | 12 | - `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans |
| 8 | 13 | |
| 9 | 14 | Alchemy stages append their sanitized stage name to isolate resources, for example |
| ... | ... | @@ -15,8 +20,9 @@ Deploy from `infra/relay` with the normal Alchemy workflow: |
| 15 | 20 | vp run deploy |
| 16 | 21 | ``` |
| 17 | 22 | |
| 18 | Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker | |
| 19 | receives only the scoped ingest token; it does not receive the diagnostics query token. | |
| 23 | Alchemy resolves account-level Axiom deployment credentials through its provider. At runtime, the | |
| 24 | Worker receives only its scoped ingest token. Mobile and relay clients use their own separately | |
| 25 | provisioned scoped ingest tokens. | |
| 20 | 26 | |
| 21 | 27 | The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans. |
| 22 | 28 | Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix. |
| ... | ... | @@ -25,18 +31,23 @@ For example: |
| 25 | 31 | ```apl |
| 26 | 32 | ['t3-code-relay-traces-prod'] |
| 27 | 33 | | where name startswith 'http.server' |
| 34 | | extend endpoint = column_ifexists('attributes.http.route', ''), | |
| 35 | customAttributes = column_ifexists('attributes.custom', dynamic({})) | |
| 28 | 36 | | project _time, name, trace_id, duration, |
| 29 | 37 | ['attributes.http.request.method'], |
| 30 | 38 | ['attributes.url.path'], |
| 31 | ['attributes.http.response.status_code'] | |
| 39 | ['attributes.http.response.status_code'], | |
| 40 | endpoint, | |
| 41 | relayOperation = customAttributes['relay']['operation'] | |
| 32 | 42 | | order by _time desc |
| 33 | 43 | | limit 200 |
| 34 | 44 | ``` |
| 35 | 45 | |
| 36 | Endpoint failure annotations and other relay-specific attributes are also emitted in the | |
| 37 | `attributes.custom` map when present on a span, for example | |
| 38 | `['attributes.custom']['relay.endpoint']`. | |
| 46 | The provisioned view also reads the endpoint from `attributes.http.route`. Relay-specific span | |
| 47 | annotations are stored under `attributes.custom`; `relay.operation` is one of the emitted custom | |
| 48 | attributes. | |
| 39 | 49 | |
| 40 | 50 | Agents should prefer the provisioned view or APL queries for completed incidents instead of |
| 41 | tailing the Cloudflare Worker. Use the read-only query token when scripted access is needed; | |
| 42 | keep the ingest token reserved for the Worker. | |
| 51 | tailing the Cloudflare Worker. The stack does not provision a separate query token. Responders who | |
| 52 | need scripted query access use the authorized account-level `AXIOM_TOKEN` together with | |
| 53 | `AXIOM_ORG_ID`; scoped ingest tokens remain write-only credentials for their producers. |
docs/operations/release.md+39-20| ... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 | # Release Checklist |
| 2 | 2 | |
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | |
| 4 | ||
| 3 | 5 | This document covers the unified release workflow for stable and nightly desktop releases. |
| 4 | 6 | |
| 5 | 7 | ## What the workflow does |
| ... | ... | @@ -30,6 +32,17 @@ This document covers the unified release workflow for stable and nightly desktop |
| 30 | 32 | - nightly releases are aliased to the `nightly` hosted app channel |
| 31 | 33 | - Signing is optional and auto-detected per platform from secrets. |
| 32 | 34 | |
| 35 | ## Required release credentials | |
| 36 | ||
| 37 | The release workflow requires these GitHub Actions secrets in addition to the platform and deployment | |
| 38 | credentials documented below: | |
| 39 | ||
| 40 | - `RELEASE_APP_ID` | |
| 41 | - `RELEASE_APP_PRIVATE_KEY` | |
| 42 | ||
| 43 | The GitHub Release job uses them to mint the token that publishes release assets. Stable releases use | |
| 44 | them again in the finalize job, which can commit and push aligned package versions to `main`. | |
| 45 | ||
| 33 | 46 | ## T3 Connect relay deployment |
| 34 | 47 | |
| 35 | 48 | The relay is a shared control plane versioned separately from client releases. Stable and nightly |
| ... | ... | @@ -148,7 +161,8 @@ One-time Vercel dashboard setup: |
| 148 | 161 | - manual `workflow_dispatch` with `channel=nightly` |
| 149 | 162 | - Runs the same desktop quality gates and artifact matrix as the tagged release flow. |
| 150 | 163 | - Publishes a GitHub prerelease only: |
| 151 | - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>` | |
| 164 | - current tag format: `vX.Y.Z-nightly.YYYYMMDD.<run_number>` | |
| 165 | - `nightly-v...` is accepted only as a legacy previous-nightly tag | |
| 152 | 166 | - release name includes the short commit SHA |
| 153 | 167 | - `make_latest` is always `false` |
| 154 | 168 | - Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`. |
| ... | ... | @@ -178,7 +192,9 @@ guidance when those environments are available. |
| 178 | 192 | |
| 179 | 193 | ## Desktop auto-update notes |
| 180 | 194 | |
| 181 | - Runtime updater: `electron-updater` in `apps/desktop/src/main.ts`. | |
| 195 | - Updater runtime: `apps/desktop/src/updates/DesktopUpdates.ts`. | |
| 196 | - `electron-updater` adapter: `apps/desktop/src/electron/ElectronUpdater.ts`. | |
| 197 | - `apps/desktop/src/main.ts` only wires the updater layers into the desktop runtime. | |
| 182 | 198 | - Update UX: |
| 183 | 199 | - Background checks run on startup delay + interval. |
| 184 | 200 | - No automatic download or install. |
| ... | ... | @@ -187,9 +203,6 @@ guidance when those environments are available. |
| 187 | 203 | - Repository slug source: |
| 188 | 204 | - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. |
| 189 | 205 | - otherwise `GITHUB_REPOSITORY` from GitHub Actions. |
| 190 | - Temporary private-repo auth workaround: | |
| 191 | - set `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment. | |
| 192 | - the app forwards it as an `Authorization: Bearer <token>` request header for updater HTTP calls. | |
| 193 | 206 | - Required release assets for updater: |
| 194 | 207 | - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads) |
| 195 | 208 | - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases |
| ... | ... | @@ -200,8 +213,9 @@ guidance when those environments are available. |
| 200 | 213 | |
| 201 | 214 | ## 0) npm OIDC trusted publishing setup (CLI) |
| 202 | 215 | |
| 203 | The workflow publishes the CLI with `npm publish` from `apps/server` after bumping | |
| 204 | the package version to the release tag version. | |
| 216 | The workflow invokes `node apps/server/scripts/cli.ts publish` after aligning package versions. That | |
| 217 | script temporarily prepares the `t3` package, then runs `vp pm publish --filter t3 ...` from the | |
| 218 | repository root so workspace publish configuration is applied correctly. | |
| 205 | 219 | |
| 206 | 220 | Checklist: |
| 207 | 221 | |
| ... | ... | @@ -213,22 +227,27 @@ Checklist: |
| 213 | 227 | - Environment (if used): match your npm trusted publishing config |
| 214 | 228 | 3. Ensure npm account and org policies allow trusted publishing for the package. |
| 215 | 229 | 4. Create release tag `vX.Y.Z` and push; workflow will: |
| 216 | - set `apps/server/package.json` version to `X.Y.Z` | |
| 230 | - align the release package versions to `X.Y.Z` | |
| 217 | 231 | - build web + server |
| 218 | - run `npm publish --access public --tag latest` | |
| 219 | 5. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`. | |
| 232 | - invoke the CLI publish script with npm dist-tag `latest` | |
| 233 | 5. Nightly runs invoke the same publish script with npm dist-tag `nightly`. | |
| 234 | ||
| 235 | ## 1) Release validation and unsigned builds | |
| 220 | 236 | |
| 221 | ## 1) Dry-run release without signing | |
| 237 | There is no dry-run tag path. Pushing any accepted non-nightly tag, including | |
| 238 | `v0.0.0-test.1`, classifies the run as the stable channel. It publishes `t3` with npm dist-tag | |
| 239 | `latest`, creates a real GitHub Release, aliases the hosted app to `latest.app.t3.codes` and | |
| 240 | `app.t3.codes`, and can commit a version bump to `main` in the finalize job. Do not push a test tag | |
| 241 | to validate the workflow. | |
| 222 | 242 | |
| 223 | Use this first to validate the release pipeline. | |
| 243 | The workflow has no non-publishing `workflow_dispatch` mode. Use normal CI or local quality gates to | |
| 244 | validate checks and builds without shipping. To exercise the complete release graph at lower stable | |
| 245 | risk, manually dispatch `channel=nightly`; this still publishes a real nightly npm package, GitHub | |
| 246 | prerelease, desktop updater release, and hosted nightly alias, but it does not update stable aliases or | |
| 247 | commit a version bump to `main`. Only run it when a real nightly release is acceptable. | |
| 224 | 248 | |
| 225 | 1. Confirm no signing secrets are required for this test. | |
| 226 | 2. Create a test tag: | |
| 227 | - `git tag v0.0.0-test.1` | |
| 228 | - `git push origin v0.0.0-test.1` | |
| 229 | 3. Wait for `.github/workflows/release.yml` to finish. | |
| 230 | 4. Verify the GitHub Release contains all platform artifacts. | |
| 231 | 5. Download each artifact and sanity-check installation on each OS. | |
| 249 | Manual `channel=stable` with a version input is also a real stable-channel release. Omitting signing | |
| 250 | secrets only makes platform artifacts unsigned; it does not prevent publication. | |
| 232 | 251 | |
| 233 | 252 | ## 2) Apple signing + notarization setup (macOS) |
| 234 | 253 | |
| ... | ... | @@ -267,7 +286,7 @@ Checklist: |
| 267 | 286 | - `APPLE_API_KEY`: contents of the downloaded `.p8` |
| 268 | 287 | - `APPLE_API_KEY_ID`: Key ID |
| 269 | 288 | - `APPLE_API_ISSUER`: Issuer ID |
| 270 | 10. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../cloud/t3-connect-clerk.md#desktop-passkeys). | |
| 289 | 10. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../internals/t3-connect.md#desktop-passkeys). | |
| 271 | 290 | 11. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected |
| 272 | 291 | `com.apple.developer.associated-domains` entitlement. |
| 273 | 292 |
docs/project/todo.md deleted-13| ... | ... | @@ -1,13 +0,0 @@ |
| 1 | # TODO | |
| 2 | ||
| 3 | ## Small things | |
| 4 | ||
| 5 | - [ ] Submitting new messages should scroll to bottom | |
| 6 | - [ ] Only show last 10 threads for a given project | |
| 7 | - [ ] Thread archiving | |
| 8 | - [ ] New projects should go on top | |
| 9 | - [ ] Projects should be sorted by latest thread update | |
| 10 | ||
| 11 | ## Bigger things | |
| 12 | ||
| 13 | - [ ] Queueing messages |
docs/providers/claude.md deleted-224| ... | ... | @@ -1,224 +0,0 @@ |
| 1 | # Claude | |
| 2 | ||
| 3 | This guide is for people who want to use more than one Claude setup in T3 Code. | |
| 4 | ||
| 5 | Common reasons: | |
| 6 | ||
| 7 | - use separate work and personal Claude accounts | |
| 8 | - try a different Claude Code configuration without disturbing your main setup | |
| 9 | - run Claude through a router such as Claude Code Router | |
| 10 | - use external providers exposed through a Claude-compatible workflow | |
| 11 | ||
| 12 | ## I Only Use One Claude Account | |
| 13 | ||
| 14 | Use the default provider. | |
| 15 | ||
| 16 | Log in with Claude Code normally: | |
| 17 | ||
| 18 | ```bash | |
| 19 | claude auth login | |
| 20 | ``` | |
| 21 | ||
| 22 | In T3 Code Settings, your Claude provider can stay like this: | |
| 23 | ||
| 24 | ```text | |
| 25 | Display name: Claude | |
| 26 | Binary path: claude | |
| 27 | Claude HOME path: empty | |
| 28 | ``` | |
| 29 | ||
| 30 | An empty `Claude HOME path` means T3 Code uses your normal home directory. | |
| 31 | ||
| 32 | ## I Want Work And Personal Claude Accounts | |
| 33 | ||
| 34 | Use a different Claude home for each account. | |
| 35 | ||
| 36 | Example: | |
| 37 | ||
| 38 | ```text | |
| 39 | default home work account | |
| 40 | ~/.claude_personal_home personal account | |
| 41 | ``` | |
| 42 | ||
| 43 | ### Set Up The First Account | |
| 44 | ||
| 45 | Log in normally: | |
| 46 | ||
| 47 | ```bash | |
| 48 | claude auth login | |
| 49 | ``` | |
| 50 | ||
| 51 | In T3 Code Settings: | |
| 52 | ||
| 53 | ```text | |
| 54 | Display name: Claude Work | |
| 55 | Binary path: claude | |
| 56 | Claude HOME path: empty | |
| 57 | ``` | |
| 58 | ||
| 59 | ### Set Up The Second Account | |
| 60 | ||
| 61 | Log in with a separate home: | |
| 62 | ||
| 63 | ```bash | |
| 64 | mkdir -p ~/.claude_personal_home | |
| 65 | HOME=~/.claude_personal_home claude auth login | |
| 66 | ``` | |
| 67 | ||
| 68 | Then add another Claude provider in T3 Code: | |
| 69 | ||
| 70 | ```text | |
| 71 | Display name: Claude Personal | |
| 72 | Binary path: claude | |
| 73 | Claude HOME path: ~/.claude_personal_home | |
| 74 | ``` | |
| 75 | ||
| 76 | Use the email shown in Settings to confirm each provider is using the intended account. Emails are | |
| 77 | blurred by default; click the blurred email to reveal it. | |
| 78 | ||
| 79 | ## Can I Switch Claude Accounts In An Existing Thread? | |
| 80 | ||
| 81 | Usually, no. | |
| 82 | ||
| 83 | T3 Code only offers Claude providers that use the same Claude home for an existing thread. A | |
| 84 | different Claude home is treated as a different Claude environment. | |
| 85 | ||
| 86 | This is different from the recommended Codex setup. Claude Code keeps account and local state across | |
| 87 | multiple files under its home directory, so T3 Code keeps separate Claude homes isolated instead of | |
| 88 | trying to share part of the state. | |
| 89 | ||
| 90 | ## I Want To Use OpenRouter | |
| 91 | ||
| 92 | Use this when you want Claude Code to talk to OpenRouter directly, without running a local router. | |
| 93 | This is the simplest external-provider setup. | |
| 94 | ||
| 95 | OpenRouter provides a Claude Code integration through Claude's Anthropic-compatible environment | |
| 96 | variables. | |
| 97 | ||
| 98 | ### Configure A Claude OpenRouter Provider | |
| 99 | ||
| 100 | Add or edit a Claude provider in T3 Code Settings: | |
| 101 | ||
| 102 | ```text | |
| 103 | Display name: Claude OpenRouter | |
| 104 | Binary path: claude | |
| 105 | Claude HOME path: ~/.claude_openrouter_home | |
| 106 | ``` | |
| 107 | ||
| 108 | In that provider's Environment variables section, add: | |
| 109 | ||
| 110 | ```text | |
| 111 | ANTHROPIC_BASE_URL https://openrouter.ai/api | |
| 112 | ANTHROPIC_AUTH_TOKEN sk-or-... Sensitive | |
| 113 | ANTHROPIC_API_KEY Empty value | |
| 114 | ``` | |
| 115 | ||
| 116 | Mark `ANTHROPIC_AUTH_TOKEN` as sensitive. T3 Code stores the value as a server secret and does not | |
| 117 | send it back to the app after saving. | |
| 118 | ||
| 119 | If you want this setup isolated from your normal Claude account, create that home first: | |
| 120 | ||
| 121 | ```bash | |
| 122 | mkdir -p ~/.claude_openrouter_home | |
| 123 | ``` | |
| 124 | ||
| 125 | If you previously used the same Claude home with a normal Anthropic login, run `/logout` in a Claude | |
| 126 | Code session for that home before using OpenRouter. Otherwise Claude Code may keep using cached | |
| 127 | Anthropic credentials instead of the OpenRouter token. | |
| 128 | ||
| 129 | ### Pick OpenRouter Models | |
| 130 | ||
| 131 | OpenRouter can route Claude Code's default model roles to OpenRouter model IDs. | |
| 132 | ||
| 133 | Example: | |
| 134 | ||
| 135 | ```text | |
| 136 | ANTHROPIC_DEFAULT_OPUS_MODEL anthropic/claude-opus-4.6 | |
| 137 | ANTHROPIC_DEFAULT_SONNET_MODEL anthropic/claude-sonnet-4.6 | |
| 138 | ANTHROPIC_DEFAULT_HAIKU_MODEL anthropic/claude-haiku-4.5 | |
| 139 | CLAUDE_CODE_SUBAGENT_MODEL anthropic/claude-sonnet-4.6 | |
| 140 | ``` | |
| 141 | ||
| 142 | Add those to the same provider's Environment variables section if you want stable model choices. | |
| 143 | ||
| 144 | ### Verify OpenRouter Is Being Used | |
| 145 | ||
| 146 | Open a Claude session and run: | |
| 147 | ||
| 148 | ```text | |
| 149 | /status | |
| 150 | ``` | |
| 151 | ||
| 152 | You should see the Anthropic base URL set to: | |
| 153 | ||
| 154 | ```text | |
| 155 | https://openrouter.ai/api | |
| 156 | ``` | |
| 157 | ||
| 158 | You can also check the OpenRouter activity dashboard for requests from your API key. | |
| 159 | ||
| 160 | ### Common OpenRouter Mistakes | |
| 161 | ||
| 162 | - Use `https://openrouter.ai/api`, not `https://openrouter.ai/api/v1`, for Claude Code. | |
| 163 | - Set `ANTHROPIC_AUTH_TOKEN` to your OpenRouter API key. | |
| 164 | - Set `ANTHROPIC_API_KEY` to an empty string so Claude Code does not try to use an Anthropic login. | |
| 165 | - Put these variables on the Claude provider instance, not in global shell startup files. | |
| 166 | ||
| 167 | OpenRouter's setup can change over time. Use its upstream Claude Code guide for the current details: | |
| 168 | <https://openrouter.ai/docs/guides/guides/claude-code-integration>. | |
| 169 | ||
| 170 | ## I Want To Use Claude Code Router | |
| 171 | ||
| 172 | Claude Code Router is useful when you want a local routing layer with more control than a direct | |
| 173 | OpenRouter setup. | |
| 174 | ||
| 175 | T3 Code does not need a special Claude Code Router provider. Treat the router as a Claude | |
| 176 | environment. | |
| 177 | ||
| 178 | Use this when you want Claude Code Router to decide which upstream model or provider handles Claude | |
| 179 | requests. | |
| 180 | ||
| 181 | High-level flow: | |
| 182 | ||
| 183 | 1. Start Claude Code Router. | |
| 184 | 2. Add or configure a Claude provider in T3 Code. | |
| 185 | 3. Put the router's required variables on that provider instance. | |
| 186 | ||
| 187 | Configure a Claude provider: | |
| 188 | ||
| 189 | ```text | |
| 190 | Display name: Claude Router | |
| 191 | Binary path: claude | |
| 192 | Claude HOME path: ~/.claude_router_home | |
| 193 | ``` | |
| 194 | ||
| 195 | Then copy the variables that `ccr activate` would export into the provider's Environment variables | |
| 196 | section. Mark tokens and API keys as sensitive. | |
| 197 | ||
| 198 | If you want the router-backed setup to stay separate from your normal Claude account, create and log | |
| 199 | in with a dedicated home first: | |
| 200 | ||
| 201 | ```bash | |
| 202 | mkdir -p ~/.claude_router_home | |
| 203 | ccr start | |
| 204 | ccr activate | |
| 205 | HOME=~/.claude_router_home claude auth login | |
| 206 | ``` | |
| 207 | ||
| 208 | Claude Code Router's setup can change over time. Use its upstream README for the current install and | |
| 209 | configuration steps: <https://github.com/musistudio/claude-code-router>. | |
| 210 | ||
| 211 | ## I Want Different Claude Settings, Not A Different Account | |
| 212 | ||
| 213 | Create another Claude provider with the same account if you want a named preset. | |
| 214 | ||
| 215 | Examples: | |
| 216 | ||
| 217 | - "Claude Default" | |
| 218 | - "Claude Router" | |
| 219 | - "Claude Experimental" | |
| 220 | ||
| 221 | If the preset needs different Claude files, give it a different `Claude HOME path`. If it needs | |
| 222 | different API keys, base URLs, or router settings, use Environment variables. | |
| 223 | ||
| 224 | Do not put environment variable assignments in `Launch arguments`. |
docs/providers/codex.md deleted-140| ... | ... | @@ -1,140 +0,0 @@ |
| 1 | # Codex | |
| 2 | ||
| 3 | This guide is for people who want to use more than one Codex account in T3 Code. | |
| 4 | ||
| 5 | Common reasons: | |
| 6 | ||
| 7 | - use a work account for work projects | |
| 8 | - use a personal account for personal projects | |
| 9 | - switch to another account when one account hits limits | |
| 10 | - keep one shared Codex history instead of maintaining two separate Codex setups | |
| 11 | ||
| 12 | ## I Only Use One Codex Account | |
| 13 | ||
| 14 | Use the default provider. | |
| 15 | ||
| 16 | In Settings, your Codex provider can stay like this: | |
| 17 | ||
| 18 | ```text | |
| 19 | Display name: Codex | |
| 20 | CODEX_HOME path: ~/.codex | |
| 21 | Shadow home path: empty | |
| 22 | ``` | |
| 23 | ||
| 24 | Log in with Codex normally: | |
| 25 | ||
| 26 | ```bash | |
| 27 | codex login | |
| 28 | ``` | |
| 29 | ||
| 30 | ## I Want Work And Personal Codex Accounts | |
| 31 | ||
| 32 | Use one real Codex home and one shadow home. | |
| 33 | ||
| 34 | Recommended setup: | |
| 35 | ||
| 36 | ```text | |
| 37 | ~/.codex shared Codex home | |
| 38 | ~/.codex_p second account auth | |
| 39 | ``` | |
| 40 | ||
| 41 | The idea is: | |
| 42 | ||
| 43 | - both accounts can see the same T3/Codex sessions | |
| 44 | - each account keeps its own login | |
| 45 | - existing threads can continue with either account | |
| 46 | ||
| 47 | ### Set Up The First Account | |
| 48 | ||
| 49 | Log in normally: | |
| 50 | ||
| 51 | ```bash | |
| 52 | codex login | |
| 53 | ``` | |
| 54 | ||
| 55 | This is the account used by `~/.codex`. | |
| 56 | ||
| 57 | In T3 Code Settings, name it something obvious: | |
| 58 | ||
| 59 | ```text | |
| 60 | Display name: Codex Work | |
| 61 | CODEX_HOME path: ~/.codex | |
| 62 | Shadow home path: empty | |
| 63 | ``` | |
| 64 | ||
| 65 | ### Set Up The Second Account | |
| 66 | ||
| 67 | Log in with a separate Codex home: | |
| 68 | ||
| 69 | ```bash | |
| 70 | mkdir -p ~/.codex_p | |
| 71 | CODEX_HOME=~/.codex_p codex login | |
| 72 | ``` | |
| 73 | ||
| 74 | In T3 Code Settings, add another Codex provider: | |
| 75 | ||
| 76 | ```text | |
| 77 | Display name: Codex Personal | |
| 78 | CODEX_HOME path: ~/.codex | |
| 79 | Shadow home path: ~/.codex_p | |
| 80 | ``` | |
| 81 | ||
| 82 | The important part is that both providers use the same `CODEX_HOME path`, but only the second one | |
| 83 | has a `Shadow home path`. | |
| 84 | ||
| 85 | ## Which Account Am I Using? | |
| 86 | ||
| 87 | Open Settings and look at the provider row. | |
| 88 | ||
| 89 | T3 Code shows the authenticated email for providers that report one. Emails are blurred by default; | |
| 90 | click the blurred email to reveal it. | |
| 91 | ||
| 92 | Use display names and accent colors to make accounts easy to tell apart in the model picker. | |
| 93 | ||
| 94 | ## I Need A Different API Key Or Endpoint | |
| 95 | ||
| 96 | Use the provider's Environment variables section in Settings. | |
| 97 | ||
| 98 | This is useful when a Codex-compatible setup needs account-specific variables. Add the variables to | |
| 99 | the provider instance that should receive them, and mark API keys or tokens as sensitive. Sensitive | |
| 100 | values are stored as server secrets and are not sent back to the app after saving. | |
| 101 | ||
| 102 | ## Can I Switch Accounts In An Existing Thread? | |
| 103 | ||
| 104 | Yes, when both Codex providers share the same `CODEX_HOME path`. | |
| 105 | ||
| 106 | For example: | |
| 107 | ||
| 108 | ```text | |
| 109 | Codex Work CODEX_HOME path: ~/.codex | |
| 110 | Codex Personal CODEX_HOME path: ~/.codex, Shadow home path: ~/.codex_p | |
| 111 | ``` | |
| 112 | ||
| 113 | Those two providers are considered compatible for continuation, so the locked model picker can show | |
| 114 | both. | |
| 115 | ||
| 116 | If you add a third Codex provider with a completely different `CODEX_HOME path`, T3 Code treats it | |
| 117 | as a different workspace. It will not be offered for existing threads created under `~/.codex`. | |
| 118 | ||
| 119 | ## If Both Accounts Look The Same | |
| 120 | ||
| 121 | If two Codex providers show the same account or the same unexpected model list: | |
| 122 | ||
| 123 | 1. Check the email in Settings. | |
| 124 | 2. Refresh provider status. | |
| 125 | 3. Confirm the second provider has `Shadow home path` set. | |
| 126 | 4. Confirm the shadow directory has its own `auth.json`. | |
| 127 | 5. If you copied `~/.codex` into the shadow directory, remove everything except `auth.json`. | |
| 128 | ||
| 129 | Example cleanup: | |
| 130 | ||
| 131 | ```bash | |
| 132 | find ~/.codex_p -mindepth 1 ! -name auth.json -exec rm -rf {} + | |
| 133 | ``` | |
| 134 | ||
| 135 | ## When To Use A Separate CODEX_HOME | |
| 136 | ||
| 137 | Use a totally separate `CODEX_HOME path` only when you want a separate Codex workspace. | |
| 138 | ||
| 139 | That means separate sessions and less account switching inside old threads. Most dual-account users | |
| 140 | should use the shared-home plus shadow-home setup instead. |
docs/reference/encyclopedia.md deleted-180| ... | ... | @@ -1,180 +0,0 @@ |
| 1 | # Encyclopedia | |
| 2 | ||
| 3 | This is a living glossary for T3 Code. It explains what common terms mean in this codebase. | |
| 4 | ||
| 5 | ## Table of contents | |
| 6 | ||
| 7 | - [Project and workspace](#project-and-workspace) | |
| 8 | - [Thread timeline](#thread-timeline) | |
| 9 | - [Orchestration](#orchestration) | |
| 10 | - [Provider runtime](#provider-runtime) | |
| 11 | - [Checkpointing](#checkpointing) | |
| 12 | ||
| 13 | ## Concepts | |
| 14 | ||
| 15 | ### Project and workspace | |
| 16 | ||
| 17 | #### Project | |
| 18 | ||
| 19 | The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot`, a title, and one or more threads. See [workspace-layout.md][2]. | |
| 20 | ||
| 21 | #### Workspace root | |
| 22 | ||
| 23 | The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2]. | |
| 24 | ||
| 25 | #### Worktree | |
| 26 | ||
| 27 | A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live in [GitCore.ts][3]. | |
| 28 | ||
| 29 | ### Thread timeline | |
| 30 | ||
| 31 | #### Thread | |
| 32 | ||
| 33 | The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4]. | |
| 34 | ||
| 35 | #### Turn | |
| 36 | ||
| 37 | A single user-to-assistant work cycle inside a thread. It starts with user input and ends when follow-up work like checkpointing settles. See [the contracts][1], [ProviderRuntimeIngestion.ts][5], and [CheckpointReactor.ts][6]. | |
| 38 | ||
| 39 | #### Activity | |
| 40 | ||
| 41 | A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4]. | |
| 42 | ||
| 43 | ### Orchestration | |
| 44 | ||
| 45 | Orchestration is the server-side domain layer that turns runtime activity into stable app state. The main entry point is [OrchestrationEngine.ts][7], with core logic in [decider.ts][8] and [projector.ts][4]. | |
| 46 | ||
| 47 | #### Aggregate | |
| 48 | ||
| 49 | The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8]. | |
| 50 | ||
| 51 | #### Command | |
| 52 | ||
| 53 | A typed request to change domain state. In [the contracts][1], commands are validated in [commandInvariants.ts][9] and turned into events by [decider.ts][8]. | |
| 54 | Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`. | |
| 55 | ||
| 56 | #### Domain Event | |
| 57 | ||
| 58 | A persisted fact that something already happened. In [the contracts][1], events are the source of truth, and [projector.ts][4] shows how they are applied. | |
| 59 | Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`. | |
| 60 | ||
| 61 | #### Decider | |
| 62 | ||
| 63 | The pure orchestration logic that turns commands plus current state into events. The core implementation is in [decider.ts][8], with preconditions in [commandInvariants.ts][9]. | |
| 64 | ||
| 65 | #### Projection | |
| 66 | ||
| 67 | A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10]. | |
| 68 | ||
| 69 | #### Projector | |
| 70 | ||
| 71 | The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11]. | |
| 72 | ||
| 73 | #### Read model | |
| 74 | ||
| 75 | The current materialized view of orchestration state. In [the contracts][1], it holds projects, threads, messages, activities, checkpoints, and session state. See [ProjectionSnapshotQuery.ts][10] and [OrchestrationEngine.ts][7]. | |
| 76 | ||
| 77 | #### Reactor | |
| 78 | ||
| 79 | A side-effecting service that handles follow-up work after events or runtime signals. Examples include [CheckpointReactor.ts][6], [ProviderCommandReactor.ts][12], and [ProviderRuntimeIngestion.ts][5]. | |
| 80 | ||
| 81 | #### Receipt | |
| 82 | ||
| 83 | A lightweight typed runtime signal emitted when an async milestone completes. See [RuntimeReceiptBus.ts][13]. | |
| 84 | Examples include `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, and `turn.processing.quiesced`, which are emitted by flows such as [CheckpointReactor.ts][6]. | |
| 85 | ||
| 86 | #### Quiesced | |
| 87 | ||
| 88 | "Quiesced" means a turn has gone quiet and stable. In [the receipt schema][13], it means the follow-up work has settled, including work in [CheckpointReactor.ts][6]. | |
| 89 | ||
| 90 | ### Provider runtime | |
| 91 | ||
| 92 | The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [provider-architecture.md][16]. | |
| 93 | ||
| 94 | #### Provider | |
| 95 | ||
| 96 | The backend agent runtime that actually performs work. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17]. | |
| 97 | ||
| 98 | #### Session | |
| 99 | ||
| 100 | The live provider-backed runtime attached to a thread. Session shape is in [the orchestration contracts][1], and lifecycle is managed in [ProviderService.ts][14]. | |
| 101 | ||
| 102 | #### Runtime mode | |
| 103 | ||
| 104 | The safety/access mode for a thread or session. In [the contracts][1], the main values are `approval-required` and `full-access`. See [runtime-modes.md][18]. | |
| 105 | ||
| 106 | #### Interaction mode | |
| 107 | ||
| 108 | The agent interaction style for a thread. In [the contracts][1], the main values are `default` and `plan`. See [runtime-modes.md][18]. | |
| 109 | ||
| 110 | #### Assistant delivery mode | |
| 111 | ||
| 112 | Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` delivers a completed result. See [ProviderService.ts][14]. | |
| 113 | ||
| 114 | #### Snapshot | |
| 115 | ||
| 116 | A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19]. | |
| 117 | ||
| 118 | ### Checkpointing | |
| 119 | ||
| 120 | Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6]. | |
| 121 | ||
| 122 | #### Checkpoint | |
| 123 | ||
| 124 | A saved snapshot of a thread workspace at a particular turn. In practice it is a hidden Git ref in [CheckpointStore.ts][19] plus a projected summary from [ProjectionCheckpoints.ts][21]. Capture and lifecycle work happen in [CheckpointReactor.ts][6]. | |
| 125 | ||
| 126 | #### Checkpoint ref | |
| 127 | ||
| 128 | The durable identifier for a filesystem checkpoint, stored as a Git ref. It is typed in [the contracts][1], constructed in [Utils.ts][22], and used by [CheckpointStore.ts][19]. | |
| 129 | ||
| 130 | #### Checkpoint baseline | |
| 131 | ||
| 132 | The starting checkpoint for diffing a thread timeline. This flow is surfaced through [RuntimeReceiptBus.ts][13], coordinated in [CheckpointReactor.ts][6], and supported by [Utils.ts][22]. | |
| 133 | ||
| 134 | #### Checkpoint diff | |
| 135 | ||
| 136 | The patch difference between two checkpoints. Query logic lives in [CheckpointDiffQuery.ts][20], diff parsing lives in [Diffs.ts][23], and finalization is coordinated by [CheckpointReactor.ts][6]. | |
| 137 | ||
| 138 | #### Turn diff | |
| 139 | ||
| 140 | The file patch and changed-file summary for one turn. It is usually computed in [CheckpointDiffQuery.ts][20], represented in [the contracts][1], and recorded into thread state by [projector.ts][4]. | |
| 141 | ||
| 142 | ## Practical Shortcuts | |
| 143 | ||
| 144 | - If you see `requested`, think "intent recorded". | |
| 145 | - If you see `completed`, think "result applied". | |
| 146 | - If you see `receipt`, think "async milestone signal". | |
| 147 | - If you see `checkpoint`, think "workspace snapshot for diff/restore". | |
| 148 | - If you see `quiesced`, think "all relevant follow-up work has gone idle". | |
| 149 | ||
| 150 | ## Related Docs | |
| 151 | ||
| 152 | - [architecture.md][24] | |
| 153 | - [provider-architecture.md][16] | |
| 154 | - [runtime-modes.md][18] | |
| 155 | - [workspace-layout.md][2] | |
| 156 | ||
| 157 | [1]: ../packages/contracts/src/orchestration.ts | |
| 158 | [2]: ./workspace-layout.md | |
| 159 | [3]: ../apps/server/src/git/Layers/GitCore.ts | |
| 160 | [4]: ../apps/server/src/orchestration/projector.ts | |
| 161 | [5]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | |
| 162 | [6]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts | |
| 163 | [7]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | |
| 164 | [8]: ../apps/server/src/orchestration/decider.ts | |
| 165 | [9]: ../apps/server/src/orchestration/commandInvariants.ts | |
| 166 | [10]: ../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts | |
| 167 | [11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts | |
| 168 | [12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | |
| 169 | [13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts | |
| 170 | [14]: ../apps/server/src/provider/Layers/ProviderService.ts | |
| 171 | [15]: ../apps/server/src/provider/Services/ProviderAdapter.ts | |
| 172 | [16]: ./provider-architecture.md | |
| 173 | [17]: ../apps/server/src/provider/Layers/CodexAdapter.ts | |
| 174 | [18]: ./runtime-modes.md | |
| 175 | [19]: ../apps/server/src/checkpointing/CheckpointStore.ts | |
| 176 | [20]: ../apps/server/src/checkpointing/CheckpointDiffQuery.ts | |
| 177 | [21]: ../apps/server/src/persistence/Services/ProjectionCheckpoints.ts | |
| 178 | [22]: ../apps/server/src/checkpointing/Utils.ts | |
| 179 | [23]: ../apps/server/src/checkpointing/Diffs.ts | |
| 180 | [24]: ./architecture.md |
docs/reference/scripts.md deleted-56| ... | ... | @@ -1,56 +0,0 @@ |
| 1 | # Scripts | |
| 2 | ||
| 3 | - `vp run dev` — Starts contracts, server, and web in watch mode. | |
| 4 | - `vp run dev --share` — Also publishes the web port over HTTPS on this machine's tailnet. The startup pairing URL is built against the shared origin, and the mapping is removed on exit. | |
| 5 | - `vp run dev:server` — Starts just the WebSocket server. The server process runs on Bun (`@effect/platform-bun` + `BunPtyAdapter`), but task running uses `vp run`. | |
| 6 | - `vp run dev:web` — Starts just the Vite dev server for the web app. | |
| 7 | - Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even when `T3CODE_HOME` is set, storing state in `<worktree>/.t3/userdata`. Pass `--home-dir <path>` to choose another isolated directory explicitly. Submodules are not worktrees and keep the normal precedence. | |
| 8 | - From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state separate from `~/.t3/userdata`. An explicit `--home-dir <path>` stores state under `<path>/userdata`; the base directory remains available for caches, worktrees, and other shared data. | |
| 9 | - Web dev commands do not auto-open a browser. Open the one-time pairing URL printed by the server so the first browser navigation is authenticated. Set `T3CODE_NO_BROWSER=0` only when interactive auto-open is intentional. | |
| 10 | - Pass dev-runner flags directly after the root task name, for example: | |
| 11 | `vp run dev --home-dir /tmp/t3code-dev` | |
| 12 | - `vp run start` — Runs the production server (serves built web app as static files). | |
| 13 | - `vp run build` — Builds contracts, web app, and server. | |
| 14 | - `vp run typecheck` — Strict TypeScript checks for all packages. | |
| 15 | - `vp run test` — Runs workspace tests. | |
| 16 | - `node apps/server/scripts/t3-sqlite-state.ts <query|exec> --base-dir <path> ...` — Inspects or seeds an isolated T3 SQLite database; writes create a private backup first. | |
| 17 | - `vp run dist:desktop:artifact -- --platform <mac|linux|win> --target <target> --arch <arch>` — Builds a desktop artifact for a specific platform/target/arch. | |
| 18 | - `vp run dist:desktop:dmg` — Builds a shareable macOS `.dmg` into `./release`. | |
| 19 | - `vp run dist:desktop:dmg:x64` — Builds an Intel macOS `.dmg`. | |
| 20 | - `vp run dist:desktop:linux` — Builds a Linux AppImage into `./release`. | |
| 21 | - `vp run dist:desktop:win` — Builds a Windows NSIS installer into `./release`. | |
| 22 | ||
| 23 | ## Desktop `.dmg` packaging notes | |
| 24 | ||
| 25 | - Default build is unsigned/not notarized for local sharing. | |
| 26 | - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. | |
| 27 | - Desktop production windows load the bundled UI from `t3code://app/index.html` (not a `127.0.0.1` document URL). | |
| 28 | ||
| 29 | - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an auth token for WebSocket/API traffic. | |
| 30 | - Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first launch. | |
| 31 | - To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg -- --keep-stage` | |
| 32 | - To allow code-signing/notarization when configured in CI/secrets, add: `--signed`. | |
| 33 | - Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and | |
| 34 | `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from | |
| 35 | `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it. | |
| 36 | - Windows `--signed` uses Azure Trusted Signing and expects: | |
| 37 | `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`, | |
| 38 | `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`. | |
| 39 | - Azure authentication env vars are also required (for example service principal with secret): | |
| 40 | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. | |
| 41 | ||
| 42 | ## Browser development | |
| 43 | ||
| 44 | `dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname. | |
| 45 | ||
| 46 | Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative. | |
| 47 | ||
| 48 | ## Running multiple dev instances | |
| 49 | ||
| 50 | Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together. | |
| 51 | ||
| 52 | - Default ports: server `13773`, web `5733` | |
| 53 | - Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`) | |
| 54 | - Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop` | |
| 55 | ||
| 56 | If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset. |
docs/reference/workspace-layout.md deleted-7| ... | ... | @@ -1,7 +0,0 @@ |
| 1 | # Workspace layout | |
| 2 | ||
| 3 | - `/apps/server`: Node.js WebSocket server. Wraps Codex app-server, serves the built web app, and opens the browser on start. | |
| 4 | - `/apps/web`: React + Vite UI. Session control, conversation, and provider event rendering. Connects to the server via WebSocket. | |
| 5 | - `/apps/desktop`: Electron shell. Spawns a desktop-scoped `t3` backend process and loads the shared web app. | |
| 6 | - `/packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. | |
| 7 | - `/packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`, `@t3tools/shared/DrainableWorker`) — no barrel index. |
docs/user/install.md created+84| ... | ... | @@ -0,0 +1,84 @@ |
| 1 | # Install T3 Code | |
| 2 | ||
| 3 | T3 Code is a web and desktop GUI for running coding agents on your machine. | |
| 4 | ||
| 5 | ## Requirements | |
| 6 | ||
| 7 | Node.js `^22.16 || ^23.11 || >=24.10` on the machine that runs the T3 Code server. | |
| 8 | ||
| 9 | At least one provider CLI, installed and authenticated. See [Providers](#providers) below. | |
| 10 | ||
| 11 | ## Run Without Installing | |
| 12 | ||
| 13 | ```bash | |
| 14 | npx t3@latest | |
| 15 | ``` | |
| 16 | ||
| 17 | This starts the T3 Code server on your machine and opens the local web app. Use | |
| 18 | `npx t3@latest --help` for the full CLI reference. | |
| 19 | ||
| 20 | ## Desktop App | |
| 21 | ||
| 22 | Download the latest release from | |
| 23 | [GitHub Releases](https://github.com/pingdotgg/t3code/releases), or install from a package | |
| 24 | registry. | |
| 25 | ||
| 26 | Windows: | |
| 27 | ||
| 28 | ```bash | |
| 29 | winget install T3Tools.T3Code | |
| 30 | ``` | |
| 31 | ||
| 32 | macOS: | |
| 33 | ||
| 34 | ```bash | |
| 35 | brew install --cask t3-code | |
| 36 | ``` | |
| 37 | ||
| 38 | Arch Linux: | |
| 39 | ||
| 40 | ```bash | |
| 41 | yay -S t3code-bin | |
| 42 | ``` | |
| 43 | ||
| 44 | ## Providers | |
| 45 | ||
| 46 | T3 Code drives provider CLIs; it does not ship them. Install the CLI for each provider you want | |
| 47 | to use, then authenticate it. | |
| 48 | ||
| 49 | | Provider | CLI | Default binary | Log in with | | |
| 50 | | ---------- | ----------------------------------------------------- | -------------- | --------------------- | | |
| 51 | | Codex | [Codex CLI](https://developers.openai.com/codex/cli) | `codex` | `codex login` | | |
| 52 | | Claude | [Claude Code](https://claude.com/product/claude-code) | `claude` | `claude auth login` | | |
| 53 | | Cursor | [Cursor CLI](https://cursor.com/cli) | `cursor-agent` | `agent login` | | |
| 54 | | Grok Build | [Grok Build CLI](https://x.ai/cli) | `grok` | `grok login` | | |
| 55 | | OpenCode | [OpenCode](https://opencode.ai) | `opencode` | `opencode auth login` | | |
| 56 | ||
| 57 | Cursor is the one to watch: install Cursor CLI, which provides the `cursor-agent` binary that | |
| 58 | T3 Code looks for, but authenticate with `agent login`, not `cursor-agent login`. | |
| 59 | ||
| 60 | Run the login command on the machine running the T3 Code server, not on the device you browse | |
| 61 | from. | |
| 62 | ||
| 63 | ### Binary Discovery | |
| 64 | ||
| 65 | Each provider CLI must be on the server's `PATH`, or have an explicit binary path set in | |
| 66 | **Settings** → the provider instance → **Binary path**. Use the explicit path when a version | |
| 67 | manager or a non-standard install location keeps the CLI off the `PATH` of the shell that | |
| 68 | started T3 Code. | |
| 69 | ||
| 70 | ### When Auth Is Needed | |
| 71 | ||
| 72 | Provider auth is required before you start a session with that provider, not before you start | |
| 73 | T3 Code. You can install T3 Code, open it, and add providers afterwards. A provider that is not | |
| 74 | authenticated shows its status in **Settings** and fails at session start with the login command | |
| 75 | to run. | |
| 76 | ||
| 77 | For multi-account setups, see [Codex](./providers-codex.md) and [Claude](./providers-claude.md). | |
| 78 | ||
| 79 | ## Next Steps | |
| 80 | ||
| 81 | - [Permission modes](./permission-modes.md): how much T3 Code asks before acting | |
| 82 | - [Remote access](./remote-access.md): connect from a phone, tablet, or another desktop | |
| 83 | - [Keeping T3 Code in sync](./updating.md): client and server version skew | |
| 84 | - [Running in the background](./background-service.md): Linux background service |
docs/user/keybindings.md+37-84| ... | ... | @@ -1,10 +1,14 @@ |
| 1 | 1 | # Keybindings |
| 2 | 2 | |
| 3 | T3 Code reads keybindings from: | |
| 3 | Edit keybindings from **Settings** → **Keybindings**. That page lists every command, its current | |
| 4 | shortcut, whether it is a default or your own, and warns about conflicts. | |
| 4 | 5 | |
| 5 | - `~/.t3/keybindings.json` | |
| 6 | The same configuration lives in `~/.t3/userdata/keybindings.json` on the machine running the | |
| 7 | server, if you prefer editing it directly. T3 Code writes the built-in defaults into that file on | |
| 8 | first run, and adds any new defaults on later startups unless a rule of yours already claims the | |
| 9 | command or the shortcut. | |
| 6 | 10 | |
| 7 | The file must be a JSON array of rules: | |
| 11 | The file is a JSON array of rules. | |
| 8 | 12 | |
| 9 | 13 | ```json |
| 10 | 14 | [ |
| ... | ... | @@ -13,110 +17,59 @@ The file must be a JSON array of rules: |
| 13 | 17 | ] |
| 14 | 18 | ``` |
| 15 | 19 | |
| 16 | See the full schema for more details: [`packages/contracts/src/keybindings.ts`](../../packages/contracts/src/keybindings.ts) | |
| 20 | Invalid rules are ignored. An invalid file is ignored entirely, and the server logs a warning. | |
| 17 | 21 | |
| 18 | ## Defaults | |
| 22 | ## Rule Shape | |
| 19 | 23 | |
| 20 | ```json | |
| 21 | [ | |
| 22 | { "key": "mod+j", "command": "terminal.toggle" }, | |
| 23 | { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" }, | |
| 24 | { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" }, | |
| 25 | { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" }, | |
| 26 | { "key": "mod+shift+j", "command": "preview.toggle" }, | |
| 27 | { "key": "mod+r", "command": "preview.refresh", "when": "previewFocus" }, | |
| 28 | { "key": "mod+l", "command": "preview.focusUrl", "when": "previewFocus" }, | |
| 29 | { "key": "mod+=", "command": "preview.zoomIn", "when": "previewFocus" }, | |
| 30 | { "key": "mod+-", "command": "preview.zoomOut", "when": "previewFocus" }, | |
| 31 | { "key": "mod+0", "command": "preview.resetZoom", "when": "previewFocus" }, | |
| 32 | { "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" }, | |
| 33 | { "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" }, | |
| 34 | { "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" }, | |
| 35 | { "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" }, | |
| 36 | { "key": "mod+o", "command": "editor.openFavorite" } | |
| 37 | ] | |
| 38 | ``` | |
| 39 | ||
| 40 | For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](../../apps/server/src/keybindings.ts) | |
| 24 | - `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d` | |
| 25 | - `command` (required): the command ID to run | |
| 26 | - `when` (optional): boolean expression controlling when the shortcut is active | |
| 41 | 27 | |
| 42 | ## Configuration | |
| 28 | ## Key Syntax | |
| 43 | 29 | |
| 44 | ### Rule Shape | |
| 30 | Modifiers: `mod` (`cmd` on macOS, `ctrl` elsewhere), `cmd` / `meta`, `ctrl` / `control`, `shift`, | |
| 31 | `alt` / `option`. | |
| 45 | 32 | |
| 46 | Each entry supports: | |
| 33 | Examples: `mod+j`, `mod+shift+d`, `ctrl+l`, `cmd+k`. | |
| 47 | 34 | |
| 48 | - `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d` | |
| 49 | - `command` (required): action ID | |
| 50 | - `when` (optional): boolean expression controlling when the shortcut is active | |
| 35 | ## Commands | |
| 51 | 36 | |
| 52 | Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server. | |
| 53 | ||
| 54 | ### Available Commands | |
| 55 | ||
| 56 | - `terminal.toggle`: open/close terminal drawer | |
| 57 | - `terminal.split`: split terminal (in focused terminal context by default) | |
| 58 | - `terminal.new`: create new terminal (in focused terminal context by default) | |
| 59 | - `terminal.close`: close/kill the focused terminal (in focused terminal context by default) | |
| 60 | - `preview.toggle`: open/close the in-app browser preview panel (desktop app only) | |
| 61 | - `preview.refresh`: reload the active preview tab (in focused preview context by default) | |
| 62 | - `preview.focusUrl`: focus the URL input of the preview panel (in focused preview context by default) | |
| 63 | - `preview.zoomIn`: zoom the preview viewport in one step (in focused preview context by default) | |
| 64 | - `preview.zoomOut`: zoom the preview viewport out one step (in focused preview context by default) | |
| 65 | - `preview.resetZoom`: reset the preview zoom to 100% (in focused preview context by default) | |
| 66 | - `commandPalette.toggle`: open or close the global command palette | |
| 67 | - `chat.new`: create a new chat thread preserving the active thread's branch/worktree state | |
| 68 | - `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`)) | |
| 69 | - `editor.openFavorite`: open current project/worktree in the last-used editor | |
| 70 | - `script.{id}.run`: run a project script by id (for example `script.test.run`) | |
| 37 | Commands are IDs like `terminal.toggle`, `commandPalette.toggle`, `preview.refresh`, and | |
| 38 | `chat.new`. Project scripts are addressable as `script.{id}.run`, for example `script.test.run`. | |
| 71 | 39 | |
| 72 | 40 | The command palette searches active thread titles, projects, branches, user messages, and final |
| 73 | 41 | agent responses across connected environments. Message matches show one labeled excerpt while |
| 74 | 42 | keeping the thread's project, branch, and machine context visible. Message search begins after two |
| 75 | 43 | characters and uses SQLite's ASCII case-insensitive matching. |
| 76 | 44 | |
| 77 | ### Key Syntax | |
| 78 | ||
| 79 | Supported modifiers: | |
| 80 | ||
| 81 | - `mod` (`cmd` on macOS, `ctrl` on non-macOS) | |
| 82 | - `cmd` / `meta` | |
| 83 | - `ctrl` / `control` | |
| 84 | - `shift` | |
| 85 | - `alt` / `option` | |
| 86 | ||
| 87 | Examples: | |
| 88 | ||
| 89 | - `mod+j` | |
| 90 | - `mod+shift+d` | |
| 91 | - `ctrl+l` | |
| 92 | - `cmd+k` | |
| 45 | The full command list and the current defaults are shown in **Settings** → **Keybindings**, which | |
| 46 | always matches the build you are running. Use that rather than a copied list. | |
| 93 | 47 | |
| 94 | ### `when` Conditions | |
| 48 | Note that `chat.new` and `chat.newLocal` both create a thread through the same path. A new thread | |
| 49 | inherits the project you were in, along with model and mode selections. Branch, worktree, and | |
| 50 | environment mode always come from your configured defaults, not from the thread you were looking | |
| 51 | at. To keep a worktree, use the explicit "new thread in this worktree" action in the branch | |
| 52 | toolbar. The only difference between the two commands: with the current sidebar and more than one | |
| 53 | project, `chat.new` opens a project chooser first. | |
| 95 | 54 | |
| 96 | Currently available context keys: | |
| 55 | ## `when` Conditions | |
| 97 | 56 | |
| 98 | - `terminalFocus` | |
| 99 | - `terminalOpen` | |
| 100 | - `previewFocus` | |
| 101 | - `previewOpen` | |
| 57 | A `when` expression is evaluated against context keys describing the current UI state. The keys | |
| 58 | the app supplies today are `terminalFocus`, `terminalOpen`, `previewFocus`, `previewOpen`, and | |
| 59 | `modelPickerOpen`. The set is open and grows over time, so treat that as the current list rather | |
| 60 | than a fixed one. Any key the running app does not supply evaluates to `false`. | |
| 102 | 61 | |
| 103 | Supported operators: | |
| 104 | ||
| 105 | - `!` (not) | |
| 106 | - `&&` (and) | |
| 107 | - `||` (or) | |
| 108 | - parentheses: `(` `)` | |
| 62 | Operators: `!` (not), `&&` (and), `||` (or), and parentheses. | |
| 109 | 63 | |
| 110 | 64 | Examples: |
| 111 | 65 | |
| 112 | 66 | - `"when": "terminalFocus"` |
| 113 | 67 | - `"when": "terminalOpen && !terminalFocus"` |
| 114 | - `"when": "terminalFocus || terminalOpen"` | |
| 115 | ||
| 116 | Unknown condition keys evaluate to `false`. | |
| 68 | - `"when": "!terminalFocus"` | |
| 117 | 69 | |
| 118 | ### Precedence | |
| 70 | ## Precedence | |
| 119 | 71 | |
| 120 | 72 | - Rules are evaluated in array order. |
| 121 | 73 | - For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins. |
| 122 | - That means precedence is across commands, not only within the same command. | |
| 74 | - Precedence is across commands, not only within the same command. A later rule for a different | |
| 75 | command can take a key away from an earlier one. |
docs/user/permission-modes.md created+48| ... | ... | @@ -0,0 +1,48 @@ |
| 1 | # Permission Modes | |
| 2 | ||
| 3 | A permission mode controls how much the agent does on its own and when it stops to ask you. | |
| 4 | ||
| 5 | The mode is set per thread, from the mode control in the message composer. Changing it in one | |
| 6 | thread does not change any other thread. A thread created from inside another thread keeps that | |
| 7 | thread's mode; otherwise new threads start in **Full access** unless you pick something else | |
| 8 | before sending. | |
| 9 | ||
| 10 | ## The Modes | |
| 11 | ||
| 12 | **Supervised**: ask before commands and file changes. The agent pauses and shows you what it | |
| 13 | wants to run or edit, and waits for approval. Work outside the workspace is restricted. | |
| 14 | ||
| 15 | **Auto-accept edits**: auto-approve edits, ask before other actions. File changes go through | |
| 16 | without prompting; commands and anything else still stop for approval. | |
| 17 | ||
| 18 | **Auto**: routine actions proceed without you; risky ones still ask. How this is enforced depends | |
| 19 | on the provider: Codex delegates routine approvals to an AI reviewer, Claude uses its own auto | |
| 20 | permission mode, and providers without an equivalent (such as OpenCode) fall back to asking, like | |
| 21 | Supervised. | |
| 22 | ||
| 23 | **Full access**: allow commands and edits without prompts. The default. The agent runs | |
| 24 | unattended until it finishes or asks a question of its own. | |
| 25 | ||
| 26 | Approvals appear inline in the conversation. Approve or reject one and the agent continues from | |
| 27 | there. | |
| 28 | ||
| 29 | ## Choosing a Mode | |
| 30 | ||
| 31 | Use **Full access** for work in a worktree or a sandbox you can throw away. | |
| 32 | ||
| 33 | Use **Supervised** on a repository where an unwanted command is expensive, or the first time you | |
| 34 | run an unfamiliar task. | |
| 35 | ||
| 36 | **Auto-accept edits** suits refactors where the edits are the point and you only care about the | |
| 37 | shell commands. | |
| 38 | ||
| 39 | ## Provider Behavior | |
| 40 | ||
| 41 | Each provider maps these modes onto its own approval and sandbox settings. Codex, for example, | |
| 42 | translates the mode into its approval policy and sandbox level, so **Supervised** runs the CLI | |
| 43 | with prompting enabled and a restricted workspace while **Full access** disables both. The | |
| 44 | labels above describe what you get; the exact per-provider translation is internal and may | |
| 45 | change. | |
| 46 | ||
| 47 | Mobile offers the same four modes. It labels the first one **Approve actions** rather than | |
| 48 | **Supervised**. |
docs/user/providers-claude.md created+210| ... | ... | @@ -0,0 +1,210 @@ |
| 1 | # Claude | |
| 2 | ||
| 3 | This guide is for people who want to use more than one Claude setup in T3 Code. For Codex, see | |
| 4 | [Codex](./providers-codex.md). For first-time setup, see [Install T3 Code](./install.md). | |
| 5 | ||
| 6 | Common reasons: | |
| 7 | ||
| 8 | - use separate work and personal Claude accounts | |
| 9 | - try a different Claude Code configuration without disturbing your main setup | |
| 10 | - run Claude through a router such as Claude Code Router | |
| 11 | - use external providers exposed through a Claude-compatible workflow | |
| 12 | ||
| 13 | ## I Only Use One Claude Account | |
| 14 | ||
| 15 | Use the default provider. | |
| 16 | ||
| 17 | Log in with Claude Code normally: | |
| 18 | ||
| 19 | ```bash | |
| 20 | claude auth login | |
| 21 | ``` | |
| 22 | ||
| 23 | In T3 Code Settings, your Claude provider can stay like this: | |
| 24 | ||
| 25 | ```text | |
| 26 | Display name: Claude | |
| 27 | Binary path: claude | |
| 28 | CLAUDE_CONFIG_DIR path: empty | |
| 29 | ``` | |
| 30 | ||
| 31 | An empty `CLAUDE_CONFIG_DIR path` means T3 Code uses Claude Code's normal config directory. | |
| 32 | ||
| 33 | When you set this field, T3 Code points Claude Code at that directory with the | |
| 34 | `CLAUDE_CONFIG_DIR` environment variable. It does not change `HOME`, so your system keychain and | |
| 35 | the rest of your environment stay as they are. | |
| 36 | ||
| 37 | ## I Want Work And Personal Claude Accounts | |
| 38 | ||
| 39 | Use a different Claude config directory for each account. | |
| 40 | ||
| 41 | Example: | |
| 42 | ||
| 43 | ```text | |
| 44 | default config dir work account | |
| 45 | ~/.claude_personal_home personal account | |
| 46 | ``` | |
| 47 | ||
| 48 | ### Set Up The First Account | |
| 49 | ||
| 50 | Log in normally: | |
| 51 | ||
| 52 | ```bash | |
| 53 | claude auth login | |
| 54 | ``` | |
| 55 | ||
| 56 | In T3 Code Settings: | |
| 57 | ||
| 58 | ```text | |
| 59 | Display name: Claude Work | |
| 60 | Binary path: claude | |
| 61 | CLAUDE_CONFIG_DIR path: empty | |
| 62 | ``` | |
| 63 | ||
| 64 | ### Set Up The Second Account | |
| 65 | ||
| 66 | Log in with a separate config directory: | |
| 67 | ||
| 68 | ```bash | |
| 69 | mkdir -p ~/.claude_personal_home | |
| 70 | CLAUDE_CONFIG_DIR=~/.claude_personal_home claude auth login | |
| 71 | ``` | |
| 72 | ||
| 73 | Use `CLAUDE_CONFIG_DIR`, not `HOME`. Setting `HOME` writes the login to | |
| 74 | `~/.claude_personal_home/.claude`, which is not where T3 Code looks. | |
| 75 | ||
| 76 | Then add another Claude provider in T3 Code: | |
| 77 | ||
| 78 | ```text | |
| 79 | Display name: Claude Personal | |
| 80 | Binary path: claude | |
| 81 | CLAUDE_CONFIG_DIR path: ~/.claude_personal_home | |
| 82 | ``` | |
| 83 | ||
| 84 | Use the email shown in Settings to confirm each provider is using the intended account. Emails are | |
| 85 | blurred by default; click the blurred email to reveal it. | |
| 86 | ||
| 87 | ## Can I Switch Claude Accounts In An Existing Thread? | |
| 88 | ||
| 89 | Usually, no. | |
| 90 | ||
| 91 | T3 Code only offers Claude providers that use the same config directory for an existing thread. A | |
| 92 | different config directory is treated as a different Claude environment. | |
| 93 | ||
| 94 | This is different from the recommended Codex setup. Claude Code keeps account and local state across | |
| 95 | multiple files under its config directory, so T3 Code keeps separate config directories isolated | |
| 96 | instead of trying to share part of the state. | |
| 97 | ||
| 98 | ## I Want To Use OpenRouter | |
| 99 | ||
| 100 | Use this when you want Claude Code to talk to OpenRouter directly, without running a local router. | |
| 101 | This is the simplest external-provider setup. | |
| 102 | ||
| 103 | OpenRouter provides a Claude Code integration through Claude's Anthropic-compatible environment | |
| 104 | variables. | |
| 105 | ||
| 106 | ### Configure A Claude OpenRouter Provider | |
| 107 | ||
| 108 | Add or edit a Claude provider in T3 Code Settings: | |
| 109 | ||
| 110 | ```text | |
| 111 | Display name: Claude OpenRouter | |
| 112 | Binary path: claude | |
| 113 | CLAUDE_CONFIG_DIR path: ~/.claude_openrouter_home | |
| 114 | ``` | |
| 115 | ||
| 116 | In that provider's Environment variables section, add: | |
| 117 | ||
| 118 | ```text | |
| 119 | ANTHROPIC_BASE_URL https://openrouter.ai/api | |
| 120 | ANTHROPIC_AUTH_TOKEN sk-or-... Sensitive | |
| 121 | ANTHROPIC_API_KEY Empty value | |
| 122 | ``` | |
| 123 | ||
| 124 | Mark `ANTHROPIC_AUTH_TOKEN` as sensitive. T3 Code stores the value as a server secret and does not | |
| 125 | send it back to the app after saving. | |
| 126 | ||
| 127 | If you want this setup isolated from your normal Claude account, create that home first: | |
| 128 | ||
| 129 | ```bash | |
| 130 | mkdir -p ~/.claude_openrouter_home | |
| 131 | ``` | |
| 132 | ||
| 133 | If you previously used the same Claude home with a normal Anthropic login, run `/logout` in a Claude | |
| 134 | Code session for that home before using OpenRouter. Otherwise Claude Code may keep using cached | |
| 135 | Anthropic credentials instead of the OpenRouter token. | |
| 136 | ||
| 137 | ### Pick OpenRouter Models | |
| 138 | ||
| 139 | OpenRouter can route Claude Code's default model roles to OpenRouter model IDs. | |
| 140 | ||
| 141 | Example: | |
| 142 | ||
| 143 | ```text | |
| 144 | ANTHROPIC_DEFAULT_OPUS_MODEL anthropic/claude-opus-4.6 | |
| 145 | ANTHROPIC_DEFAULT_SONNET_MODEL anthropic/claude-sonnet-4.6 | |
| 146 | ANTHROPIC_DEFAULT_HAIKU_MODEL anthropic/claude-haiku-4.5 | |
| 147 | CLAUDE_CODE_SUBAGENT_MODEL anthropic/claude-sonnet-4.6 | |
| 148 | ``` | |
| 149 | ||
| 150 | Add those to the same provider's Environment variables section if you want stable model choices. | |
| 151 | ||
| 152 | ### Verify OpenRouter Is Being Used | |
| 153 | ||
| 154 | Open a Claude session and run: | |
| 155 | ||
| 156 | ```text | |
| 157 | /status | |
| 158 | ``` | |
| 159 | ||
| 160 | You should see the Anthropic base URL set to: | |
| 161 | ||
| 162 | ```text | |
| 163 | https://openrouter.ai/api | |
| 164 | ``` | |
| 165 | ||
| 166 | You can also check the OpenRouter activity dashboard for requests from your API key. | |
| 167 | ||
| 168 | ### Common OpenRouter Mistakes | |
| 169 | ||
| 170 | - Use `https://openrouter.ai/api`, not `https://openrouter.ai/api/v1`, for Claude Code. | |
| 171 | - Set `ANTHROPIC_AUTH_TOKEN` to your OpenRouter API key. | |
| 172 | - Set `ANTHROPIC_API_KEY` to an empty string so Claude Code does not try to use an Anthropic login. | |
| 173 | - Put these variables on the Claude provider instance, not in global shell startup files. | |
| 174 | ||
| 175 | OpenRouter's setup can change over time. Use its upstream Claude Code guide for the current details: | |
| 176 | <https://openrouter.ai/docs/guides/guides/claude-code-integration>. | |
| 177 | ||
| 178 | ## I Want To Use Claude Code Router | |
| 179 | ||
| 180 | Claude Code Router is useful when you want a local routing layer with more control than a direct | |
| 181 | OpenRouter setup. | |
| 182 | ||
| 183 | T3 Code does not need a special Claude Code Router provider. Treat the router as a Claude | |
| 184 | environment: give a Claude provider its own `CLAUDE_CONFIG_DIR path`, and put whatever variables | |
| 185 | the router tells you to export into that provider's Environment variables section. Mark tokens | |
| 186 | and API keys as sensitive. | |
| 187 | ||
| 188 | ```text | |
| 189 | Display name: Claude Router | |
| 190 | Binary path: claude | |
| 191 | CLAUDE_CONFIG_DIR path: ~/.claude_router_home | |
| 192 | ``` | |
| 193 | ||
| 194 | Follow the upstream project's README for the router's own install, startup, and configuration | |
| 195 | steps: <https://github.com/musistudio/claude-code-router>. | |
| 196 | ||
| 197 | ## I Want Different Claude Settings, Not A Different Account | |
| 198 | ||
| 199 | Create another Claude provider with the same account if you want a named preset. | |
| 200 | ||
| 201 | Examples: | |
| 202 | ||
| 203 | - "Claude Default" | |
| 204 | - "Claude Router" | |
| 205 | - "Claude Experimental" | |
| 206 | ||
| 207 | If the preset needs different Claude files, give it a different `CLAUDE_CONFIG_DIR path`. If it needs | |
| 208 | different API keys, base URLs, or router settings, use Environment variables. | |
| 209 | ||
| 210 | Do not put environment variable assignments in `Launch arguments`. |
docs/user/providers-codex.md created+141| ... | ... | @@ -0,0 +1,141 @@ |
| 1 | # Codex | |
| 2 | ||
| 3 | This guide is for people who want to use more than one Codex account in T3 Code. For Claude, see | |
| 4 | [Claude](./providers-claude.md). For first-time setup, see [Install T3 Code](./install.md). | |
| 5 | ||
| 6 | Common reasons: | |
| 7 | ||
| 8 | - use a work account for work projects | |
| 9 | - use a personal account for personal projects | |
| 10 | - switch to another account when one account hits limits | |
| 11 | - keep one shared Codex history instead of maintaining two separate Codex setups | |
| 12 | ||
| 13 | ## I Only Use One Codex Account | |
| 14 | ||
| 15 | Use the default provider. | |
| 16 | ||
| 17 | In Settings, your Codex provider can stay like this: | |
| 18 | ||
| 19 | ```text | |
| 20 | Display name: Codex | |
| 21 | CODEX_HOME path: ~/.codex | |
| 22 | Shadow home path: empty | |
| 23 | ``` | |
| 24 | ||
| 25 | Log in with Codex normally: | |
| 26 | ||
| 27 | ```bash | |
| 28 | codex login | |
| 29 | ``` | |
| 30 | ||
| 31 | ## I Want Work And Personal Codex Accounts | |
| 32 | ||
| 33 | Use one real Codex home and one shadow home. | |
| 34 | ||
| 35 | Recommended setup: | |
| 36 | ||
| 37 | ```text | |
| 38 | ~/.codex shared Codex home | |
| 39 | ~/.codex_p second account auth | |
| 40 | ``` | |
| 41 | ||
| 42 | The idea is: | |
| 43 | ||
| 44 | - both accounts can see the same T3/Codex sessions | |
| 45 | - each account keeps its own login | |
| 46 | - existing threads can continue with either account | |
| 47 | ||
| 48 | ### Set Up The First Account | |
| 49 | ||
| 50 | Log in normally: | |
| 51 | ||
| 52 | ```bash | |
| 53 | codex login | |
| 54 | ``` | |
| 55 | ||
| 56 | This is the account used by `~/.codex`. | |
| 57 | ||
| 58 | In T3 Code Settings, name it something obvious: | |
| 59 | ||
| 60 | ```text | |
| 61 | Display name: Codex Work | |
| 62 | CODEX_HOME path: ~/.codex | |
| 63 | Shadow home path: empty | |
| 64 | ``` | |
| 65 | ||
| 66 | ### Set Up The Second Account | |
| 67 | ||
| 68 | Log in with a separate Codex home: | |
| 69 | ||
| 70 | ```bash | |
| 71 | mkdir -p ~/.codex_p | |
| 72 | CODEX_HOME=~/.codex_p codex login | |
| 73 | ``` | |
| 74 | ||
| 75 | In T3 Code Settings, add another Codex provider: | |
| 76 | ||
| 77 | ```text | |
| 78 | Display name: Codex Personal | |
| 79 | CODEX_HOME path: ~/.codex | |
| 80 | Shadow home path: ~/.codex_p | |
| 81 | ``` | |
| 82 | ||
| 83 | The important part is that both providers use the same `CODEX_HOME path`, but only the second one | |
| 84 | has a `Shadow home path`. | |
| 85 | ||
| 86 | ## Which Account Am I Using? | |
| 87 | ||
| 88 | Open Settings and look at the provider row. | |
| 89 | ||
| 90 | T3 Code shows the authenticated email for providers that report one. Emails are blurred by default; | |
| 91 | click the blurred email to reveal it. | |
| 92 | ||
| 93 | Use display names and accent colors to make accounts easy to tell apart in the model picker. | |
| 94 | ||
| 95 | ## I Need A Different API Key Or Endpoint | |
| 96 | ||
| 97 | Use the provider's Environment variables section in Settings. | |
| 98 | ||
| 99 | This is useful when a Codex-compatible setup needs account-specific variables. Add the variables to | |
| 100 | the provider instance that should receive them, and mark API keys or tokens as sensitive. Sensitive | |
| 101 | values are stored as server secrets and are not sent back to the app after saving. | |
| 102 | ||
| 103 | ## Can I Switch Accounts In An Existing Thread? | |
| 104 | ||
| 105 | Yes, when both Codex providers share the same `CODEX_HOME path`. | |
| 106 | ||
| 107 | For example: | |
| 108 | ||
| 109 | ```text | |
| 110 | Codex Work CODEX_HOME path: ~/.codex | |
| 111 | Codex Personal CODEX_HOME path: ~/.codex, Shadow home path: ~/.codex_p | |
| 112 | ``` | |
| 113 | ||
| 114 | Those two providers are considered compatible for continuation, so the locked model picker can show | |
| 115 | both. | |
| 116 | ||
| 117 | If you add a third Codex provider with a completely different `CODEX_HOME path`, T3 Code treats it | |
| 118 | as a different workspace. It will not be offered for existing threads created under `~/.codex`. | |
| 119 | ||
| 120 | ## If Both Accounts Look The Same | |
| 121 | ||
| 122 | If two Codex providers show the same account or the same unexpected model list: | |
| 123 | ||
| 124 | 1. Check the email in Settings. | |
| 125 | 2. Refresh provider status. | |
| 126 | 3. Confirm the second provider has `Shadow home path` set. | |
| 127 | 4. Confirm the shadow directory has its own `auth.json`. | |
| 128 | 5. If you copied `~/.codex` into the shadow directory, remove everything except `auth.json`. | |
| 129 | ||
| 130 | Example cleanup: | |
| 131 | ||
| 132 | ```bash | |
| 133 | find ~/.codex_p -mindepth 1 ! -name auth.json -exec rm -rf {} + | |
| 134 | ``` | |
| 135 | ||
| 136 | ## When To Use A Separate CODEX_HOME | |
| 137 | ||
| 138 | Use a totally separate `CODEX_HOME path` only when you want a separate Codex workspace. | |
| 139 | ||
| 140 | That means separate sessions and less account switching inside old threads. Most dual-account users | |
| 141 | should use the shared-home plus shadow-home setup instead. |
docs/user/remote-access.md+15-22| ... | ... | @@ -14,14 +14,15 @@ That gives you: |
| 14 | 14 | |
| 15 | 15 | ## Enabling Network Access |
| 16 | 16 | |
| 17 | There are two ways to expose your server for remote connections: from the desktop app or from the CLI. | |
| 17 | There are three ways to reach your server from another device: expose the desktop app's backend, | |
| 18 | run a headless server from the CLI, or have the desktop app launch T3 Code over SSH. | |
| 18 | 19 | |
| 19 | 20 | ### Option 1: Desktop App |
| 20 | 21 | |
| 21 | 22 | If you are already running the desktop app and want to make it reachable from other devices: |
| 22 | 23 | |
| 23 | 24 | 1. Open **Settings** → **Connections**. |
| 24 | 2. Under **Manage Local Backend**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. | |
| 25 | 2. Under **This environment**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. | |
| 25 | 26 | 3. The settings panel will show the default reachable endpoint, with a `+N` control when more endpoints are available. Expand it to inspect alternatives such as loopback, LAN, private-network, or HTTPS endpoints. |
| 26 | 27 | 4. Use **Create Link** to generate a pairing link you can share with another device. |
| 27 | 28 | |
| ... | ... | @@ -48,10 +49,10 @@ Depending on your Tailscale setup, this may include: |
| 48 | 49 | - an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend |
| 49 | 50 | |
| 50 | 51 | The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as |
| 51 | `https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this | |
| 52 | backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend | |
| 53 | with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale | |
| 54 | Serve to proxy HTTPS traffic to the local backend. | |
| 52 | `https://machine.tailnet.ts.net/`, and is off until you opt in. Turn on **Enable Tailscale HTTPS** | |
| 53 | on the **Tailscale HTTPS** row in **Settings** → **Connections**. The desktop app restarts the | |
| 54 | backend with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks | |
| 55 | Tailscale Serve to proxy HTTPS traffic to the local backend. Turn the same switch off to stop it. | |
| 55 | 56 | |
| 56 | 57 | The Tailscale support is an endpoint provider add-on. The core remote model still works without Tailscale: LAN HTTP endpoints, custom HTTPS endpoints, future tunnels, and SSH-launched environments all use the same saved environment and pairing flow. |
| 57 | 58 | |
| ... | ... | @@ -96,10 +97,8 @@ By default this configures Tailscale Serve on HTTPS port 443 and advertises |
| 96 | 97 | npx t3 serve --tailscale-serve --tailscale-serve-port 8443 |
| 97 | 98 | ``` |
| 98 | 99 | |
| 99 | > Note | |
| 100 | > The GUIs do not currently support adding projects on remote environments. | |
| 101 | > For now, use `t3 project ...` on the server machine instead. | |
| 102 | > Full GUI support for remote project management is coming soon. | |
| 100 | Once paired, add projects normally: open the Command Palette and choose **Add Project**, then pick | |
| 101 | the environment the project lives on. Every saved environment is offered, not only the local one. | |
| 103 | 102 | |
| 104 | 103 | ### Option 3: Desktop-Managed SSH Launch |
| 105 | 104 | |
| ... | ... | @@ -125,16 +124,10 @@ The remote host must have a compatible Node.js runtime. T3 Code uses the server |
| 125 | 124 | ^22.16 || ^23.11 || >=24.10 |
| 126 | 125 | ``` |
| 127 | 126 | |
| 128 | During SSH launch, T3 Code first checks whether `node` is already available on `PATH`. If it is missing, the launcher tries common non-interactive shell locations and version-manager shims/activation hooks: | |
| 129 | ||
| 130 | - `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` | |
| 131 | - Volta via `~/.volta/bin` | |
| 132 | - asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh` | |
| 133 | - mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh` | |
| 134 | - fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh` | |
| 135 | - nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -` | |
| 136 | - nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts` | |
| 137 | - installed nvm versions under `$NVM_DIR/versions/node/*/bin` | |
| 127 | During SSH launch, T3 Code first checks whether `node` is on `PATH`. If it is missing, the launcher | |
| 128 | looks in the usual install directories and tries to activate a version manager if it finds one | |
| 129 | (Volta, asdf, mise, fnm, nodenv, nvm). That covers most setups, but a version manager that only | |
| 130 | initializes from an interactive shell profile will not be picked up. | |
| 138 | 131 | |
| 139 | 132 | If launch fails with `node: command not found`, a port-scan failure, or a message that the remote Node version does not satisfy the required range, SSH into the host and check the same non-interactive shell path T3 Code uses: |
| 140 | 133 | |
| ... | ... | @@ -148,7 +141,7 @@ If that does not print a compatible Node version, configure your version manager |
| 148 | 141 | nvm alias default 24 |
| 149 | 142 | ``` |
| 150 | 143 | |
| 151 | With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above. | |
| 144 | With mise, asdf, fnm, or nodenv, make sure the tool's shim directory is installed and resolves to a Node version satisfying the range above without an interactive shell. | |
| 152 | 145 | |
| 153 | 146 | If reconnecting after an app update fails, retry the SSH launch once. The launcher now compares its generated runner script, stops stale launcher-managed remote servers, clears the SSH launch PID/port state, and starts a fresh remote server. You should not normally need to delete `~/.t3/ssh-launch` or kill `t3` processes manually. |
| 154 | 147 | |
| ... | ... | @@ -160,7 +153,7 @@ be able to update and reconnect the server for you, or it may ask you to update |
| 160 | 153 | run a copied command on the server machine. |
| 161 | 154 | |
| 162 | 155 | Finish active work before updating because the server restarts briefly. For step-by-step guidance, |
| 163 | see [Keeping T3 Code in Sync](./server-updates.md). | |
| 156 | see [Keeping T3 Code in Sync](./updating.md). | |
| 164 | 157 | |
| 165 | 158 | On a Linux host, you can keep the server running after logout and manage it independently of the |
| 166 | 159 | connection method. See [Running T3 Code in the Background](./background-service.md). |
docs/user/server-updates.md deleted-61| ... | ... | @@ -1,61 +0,0 @@ |
| 1 | # Keeping T3 Code in Sync | |
| 2 | ||
| 3 | The T3 Code web or desktop app and the server it connects to work best when they use the same | |
| 4 | version. If they do not match, T3 Code shows a warning with the right update option for that server. | |
| 5 | ||
| 6 | ## Where to Find the Update | |
| 7 | ||
| 8 | You may see the warning in either of these places: | |
| 9 | ||
| 10 | - above the message box in the current conversation | |
| 11 | - **Settings** → **Connections**, beside the affected connection | |
| 12 | ||
| 13 | Dismissing the conversation warning only hides that reminder for those two versions. It does not | |
| 14 | update the server, and the version difference remains visible in Connections. | |
| 15 | ||
| 16 | ## Before You Update | |
| 17 | ||
| 18 | Let active agent work and terminal commands finish first. Updating restarts the server, so the | |
| 19 | connection will disappear briefly and work that is still running may be interrupted. | |
| 20 | ||
| 21 | The update does not remove saved threads, settings, or project files. | |
| 22 | ||
| 23 | ## Choose the Action You See | |
| 24 | ||
| 25 | | Action | What to do | | |
| 26 | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| 27 | | **Update server** | Select the button and leave T3 Code open. It prepares the matching version, restarts the server, and reconnects automatically. This can take several minutes. | | |
| 28 | | **Update the desktop app** | Open the T3 Code desktop app on the machine that runs the server and install the app update there. Reopen it if needed. | | |
| 29 | | **Copy update command** | Copy the command, open a terminal on the server machine, stop the current T3 Code server, and relaunch it with the copied command and any startup options you normally use. | | |
| 30 | ||
| 31 | The available action depends on how that server was started. T3 Code does not update connected | |
| 32 | servers silently in the background. | |
| 33 | ||
| 34 | After selecting **Update server**, the warning becomes a three-step progress rail: | |
| 35 | **Download**, **Install**, and **Resume**. The same progress appears in the conversation and in | |
| 36 | Connections, so navigating between them does not lose the update. A failed step remains visible | |
| 37 | with its error and an option to retry. | |
| 38 | ||
| 39 | If the server uses the T3 Code background service, you can also update it directly on the host: | |
| 40 | ||
| 41 | ```sh | |
| 42 | npx t3@latest service update | |
| 43 | ``` | |
| 44 | ||
| 45 | See [Running T3 Code in the Background](./background-service.md) for install, status, and removal | |
| 46 | commands. | |
| 47 | ||
| 48 | ## After the Update | |
| 49 | ||
| 50 | Keep the web or desktop app open while the server restarts. The update completes only after the | |
| 51 | replacement server reports the requested version and is ready to accept commands. The warning and | |
| 52 | progress rail then disappear. | |
| 53 | ||
| 54 | If a step fails: | |
| 55 | ||
| 56 | 1. Retry the offered action once. | |
| 57 | 2. Make sure you updated the machine named in the warning, not only the device you are using. | |
| 58 | 3. For a command-line server, relaunch it with `npx t3@<client-version>`, replacing | |
| 59 | `<client-version>` with the client version shown in the warning. | |
| 60 | ||
| 61 | For remote connection setup and access troubleshooting, see [Remote Access](./remote-access.md). |
docs/user/source-control.md created+137| ... | ... | @@ -0,0 +1,137 @@ |
| 1 | # Source Control Integrations | |
| 2 | ||
| 3 | T3 Code connects to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving the app. | |
| 4 | ||
| 5 | ## Supported Providers | |
| 6 | ||
| 7 | T3 Code works with the platforms your team already uses: | |
| 8 | ||
| 9 | - **GitHub** – Pull requests, repository creation, and clone integration | |
| 10 | - **GitLab** – Merge requests, repository publishing, and hosted clones | |
| 11 | - **Bitbucket** – Pull request workflows (via API token authentication) | |
| 12 | - **Azure DevOps** – Pull request support for Microsoft-hosted repositories | |
| 13 | ||
| 14 | ## What You Can Do | |
| 15 | ||
| 16 | ### Start Projects from Anywhere | |
| 17 | ||
| 18 | **Clone repositories directly** | |
| 19 | ||
| 20 | - Open the Command Palette (`Cmd/Ctrl + K`) → **Add Project** | |
| 21 | - Choose **GitHub repository**, **GitLab repository**, **Bitbucket repository**, **Azure DevOps repository**, or paste any **Git URL** | |
| 22 | - Enter the repository path (`owner/repo`, `group/project`, `workspace/repository`, or `project/repository`) or a full Git URL, pick a destination, and start coding | |
| 23 | ||
| 24 | **Publish local projects to the cloud** | |
| 25 | ||
| 26 | - Have a local Git repository without a remote? | |
| 27 | - Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push, in one flow | |
| 28 | - If the local repository has no commits yet, publishing creates the remote and wires it up but does not push. Make a commit, then push normally. | |
| 29 | ||
| 30 | ### Manage Code Reviews Without Context Switching | |
| 31 | ||
| 32 | **Create pull requests while you work** | |
| 33 | ||
| 34 | - Push a branch and create a pull request from the Git actions controls in the toolbar | |
| 35 | - T3 Code can suggest titles and descriptions based on your commits | |
| 36 | - Supports GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Azure DevOps Pull Requests | |
| 37 | ||
| 38 | **Stay on top of open reviews** | |
| 39 | ||
| 40 | - See if your current branch already has an open PR/MR | |
| 41 | - Open the review directly in your browser with one click | |
| 42 | - Check out a teammate's branch to review code locally | |
| 43 | ||
| 44 | ### Know Your Setup at a Glance | |
| 45 | ||
| 46 | The **Source Control settings** page shows you exactly what's connected: | |
| 47 | ||
| 48 | - ✅ Which providers are authenticated and ready | |
| 49 | - ⚠️ What's missing and how to fix it | |
| 50 | - 👤 Which account is signed in (when available) | |
| 51 | ||
| 52 | Run a quick **Rescan** after setting up a new machine or changing credentials. | |
| 53 | ||
| 54 | ## Getting Started | |
| 55 | ||
| 56 | ### For GitHub (Recommended for most users) | |
| 57 | ||
| 58 | 1. Install the GitHub CLI on the machine running T3 Code: | |
| 59 | ```bash | |
| 60 | brew install gh | |
| 61 | ``` | |
| 62 | 2. Sign in: | |
| 63 | ```bash | |
| 64 | gh auth login | |
| 65 | ``` | |
| 66 | 3. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated | |
| 67 | ||
| 68 | You can now clone, publish, and create pull requests. | |
| 69 | ||
| 70 | ### For GitLab | |
| 71 | ||
| 72 | 1. Install the GitLab CLI: | |
| 73 | ```bash | |
| 74 | brew install glab | |
| 75 | ``` | |
| 76 | 2. Authenticate: | |
| 77 | ```bash | |
| 78 | glab auth login | |
| 79 | ``` | |
| 80 | 3. Check **Settings → Source Control** to confirm the connection | |
| 81 | ||
| 82 | ### For Bitbucket | |
| 83 | ||
| 84 | Bitbucket uses tokens instead of a CLI tool. Two options, both set as environment variables on the | |
| 85 | machine running T3 Code. | |
| 86 | ||
| 87 | Recommended, a Bitbucket access token: | |
| 88 | ||
| 89 | ```bash | |
| 90 | export T3CODE_BITBUCKET_ACCESS_TOKEN="your-access-token" | |
| 91 | ``` | |
| 92 | ||
| 93 | Or an Atlassian account email plus API token, with read/write access to pull requests and | |
| 94 | repositories: | |
| 95 | ||
| 96 | ```bash | |
| 97 | export T3CODE_BITBUCKET_EMAIL="you@example.com" | |
| 98 | export T3CODE_BITBUCKET_API_TOKEN="your-token" | |
| 99 | ``` | |
| 100 | ||
| 101 | If both are set, the access token wins. Restart T3 Code and verify the connection in **Source | |
| 102 | Control settings**. | |
| 103 | ||
| 104 | ### For Azure DevOps | |
| 105 | ||
| 106 | 1. Install Azure CLI: | |
| 107 | ```bash | |
| 108 | brew install azure-cli | |
| 109 | ``` | |
| 110 | 2. Add the DevOps extension: | |
| 111 | ```bash | |
| 112 | az extension add --name azure-devops | |
| 113 | ``` | |
| 114 | 3. Sign in: | |
| 115 | ```bash | |
| 116 | az login | |
| 117 | ``` | |
| 118 | ||
| 119 | --- | |
| 120 | ||
| 121 | ## Requirements & Troubleshooting | |
| 122 | ||
| 123 | **Git is required** – T3 Code uses Git for all local operations. Ensure `git` is installed on your server. | |
| 124 | ||
| 125 | **Server-side setup** – Authentication happens on the machine running T3 Code (the server), not your local browser. If you're using a hosted or team instance, your administrator may have already configured providers. | |
| 126 | ||
| 127 | **Common issues:** | |
| 128 | ||
| 129 | - **Provider shows "Not authenticated"** – Run the login command for that provider (e.g., `gh auth login`) in a terminal on the server, then rescan in Settings | |
| 130 | - **Bitbucket not connecting** – Double-check your environment variables are set in the correct shell profile and the server was restarted | |
| 131 | - **Can't push to a remote** – Verify your Git remote URL matches the provider you've authenticated with (SSH vs HTTPS remotes may need different credentials) | |
| 132 | ||
| 133 | **Need more help?** Check your provider's CLI documentation: | |
| 134 | ||
| 135 | - [GitHub CLI](https://cli.github.com/) | |
| 136 | - [GitLab CLI](https://gitlab.com/gitlab-org/cli) | |
| 137 | - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/) |
docs/user/updating.md created+69| ... | ... | @@ -0,0 +1,69 @@ |
| 1 | # Keeping T3 Code in Sync | |
| 2 | ||
| 3 | The T3 Code web or desktop app and the server it connects to work best when they use the same | |
| 4 | version. If they do not match, T3 Code shows a warning with the right update option for that server. | |
| 5 | ||
| 6 | ## Where to Find the Update | |
| 7 | ||
| 8 | You may see the warning in either of these places: | |
| 9 | ||
| 10 | - above the message box in the current conversation | |
| 11 | - **Settings** → **Connections**, beside the affected connection | |
| 12 | ||
| 13 | Dismissing the conversation warning only hides that reminder for those two versions. It does not | |
| 14 | update the server, and the version difference remains visible in Connections. | |
| 15 | ||
| 16 | ## Before You Update | |
| 17 | ||
| 18 | Let active agent work and terminal commands finish first. Updating restarts the server, so the | |
| 19 | connection will disappear briefly and work that is still running may be interrupted. | |
| 20 | ||
| 21 | The update does not remove saved threads, settings, or project files. | |
| 22 | ||
| 23 | ## Choose the Action You See | |
| 24 | ||
| 25 | | Action | What to do | | |
| 26 | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| 27 | | **Update server** | Select the button and leave T3 Code open. It prepares the matching version, restarts the server, and reconnects automatically. This can take several minutes. | | |
| 28 | | **Update the desktop app** | Open the T3 Code desktop app on the machine that runs the server and install the app update there. Reopen it if needed. | | |
| 29 | | **Copy update command** | Copy the command, open a terminal on the server machine, stop the current T3 Code server, and relaunch it with the copied command and any startup options you normally use. | | |
| 30 | ||
| 31 | The available action depends on how that server was started. T3 Code does not update connected | |
| 32 | servers silently in the background. | |
| 33 | ||
| 34 | After selecting **Update server**, the warning becomes a three-step progress rail: | |
| 35 | **Download**, **Install**, and **Resume**. The same progress appears in the conversation and in | |
| 36 | Connections, so navigating between them does not lose the update. A failed step remains visible | |
| 37 | with its error and an option to retry. | |
| 38 | ||
| 39 | **Copy update command** gives you `npx t3@<client-version>`, which relaunches the server directly | |
| 40 | at the matching version. Add whatever startup options you normally use. | |
| 41 | ||
| 42 | If the server instead runs as the T3 Code background service, update the service on the host and | |
| 43 | pin the same version: | |
| 44 | ||
| 45 | ```sh | |
| 46 | npx t3@<client-version> service update | |
| 47 | ``` | |
| 48 | ||
| 49 | `service update` installs the version of the CLI that invoked it, so `npx t3@latest service update` | |
| 50 | only resolves the skew when your client happens to be on the latest release. The exact version from | |
| 51 | the warning always works. | |
| 52 | ||
| 53 | See [Running T3 Code in the Background](./background-service.md) for install, status, and removal | |
| 54 | commands. | |
| 55 | ||
| 56 | ## After the Update | |
| 57 | ||
| 58 | Keep the web or desktop app open while the server restarts. The update completes only after the | |
| 59 | replacement server reports the requested version and is ready to accept commands. The warning and | |
| 60 | progress rail then disappear. | |
| 61 | ||
| 62 | If a step fails: | |
| 63 | ||
| 64 | 1. Retry the offered action once. | |
| 65 | 2. Make sure you updated the machine named in the warning, not only the device you are using. | |
| 66 | 3. For a command-line server, relaunch it with `npx t3@<client-version>`, replacing | |
| 67 | `<client-version>` with the client version shown in the warning. | |
| 68 | ||
| 69 | For remote connection setup and access troubleshooting, see [Remote Access](./remote-access.md). |
infra/relay/README.md+7-7| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | # T3 Connect Relay |
| 2 | 2 | |
| 3 | > [!WARNING] | |
| 4 | > T3 Connect is currently in private beta. Join the waitlist in the app under Settings > T3 Connect. | |
| 3 | > [!NOTE] | |
| 4 | > Sign in to T3 Connect from the app under Settings > Connections. | |
| 5 | 5 | |
| 6 | 6 | The relay is the hosted control plane for T3 Connect. It helps clients discover and connect to |
| 7 | 7 | remote environments, manages the cloud-side records needed for those connections, and delivers |
| ... | ... | @@ -9,7 +9,7 @@ optional mobile notifications and Live Activities. |
| 9 | 9 | |
| 10 | 10 | The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects, |
| 11 | 11 | regular API and WebSocket traffic goes directly between that client and the selected environment. |
| 12 | See the [T3 Connect architecture overview](../../docs/cloud/t3-code-connect-auth-flow.html) for the larger system | |
| 12 | See the [T3 Connect architecture overview](../../docs/internals/t3-code-connect-auth-flow.html) for the larger system | |
| 13 | 13 | design. |
| 14 | 14 | |
| 15 | 15 | ## Responsibilities |
| ... | ... | @@ -25,7 +25,7 @@ The relay currently owns: |
| 25 | 25 | - Persisting relay state and exposing relay-specific traces for diagnostics. |
| 26 | 26 | |
| 27 | 27 | The environment server and relay have separate credentials and trust boundaries. Read |
| 28 | [Environment Authentication Profile](../../docs/environment-auth.md) before changing token, | |
| 28 | [Environment Authentication Profile](../../docs/internals/environment-auth.md) before changing token, | |
| 29 | 29 | credential, or authorization behavior. |
| 30 | 30 | |
| 31 | 31 | ## Code Map |
| ... | ... | @@ -159,8 +159,8 @@ and hosted web builds. |
| 159 | 159 | |
| 160 | 160 | See: |
| 161 | 161 | |
| 162 | - [T3 Connect Clerk Setup](../../docs/cloud/t3-connect-clerk.md) for Clerk keys, JWT templates, and waitlist | |
| 162 | - [T3 Connect Clerk Setup](../../docs/internals/t3-connect.md) for Clerk keys, JWT templates, and sign-up restrictions | |
| 163 | 163 | setup. |
| 164 | - [Relay Observability](../../docs/relay-observability.md) for deployment tracing and diagnostics. | |
| 165 | - [T3 Connect Architecture Overview](../../docs/cloud/t3-code-connect-auth-flow.html) for the full link, | |
| 164 | - [Relay Observability](../../docs/operations/relay-observability.md) for deployment tracing and diagnostics. | |
| 165 | - [T3 Connect Architecture Overview](../../docs/internals/t3-code-connect-auth-flow.html) for the full link, | |
| 166 | 166 | connect, endpoint, and notification flows. |