authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-30 06:10:47-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 06:10:47-07:00
log9dd425b2234c062b4767583e42d4b2c1aabab15d
tree6955e51143b23e187de10466b7a76dcfabb3dcbb
parent50871eb5de641ffd41b1f9d0151668982d276393
signature Signed by PGP key B5690EEEBB952194

docs: split user and maintainer docs, fix 100+ stale claims (#4807)

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,7 +28,7 @@ T3 Code has 3 key app surfaces: **web**, **desktop**, and **mobile**.
2828
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.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.
3030
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.
3232
33## A note from Theo33## A note from Theo
3434
...@@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste...@@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste
72- **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.72- **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.
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.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- **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real.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`.
7676
77## Dev servers77## Dev servers
7878
...@@ -123,13 +123,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real...@@ -123,13 +123,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
123123
124Clients 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.124Clients 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.
125125
126Full glossary with file links: `docs/reference/encyclopedia.md`126Full glossary with file links: `docs/internals/glossary.md`
127127
128## Where code lives128## Where code lives
129129
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- `apps/web` - React/Vite UI. `apps/desktop` wraps it, `apps/mobile` is React Native, `apps/marketing` is the site.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- `packages/shared` - shared runtime utils, subpath exports, no barrel.133- `packages/shared` - shared runtime utils, subpath exports, no barrel.
134- `packages/client-runtime` - client code shared by web and mobile.134- `packages/client-runtime` - client code shared by web and mobile.
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.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,13 +17,13 @@ We wanted something performant, remote-ready, and truly open. If we ever go the
17>17>
18> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`18> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
19> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`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> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`21> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
22> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`22> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
2323
24### Try it out (install-free)24### Try it out (install-free)
2525
26The easiest way to test T3 Code is to run the server in your terminal:26The easiest way to test T3 Code is to run the server in your terminal (requires Node.js 22.16+, 23.11+, or 24.10+):
2727
28```bash28```bash
29npx t3@latest29npx t3@latest
...@@ -61,17 +61,20 @@ We are very very early in this project. Expect bugs....@@ -61,17 +61,20 @@ We are very very early in this project. Expect bugs.
6161
62We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be.62We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be.
6363
64There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs).
65
66## Documentation64## Documentation
6765
68- [Getting started](./docs/getting-started/quick-start.md)66Full docs live in [docs/](./docs). There's no docs site yet.
69- [Remote access](./docs/user/remote-access.md)67
70- [Keeping T3 Code in sync](./docs/user/server-updates.md)68- [Install and first run](./docs/user/install.md)
71- [Architecture overview](./docs/architecture/overview.md)69- [Permission modes](./docs/user/permission-modes.md)
72- [Provider guides](./docs/providers/codex.md)70- [Keyboard shortcuts](./docs/user/keybindings.md)
73- [Operations](./docs/operations/ci.md)71- [Remote access from a phone or another machine](./docs/user/remote-access.md)
74- [Reference](./docs/reference/encyclopedia.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
77Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md).
7578
76## If you REALLY want to contribute still.... read this first79## If you REALLY want to contribute still.... read this first
7780
docs/README.md+41-19
...@@ -1,19 +1,41 @@...@@ -1,19 +1,41 @@
1# Documentation1# T3 Code docs
22
3- [Getting started](./getting-started/quick-start.md)3## Using T3 Code
4- Architecture4
5 - [Overview](./architecture/overview.md)5- [Install and first run](./user/install.md)
6 - [Connection runtime](./architecture/connection-runtime.md)6- [Permission modes](./user/permission-modes.md)
7 - [Remote environments](./architecture/remote.md)7- [Keyboard shortcuts](./user/keybindings.md)
8 - [Server updates](./architecture/server-updates.md)8- [Remote access](./user/remote-access.md)
9- User guides9- [Keeping app and server in sync](./user/updating.md)
10 - [Background service](./user/background-service.md)10- [Source control integrations](./user/source-control.md)
11 - [Remote access](./user/remote-access.md)11- [Background service (Linux)](./user/background-service.md)
12 - [Keeping T3 Code in sync](./user/server-updates.md)12- Providers: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md)
13 - [Keybindings](./user/keybindings.md)13
14- [T3 Connect](./cloud/t3-connect-clerk.md)14Mobile app: [apps/mobile/README.md](../apps/mobile/README.md)
15- [Integrations](./integrations/source-control-providers.md)15
16- [Mobile](./mobile/app.md)16---
17- [Operations](./operations/ci.md)17
18- [Providers](./providers/codex.md)18## Working on T3 Code
19- [Reference](./reference/encyclopedia.md)19
20Everything below is for maintainers. Setup lives in the [root README](../README.md);
21policy 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
3The connection runtime is shared by web and mobile. It owns connectivity,
4authentication, retries, transport lifetime, cached environment data, and
5environment-scoped operations.
6
7Web and mobile mount this runtime once at the application root. There is no
8legacy connection owner or supported mixed mode.
9
10## Ownership
11
12Each registered environment has one scoped Effect `Context` containing focused
13services:
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`
27owns its scope. There is no aggregate environment runtime facade. React
28components do not create connections, transports, retry loops, or RPC clients.
29
30## Connection State
31
32The supervisor is the only retry owner.
33
341. A persisted or platform registration marks an environment as desired.
352. If the device is offline, the supervisor releases the active session and
36 waits without consuming retry attempts.
373. When online, the supervisor asks the broker for one prepared connection and
38 asks the session factory for one RPC session.
394. Transient failures retry forever with exponential backoff capped at 16
40 seconds.
415. 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.
466. Authentication or configuration failures remain blocked until an external
47 wakeup changes the relevant input.
487. An involuntary session close keeps the registration and cache, then retries.
498. Explicit removal closes the session and deletes the registration,
50 credentials, shell cache, and thread cache.
51
52The UI derives `available`, `offline`, `connecting`, `reconnecting`,
53`connected`, and `error` from supervisor state plus explicit data-sync state.
54It does not infer connection health from cached data or the existence of a
55transport object. An environment becomes `connected` after the socket opens and
56the initial config RPC succeeds, proving that the server is responsive. Shell
57and thread synchronization are independent data states. A healthy RPC
58transport with a failed shell subscription is shown as connected with a
59synchronization error, not as a reconnect that is not actually scheduled.
60
61## Data Boundary
62
63Finite 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
79The Promise bridge exists only at the React/Atom boundary. Runtime and business
80logic remain Effect-native.
81
82## Platform Layers
83
84Web 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
94Platform layers adapt operating-system capabilities. They do not implement
95connection policy.
96
97## Source Boundaries
98
99The 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
110Other reusable state lives in domain subpaths such as `shell`, `threads`,
111`terminal`, and `vcs`. Applications must import explicit package subpaths; the
112package intentionally has no root export.
113
114## Application Boundary
115
116The application root mounts the shared connection application layer, creates
117its own Atom runtime, and selects the domain atom factories required by that
118platform. Web and mobile may expose different hooks and features without
119changing connection ownership.
120
121Application code must not construct `WsTransport`, RPC clients, retry loops, or
122raw orchestration commands. Persistence paths belong to the platform
123registration and cache stores, with explicit migration or invalidation policy.
124
125## Verification
126
127Core state-machine tests use `@effect/vitest` and deterministic service layers.
128Required 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
3T3 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
42Related design:
43
44- [Resource telemetry architecture](./resource-telemetry.md)
45
46## Event Lifecycle
47
48### Startup and client connect
49
50```mermaid
51sequenceDiagram
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
691. The browser boots `WsTransport` and registers typed listeners in `wsNativeApi`.
702. The server accepts the connection in `wsServer` and brings up the runtime graph defined in `serverLayers`.
713. `ServerReadiness` waits until the key startup barriers are complete.
724. Once the server is ready, `wsServer` sends `server.welcome` from the contracts in `ws.ts` through `ServerPushBus`.
735. 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
78sequenceDiagram
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
991. A user action in the browser becomes a typed request through `WsTransport` and the browser API layer in `nativeApi`.
1002. `wsServer` decodes that request using the shared WebSocket contracts in `ws.ts` and routes it to the right service.
1013. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio.
1024. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events.
1035. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events.
1046. `wsServer` pushes those updates to the browser through `ServerPushBus` on channels defined in [`orchestration.ts`][11].
105
106### Async completion flow
107
108```mermaid
109sequenceDiagram
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
1281. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14].
1292. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic.
1303. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence.
1314. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers.
1325. 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
3The 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
8Push 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
10Methods 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
16Codex 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
24Provider runtime events flow through queue-based workers:
25
261. **ProviderRuntimeIngestion** — consumes provider runtime streams, emits orchestration commands
272. **ProviderCommandReactor** — reacts to orchestration intent events, dispatches provider calls
283. **CheckpointReactor** — captures git checkpoints on turn start/complete, publishes runtime receipts
29
30All 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
3This document describes the target architecture for first-class remote environments in T3 Code.
4
5It 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
24T3 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
26Remote 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
59The 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
65An `ExecutionEnvironment` is one running T3 server instance.
66
67It 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
77It is identified by a stable `environmentId`.
78
79This is the shared cross-client primitive. Desktop, mobile, and web should all reason about the same concept here.
80
81### KnownEnvironment
82
83A `KnownEnvironment` is a client-side saved entry for an environment the client knows how to reach.
84
85It is not server-authored. It is local to a device or client profile.
86
87Examples:
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
94A known environment may or may not know the target `environmentId` before first successful connect.
95
96In 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
100An `AccessEndpoint` is one concrete way to reach a known environment.
101
102This is the key abstraction that keeps SSH from taking over the model.
103
104A 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
111The environment stays the same. Only the access path changes.
112
113### AdvertisedEndpoint
114
115An `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
124Clients 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
126The 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
128Persist 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
130When no user default is saved, endpoint selection should prefer:
131
1321. endpoints compatible with the hosted HTTPS app
1332. explicitly default endpoints
1343. non-loopback endpoints
1354. loopback endpoints only for same-machine clients
136
137This 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
141Endpoint providers are add-ons that contribute advertised endpoints for the current environment.
142
143The 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
150The 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
152Provider-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
156A hosted pairing request is a bootstrap URL for the static web app, not a transport.
157
158Example:
159
160```text
161https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE
162```
163
164The 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
166Important 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
178It 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
182The current `Project` model remains environment-local.
183
184That 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
192Access methods answer one question:
193
194How does the client speak WebSocket to a T3 server?
195
196They 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
204Examples:
205
206- `ws://10.0.0.15:3773`
207- `wss://t3.example.com`
208
209This is the base model and should be the first-class default.
210
211Benefits:
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
217Browser 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
221Examples:
222
223- public relay URLs
224- private network relay URLs
225- local tunnel products such as pipenet
226
227This is still direct WebSocket access from the client's perspective. The difference is that the route is mediated by a tunnel or relay.
228
229For T3, tunnels are best modeled as another `AccessEndpoint`, not as a different kind of environment.
230
231This 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
238Tailscale-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
242SSH is an access and launch helper, not a separate environment type.
243
244The 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
251After that, the renderer should still connect using an ordinary WebSocket URL against the forwarded local port.
252
253This keeps the renderer transport model consistent with every other access method.
254
255The 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
259Launch methods answer a different question:
260
261How does a T3 server come to exist on the target machine?
262
263Launch and access should stay separate in the design.
264
265### 1. Pre-existing server
266
267The simplest launch method is no launch at all.
268
269The user or operator already runs T3 on the target machine, and the client connects through a direct or tunneled WebSocket endpoint.
270
271This 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
275This is the main place where Zed is a useful reference.
276
277Useful 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
285What 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
291The recommended T3 flow is:
292
2931. Desktop connects over SSH.
2942. Desktop probes the remote machine and verifies T3 availability.
2953. Desktop launches or reuses a remote T3 server.
2964. Desktop establishes local port forwarding.
2975. Renderer connects to the forwarded WebSocket endpoint as a normal environment.
298
299The 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
301Failure 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
310This is the inverse of remote launch: a local T3 server is already running, and the client publishes it through a tunnel.
311
312This is useful for:
313
314- exposing a desktop-hosted environment to mobile
315- temporary remote access without changing router or firewall settings
316
317This is still a launch concern, not a new environment kind.
318
319## Why access and launch must stay separate
320
321These concerns are easy to conflate, but separating them prevents architectural drift.
322
323Examples:
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
330In all of those cases, the `ExecutionEnvironment` is the same kind of thing.
331
332Only the launch and access paths differ.
333
334## Version Coordination
335
336Remote environments may stay online while web, desktop, or mobile clients move to a newer release.
337The environment descriptor therefore carries the running server version and may advertise a safe
338replacement path. The web and desktop UI use that information to show the appropriate action
339without making the connection transport responsible for process management.
340
341Published CLI servers on supported hosts can install and hand off to the client's exact version. A
342desktop-managed backend instead points the user to the desktop app on that machine, while older or
343unsupported servers fall back to a manual relaunch. The existing connection supervisor owns the
344disconnect and reconnect just as it would for any other involuntary socket close.
345
346See [Server Update Architecture](./server-updates.md) for capability detection, installation safety,
347and restart sequencing.
348
349## Security model
350
351Remote support must assume that some environments will be reachable over untrusted networks.
352
353That 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
359T3 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
361For publicly reachable environments, authenticated access should be treated as required.
362
363Hosted 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
367Zed is a useful reference implementation for managed remote launch and reconnect behavior.
368
369The 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
376The important mismatch is transport shape.
377
378Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime.
379
380T3 should not copy that part.
381
382T3 already has the right runtime boundary:
383
384- one T3 server per environment
385- ordinary HTTP/WebSocket between client and environment
386
387So T3 should borrow Zed's launch discipline, not its transport protocol.
388
389## Recommended rollout
390
3911. First-class known environments and access endpoints.
3922. Direct `ws` / `wss` remote environments.
3933. Authenticated tunnel-backed environments.
3944. Desktop-managed SSH launch and forwarding.
3955. Multi-environment UI improvements after the base runtime path is proven.
396
397This 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
3Status: implemented
4
5## Purpose
6
7Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset`
8subprocess probes with two persistent, direct data sources:
9
101. a standalone Rust resource-monitor executable that reads process counters
11 through operating-system APIs via `sysinfo`;
122. Electron main-process APIs for Electron process metrics and host power state.
13
14The native monitor owns bounded in-memory history. The server only merges and
15summarizes that history when diagnostics requests it. Telemetry history is not
16persisted to disk or continuously copied into Node.
17
18## Why a standalone executable
19
20The 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
30The cost is one persistent child process and NDJSON serialization. That is a
31better failure boundary than repeatedly spawning shell utilities or loading
32native code into Node.
33
34## Runtime topology
35
36### Desktop
37
38```text
39Electron main
40 ├─ powerMonitor
41 ├─ app.getAppMetrics() while diagnostics is open
42 ├─ inherited fd 4, telemetry NDJSON ─────────────┐
43 └─ inherited fd 5, demand-control NDJSON ◀──────┤
44 ▼
45Node 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
54Electron telemetry is unavailable. The native monitor still runs beside the
55server and tracks the server process tree. Power fields degrade to `unknown`
56instead of invoking platform shell commands.
57
58### WSL backend limitation
59
60Windows desktop packages currently ship the Windows resource-monitor executable.
61That executable cannot run inside the Linux WSL backend, so a WSL-only backend
62does not receive `resourceMonitorPath` and reports native process telemetry as
63unavailable. Electron host-power telemetry remains available over the inherited
64desktop pipe. Supporting native WSL process telemetry requires publishing a
65Linux sidecar for each supported architecture in the Windows artifact and
66converting its packaged path into the selected distro; the configuration
67deliberately does not pass the Windows `.exe` into WSL as a false fallback.
68
69## Native monitor
70
71The executable lives in `native/resource-monitor`.
72
73It receives schema-compatible commands on stdin and emits one JSON object per
74line 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
88The protocol version is defined by
89`RESOURCE_MONITOR_PROTOCOL_VERSION` in
90`packages/contracts/src/resourceTelemetry.ts`.
91
92### Collection
93
94The monitor keeps one `sysinfo::System` instance and refreshes it at the
95power-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
104On Linux, task/thread enumeration is disabled. Command lines are loaded only
105when 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
110Each sample scans the accessible process table, builds the PID/PPID graph, and
111retains:
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
120Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native
121start times are matched with a two-second tolerance because native start times
122can have coarser platform resolution.
123
124The process list is emitted in depth-first tree order so renderer collapse and
125expansion preserves complete subtrees.
126
127### Native history and streaming
128
129Every native sample is appended to a one-hour in-memory ring bounded to 3,600
130snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes.
131History stays in the sidecar until a `readHistory` request and is returned in
132bounded chunks. The first bound reached wins, so high process counts or large
133process names and command lines shorten the effective history window.
134
135Periodic snapshot streaming is disabled by default. The server enables it only
136while at least one diagnostics subscription is retained. `sampleNow` remains
137available for explicit refreshes and identity validation.
138
139The 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
149This 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
157Those deeper tracing systems can be added later as opt-in diagnostic modes
158without changing the public `ResourceTelemetry` model.
159
160## I/O semantics
161
162The monitor preserves platform semantics instead of presenting all counters as
163equivalent:
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
170The UI therefore labels these values as I/O reads and writes and exposes the
171per-process `ioSemantics` value.
172
173Group totals are observed deltas since telemetry startup. Per-process total
174columns are the operating system's cumulative counters for that process.
175
176## Electron telemetry
177
178Electron main owns `DesktopTelemetryPublisher`.
179
180Power events trigger an immediate snapshot. While diagnostics is closed, the
181server sends the configured active and idle host-power intervals to Electron
182(30 seconds and 2 minutes in the balanced profile). During those heartbeats
183Electron 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
191live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while
192locked, suspended, or thermally constrained.
193
194It 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
202Suspension stays latched across event-driven snapshots. An explicit resume
203clears it immediately; a periodic heartbeat also clears a stale latch so a
204missed resume event cannot leave telemetry permanently constrained.
205
206Electron does not expose a cross-platform low-power-mode getter, so that field
207remains `unknown`.
208
209The 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
215These are private Electron-main/server pipes. They do not use the renderer
216WebSocket and are recreated for every backend restart.
217
218## Server Effect services
219
220The implementation is under `apps/server/src/resourceTelemetry`.
221
222### `ResourceMonitorBinary`
223
224Resolves an executable from:
225
2261. `T3CODE_RESOURCE_MONITOR_PATH`;
2272. desktop bootstrap configuration;
2283. bundled CLI resources;
2294. local Cargo build outputs.
230
231Unsupported platforms, missing binaries, and non-executable binaries use
232schema-backed tagged errors with descriptive messages.
233
234### `NativeTelemetryClient`
235
236Owns 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
251Snapshot sequence numbers are scoped to a monitor generation. Server ingestion
252uses the monitor restart count as the generation key, so sequence reset after a
253restart cannot freeze telemetry.
254
255### `DesktopTelemetryReceiver`
256
257Reads fd 4, decodes schema-validated messages, stores the latest Electron
258snapshot, and publishes desktop health. It writes diagnostics demand to fd 5
259and gives the first sample a 90-second startup deadline. Once samples arrive,
260the stale deadline stays beyond the slower configured host-power interval with
26130 seconds of scheduling grace, so intentional 2–10 minute idle polling does
262not oscillate the policy between constrained and unconstrained states. Decode
263errors, protocol mismatch, control-write failure, stream failure, stale input,
264and normal stream closure are represented explicitly.
265
266### `ResourceTelemetry`
267
268Merges 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
282Electron and monitor processes are visible but are not valid targets for the
283existing process-signal RPC.
284
285### History projection
286
287`ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native
288snapshots to derive rates, lifecycle counters, buckets, and process summaries.
289Current Electron process metrics are intentionally excluded from historical
290replay so they cannot overwrite older native CPU or memory samples.
291
292### `ResourceAttribution`
293
294Tracks known logical application I/O separately from OS counters. Current
295integration points record successful writes for:
296
297- provider native and canonical event logs;
298- the local server trace sink.
299
300Entries contain component, operation, logical bytes, count, and elapsed time.
301Future persistence paths should call `ResourceAttribution.record` rather than
302adding diagnostics-specific counters.
303
304## Background policy integration
305
306`HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host
307power does not retain live resource diagnostics or invoke shell probes.
308
309The monitor updates its latest timestamp on every Electron sample but only
310publishes semantic state changes. Increasing idle seconds alone does not cause a
311background-policy broadcast every second.
312
313## Public API and UI
314
315The WebSocket RPC surface provides:
316
317- current snapshot;
318- bounded history;
319- explicit monitor retry;
320- a live snapshot subscription.
321
322The 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
333Legacy process diagnostics RPCs are projected from the same service so they no
334longer start recurring process-table commands.
335
336## Packaging
337
338Desktop artifact builds compile the Rust target, stage it as
339`resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the
340backend bootstrap.
341
342CLI release jobs upload each active platform monitor artifact and copy it into:
343
344```text
345apps/server/dist/resource-monitor/<platform>-<arch>/
346```
347
348The published server package already includes `dist`, so those executables ship
349with the CLI. Missing platform artifacts degrade native telemetry to
350`unavailable`; the server continues running.
351
352## Resource and failure behavior
353
354Steady 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
365The diagnostics page exposes the monitor's own process resource usage and
366collection duration so the observer's cost is measurable.
367
368Failures 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
379High-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
389These additions should preserve the current rules: direct platform APIs,
390schema-validated boundaries, explicit metric semantics, bounded retention, and
391no mandatory telemetry persistence.
docs/architecture/runtime-modes.md deleted-6
...@@ -1,6 +0,0 @@
1# Runtime modes
2
3T3 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
3T3 Code can update a connected server to the exact version of the client that detected version
4drift. This path exists primarily for remote environments, where the user may not have a terminal
5open on the server machine.
6
7The 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`
18after loading server config.
19
20The optional capability is intentionally backward compatible. An older server does not know about
21the field, so a missing value means the client must offer a manual relaunch instead of sending an
22unknown RPC.
23
24The 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
29Both surfaces target the client's exact version. When the reconnected server reports that version,
30the mismatch and action disappear.
31
32The operation state lives in `packages/client-runtime`, keyed by environment. Both web surfaces read
33the same `downloading`, `installing`, or `resuming` state, so route changes do not own or cancel the
34operation.
35
36## Capability Selection
37
38The 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
47Desktop ownership takes precedence over process-shape detection. A desktop-managed backend must
48never spawn a second CLI server beside the app-owned process. Likewise, a process launched by an
49unrecognized systemd unit does not claim the foreground respawn path because its supervisor could
50bring the old version back.
51
52## Update Flow
53
54```mermaid
55flowchart 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
74Both update RPCs require the environment's `orchestration:operate` authorization scope. Their
75payload 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
77new client can still repair skew with an older server.
78
79The 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
81successfully. Boot-service setup and self-update share the same process-wide installation lock, so
82they cannot mutate a pinned runtime concurrently.
83
84Before any restart, the current Node executable runs the replacement with `--version`. A failed
85install, failed preflight, or wrong reported version leaves the current server running. A failed
86preflight also removes the candidate runtime so retrying the same version performs a clean install.
87
88## Host Service Lifecycle
89
90The 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
92reconcile the unit through `BootService`; running `npx t3@latest service update` therefore pins and
93activates the latest CLI release without requiring a connected client.
94
95The `t3 connect` onboarding flow may offer service installation, but it calls the same reconciliation
96operation as `t3 service install`. Connect logout only disables cloud access and clears its
97authorization; it does not uninstall the host service.
98
99## Process Handoff
100
101For `boot-service`, the server atomically rewrites the T3-managed user unit to point at the verified
102runtime and reloads systemd. It acknowledges the handoff, then restarts the unit after the same
103short grace period used by foreground respawn. A rejected deferred restart restores the previous
104unit and is logged by the still-running process.
105
106For `respawn`, the server starts a detached, delayed replacement that replays the original CLI
107arguments. It then acknowledges the request and schedules the current process to exit. The delays
108give the acknowledgement time to cross direct or relayed connections before the socket closes.
109
110Progress-capable servers emit `downloading` before installing the pinned runtime and `installing`
111before preflight and handoff. A terminal stream event acknowledges that restart is scheduled. The
112client then enters `resuming`, waits for the replacement lifecycle stream to publish `ready` with
113the target version, and only then completes the operation. It watches for the intentional
114disconnect's first backoff state and requests one fresh retry, which clears historical backoff debt
115without adding a separate reconnect loop.
116
117## Release Invariant
118
119The exact client version must exist as the `t3` npm package before a client carrying that version is
120published. The release workflow therefore makes the GitHub release depend on CLI publication, and
121the 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
3The environment server and the relay use separate credentials, issuers, and trust
4boundaries. They intentionally use a similar OAuth-shaped model so that permission
5checks and token exchange behavior can be audited against established concepts.
6
7## Authorization Model
8
9Environment authorization is capability-based. A session carries zero or more
10OAuth-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
23Ordinary pairing links grant the four client-operation scopes and read access to
24managed relay connectivity:
25`orchestration:read orchestration:operate terminal:operate review:write relay:read`.
26The desktop bootstrap credential and command-line administrative bootstrap
27credentials 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
34browser session cookie. The cookie is an HTTP transport adapter for the same
35scoped session model; the response never exposes the session secret to browser
36JavaScript.
37
38### Bearer Access Token
39
40Non-browser clients use `POST /oauth/token` with an
41`application/x-www-form-urlencoded` body:
42
43```text
44grant_type=urn:ietf:params:oauth:grant-type:token-exchange
45subject_token=<bootstrap credential>
46subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap
47requested_token_type=urn:ietf:params:oauth:token-type:access_token
48scope=orchestration:read orchestration:operate terminal:operate review:write relay:read
49```
50
51Clients may additionally submit `client_label`, `client_device_type`, and
52`client_os` extension parameters so the authorized-clients UI can identify the
53device that established the session. These are presentation hints only; the
54environment derives transport metadata such as IP address and user agent from
55the request and does not use these fields for authorization.
56
57The 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
69Requested scopes must be a subset of the one-time bootstrap credential grant.
70An 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
76a short-lived, single-purpose WebSocket ticket. This keeps bearer tokens and
77browser cookies out of WebSocket URLs while allowing the socket handshake to
78authenticate. The ticket carries its session's scopes; each RPC method then
79enforces `orchestration:read`, `orchestration:operate`, `terminal:operate`,
80`review:write`, or `access:read` as appropriate. Review feedback submission
81currently dispatches an orchestration operation, so clients performing it also
82need `orchestration:operate`. Creating a ticket is not
83authorization 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
95This is intentionally not a general-purpose OAuth authorization server. The
96environment bootstrap token type is private, the bootstrap cookie and WebSocket
97connection-token routes are product-specific adapters, and the API returns its
98typed `HttpApi` errors rather than implementing every OAuth error response
99surface.
100
101## Upgrade Behavior
102
103Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth
104records to scoped records. It deletes existing pairing links and sessions while
105leaving non-authentication environment state unchanged. Upgraded clients must
106pair again; old `owner` or `client` credentials are never silently mapped to new
107capabilities.
108
109## Relay Boundary
110
111Relay-managed tunnels use their own tokens and keys. The relay can reuse scope
112parsing and token-exchange conventions, but an environment access token is not a
113relay 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">
347flowchart 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">
542sequenceDiagram
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">
586sequenceDiagram
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">
619sequenceDiagram
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">
650sequenceDiagram
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
3T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay accepts
4Clerk JWTs only when they are generated from the `t3-relay` template with the shared
5`t3-code-relay` audience.
6
7## Application Keys
8
9T3 Connect is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env`
10or `.env.local` file:
11
12```dotenv
13T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key>
14T3CODE_CLERK_JWT_TEMPLATE=<JWT template name>
15T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID>
16T3CODE_RELAY_URL=https://relay.example.com
17```
18
19The 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
21client configuration should use the canonical names.
22
23Configuration precedence is:
24
251. Process or CI environment variables.
262. Repository-root `.env.local`.
273. Repository-root `.env`.
28
29The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public
30identifiers, not secrets.
31Web, desktop, mobile, and bundled server builds statically inject the values they consume during
32their build step. A built artifact does not need an environment file at runtime. CI release builds
33should 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
35production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS
36environment.
37
38When any client-facing public value is absent, cloud UI is omitted. When the CLI public values are
39absent, the `t3 connect` CLI command group is omitted. The bundled server still accepts runtime
40overrides for self-hosted or operator-managed
41deployments.
42
43For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay
44deployment 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
46deployment 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`
49with 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
51environment or commit it to the repository.
52
53The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference
54that database and provision isolated PlanetScale branches, so deploy `prod` before creating a
55personal developer stage.
56
57## Headless CLI OAuth Application
58
59The `t3 connect` commands authorize a headless environment with a separate Clerk OAuth application.
60This uses an OAuth public client with PKCE, so the CLI stores no client secret.
61
62In **Clerk Dashboard > OAuth applications**:
63
641. Create an OAuth application for the T3 CLI.
652. Enable the **Public** option so authorization-code exchange uses PKCE.
663. Add `http://127.0.0.1:34338/callback` as an allowed redirect URI.
674. Enable the `openid`, `profile`, and `email` scopes.
685. 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
71The 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
73handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link.
74
75The CLI supports these headless operations:
76
77```sh
78t3 connect login
79t3 connect link
80t3 connect status
81t3 connect unlink
82t3 connect logout
83t3 serve
84```
85
86`t3 connect login` opens the Clerk authorization flow and stores the CLI credential without enabling
87cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed,
88authorizes when needed, and records durable intent to expose the environment. It works without a
89running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the
90managed tunnel. `t3 connect unlink` records disabled intent immediately, stops a reachable running
91connector, and attempts to revoke the relay-side environment record. It retains the stored CLI
92authorization so `t3 connect link` can re-enable exposure without another browser flow. `t3 connect
93logout` performs the same cleanup and removes the stored CLI authorization.
94
95The background service has an independent lifecycle. Connect setup may offer to install it, but
96logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`.
97
98The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH,
99forward that port before running `t3 connect login` or `t3 connect link`:
100
101```sh
102ssh -L 34338:127.0.0.1:34338 <host>
103```
104
105A relay-hosted callback broker can remove this port-forward requirement later without changing the
106stored PKCE token model.
107
108## JWT Template
109
110In **Clerk Dashboard > JWT templates**, create a template with:
111
112| Setting | Value |
113| ------- | ---------------------------- |
114| Name | `t3-relay` |
115| Claims | `{ "aud": "t3-code-relay" }` |
116
117Set `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
120is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still
121selects the concrete relay deployment, but changing that URL does not require a JWT template change.
122
123## Desktop OAuth Redirect Allowlist
124
125The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme.
126In **Clerk Dashboard > Native applications**, enable the Native API and add these entries under the
127mobile SSO redirect allowlist:
128
129```text
130t3code-dev://app/
131t3code://app/
132```
133
134Local desktop development uses `t3code-dev://app`, while packaged builds use `t3code://app`. Add the
135matching origin to each Clerk instance's Backend API `allowed_origins` array as well. The development
136Clerk 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,
138external-browser OAuth transport, and callback delivery for initial sign-in and linked-account flows.
139
140There is currently no Dashboard UI for `allowed_origins`. Preserve any existing entries and update
141the instance through the Backend API:
142
143```sh
144curl -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
150Never put `CLERK_SECRET_KEY` in the desktop app, a client-facing environment file, or a build
151artifact.
152
153## Desktop Passkeys
154
155The production macOS bundle ID is `com.t3tools.t3code`. To enable native passkeys:
156
1571. Create an explicit macOS App ID for `com.t3tools.t3code` in the Apple Developer portal and enable
158 **Associated Domains**.
1592. Create a compatible macOS provisioning profile for that App ID and the certificate used to sign
160 the distributed app.
1613. 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.
1634. Confirm Clerk serves `https://<frontend-api>/.well-known/apple-app-site-association` and that
164 `webcredentials.apps` contains `<TEAM_ID>.com.t3tools.t3code`.
1655. Set the local or CI signing configuration described below.
166
167For a local signed build, add these values to `.env.local` or export them before invoking the
168desktop artifact command:
169
170```dotenv
171T3CODE_APPLE_TEAM_ID=ABC1234567
172T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile
173# Optional: comma-separated override when Clerk's RP ID differs from the Frontend API hostname.
174T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com
175```
176
177When `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,
179or 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
182The normal `dev:desktop` launcher is unsigned and cannot complete macOS passkey ceremonies. For
183renderer HMR, build and install a signed app first, run the renderer dev server, then launch the
184installed app executable with `VITE_DEV_SERVER_URL` and `T3CODE_PORT` set. Rebuild the signed app
185after native dependency, main-process, preload, entitlement, provisioning, or signing changes;
186renderer-only changes can reuse the installed app.
187
188For the default development ports, run `pnpm dev:web` in one terminal and launch the installed
189binary from another:
190
191```sh
192VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \
193T3CODE_PORT=13773 \
194 "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)"
195```
196
197After changing Associated Domains, bump the build version before rebuilding; macOS may otherwise
198reuse stale Shared Web Credentials metadata for the same app/version pair.
199
200Verify the installed bundle before testing:
201
202```sh
203codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app"
204codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app"
205```
206
207The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth
208flow uses a custom redirect URI, add that exact URI to the same allowlist.
209
210## Enable Waitlist Access
211
212For a private beta where people should request access, use **Clerk Dashboard > Waitlist**:
213
2141. Toggle on **Enable waitlist** and save.
2152. Review requests on the same page and select **Invite** or **Deny**.
216
217Approved signed-in users manage T3 Connect under **Connections**. The web and desktop sidebars do
218not expose a dedicated account or waitlist control. Signed-out users reach Clerk's waitlist and
219sign-in flow contextually from the T3 Connect controls on the Connections page.
220
221On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the
222Settings 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
224screen.
225
226## Alternative: Known-User Allowlist
227
228For a closed beta where all permitted users are known in advance, use an allowlist instead of a
229request-and-approval waitlist:
230
231To restrict the beta to permitted email addresses or domains:
232
2331. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email
234 domain.
2352. Enable the allowlist and save.
2363. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or
237 manually created without a waitlist request flow.
238
239Do not enable an empty allowlist: it blocks all new sign-ups.
240
241Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud
242access. To remove an already-created user's access, ban that user in Clerk so their active
243sessions 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)
5bun run dev
6
7# Desktop development
8bun run dev:desktop
9
10# Desktop development on an isolated port set
11T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop
12
13# Production
14bun run build
15bun run start
16
17# Build a shareable macOS .dmg (arm64 by default)
18bun run dist:desktop:dmg
19
20# Or from any project directory after publishing:
21npx t3
22```
docs/integrations/source-control-providers.md deleted-127
...@@ -1,127 +0,0 @@
1# Source Control Integrations
2
3T3 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
7T3 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
46The **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
52Run 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
581. Install the GitHub CLI on the machine running T3 Code:
59 ```bash
60 brew install gh
61 ```
622. Sign in:
63 ```bash
64 gh auth login
65 ```
663. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated
67
68That's it—you can now clone, publish, and create pull requests.
69
70### For GitLab
71
721. Install the GitLab CLI:
73 ```bash
74 brew install glab
75 ```
762. Authenticate:
77 ```bash
78 glab auth login
79 ```
803. Check **Settings → Source Control** to confirm the connection
81
82### For Bitbucket
83
84Bitbucket uses API tokens instead of a CLI tool:
85
861. Create an API token in your Atlassian account with read/write access to pull requests and repositories
872. 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 ```
923. Restart T3 Code and verify the connection in **Source Control settings**
93
94### For Azure DevOps
95
961. Install Azure CLI:
97 ```bash
98 brew install azure-cli
99 ```
1002. Add the DevOps extension:
101 ```bash
102 az extension add --name azure-devops
103 ```
1043. 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
6pushes 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`)
19desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. It auto-enables
20signing 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.
22Without the core signing credentials, it still releases unsigned artifacts.
23
24See [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
5The connection runtime is shared by web and mobile. It owns connectivity,
6authentication, retries, transport lifetime, cached environment data, and
7environment-scoped operations.
8
9Web and mobile mount this runtime once at the application root and compose it
10identically: `apps/web/src/connection/runtime.ts` and
11`apps/mobile/src/connection/runtime.ts` differ only in the platform layer they
12supply. 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
31The registry creates one environment-scoped supervisor per environment.
32`acquireSupervisor` serializes access per environment, reuses an existing
33supervisor when the catalog entry is unchanged, and closes and recreates the
34scope when it changed. `createServiceScope` builds an `EnvironmentSupervisor`
35bound to a closeable scope and connects it; `run` and `runStream` execute caller
36effects with that supervisor provided.
37
38`EnvironmentSupervisor` owns desired state, retry scheduling, and the active
39session scope. React components do not create connections, transports, retry
40loops, or RPC clients.
41
42## Connection State
43
44The supervisor is the only retry owner.
45
461. A persisted or platform registration marks an environment as desired.
472. 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.
493. When online, it asks the driver for one prepared connection and one RPC
50 session.
514. 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.
545. Authentication or configuration failures remain blocked until an external
55 wakeup changes the relevant input.
566. An involuntary session close keeps the registration and cache, then retries.
577. Explicit removal closes the session and deletes the registration,
58 credentials, shell cache, and thread cache.
59
60### Wakeups
61
62Wakeup 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
84The UI derives `available`, `offline`, `connecting`, `reconnecting`,
85`connected`, and `error` from supervisor state plus explicit data-sync state.
86It does not infer connection health from cached data or the existence of a
87transport object. An environment becomes `connected` after the socket opens and
88the initial config RPC succeeds, proving that the server is responsive. Shell
89and thread synchronization are independent data states. A healthy RPC transport
90with a failed shell subscription is shown as connected with a synchronization
91error, not as a reconnect that is not actually scheduled.
92
93## Data Boundary
94
95Finite 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
121The Promise bridge exists only at the React/Atom boundary. Runtime and business
122logic remain Effect-native.
123
124## Platform Layers
125
126Web 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
136Platform layers adapt operating-system capabilities. They do not implement
137connection policy. `EnvironmentOwnedDataCleanup` is part of this contract: on
138removal the registry clears its cache and calls the platform implementation, so
139web clears composer drafts and mobile clears drafts plus the thread outbox.
140
141## Source Boundaries
142
143Applications must import explicit package subpaths; the package intentionally
144has no root export. The subpaths are documented in
145[packages/client-runtime/README.md](../../packages/client-runtime/README.md),
146with the `exports` map in that package's `package.json` as the authoritative
147list. Files that are not exported are implementation details.
148
149## Application Boundary
150
151The application root mounts the shared connection layer, creates its own Atom
152runtime, and selects the domain atom factories required by that platform. Web
153and mobile may expose different hooks and features without changing connection
154ownership.
155
156Application code must not construct RPC clients, retry loops, or raw
157orchestration commands. Persistence paths belong to the platform registration
158and cache stores, with explicit migration or invalidation policy.
159
160## Verification
161
162Core state-machine tests use `@effect/vitest` and deterministic service layers.
163Required 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
5The environment server and the relay use separate credentials, issuers, and trust
6boundaries. They intentionally use a similar OAuth-shaped model so that permission
7checks and token exchange behavior can be audited against established concepts.
8
9## Authorization Model
10
11Environment authorization is capability-based. A session carries zero or more
12OAuth-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
25Ordinary pairing links grant the four client-operation scopes and read access to
26managed relay connectivity:
27`orchestration:read orchestration:operate terminal:operate review:write relay:read`.
28The desktop bootstrap credential and command-line administrative bootstrap
29credentials 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
36browser session cookie. The cookie is an HTTP transport adapter for the same
37scoped session model; the response never exposes the session secret to browser
38JavaScript.
39
40### Bearer Access Token
41
42Non-browser clients use `POST /oauth/token` with an
43`application/x-www-form-urlencoded` body:
44
45```text
46grant_type=urn:ietf:params:oauth:grant-type:token-exchange
47subject_token=<bootstrap credential>
48subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap
49requested_token_type=urn:ietf:params:oauth:token-type:access_token
50scope=orchestration:read orchestration:operate terminal:operate review:write relay:read
51```
52
53Clients may additionally submit `client_label`, `client_device_type`, and
54`client_os` extension parameters so the authorized-clients UI can identify the
55device that established the session. These are presentation hints only; the
56environment derives transport metadata such as IP address and user agent from
57the request and does not use these fields for authorization.
58
59The 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
71Sessions 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
73only to DPoP-bound exchanges, where the token is additionally constrained by a
74proof key. See `SessionStore.ts` and `EnvironmentAuth.ts`.
75
76Requested scopes must be a subset of the one-time bootstrap credential grant.
77An ordinary paired client therefore cannot exchange its grant for
78`access:read`, `access:write`, or `relay:write`.
79
80### DPoP-Bound Access Token
81
82The same `/oauth/token` exchange supports proof-of-possession tokens. A client
83that sends a `DPoP` header has its proof verified by `verifyRequestDpopProof`;
84the resulting JWK thumbprint is stored on the session, which is then issued with
85method `dpop-access-token` and a one-hour TTL instead of the bearer default. An
86invalid proof gets a DPoP challenge header and a credential error rather than a
87bearer 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
92assume it. Relay-brokered clients use this mode so that a leaked token cannot be
93replayed without the corresponding key.
94
95### WebSocket Ticket
96
97`POST /api/auth/websocket-ticket` accepts any authenticated session and returns
98a short-lived, single-purpose WebSocket ticket, issued through
99`EnvironmentAuth.issueWebSocketTicket` with a five-minute default TTL. The
100client presents its bearer or DPoP credential in headers to get the ticket, then
101appends only that ticket to the socket URL as `wsTicket`. This keeps long-lived
102tokens and browser cookies out of WebSocket URLs while letting the handshake
103authenticate.
104
105The 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
109an orchestration operation, so clients performing it also need
110`orchestration:operate`. Creating a ticket is not authorization to call every
111RPC 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
123This is intentionally not a general-purpose OAuth authorization server. The
124environment bootstrap token type is private, the bootstrap cookie and WebSocket
125connection-token routes are product-specific adapters, and the API returns its
126typed `HttpApi` errors rather than implementing every OAuth error response
127surface.
128
129## Upgrade Behavior
130
131Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth
132records to scoped records. It deletes existing pairing links and sessions while
133leaving non-authentication environment state unchanged. Upgraded clients must
134pair again; old `owner` or `client` credentials are never silently mapped to new
135capabilities.
136
137## Relay Boundary
138
139Relay-managed tunnels use their own tokens and keys. The relay can reuse scope
140parsing and token-exchange conventions, but an environment access token is not a
141relay 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
5This 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
21The 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
25The 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
29A 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
35The 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
39A 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
43A 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
47Orchestration 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
51The 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
55A 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].
56Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`.
57
58#### Domain Event
59
60A 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.
61Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`.
62
63#### Decider
64
65The 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
69A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10].
70
71#### Projector
72
73The 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
77The 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
81A 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
85A 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
93The 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
97The 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
101The 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
105The 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
109The agent interaction style for a thread. In [the contracts][1], the values are `default` and `plan`.
110
111#### Assistant delivery mode
112
113Controls 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
117A 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
121Checkpointing 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
125A 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
129The 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
133The 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
137The 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
141The 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
5T3 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
7execution boundary: every provider process, terminal, git operation, and filesystem read happens
8there, 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
33The client/server contract is an Effect RPC group, not a hand-rolled push protocol. [`rpc.ts`][rpc]
34declares `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
37be a broadcast push bus: a client subscribes to what it needs and the server pushes only on that
38subscription.
39
40[`ws.ts`][ws] serves the group. `websocketRpcRouteLayer` mounts `GET /ws`, authenticates the upgrade
41through `EnvironmentAuth.authenticateWebSocketUpgrade`, then hands the socket to
42`RpcServer.toHttpEffectWebsocket`. Authorization is per method: `RPC_REQUIRED_SCOPE` maps each method
43to a scope, and `authorizeEffect`/`authorizeStream` enforce it. Holding a valid socket is not
44authorization to call everything on it. See [environment-auth.md](./environment-auth.md).
45
46On 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`,
48and `closed`. It performs one attempt and does not retry. Retry, backoff, and offline policy belong
49to the connection supervisor.
50
51## Shared client runtime
52
53`packages/client-runtime` holds every non-visual client concern: connection lifecycle,
54authentication, RPC, cached environment data, and domain state as Atom factories. Web and mobile
55compose 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
57background-activity layers) and differ beyond that only in the platform layer they supply and the
58UI they build on top. React components never construct transports, retry loops,
59or RPC clients. See [connection-runtime.md](./connection-runtime.md).
60
61## Orchestration is event-sourced
62
63The server does not mutate app state directly. Clients dispatch typed commands; the engine turns them
64into 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
68processing is totally ordered. For each envelope `processEnvelope`:
69
701. checks the durable command receipt, making retries idempotent;
712. runs `decideOrchestrationCommand` ([`decider.ts`][decider]) to produce events from command plus
72 current state, pure and side-effect free;
733. 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;
764. after commit, swaps in the new read model and publishes committed events to subscribers.
77
78Because persistence and projection share a transaction, the read model cannot durably disagree with
79the event log. On dispatch failure the engine rereads persisted events past the starting sequence and
80reconciles.
81
82Command and event names live in [`orchestration.ts`][contracts]. Some commands are client
83dispatchable (`thread.create`, `thread.turn.start`, `thread.approval.respond`); others are internal
84and produced only by server-side reactors (`thread.message.assistant.delta`,
85`thread.turn.diff.complete`).
86
87A turn is complete when its session leaves `running` status, projected by
88`settledTurnStateForSessionStatus` in [`projector.ts`][projector]. Checkpoint work settling later
89does not define turn end.
90
91## Drainable workers
92
93Follow-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
100count reaches zero, so a test can await "queue empty and current item finished" instead of sleeping.
101Each of the three services exposes `drain` for exactly this.
102
103Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in
104[`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not
105build production behavior on receipts.
106
107## Provider drivers
108
109Five drivers ship built in, registered in [`builtInDrivers.ts`][drivers] as `BUILT_IN_DRIVERS`:
110Codex, Claude, Cursor, Grok, and OpenCode. A driver declares its kind and config schema and creates a
111scoped adapter; `ProviderInstanceRegistry` owns live instances and `ProviderAdapterRegistry` resolves
112an instance to its adapter, so `ProviderService` routes session and turn operations without knowing
113which agent is behind them. See [providers.md](./providers.md).
114
115## Checkpointing
116
117Each turn is bracketed by workspace checkpoints so diffs and reverts are exact. `CheckpointStore`
118captures state as hidden Git refs through the VCS driver's checkpoint operations;
119`CheckpointDiffQuery` answers turn and full-thread diff requests; `CheckpointReactor` coordinates
120baseline capture, completed-turn capture, diff projection, and reverting both the workspace and the
121provider 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
127reactors; publish welcome; signal command readiness (logged as `Accepting commands`); wait for the
128HTTP listener via `markHttpListening`; publish ready; fork the heartbeat; then either print headless
129output or open the browser. Command readiness precedes the listener, so a socket that opens can
130already 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
5A provider is the agent runtime that does the actual work. T3 Code supports several, and the
6orchestration 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
20Each driver declares its `driverKind`, a `configSchema`, and a `create` function that builds an
21adapter 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
24transport, config, and event shapes are mapped.
25
26## Registry and routing
27
28Two 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
37directory to route session and turn operations for a thread, so callers name a thread, not an agent.
38
39Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No
40orchestration, contract, or client change is required for the common case.
41
42## How provider work is requested
43
44Clients 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
49setters `thread.runtime-mode.set` and `thread.interaction-mode.set`.
50
51The engine persists an event for the command, and a server-side reactor performs the provider call.
52Provider 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
58Provider work flows through three queue-backed workers. All three are built with
59`makeDrainableWorker` from [`DrainableWorker.ts`][worker] and expose `drain` for deterministic test
60synchronization.
61
621. [`ProviderRuntimeIngestion`][ingest] consumes provider runtime streams and emits orchestration
63 commands.
642. [`ProviderCommandReactor`][cmd] reacts to orchestration intent events and dispatches provider
65 calls.
663. [`CheckpointReactor`][checkpoint] captures workspace checkpoints on turn start and completion, and
67 performs reverts.
68
69### Buffered assistant delivery
70
71A thread in `buffered` assistant delivery mode accumulates assistant text instead of streaming each
72delta. 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
74spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries,
75when 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
5Remote environments are shipped, not planned. Direct, bearer-paired, relay-tunneled, Tailscale, and
6desktop-managed SSH access all exist today. This document describes the model they share and where
7each piece lives. For the user-facing setup guide see
8[remote access](../user/remote-access.md).
9
10## The model
11
12T3 has one runtime boundary: a client talks to a T3 server over HTTP and WebSocket, and the server
13owns orchestration, providers, terminals, git, and filesystem operations. Remoteness is expressed at
14the 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
37One running T3 server instance. It owns provider availability and auth, model availability, projects
38and threads, terminal processes, filesystem access, git operations, and server settings.
39
40It is identified by a stable `environmentId`, persisted by the server at `<stateDir>/environment-id`
41and generated on first start (`apps/server/src/environment/ServerEnvironment.ts`). Desktop, mobile,
42and web all reason about the same concept.
43
44### Known environments and connection targets
45
46A saved client-side entry for an environment the client knows how to reach. It is not
47server-authored; it is local to a device or client profile. In the hosted web app these entries are
48browser-local. A hosted pairing URL can create one, but it does not give the hosted app a server-side
49control 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
60Bearer, relay, and SSH are persisted; primary is platform-managed. Note that Tailscale is not a
61separate 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
63host plus pairing code. Tailscale is an endpoint provider and transport, not a distinct runtime
64concept.
65
66### AdvertisedEndpoint
67
68A server- or desktop-authored candidate endpoint for an environment: a concrete HTTP and WebSocket
69base URL pair, a default/available/unavailable marker, reachability hints (loopback, LAN, private,
70public, tunnel), and compatibility hints such as whether the hosted HTTPS app can use it.
71
72Clients treat advertised endpoints as hints, not proof that a route works from the current device.
73The connection attempt decides.
74
75The UI shows one default endpoint in the network-access summary and keeps the rest behind an advanced
76list. `selectPairingEndpoint` in
77[`ConnectionsSettings.tsx`](../../apps/web/src/components/settings/ConnectionsSettings.tsx) excludes
78unavailable endpoints and then picks, in order:
79
801. the saved `defaultEndpointKey` override;
812. the first endpoint marked `isDefault`;
823. the first endpoint whose reachability is not `loopback`;
834. the first endpoint compatible with the hosted HTTPS app;
845. otherwise nothing.
85
86There is no unconditional loopback fallback. A loopback endpoint only wins through an explicit saved
87override or `isDefault`. Persist the override by stable endpoint kind rather than raw URL where
88possible, since LAN addresses change with networks; Tailscale endpoints use provider-specific stable
89keys (`tailscale-ip:`, `tailscale-magicdns:`).
90
91### Endpoint providers
92
93Endpoint providers contribute advertised endpoints without becoming part of the core environment
94model: core owns environments, pairing, and connection lifecycle, and providers return normalized
95`AdvertisedEndpoint` records.
96
97Tailscale is the first provider, and T3 manages more than discovery. When `tailscaleServeEnabled` is
98set, 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)).
101Endpoint identifiers are synthesized in `apps/desktop/src/backend/tailscaleEndpointProvider.ts` with
102`private-network` reachability.
103
104### Hosted pairing request
105
106A hosted pairing request is a bootstrap URL for the static web app, not a transport:
107
108```text
109https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE
110```
111
112The hosted app reads `host`, takes the token from the URL hash, exchanges it directly with that
113backend, strips the token from browser history, and saves the environment record locally. Helpers
114live in [`shared/remote.ts`](../../packages/shared/src/remote.ts) (`setPairingTokenOnUrl`,
115`getPairingTokenFromUrl`, `stripPairingTokenFromUrl`) and `apps/web/src/hostedPairing.ts`.
116
117Constraints:
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
128and correlation only, never for routing. `Project` remains environment-local: a local clone and a
129remote clone are different projects that may share a `RepositoryIdentity`, and threads bind to one
130project in one environment.
131
132## Access methods
133
134Access answers one question: how does the client speak WebSocket to a T3 server? It does not answer
135how 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.
140It works for desktop, mobile, and web with no client-side process management. Browser security rules
141are part of it: a hosted HTTPS client cannot connect to plain `ws://` or `http://` LAN backends.
142
143### Relay-tunneled access
144
145Managed T3 Connect relay tunnels use `RelayConnectionTarget` and are the answer when the host is
146behind NAT, inbound ports are unavailable, or mobile must reach a desktop-hosted environment. From
147the client's perspective this is still an ordinary WebSocket connection; the route is mediated. The
148relay Worker only brokers credentials and a managed endpoint; application traffic then flows over
149the provisioned Cloudflare tunnel hostname for the life of the connection, not through the relay
150Worker itself. See [t3-connect.md](./t3-connect.md).
151
152### Tailscale access
153
154A T3-managed `tailscale serve` mapping exposes the server on the tailnet over HTTPS, and the
155resulting private-network endpoints are advertised for pairing. Connection then follows the ordinary
156bearer path.
157
158### Desktop-managed SSH access
159
160SSH 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
163hosts, 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
165server, opens a local tunnel, checks HTTP readiness, optionally issues a remote pairing token, and
166returns local HTTP/WS endpoints. Disconnect closes the tunnel and stops the remote server if the
167launcher started it; a server that was already running (marked `external`) is left running.
168
169The desktop main process owns this because it can spawn SSH, manage prompts, write launch scripts,
170and clean up forwards. The renderer connects through the forwarded URL like any other environment and
171needs no SSH-specific RPC path.
172
173Failure handling is explicit: SSH auth failure surfaces before an environment is saved, remote launch
174failure includes launcher output where available, forwarded-port failure leaves the environment
175disconnected rather than falling back to an unrelated endpoint, and reconnect restores the SSH bridge
176before reconnecting the WebSocket client.
177
178## Launch methods
179
180Launch answers a different question: how does a T3 server come to exist on the target machine? Keep
181it 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
193The same `ExecutionEnvironment` can be reached several of these ways. Only the launch and access
194paths differ.
195
196## Security model
197
198Some environments are reachable over untrusted networks, so remote-capable environments require
199explicit authentication, tunnel exposure never relies on obscurity, and saved endpoints carry enough
200auth metadata to reconnect safely.
201
202WebSocket authentication is a dedicated short-lived ticket, not a token in a query string. The client
203presents 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
205returned ticket as `wsTicket` on the socket URL. The server issues it through
206`EnvironmentAuth.issueWebSocketTicket`; tickets are tagged `kind: "websocket"` and default to a
207five-minute TTL (`DEFAULT_WEBSOCKET_TOKEN_TTL` in `apps/server/src/auth/SessionStore.ts`). The
208handshake verifies the ticket, and each RPC method still enforces its own scope. See
209[environment-auth.md](./environment-auth.md).
210
211Hosted pairing is a client-side convenience only. The hosted app must not receive pairing tokens
212through query parameters, must not store pairing state server-side, and must not imply that an HTTP
213backend is reachable from an HTTPS browser context.
214
215## Version coordination
216
217Remote environments stay online while clients move to newer releases. The environment descriptor
218carries the running server version and may advertise a safe replacement path, so the UI can show the
219right action without making the transport responsible for process management. The connection
220supervisor owns the resulting disconnect and reconnect like any other involuntary close. See
221[server-updates.md](./server-updates.md).
222
223## Future work
224
225These 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
5Status: implemented
6
7## Purpose
8
9Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset`
10subprocess probes with two persistent, direct data sources:
11
121. a standalone Rust resource-monitor executable that reads process counters
13 through operating-system APIs via `sysinfo`;
142. Electron main-process APIs for Electron process metrics and host power state.
15
16The native monitor owns bounded in-memory history. The server only merges and
17summarizes that history when diagnostics requests it. Telemetry history is not
18persisted to disk or continuously copied into Node.
19
20## Why a standalone executable
21
22The 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
32The cost is one persistent child process and NDJSON serialization. That is a
33better failure boundary than repeatedly spawning shell utilities or loading
34native code into Node.
35
36## Runtime topology
37
38### Desktop
39
40```text
41Electron main
42 ├─ powerMonitor
43 ├─ app.getAppMetrics() while diagnostics is open
44 ├─ inherited fd 4, telemetry NDJSON ─────────────┐
45 └─ inherited fd 5, demand-control NDJSON ◀──────┤
46 ▼
47Node 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
56Electron telemetry is unavailable. The native monitor still runs beside the
57server and tracks the server process tree. Power fields degrade to `unknown`
58instead of invoking platform shell commands.
59
60### WSL backend limitation
61
62Windows desktop packages currently ship the Windows resource-monitor executable.
63That executable cannot run inside the Linux WSL backend, so a WSL-only backend
64does not receive `resourceMonitorPath` and reports native process telemetry as
65unavailable. Electron host-power telemetry remains available over the inherited
66desktop pipe. Supporting native WSL process telemetry requires publishing a
67Linux sidecar for each supported architecture in the Windows artifact and
68converting its packaged path into the selected distro; the configuration
69deliberately does not pass the Windows `.exe` into WSL as a false fallback.
70
71## Native monitor
72
73The executable lives in `native/resource-monitor`.
74
75It receives schema-compatible commands on stdin and emits one JSON object per
76line 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
90The protocol version is defined by
91`RESOURCE_MONITOR_PROTOCOL_VERSION` in
92`packages/contracts/src/resourceTelemetry.ts`.
93
94### Collection
95
96The monitor keeps one `sysinfo::System` instance and refreshes it at the
97power-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
106On Linux, task/thread enumeration is disabled. Command lines are loaded only
107when 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
112Each sample scans the accessible process table, builds the PID/PPID graph, and
113retains:
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
122Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native
123start times are matched with a two-second tolerance because native start times
124can have coarser platform resolution.
125
126The process list is emitted in depth-first tree order so renderer collapse and
127expansion preserves complete subtrees.
128
129### Native history and streaming
130
131Every native sample is appended to a one-hour in-memory ring bounded to 3,600
132snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes.
133History stays in the sidecar until a `readHistory` request and is returned in
134bounded chunks. The first bound reached wins, so high process counts or large
135process names and command lines shorten the effective history window.
136
137Periodic snapshot streaming is disabled by default. The server enables it only
138while at least one diagnostics subscription is retained. `sampleNow` remains
139available for explicit refreshes and identity validation.
140
141The 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
151This 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
159Those deeper tracing systems can be added later as opt-in diagnostic modes
160without changing the public `ResourceTelemetry` model.
161
162## I/O semantics
163
164The monitor preserves platform semantics instead of presenting all counters as
165equivalent:
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
172The UI therefore labels these values as I/O reads and writes and exposes the
173per-process `ioSemantics` value.
174
175Group totals are observed deltas since telemetry startup. Per-process total
176columns are the operating system's cumulative counters for that process.
177
178## Electron telemetry
179
180Electron main owns `DesktopTelemetryPublisher`.
181
182Power events trigger an immediate snapshot. While diagnostics is closed, the
183server sends the configured active and idle host-power intervals to Electron
184(30 seconds and 2 minutes in the balanced profile). During those heartbeats
185Electron 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
193live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while
194locked, suspended, or thermally constrained.
195
196It 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
204Suspension stays latched across event-driven snapshots. An explicit resume
205clears it immediately; a periodic heartbeat also clears a stale latch so a
206missed resume event cannot leave telemetry permanently constrained.
207
208Electron does not expose a cross-platform low-power-mode getter, so that field
209remains `unknown`.
210
211The 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
217These are private Electron-main/server pipes. They do not use the renderer
218WebSocket and are recreated for every backend restart.
219
220## Server Effect services
221
222The implementation is under `apps/server/src/resourceTelemetry`.
223
224### `ResourceMonitorBinary`
225
226Resolves an executable from:
227
2281. `T3CODE_RESOURCE_MONITOR_PATH`;
2292. desktop bootstrap configuration;
2303. bundled CLI resources;
2314. local Cargo build outputs.
232
233Unsupported platforms, missing binaries, and non-executable binaries use
234schema-backed tagged errors with descriptive messages.
235
236### `NativeTelemetryClient`
237
238Owns 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
253Snapshot sequence numbers are scoped to a monitor generation. Server ingestion
254uses the monitor restart count as the generation key, so sequence reset after a
255restart cannot freeze telemetry.
256
257### `DesktopTelemetryReceiver`
258
259Reads fd 4, decodes schema-validated messages, stores the latest Electron
260snapshot, and publishes desktop health. It writes diagnostics demand to fd 5
261and gives the first sample a 90-second startup deadline. Once samples arrive,
262the stale deadline stays beyond the slower configured host-power interval with
26330 seconds of scheduling grace, so intentional 2–10 minute idle polling does
264not oscillate the policy between constrained and unconstrained states. Decode
265errors, protocol mismatch, control-write failure, stream failure, stale input,
266and normal stream closure are represented explicitly.
267
268### `ResourceTelemetry`
269
270Merges 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
284Electron and monitor processes are visible but are not valid targets for the
285existing process-signal RPC.
286
287### History projection
288
289`ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native
290snapshots to derive rates, lifecycle counters, buckets, and process summaries.
291Current Electron process metrics are intentionally excluded from historical
292replay so they cannot overwrite older native CPU or memory samples.
293
294### `ResourceAttribution`
295
296Tracks known logical application I/O separately from OS counters. Current
297integration points record successful writes for:
298
299- provider native and canonical event logs;
300- the local server trace sink.
301
302Entries contain component, operation, logical bytes, count, and elapsed time.
303Future persistence paths should call `ResourceAttribution.record` rather than
304adding diagnostics-specific counters.
305
306## Background policy integration
307
308`HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host
309power does not retain live resource diagnostics or invoke shell probes.
310
311The monitor updates its latest timestamp on every Electron sample but only
312publishes semantic state changes. Increasing idle seconds alone does not cause a
313background-policy broadcast every second.
314
315## Public API and UI
316
317The WebSocket RPC surface provides:
318
319- current snapshot;
320- bounded history;
321- explicit monitor retry;
322- a live snapshot subscription.
323
324The 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
335Legacy process diagnostics RPCs are projected from the same service so they no
336longer start recurring process-table commands.
337
338## Packaging
339
340Desktop artifact builds compile the Rust target, stage it as
341`resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the
342backend bootstrap.
343
344CLI release jobs upload each active platform monitor artifact and copy it into:
345
346```text
347apps/server/dist/resource-monitor/<platform>-<arch>/
348```
349
350The published server package already includes `dist`, so those executables ship
351with the CLI. Missing platform artifacts degrade native telemetry to
352`unavailable`; the server continues running.
353
354## Resource and failure behavior
355
356Steady 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
367The diagnostics page exposes the monitor's own process resource usage and
368collection duration so the observer's cost is measurable.
369
370Failures 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
381High-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
391These additions should preserve the current rules: direct platform APIs,
392schema-validated boundaries, explicit metric semantics, bounded retention, and
393no 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
7T3 Code uses [Vite+](https://viteplus.dev/guide/). Install the global `vp` command, install
8dependencies, then start the dev stack:
9
10```bash
11curl -fsSL https://vite.plus | bash # Windows: irm https://vite.plus/ps1 | iex
12vp i
13vp run dev
14```
15
16Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to
17Node 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
20authenticated.
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
98from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the
99server, allowing the same bundle to work from localhost or a tailnet hostname.
100
101## Running multiple dev instances
102
103Worktrees 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
109Offset resolution, in order:
110
1111. `T3CODE_PORT_OFFSET`, which must be a non-negative integer. Negative values are rejected.
1122. `T3CODE_DEV_INSTANCE`. An all-digit value is used directly as the offset; any other non-empty
113 value is hashed into one.
1143. The worktree path hash.
115
116Collision scanning depends on the mode. `dev:web` scans only the web port and shifts only the web
117offset. `dev:server` scans only the server port. `dev` and `dev:desktop` scan both and shift them
118together as one shared offset. Explicit server or dev-URL overrides remove the corresponding port
119from 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
5T3 Code can update a connected server to the exact version of the client that detected version
6drift. This path exists primarily for remote environments, where the user may not have a terminal
7open on the server machine.
8
9The 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`
20after loading server config.
21
22The optional capability is intentionally backward compatible. An older server does not know about
23the field, so a missing value means the client must offer a manual relaunch instead of sending an
24unknown RPC.
25
26The 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
31Both surfaces target the client's exact version. When the reconnected server reports that version,
32the mismatch and action disappear.
33
34The operation state lives in `packages/client-runtime`, keyed by environment. Both web surfaces read
35the same `downloading`, `installing`, or `resuming` state, so route changes do not own or cancel the
36operation.
37
38## Capability Selection
39
40The 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
49Desktop ownership takes precedence over process-shape detection. A desktop-managed backend must
50never spawn a second CLI server beside the app-owned process. Likewise, a process launched by an
51unrecognized systemd unit does not claim the foreground respawn path because its supervisor could
52bring the old version back.
53
54## Update Flow
55
56```mermaid
57flowchart 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
77Both update RPCs require the environment's `orchestration:operate` authorization scope. Their
78payload 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
80new client can still repair skew with an older server.
81
82The 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
84successfully. Boot-service setup and self-update share the same process-wide installation lock, so
85they cannot mutate a pinned runtime concurrently.
86
87Before any restart, the current Node executable runs the replacement with `--version`. A failed
88install, failed preflight, or wrong reported version leaves the current server running.
89
90Candidate cleanup is narrower than "any failed preflight". The candidate runtime is removed only when
91the preflight process actually completes and reports a bad exit code or the wrong version: that is
92the case where a completed npm install produced an unusable tree, so retrying the same version must
93perform a clean install rather than reuse it. If the preflight cannot run at all, for example a spawn
94error or the `PREFLIGHT_TIMEOUT` elapsing, the update fails before reaching cleanup and the candidate
95directory is left in place.
96
97## Host Service Lifecycle
98
99The 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
101reconcile the unit through `BootService`; running `npx t3@latest service update` therefore pins and
102activates the latest CLI release without requiring a connected client.
103
104The `t3 connect` onboarding flow may offer service installation, but it calls the same reconciliation
105operation as `t3 service install`. Connect logout only disables cloud access and clears its
106authorization; it does not uninstall the host service.
107
108## Process Handoff
109
110For `boot-service`, the server atomically rewrites the T3-managed user unit to point at the verified
111runtime and reloads systemd. It acknowledges the handoff, then restarts the unit after the same
112short grace period used by foreground respawn. A rejected deferred restart restores the previous
113unit and is logged by the still-running process.
114
115For `respawn`, the server starts a detached, delayed replacement that replays the original CLI
116arguments. It then acknowledges the request and schedules the current process to exit. The delays
117give the acknowledgement time to cross direct or relayed connections before the socket closes.
118
119Progress-capable servers emit `downloading` before installing the pinned runtime and `installing`
120before preflight and handoff. A terminal stream event acknowledges that restart is scheduled. The
121client then enters `resuming`, waits for the replacement lifecycle stream to publish `ready` with
122the target version, and only then completes the operation. It watches for the intentional
123disconnect's first backoff state and requests one fresh retry, which clears historical backoff debt
124without adding a separate reconnect loop.
125
126## Release Invariant
127
128The exact client version must exist as the `t3` npm package before a client carrying that version is
129published. The release workflow therefore makes the GitHub release depend on CLI publication, and
130the 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">
347flowchart 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">
542sequenceDiagram
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">
586sequenceDiagram
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">
619sequenceDiagram
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">
650sequenceDiagram
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
5T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay verifies
6two 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
9verification (`acceptsToken: "oauth_token"`), so the CLI's OAuth credential works without a JWT
10template.
11
12For the wider system diagram, see
13[t3-code-connect-auth-flow.html](./t3-code-connect-auth-flow.html).
14
15## Application Keys
16
17T3 Connect is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env`
18or `.env.local` file:
19
20```dotenv
21T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key>
22T3CODE_CLERK_JWT_TEMPLATE=<JWT template name>
23T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID>
24T3CODE_RELAY_URL=https://relay.example.com
25```
26
27The 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
29client configuration should use the canonical names.
30
31Configuration precedence is:
32
331. Process or CI environment variables.
342. Repository-root `.env.local`.
353. Repository-root `.env`.
36
37The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public
38identifiers, not secrets.
39Web, desktop, mobile, and bundled server builds statically inject the values they consume during
40their build step. A built artifact does not need an environment file at runtime. CI release builds
41should 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
43production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS
44environment.
45
46When any client-facing public value is absent, cloud UI is omitted. The `t3 connect` command group is
47always registered: when the CLI public values are absent, `makeCli` in `apps/server/src/bin.ts`
48registers a hidden fallback `connect` command that reports the missing configuration instead of
49silently vanishing from help. The bundled server still accepts runtime overrides for self-hosted or
50operator-managed deployments.
51
52For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay
53deployment 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
55deployment 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`
58with 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
60environment or commit it to the repository.
61
62The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference
63that database and provision isolated PlanetScale branches, so deploy `prod` before creating a
64personal developer stage.
65
66## Headless CLI OAuth Application
67
68The `t3 connect` commands authorize a headless environment with a separate Clerk OAuth application.
69This uses an OAuth public client with PKCE, so the CLI stores no client secret.
70
71In **Clerk Dashboard > OAuth applications**:
72
731. Create an OAuth application for the T3 CLI.
742. Enable the **Public** option so authorization-code exchange uses PKCE.
753. 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.
814. Enable the `openid`, `profile`, and `email` scopes.
825. 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
85The 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
87handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link.
88
89The connect command group is:
90
91```sh
92t3 connect # default: onboarding
93t3 connect login
94t3 connect link # --publish-only
95t3 connect status # --json
96t3 connect publish # --disable
97t3 connect unlink
98t3 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
104cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed,
105authorizes when needed, and records durable intent to expose the environment. It works without a
106running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the
107managed tunnel. `t3 connect unlink` records disabled intent immediately, stops a reachable running
108connector, and attempts to revoke the relay-side environment record. It retains the stored CLI
109authorization so `t3 connect link` can re-enable exposure without another browser flow. `t3 connect
110logout` performs the same cleanup and removes the stored CLI authorization.
111
112The background service has an independent lifecycle. Connect setup may offer to install it, but
113logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`.
114
115### Headless and SSH authorization
116
117The loopback OAuth callback listener binds to port `34338`. That path only works when a browser on
118the same machine can reach it, so `authorizeCli` in `apps/server/src/cli/connect.ts` automatically
119selects 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
121and accepts a pasted authorization code, so no port is involved.
122
123Port forwarding is therefore optional, not required. Forward the port only if you specifically want
124the loopback flow over SSH:
125
126```sh
127ssh -L 34338:127.0.0.1:34338 <host>
128```
129
130## JWT Template
131
132In **Clerk Dashboard > JWT templates**, create a template with:
133
134| Setting | Value |
135| ------- | ---------------------------- |
136| Name | `t3-relay` |
137| Claims | `{ "aud": "t3-code-relay" }` |
138
139Set `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
142is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still
143selects the concrete relay deployment, but changing that URL does not require a JWT template change.
144
145## Desktop OAuth Redirect Allowlist
146
147The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme.
148In **Clerk Dashboard > Native applications**, enable the Native API and add these entries under the
149mobile SSO redirect allowlist:
150
151```text
152t3code-dev://app/
153t3code://app/
154```
155
156Local desktop development uses `t3code-dev://app`, while packaged builds use `t3code://app`. Add the
157matching origin to each Clerk instance's Backend API `allowed_origins` array as well. The development
158Clerk 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,
160external-browser OAuth transport, and callback delivery for initial sign-in and linked-account flows.
161
162There is currently no Dashboard UI for `allowed_origins`. Preserve any existing entries and update
163the instance through the Backend API:
164
165```sh
166curl -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
172Never put `CLERK_SECRET_KEY` in the desktop app, a client-facing environment file, or a build
173artifact.
174
175## Desktop Passkeys
176
177The production macOS bundle ID is `com.t3tools.t3code`. To enable native passkeys:
178
1791. Create an explicit macOS App ID for `com.t3tools.t3code` in the Apple Developer portal and enable
180 **Associated Domains**.
1812. Create a compatible macOS provisioning profile for that App ID and the certificate used to sign
182 the distributed app.
1833. 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.
1854. Confirm Clerk serves `https://<frontend-api>/.well-known/apple-app-site-association` and that
186 `webcredentials.apps` contains `<TEAM_ID>.com.t3tools.t3code`.
1875. Set the local or CI signing configuration described below.
188
189For a local signed build, add these values to `.env.local` or export them before invoking the
190desktop artifact command:
191
192```dotenv
193T3CODE_APPLE_TEAM_ID=ABC1234567
194T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile
195# Optional: comma-separated override when Clerk's RP ID differs from the Frontend API hostname.
196T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com
197```
198
199When `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,
201or 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
204The normal `dev:desktop` launcher is unsigned and cannot complete macOS passkey ceremonies. For
205renderer HMR, build and install a signed app first, run the renderer dev server, then launch the
206installed app executable with `VITE_DEV_SERVER_URL` and `T3CODE_PORT` set. Rebuild the signed app
207after native dependency, main-process, preload, entitlement, provisioning, or signing changes;
208renderer-only changes can reuse the installed app.
209
210For the default development ports, run `pnpm dev:web` in one terminal and launch the installed
211binary from another:
212
213```sh
214VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \
215T3CODE_PORT=13773 \
216 "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)"
217```
218
219After changing Associated Domains, bump the build version before rebuilding; macOS may otherwise
220reuse stale Shared Web Credentials metadata for the same app/version pair.
221
222Verify the installed bundle before testing:
223
224```sh
225codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app"
226codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app"
227```
228
229The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth
230flow uses a custom redirect URI, add that exact URI to the same allowlist.
231
232## Sign-in Surfaces
233
234Signed-in users manage T3 Connect under **Connections**. The settings sidebar also has dedicated
235controls, 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.
238Desktop renders the same web bundle, so it has them too. The waitlist enrollment flow from the
239private beta was removed when Connect went GA; sign-up is open unless a Clerk restriction below is
240enabled.
241
242## Restricting Sign-ups: Known-User Allowlist
243
244For a closed deployment where all permitted users are known in advance, restrict sign-up to
245permitted email addresses or domains:
246
2471. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email
248 domain.
2492. Enable the allowlist and save.
2503. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or
251 manually created.
252
253Do not enable an empty allowlist: it blocks all new sign-ups.
254
255Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud
256access. To remove an already-created user's access, ban that user in Clerk so their active
257sessions 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
5A pnpm workspace driven by [vite-plus](https://vite.plus) (`vp`). See [scripts.md](./scripts.md) for
6the 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
60no 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
62are 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
3Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`.
4
5Refactor Method:
6
7```ts
8// Old
9function old () {
10 return Effect.gen(function* () {
11 ...
12 });
13}
14
15const old2 = () => Effect.gen(function* () {
16 ...
17});
18```
19
20```ts
21// New
22const 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
32Effect.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,5 +1,7 @@
1# Mobile app-store screenshot harness1# Mobile app-store screenshot harness
22
3> For maintainers. Using T3 Code? See [docs/user](../user/).
4
3The screenshot harness runs the real mobile application against three disposable local T35The screenshot harness runs the real mobile application against three disposable local T3
4environments. It creates an isolated base directory and server for each environment, real Git6environments. It creates an isolated base directory and server for each environment, real Git
5projects with deterministic content, seeded orchestration projections, and persisted terminal7projects 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,35 +46,44 @@ delay allows native terminal and Git review data to finish rendering.
44A full capture regenerates the selected native project with Expo's clean development prebuild before46A full capture regenerates the selected native project with Expo's clean development prebuild before
45building it. Use --skip-build for repeated captures after the first build.47building it. Use --skip-build for repeated captures after the first build.
4648
47The harness uses its own Metro port (8199 by default), so an ordinary mobile server or another49The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is
48worktree cannot accidentally provide the bundle being photographed.50shared across every checkout. The readiness check only verifies that the port is open; it does not
51verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore
52collide or attach to the wrong Metro process.
53
54Every configured device defaults to dark appearance, so plain `pnpm screenshots:mobile` produces
5530 dark PNGs. Pass `--appearance light`, `--appearance dark`, or `--appearance both` to override the
56configured appearance; `both` produces 60 PNGs.
4957
50The default matrix is:58The default matrix is:
5159
52| Output folder | Capture target | Upload dimensions | Store slot |60| Output folder | Capture target | Upload dimensions | Store slot |
53| ------------------------------------- | ------------------------- | ----------------- | ----------------------------------------- |61| ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- |
54| `apple/iphone-6.9/{light,dark}/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch |62| `apple/iphone-6.9/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 |63| `apple/iphone-6.5/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 |64| `apple/ipad-13/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 |65| `google-play/phone/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 |66| `google-play/tablet-7/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 |67| `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 |
6068
61Each target captures thread, terminal, review, thread list, and environments, producing 30 PNG69Each target captures thread, terminal, review, thread list, and environments. Each appearance
62files for one appearance or 60 for both. Each appearance folder's five screenshots satisfy the configured Apple limit of 1–10, Google70folder's five screenshots satisfy the configured Apple limit of 1–10, Google
63phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8.71phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8.
6472
65The generated tree is deliberately aligned with the store upload fields:73The generated tree is deliberately aligned with the store upload fields:
6674
67 artifacts/app-store/screenshots/75 artifacts/app-store/screenshots/
68 ├── apple/76 ├── apple/
69 │ ├── iphone-6.9/{light,dark}/{thread,terminal,review,threads,environments}.png77 │ ├── iphone-6.9/dark/{thread,terminal,review,threads,environments}.png
70 │ ├── iphone-6.5/{light,dark}/{thread,terminal,review,threads,environments}.png78 │ ├── iphone-6.5/dark/{thread,terminal,review,threads,environments}.png
71 │ └── ipad-13/{light,dark}/{thread,terminal,review,threads,environments}.png79 │ └── ipad-13/dark/{thread,terminal,review,threads,environments}.png
72 └── google-play/80 └── google-play/
73 ├── phone/{light,dark}/{thread,terminal,review,threads,environments}.png81 ├── phone/dark/{thread,terminal,review,threads,environments}.png
74 ├── tablet-7/{light,dark}/{thread,terminal,review,threads,environments}.png82 ├── tablet-7/dark/{thread,terminal,review,threads,environments}.png
75 └── tablet-10/{light,dark}/{thread,terminal,review,threads,environments}.png83 └── tablet-10/dark/{thread,terminal,review,threads,environments}.png
84
85A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance
86folders.
7687
77Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD88Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD
78names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport.89names, 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,10 +96,11 @@ runs iOS and Android concurrently: iPhone and iPad capture on a
8512-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a9612-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a
8616-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator.9716-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator.
8798
88Every job uploads its PNGs even when a later capture fails, which makes partial runs useful for99Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis.
89diagnosis. Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow100The separate validation step is success-gated: it runs before upload only when capture succeeds. If
90run's Artifacts section. Each job runs validation again immediately before upload. Artifacts are101capture fails, the `always()` upload still publishes partial PNGs without re-validating them.
91retained for 14 days.102Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's
103Artifacts section. Artifacts are retained for 14 days.
92104
93The workflow uses the same checked-in device and scene matrix as local capture. Android remains105The workflow uses the same checked-in device and scene matrix as local capture. Android remains
94ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the106ARM64 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,11 +123,19 @@ Reuse the native build and retain the disposable environment:
111123
112 pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running124 pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running
113125
114Run Metro separately:126By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate
127terminal, 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
132Then run the capture from the repository root:
115133
116 pnpm --filter @t3tools/mobile showcase
117 pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9134 pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9
118135
136`pnpm --filter @t3tools/mobile showcase` starts Expo on its normal port, so it is not compatible with
137the harness's `--skip-metro` mode.
138
119List the matrix and flags:139List the matrix and flags:
120140
121 pnpm screenshots:mobile --list141 pnpm screenshots:mobile --list
...@@ -151,7 +171,9 @@ remote-first while the harness retains reliable loopback connections to its ephe...@@ -151,7 +171,9 @@ remote-first while the harness retains reliable loopback connections to its ephe
151## Local prerequisites171## Local prerequisites
152172
153- iOS: Xcode command-line tools, the configured simulator runtimes, and installed CocoaPods.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.
155177
156The harness is the source of truth for upload dimensions; do not resize its output. If store rules178The harness is the source of truth for upload dimensions; do not resize its output. If store rules
157change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size,179change, 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,39 +1,53 @@
1# Observability1# Observability
22
3> For maintainers. Using T3 Code? See [docs/user](../user/).
4
3T3 Code has one server-side observability model:5T3 Code has one server-side observability model:
46
5- pretty logs go to stdout for humans7- pretty logs go to stdout for humans
6- completed spans go to a local NDJSON trace file8- completed spans go to a local NDJSON trace file
7- traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM9- traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM
810
9The local trace file is the persisted source of truth. There is no separate persisted server log file anymore.11The local trace file is the persisted source of truth for normal local launches. Those launches do not
12write a separate server log file, but SSH-managed launches also persist the remote process's
13stdout/stderr at `~/.t3/ssh-launch/<state>/server.log`.
1014
11## Where To Find Things15## Where To Find Things
1216
13### Logs17### Logs
1418
15Logs are human-facing only:19Logs are human-facing:
1620
17- destination: stdout21- destination: stdout
18- format: `Logger.consolePretty()`22- format: `Logger.consolePretty()`
19- persistence: none23- normal local persistence: none
24- SSH-managed launch persistence: `~/.t3/ssh-launch/<state>/server.log`
2025
21If 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.26If 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.
2227
23### Traces28### Traces
2429
25Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`).30Completed spans are written as NDJSON records to `serverTracePath`. The default depends on how the
31server 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
35worktree uses `~/.t3/dev/logs/server.trace.ndjson`.
2636
27Important fields in each record:37Important fields common to both record types:
2838
39- `type`: `effect-span` or `otlp-span`
29- `name`: span name40- `name`: span name
30- `traceId`, `spanId`, `parentSpanId`: correlation41- `traceId`, `spanId`, `parentSpanId`: correlation
31- `durationMs`: elapsed time42- `durationMs`: elapsed time
32- `attributes`: structured context43- `attributes`: structured context
33- `events`: embedded logs and custom events44- `events`: embedded logs and custom events
34- `exit`: `Success`, `Failure`, or `Interrupted`
3545
36The schema lives in `apps/server/src/observability/TraceRecord.ts`.46`effect-span` records also contain `exit` with `Success`, `Failure`, or `Interrupted`. `otlp-span`
47records instead carry OTLP resource, scope, and optional status fields.
48
49The `TraceRecord`, `EffectTraceRecord`, and `OtlpTraceRecord` schemas live in
50`packages/shared/src/observability.ts`.
3751
38### Metrics52### Metrics
3953
...@@ -165,30 +179,35 @@ The backend reads observability config at process start. If you change OTLP env...@@ -165,30 +179,35 @@ The backend reads observability config at process start. If you change OTLP env
165179
166The trace file is the fastest way to inspect raw span data.180The trace file is the fastest way to inspect raw span data.
167181
168Resolve the production or explicitly configured trace file once. Runtime state lives under the182Resolve the path for the launch mode once. Production and explicitly configured homes store runtime
169base directory's `userdata` folder:183state under the base directory's `userdata` folder:
170184
171```bash185```bash
172TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson"186TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson"
173```187```
174188
175Tail it:189A dev server started from a linked worktree defaults to that worktree's local home:
176190
177```bash191```bash
178tail -f "$TRACE_FILE"192TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson"
179```193```
180194
181For an implicit monorepo dev server, use:195Only an implicit dev run outside a linked worktree uses the shared dev directory:
182196
183```bash197```bash
184TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson"198TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson"
199```
200
201Tail the selected file:
202
203```bash
185tail -f "$TRACE_FILE"204tail -f "$TRACE_FILE"
186```205```
187206
188Show failed spans:207Show failed spans:
189208
190```bash209```bash
191jq -c 'select(.exit._tag != "Success") | {210jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | {
192 name,211 name,
193 durationMs,212 durationMs,
194 exit,213 exit,
...@@ -281,10 +300,12 @@ Recommended flow in Grafana:...@@ -281,10 +300,12 @@ Recommended flow in Grafana:
281Good first searches:300Good first searches:
282301
283- service name such as `t3-local`, `t3-dev`, or `t3-desktop`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- orchestration spans with attributes like `orchestration.command_type`305- orchestration spans with attributes like `orchestration.command_type`
286306
287Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful.307Once you know traces are arriving, narrower TraceQL queries for names such as `sendTurn` or Git
308operation names become useful.
288309
289### Use Metrics To See Systemic Problems310### Use Metrics To See Systemic Problems
290311
...@@ -297,7 +318,6 @@ Good metric families to watch:...@@ -297,7 +318,6 @@ Good metric families to watch:
297- `t3_orchestration_command_ack_duration`318- `t3_orchestration_command_ack_duration`
298- `t3_provider_turn_duration`319- `t3_provider_turn_duration`
299- `t3_git_command_duration`320- `t3_git_command_duration`
300- `t3_db_query_duration`
301321
302Counters tell you volume and failure rate:322Counters tell you volume and failure rate:
303323
...@@ -305,7 +325,6 @@ Counters tell you volume and failure rate:...@@ -305,7 +325,6 @@ Counters tell you volume and failure rate:
305- `t3_orchestration_commands_total`325- `t3_orchestration_commands_total`
306- `t3_provider_turns_total`326- `t3_provider_turns_total`
307- `t3_git_commands_total`327- `t3_git_commands_total`
308- `t3_db_queries_total`
309328
310Use metrics when the question is:329Use metrics when the question is:
311330
...@@ -339,7 +358,7 @@ If you need those later, add client-side instrumentation or a dedicated server f...@@ -339,7 +358,7 @@ If you need those later, add client-side instrumentation or a dedicated server f
339### "Why did this request fail?"358### "Why did this request fail?"
340359
3411. Start with the local NDJSON file.3601. Start with the local NDJSON file.
3422. Find spans where `exit._tag != "Success"`.3612. Find `effect-span` records where `exit._tag != "Success"`.
3433. Group by `traceId`.3623. Group by `traceId`.
3444. Inspect sibling spans and span events.3634. Inspect sibling spans and span events.
3455. If needed, move to Tempo for the full trace tree.3645. If needed, move to Tempo for the full trace tree.
...@@ -522,6 +541,8 @@ Current high-value span and metric boundaries include:...@@ -522,6 +541,8 @@ Current high-value span and metric boundaries include:
522541
523### Current Constraints542### Current Constraints
524543
525- logs outside spans are not persisted544- logs outside spans are not persisted in the trace file; SSH-managed launch stdout/stderr is still
545 captured in its launcher log
526- metrics are not snapshotted locally546- metrics are not snapshotted locally
527- the old `serverLogPath` still exists in config for compatibility, but the trace file is the persisted artifact that matters547- 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,9 +1,14 @@
1# Relay observability1# Relay observability
22
3The relay Alchemy stack owns a focused Axiom trace setup:3> For maintainers. Using T3 Code? See [docs/user](../user/).
44
5- `t3-code-relay-traces-prod`, an OpenTelemetry trace dataset for Worker requests5The relay Alchemy stack owns a shared Axiom trace setup:
6- `t3-code-relay-otel-ingest-prod`, a dataset-scoped ingest token bound to the Worker6
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- `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans12- `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans
813
9Alchemy stages append their sanitized stage name to isolate resources, for example14Alchemy 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,8 +20,9 @@ Deploy from `infra/relay` with the normal Alchemy workflow:
15vp run deploy20vp run deploy
16```21```
1722
18Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker23Alchemy resolves account-level Axiom deployment credentials through its provider. At runtime, the
19receives only the scoped ingest token; it does not receive the diagnostics query token.24Worker receives only its scoped ingest token. Mobile and relay clients use their own separately
25provisioned scoped ingest tokens.
2026
21The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans.27The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans.
22Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix.28Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix.
...@@ -25,18 +31,23 @@ For example:...@@ -25,18 +31,23 @@ For example:
25```apl31```apl
26['t3-code-relay-traces-prod']32['t3-code-relay-traces-prod']
27| where name startswith 'http.server'33| where name startswith 'http.server'
34| extend endpoint = column_ifexists('attributes.http.route', ''),
35 customAttributes = column_ifexists('attributes.custom', dynamic({}))
28| project _time, name, trace_id, duration,36| project _time, name, trace_id, duration,
29 ['attributes.http.request.method'],37 ['attributes.http.request.method'],
30 ['attributes.url.path'],38 ['attributes.url.path'],
31 ['attributes.http.response.status_code']39 ['attributes.http.response.status_code'],
40 endpoint,
41 relayOperation = customAttributes['relay']['operation']
32| order by _time desc42| order by _time desc
33| limit 20043| limit 200
34```44```
3545
36Endpoint failure annotations and other relay-specific attributes are also emitted in the46The provisioned view also reads the endpoint from `attributes.http.route`. Relay-specific span
37`attributes.custom` map when present on a span, for example47annotations are stored under `attributes.custom`; `relay.operation` is one of the emitted custom
38`['attributes.custom']['relay.endpoint']`.48attributes.
3949
40Agents should prefer the provisioned view or APL queries for completed incidents instead of50Agents should prefer the provisioned view or APL queries for completed incidents instead of
41tailing the Cloudflare Worker. Use the read-only query token when scripted access is needed;51tailing the Cloudflare Worker. The stack does not provision a separate query token. Responders who
42keep the ingest token reserved for the Worker.52need 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,5 +1,7 @@
1# Release Checklist1# Release Checklist
22
3> For maintainers. Using T3 Code? See [docs/user](../user/).
4
3This document covers the unified release workflow for stable and nightly desktop releases.5This document covers the unified release workflow for stable and nightly desktop releases.
46
5## What the workflow does7## What the workflow does
...@@ -30,6 +32,17 @@ This document covers the unified release workflow for stable and nightly desktop...@@ -30,6 +32,17 @@ This document covers the unified release workflow for stable and nightly desktop
30 - nightly releases are aliased to the `nightly` hosted app channel32 - nightly releases are aliased to the `nightly` hosted app channel
31- Signing is optional and auto-detected per platform from secrets.33- Signing is optional and auto-detected per platform from secrets.
3234
35## Required release credentials
36
37The release workflow requires these GitHub Actions secrets in addition to the platform and deployment
38credentials documented below:
39
40- `RELEASE_APP_ID`
41- `RELEASE_APP_PRIVATE_KEY`
42
43The GitHub Release job uses them to mint the token that publishes release assets. Stable releases use
44them again in the finalize job, which can commit and push aligned package versions to `main`.
45
33## T3 Connect relay deployment46## T3 Connect relay deployment
3447
35The relay is a shared control plane versioned separately from client releases. Stable and nightly48The relay is a shared control plane versioned separately from client releases. Stable and nightly
...@@ -148,7 +161,8 @@ One-time Vercel dashboard setup:...@@ -148,7 +161,8 @@ One-time Vercel dashboard setup:
148 - manual `workflow_dispatch` with `channel=nightly`161 - manual `workflow_dispatch` with `channel=nightly`
149- Runs the same desktop quality gates and artifact matrix as the tagged release flow.162- Runs the same desktop quality gates and artifact matrix as the tagged release flow.
150- Publishes a GitHub prerelease only: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 - release name includes the short commit SHA166 - release name includes the short commit SHA
153 - `make_latest` is always `false`167 - `make_latest` is always `false`
154- Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`.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,7 +192,9 @@ guidance when those environments are available.
178192
179## Desktop auto-update notes193## Desktop auto-update notes
180194
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- Update UX:198- Update UX:
183 - Background checks run on startup delay + interval.199 - Background checks run on startup delay + interval.
184 - No automatic download or install.200 - No automatic download or install.
...@@ -187,9 +203,6 @@ guidance when those environments are available....@@ -187,9 +203,6 @@ guidance when those environments are available.
187- Repository slug source:203- Repository slug source:
188 - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set.204 - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set.
189 - otherwise `GITHUB_REPOSITORY` from GitHub Actions.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- Required release assets for updater:206- Required release assets for updater:
194 - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads)207 - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads)
195 - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases208 - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases
...@@ -200,8 +213,9 @@ guidance when those environments are available....@@ -200,8 +213,9 @@ guidance when those environments are available.
200213
201## 0) npm OIDC trusted publishing setup (CLI)214## 0) npm OIDC trusted publishing setup (CLI)
202215
203The workflow publishes the CLI with `npm publish` from `apps/server` after bumping216The workflow invokes `node apps/server/scripts/cli.ts publish` after aligning package versions. That
204the package version to the release tag version.217script temporarily prepares the `t3` package, then runs `vp pm publish --filter t3 ...` from the
218repository root so workspace publish configuration is applied correctly.
205219
206Checklist:220Checklist:
207221
...@@ -213,22 +227,27 @@ Checklist:...@@ -213,22 +227,27 @@ Checklist:
213 - Environment (if used): match your npm trusted publishing config227 - Environment (if used): match your npm trusted publishing config
2143. Ensure npm account and org policies allow trusted publishing for the package.2283. Ensure npm account and org policies allow trusted publishing for the package.
2154. Create release tag `vX.Y.Z` and push; workflow will:2294. 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 - build web + server231 - build web + server
218 - run `npm publish --access public --tag latest`232 - invoke the CLI publish script with npm dist-tag `latest`
2195. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`.2335. Nightly runs invoke the same publish script with npm dist-tag `nightly`.
234
235## 1) Release validation and unsigned builds
220236
221## 1) Dry-run release without signing237There 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
241to validate the workflow.
222242
223Use this first to validate the release pipeline.243The workflow has no non-publishing `workflow_dispatch` mode. Use normal CI or local quality gates to
244validate checks and builds without shipping. To exercise the complete release graph at lower stable
245risk, manually dispatch `channel=nightly`; this still publishes a real nightly npm package, GitHub
246prerelease, desktop updater release, and hosted nightly alias, but it does not update stable aliases or
247commit a version bump to `main`. Only run it when a real nightly release is acceptable.
224248
2251. Confirm no signing secrets are required for this test.249Manual `channel=stable` with a version input is also a real stable-channel release. Omitting signing
2262. Create a test tag:250secrets only makes platform artifacts unsigned; it does not prevent publication.
227 - `git tag v0.0.0-test.1`
228 - `git push origin v0.0.0-test.1`
2293. Wait for `.github/workflows/release.yml` to finish.
2304. Verify the GitHub Release contains all platform artifacts.
2315. Download each artifact and sanity-check installation on each OS.
232251
233## 2) Apple signing + notarization setup (macOS)252## 2) Apple signing + notarization setup (macOS)
234253
...@@ -267,7 +286,7 @@ Checklist:...@@ -267,7 +286,7 @@ Checklist:
267 - `APPLE_API_KEY`: contents of the downloaded `.p8`286 - `APPLE_API_KEY`: contents of the downloaded `.p8`
268 - `APPLE_API_KEY_ID`: Key ID287 - `APPLE_API_KEY_ID`: Key ID
269 - `APPLE_API_ISSUER`: Issuer ID288 - `APPLE_API_ISSUER`: Issuer ID
27010. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../cloud/t3-connect-clerk.md#desktop-passkeys).28910. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../internals/t3-connect.md#desktop-passkeys).
27111. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected29011. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected
272 `com.apple.developer.associated-domains` entitlement.291 `com.apple.developer.associated-domains` entitlement.
273292
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
3This guide is for people who want to use more than one Claude setup in T3 Code.
4
5Common 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
14Use the default provider.
15
16Log in with Claude Code normally:
17
18```bash
19claude auth login
20```
21
22In T3 Code Settings, your Claude provider can stay like this:
23
24```text
25Display name: Claude
26Binary path: claude
27Claude HOME path: empty
28```
29
30An empty `Claude HOME path` means T3 Code uses your normal home directory.
31
32## I Want Work And Personal Claude Accounts
33
34Use a different Claude home for each account.
35
36Example:
37
38```text
39default home work account
40~/.claude_personal_home personal account
41```
42
43### Set Up The First Account
44
45Log in normally:
46
47```bash
48claude auth login
49```
50
51In T3 Code Settings:
52
53```text
54Display name: Claude Work
55Binary path: claude
56Claude HOME path: empty
57```
58
59### Set Up The Second Account
60
61Log in with a separate home:
62
63```bash
64mkdir -p ~/.claude_personal_home
65HOME=~/.claude_personal_home claude auth login
66```
67
68Then add another Claude provider in T3 Code:
69
70```text
71Display name: Claude Personal
72Binary path: claude
73Claude HOME path: ~/.claude_personal_home
74```
75
76Use the email shown in Settings to confirm each provider is using the intended account. Emails are
77blurred by default; click the blurred email to reveal it.
78
79## Can I Switch Claude Accounts In An Existing Thread?
80
81Usually, no.
82
83T3 Code only offers Claude providers that use the same Claude home for an existing thread. A
84different Claude home is treated as a different Claude environment.
85
86This is different from the recommended Codex setup. Claude Code keeps account and local state across
87multiple files under its home directory, so T3 Code keeps separate Claude homes isolated instead of
88trying to share part of the state.
89
90## I Want To Use OpenRouter
91
92Use this when you want Claude Code to talk to OpenRouter directly, without running a local router.
93This is the simplest external-provider setup.
94
95OpenRouter provides a Claude Code integration through Claude's Anthropic-compatible environment
96variables.
97
98### Configure A Claude OpenRouter Provider
99
100Add or edit a Claude provider in T3 Code Settings:
101
102```text
103Display name: Claude OpenRouter
104Binary path: claude
105Claude HOME path: ~/.claude_openrouter_home
106```
107
108In that provider's Environment variables section, add:
109
110```text
111ANTHROPIC_BASE_URL https://openrouter.ai/api
112ANTHROPIC_AUTH_TOKEN sk-or-... Sensitive
113ANTHROPIC_API_KEY Empty value
114```
115
116Mark `ANTHROPIC_AUTH_TOKEN` as sensitive. T3 Code stores the value as a server secret and does not
117send it back to the app after saving.
118
119If you want this setup isolated from your normal Claude account, create that home first:
120
121```bash
122mkdir -p ~/.claude_openrouter_home
123```
124
125If you previously used the same Claude home with a normal Anthropic login, run `/logout` in a Claude
126Code session for that home before using OpenRouter. Otherwise Claude Code may keep using cached
127Anthropic credentials instead of the OpenRouter token.
128
129### Pick OpenRouter Models
130
131OpenRouter can route Claude Code's default model roles to OpenRouter model IDs.
132
133Example:
134
135```text
136ANTHROPIC_DEFAULT_OPUS_MODEL anthropic/claude-opus-4.6
137ANTHROPIC_DEFAULT_SONNET_MODEL anthropic/claude-sonnet-4.6
138ANTHROPIC_DEFAULT_HAIKU_MODEL anthropic/claude-haiku-4.5
139CLAUDE_CODE_SUBAGENT_MODEL anthropic/claude-sonnet-4.6
140```
141
142Add those to the same provider's Environment variables section if you want stable model choices.
143
144### Verify OpenRouter Is Being Used
145
146Open a Claude session and run:
147
148```text
149/status
150```
151
152You should see the Anthropic base URL set to:
153
154```text
155https://openrouter.ai/api
156```
157
158You 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
167OpenRouter'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
172Claude Code Router is useful when you want a local routing layer with more control than a direct
173OpenRouter setup.
174
175T3 Code does not need a special Claude Code Router provider. Treat the router as a Claude
176environment.
177
178Use this when you want Claude Code Router to decide which upstream model or provider handles Claude
179requests.
180
181High-level flow:
182
1831. Start Claude Code Router.
1842. Add or configure a Claude provider in T3 Code.
1853. Put the router's required variables on that provider instance.
186
187Configure a Claude provider:
188
189```text
190Display name: Claude Router
191Binary path: claude
192Claude HOME path: ~/.claude_router_home
193```
194
195Then copy the variables that `ccr activate` would export into the provider's Environment variables
196section. Mark tokens and API keys as sensitive.
197
198If you want the router-backed setup to stay separate from your normal Claude account, create and log
199in with a dedicated home first:
200
201```bash
202mkdir -p ~/.claude_router_home
203ccr start
204ccr activate
205HOME=~/.claude_router_home claude auth login
206```
207
208Claude Code Router's setup can change over time. Use its upstream README for the current install and
209configuration steps: <https://github.com/musistudio/claude-code-router>.
210
211## I Want Different Claude Settings, Not A Different Account
212
213Create another Claude provider with the same account if you want a named preset.
214
215Examples:
216
217- "Claude Default"
218- "Claude Router"
219- "Claude Experimental"
220
221If the preset needs different Claude files, give it a different `Claude HOME path`. If it needs
222different API keys, base URLs, or router settings, use Environment variables.
223
224Do not put environment variable assignments in `Launch arguments`.
docs/providers/codex.md deleted-140
...@@ -1,140 +0,0 @@
1# Codex
2
3This guide is for people who want to use more than one Codex account in T3 Code.
4
5Common 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
14Use the default provider.
15
16In Settings, your Codex provider can stay like this:
17
18```text
19Display name: Codex
20CODEX_HOME path: ~/.codex
21Shadow home path: empty
22```
23
24Log in with Codex normally:
25
26```bash
27codex login
28```
29
30## I Want Work And Personal Codex Accounts
31
32Use one real Codex home and one shadow home.
33
34Recommended setup:
35
36```text
37~/.codex shared Codex home
38~/.codex_p second account auth
39```
40
41The 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
49Log in normally:
50
51```bash
52codex login
53```
54
55This is the account used by `~/.codex`.
56
57In T3 Code Settings, name it something obvious:
58
59```text
60Display name: Codex Work
61CODEX_HOME path: ~/.codex
62Shadow home path: empty
63```
64
65### Set Up The Second Account
66
67Log in with a separate Codex home:
68
69```bash
70mkdir -p ~/.codex_p
71CODEX_HOME=~/.codex_p codex login
72```
73
74In T3 Code Settings, add another Codex provider:
75
76```text
77Display name: Codex Personal
78CODEX_HOME path: ~/.codex
79Shadow home path: ~/.codex_p
80```
81
82The important part is that both providers use the same `CODEX_HOME path`, but only the second one
83has a `Shadow home path`.
84
85## Which Account Am I Using?
86
87Open Settings and look at the provider row.
88
89T3 Code shows the authenticated email for providers that report one. Emails are blurred by default;
90click the blurred email to reveal it.
91
92Use 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
96Use the provider's Environment variables section in Settings.
97
98This is useful when a Codex-compatible setup needs account-specific variables. Add the variables to
99the provider instance that should receive them, and mark API keys or tokens as sensitive. Sensitive
100values 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
104Yes, when both Codex providers share the same `CODEX_HOME path`.
105
106For example:
107
108```text
109Codex Work CODEX_HOME path: ~/.codex
110Codex Personal CODEX_HOME path: ~/.codex, Shadow home path: ~/.codex_p
111```
112
113Those two providers are considered compatible for continuation, so the locked model picker can show
114both.
115
116If you add a third Codex provider with a completely different `CODEX_HOME path`, T3 Code treats it
117as a different workspace. It will not be offered for existing threads created under `~/.codex`.
118
119## If Both Accounts Look The Same
120
121If two Codex providers show the same account or the same unexpected model list:
122
1231. Check the email in Settings.
1242. Refresh provider status.
1253. Confirm the second provider has `Shadow home path` set.
1264. Confirm the shadow directory has its own `auth.json`.
1275. If you copied `~/.codex` into the shadow directory, remove everything except `auth.json`.
128
129Example cleanup:
130
131```bash
132find ~/.codex_p -mindepth 1 ! -name auth.json -exec rm -rf {} +
133```
134
135## When To Use A Separate CODEX_HOME
136
137Use a totally separate `CODEX_HOME path` only when you want a separate Codex workspace.
138
139That means separate sessions and less account switching inside old threads. Most dual-account users
140should use the shared-home plus shadow-home setup instead.
docs/reference/encyclopedia.md deleted-180
...@@ -1,180 +0,0 @@
1# Encyclopedia
2
3This 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
19The 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
23The 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
27A 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
33The 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
37A 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
41A 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
45Orchestration 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
49The 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
53A 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].
54Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`.
55
56#### Domain Event
57
58A 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.
59Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`.
60
61#### Decider
62
63The 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
67A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10].
68
69#### Projector
70
71The 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
75The 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
79A 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
83A lightweight typed runtime signal emitted when an async milestone completes. See [RuntimeReceiptBus.ts][13].
84Examples 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
92The 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
96The backend agent runtime that actually performs work. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17].
97
98#### Session
99
100The 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
104The 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
108The 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
112Controls 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
116A 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
120Checkpointing 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
124A 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
128The 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
132The 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
136The 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
140The 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
46Worktrees 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
50Set `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
56If 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
3T3 Code is a web and desktop GUI for running coding agents on your machine.
4
5## Requirements
6
7Node.js `^22.16 || ^23.11 || >=24.10` on the machine that runs the T3 Code server.
8
9At least one provider CLI, installed and authenticated. See [Providers](#providers) below.
10
11## Run Without Installing
12
13```bash
14npx t3@latest
15```
16
17This 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
22Download the latest release from
23[GitHub Releases](https://github.com/pingdotgg/t3code/releases), or install from a package
24registry.
25
26Windows:
27
28```bash
29winget install T3Tools.T3Code
30```
31
32macOS:
33
34```bash
35brew install --cask t3-code
36```
37
38Arch Linux:
39
40```bash
41yay -S t3code-bin
42```
43
44## Providers
45
46T3 Code drives provider CLIs; it does not ship them. Install the CLI for each provider you want
47to 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
57Cursor is the one to watch: install Cursor CLI, which provides the `cursor-agent` binary that
58T3 Code looks for, but authenticate with `agent login`, not `cursor-agent login`.
59
60Run the login command on the machine running the T3 Code server, not on the device you browse
61from.
62
63### Binary Discovery
64
65Each 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
67manager or a non-standard install location keeps the CLI off the `PATH` of the shell that
68started T3 Code.
69
70### When Auth Is Needed
71
72Provider auth is required before you start a session with that provider, not before you start
73T3 Code. You can install T3 Code, open it, and add providers afterwards. A provider that is not
74authenticated shows its status in **Settings** and fails at session start with the login command
75to run.
76
77For 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,10 +1,14 @@
1# Keybindings1# Keybindings
22
3T3 Code reads keybindings from:3Edit keybindings from **Settings** → **Keybindings**. That page lists every command, its current
4shortcut, whether it is a default or your own, and warns about conflicts.
45
5- `~/.t3/keybindings.json`6The same configuration lives in `~/.t3/userdata/keybindings.json` on the machine running the
7server, if you prefer editing it directly. T3 Code writes the built-in defaults into that file on
8first run, and adds any new defaults on later startups unless a rule of yours already claims the
9command or the shortcut.
610
7The file must be a JSON array of rules:11The file is a JSON array of rules.
812
9```json13```json
10[14[
...@@ -13,110 +17,59 @@ The file must be a JSON array of rules:...@@ -13,110 +17,59 @@ The file must be a JSON array of rules:
13]17]
14```18```
1519
16See the full schema for more details: [`packages/contracts/src/keybindings.ts`](../../packages/contracts/src/keybindings.ts)20Invalid rules are ignored. An invalid file is ignored entirely, and the server logs a warning.
1721
18## Defaults22## Rule Shape
1923
20```json24- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d`
21[25- `command` (required): the command ID to run
22 { "key": "mod+j", "command": "terminal.toggle" },26- `when` (optional): boolean expression controlling when the shortcut is active
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
40For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](../../apps/server/src/keybindings.ts)
4127
42## Configuration28## Key Syntax
4329
44### Rule Shape30Modifiers: `mod` (`cmd` on macOS, `ctrl` elsewhere), `cmd` / `meta`, `ctrl` / `control`, `shift`,
31`alt` / `option`.
4532
46Each entry supports:33Examples: `mod+j`, `mod+shift+d`, `ctrl+l`, `cmd+k`.
4734
48- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d`35## Commands
49- `command` (required): action ID
50- `when` (optional): boolean expression controlling when the shortcut is active
5136
52Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server.37Commands are IDs like `terminal.toggle`, `commandPalette.toggle`, `preview.refresh`, and
5338`chat.new`. Project scripts are addressable as `script.{id}.run`, for example `script.test.run`.
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`)
7139
72The command palette searches active thread titles, projects, branches, user messages, and final40The command palette searches active thread titles, projects, branches, user messages, and final
73agent responses across connected environments. Message matches show one labeled excerpt while41agent responses across connected environments. Message matches show one labeled excerpt while
74keeping the thread's project, branch, and machine context visible. Message search begins after two42keeping the thread's project, branch, and machine context visible. Message search begins after two
75characters and uses SQLite's ASCII case-insensitive matching.43characters and uses SQLite's ASCII case-insensitive matching.
7644
77### Key Syntax45The full command list and the current defaults are shown in **Settings** → **Keybindings**, which
7846always matches the build you are running. Use that rather than a copied list.
79Supported modifiers:
80
81- `mod` (`cmd` on macOS, `ctrl` on non-macOS)
82- `cmd` / `meta`
83- `ctrl` / `control`
84- `shift`
85- `alt` / `option`
86
87Examples:
88
89- `mod+j`
90- `mod+shift+d`
91- `ctrl+l`
92- `cmd+k`
9347
94### `when` Conditions48Note that `chat.new` and `chat.newLocal` both create a thread through the same path. A new thread
49inherits the project you were in, along with model and mode selections. Branch, worktree, and
50environment mode always come from your configured defaults, not from the thread you were looking
51at. To keep a worktree, use the explicit "new thread in this worktree" action in the branch
52toolbar. The only difference between the two commands: with the current sidebar and more than one
53project, `chat.new` opens a project chooser first.
9554
96Currently available context keys:55## `when` Conditions
9756
98- `terminalFocus`57A `when` expression is evaluated against context keys describing the current UI state. The keys
99- `terminalOpen`58the app supplies today are `terminalFocus`, `terminalOpen`, `previewFocus`, `previewOpen`, and
100- `previewFocus`59`modelPickerOpen`. The set is open and grows over time, so treat that as the current list rather
101- `previewOpen`60than a fixed one. Any key the running app does not supply evaluates to `false`.
10261
103Supported operators:62Operators: `!` (not), `&&` (and), `||` (or), and parentheses.
104
105- `!` (not)
106- `&&` (and)
107- `||` (or)
108- parentheses: `(` `)`
10963
110Examples:64Examples:
11165
112- `"when": "terminalFocus"`66- `"when": "terminalFocus"`
113- `"when": "terminalOpen && !terminalFocus"`67- `"when": "terminalOpen && !terminalFocus"`
114- `"when": "terminalFocus || terminalOpen"`68- `"when": "!terminalFocus"`
115
116Unknown condition keys evaluate to `false`.
11769
118### Precedence70## Precedence
11971
120- Rules are evaluated in array order.72- Rules are evaluated in array order.
121- For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins.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
3A permission mode controls how much the agent does on its own and when it stops to ask you.
4
5The mode is set per thread, from the mode control in the message composer. Changing it in one
6thread does not change any other thread. A thread created from inside another thread keeps that
7thread's mode; otherwise new threads start in **Full access** unless you pick something else
8before sending.
9
10## The Modes
11
12**Supervised**: ask before commands and file changes. The agent pauses and shows you what it
13wants 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
16without 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
19on the provider: Codex delegates routine approvals to an AI reviewer, Claude uses its own auto
20permission mode, and providers without an equivalent (such as OpenCode) fall back to asking, like
21Supervised.
22
23**Full access**: allow commands and edits without prompts. The default. The agent runs
24unattended until it finishes or asks a question of its own.
25
26Approvals appear inline in the conversation. Approve or reject one and the agent continues from
27there.
28
29## Choosing a Mode
30
31Use **Full access** for work in a worktree or a sandbox you can throw away.
32
33Use **Supervised** on a repository where an unwanted command is expensive, or the first time you
34run an unfamiliar task.
35
36**Auto-accept edits** suits refactors where the edits are the point and you only care about the
37shell commands.
38
39## Provider Behavior
40
41Each provider maps these modes onto its own approval and sandbox settings. Codex, for example,
42translates the mode into its approval policy and sandbox level, so **Supervised** runs the CLI
43with prompting enabled and a restricted workspace while **Full access** disables both. The
44labels above describe what you get; the exact per-provider translation is internal and may
45change.
46
47Mobile 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
3This 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
6Common 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
15Use the default provider.
16
17Log in with Claude Code normally:
18
19```bash
20claude auth login
21```
22
23In T3 Code Settings, your Claude provider can stay like this:
24
25```text
26Display name: Claude
27Binary path: claude
28CLAUDE_CONFIG_DIR path: empty
29```
30
31An empty `CLAUDE_CONFIG_DIR path` means T3 Code uses Claude Code's normal config directory.
32
33When 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
35the rest of your environment stay as they are.
36
37## I Want Work And Personal Claude Accounts
38
39Use a different Claude config directory for each account.
40
41Example:
42
43```text
44default config dir work account
45~/.claude_personal_home personal account
46```
47
48### Set Up The First Account
49
50Log in normally:
51
52```bash
53claude auth login
54```
55
56In T3 Code Settings:
57
58```text
59Display name: Claude Work
60Binary path: claude
61CLAUDE_CONFIG_DIR path: empty
62```
63
64### Set Up The Second Account
65
66Log in with a separate config directory:
67
68```bash
69mkdir -p ~/.claude_personal_home
70CLAUDE_CONFIG_DIR=~/.claude_personal_home claude auth login
71```
72
73Use `CLAUDE_CONFIG_DIR`, not `HOME`. Setting `HOME` writes the login to
74`~/.claude_personal_home/.claude`, which is not where T3 Code looks.
75
76Then add another Claude provider in T3 Code:
77
78```text
79Display name: Claude Personal
80Binary path: claude
81CLAUDE_CONFIG_DIR path: ~/.claude_personal_home
82```
83
84Use the email shown in Settings to confirm each provider is using the intended account. Emails are
85blurred by default; click the blurred email to reveal it.
86
87## Can I Switch Claude Accounts In An Existing Thread?
88
89Usually, no.
90
91T3 Code only offers Claude providers that use the same config directory for an existing thread. A
92different config directory is treated as a different Claude environment.
93
94This is different from the recommended Codex setup. Claude Code keeps account and local state across
95multiple files under its config directory, so T3 Code keeps separate config directories isolated
96instead of trying to share part of the state.
97
98## I Want To Use OpenRouter
99
100Use this when you want Claude Code to talk to OpenRouter directly, without running a local router.
101This is the simplest external-provider setup.
102
103OpenRouter provides a Claude Code integration through Claude's Anthropic-compatible environment
104variables.
105
106### Configure A Claude OpenRouter Provider
107
108Add or edit a Claude provider in T3 Code Settings:
109
110```text
111Display name: Claude OpenRouter
112Binary path: claude
113CLAUDE_CONFIG_DIR path: ~/.claude_openrouter_home
114```
115
116In that provider's Environment variables section, add:
117
118```text
119ANTHROPIC_BASE_URL https://openrouter.ai/api
120ANTHROPIC_AUTH_TOKEN sk-or-... Sensitive
121ANTHROPIC_API_KEY Empty value
122```
123
124Mark `ANTHROPIC_AUTH_TOKEN` as sensitive. T3 Code stores the value as a server secret and does not
125send it back to the app after saving.
126
127If you want this setup isolated from your normal Claude account, create that home first:
128
129```bash
130mkdir -p ~/.claude_openrouter_home
131```
132
133If you previously used the same Claude home with a normal Anthropic login, run `/logout` in a Claude
134Code session for that home before using OpenRouter. Otherwise Claude Code may keep using cached
135Anthropic credentials instead of the OpenRouter token.
136
137### Pick OpenRouter Models
138
139OpenRouter can route Claude Code's default model roles to OpenRouter model IDs.
140
141Example:
142
143```text
144ANTHROPIC_DEFAULT_OPUS_MODEL anthropic/claude-opus-4.6
145ANTHROPIC_DEFAULT_SONNET_MODEL anthropic/claude-sonnet-4.6
146ANTHROPIC_DEFAULT_HAIKU_MODEL anthropic/claude-haiku-4.5
147CLAUDE_CODE_SUBAGENT_MODEL anthropic/claude-sonnet-4.6
148```
149
150Add those to the same provider's Environment variables section if you want stable model choices.
151
152### Verify OpenRouter Is Being Used
153
154Open a Claude session and run:
155
156```text
157/status
158```
159
160You should see the Anthropic base URL set to:
161
162```text
163https://openrouter.ai/api
164```
165
166You 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
175OpenRouter'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
180Claude Code Router is useful when you want a local routing layer with more control than a direct
181OpenRouter setup.
182
183T3 Code does not need a special Claude Code Router provider. Treat the router as a Claude
184environment: give a Claude provider its own `CLAUDE_CONFIG_DIR path`, and put whatever variables
185the router tells you to export into that provider's Environment variables section. Mark tokens
186and API keys as sensitive.
187
188```text
189Display name: Claude Router
190Binary path: claude
191CLAUDE_CONFIG_DIR path: ~/.claude_router_home
192```
193
194Follow the upstream project's README for the router's own install, startup, and configuration
195steps: <https://github.com/musistudio/claude-code-router>.
196
197## I Want Different Claude Settings, Not A Different Account
198
199Create another Claude provider with the same account if you want a named preset.
200
201Examples:
202
203- "Claude Default"
204- "Claude Router"
205- "Claude Experimental"
206
207If the preset needs different Claude files, give it a different `CLAUDE_CONFIG_DIR path`. If it needs
208different API keys, base URLs, or router settings, use Environment variables.
209
210Do not put environment variable assignments in `Launch arguments`.
docs/user/providers-codex.md created+141
...@@ -0,0 +1,141 @@
1# Codex
2
3This 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
6Common 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
15Use the default provider.
16
17In Settings, your Codex provider can stay like this:
18
19```text
20Display name: Codex
21CODEX_HOME path: ~/.codex
22Shadow home path: empty
23```
24
25Log in with Codex normally:
26
27```bash
28codex login
29```
30
31## I Want Work And Personal Codex Accounts
32
33Use one real Codex home and one shadow home.
34
35Recommended setup:
36
37```text
38~/.codex shared Codex home
39~/.codex_p second account auth
40```
41
42The 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
50Log in normally:
51
52```bash
53codex login
54```
55
56This is the account used by `~/.codex`.
57
58In T3 Code Settings, name it something obvious:
59
60```text
61Display name: Codex Work
62CODEX_HOME path: ~/.codex
63Shadow home path: empty
64```
65
66### Set Up The Second Account
67
68Log in with a separate Codex home:
69
70```bash
71mkdir -p ~/.codex_p
72CODEX_HOME=~/.codex_p codex login
73```
74
75In T3 Code Settings, add another Codex provider:
76
77```text
78Display name: Codex Personal
79CODEX_HOME path: ~/.codex
80Shadow home path: ~/.codex_p
81```
82
83The important part is that both providers use the same `CODEX_HOME path`, but only the second one
84has a `Shadow home path`.
85
86## Which Account Am I Using?
87
88Open Settings and look at the provider row.
89
90T3 Code shows the authenticated email for providers that report one. Emails are blurred by default;
91click the blurred email to reveal it.
92
93Use 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
97Use the provider's Environment variables section in Settings.
98
99This is useful when a Codex-compatible setup needs account-specific variables. Add the variables to
100the provider instance that should receive them, and mark API keys or tokens as sensitive. Sensitive
101values 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
105Yes, when both Codex providers share the same `CODEX_HOME path`.
106
107For example:
108
109```text
110Codex Work CODEX_HOME path: ~/.codex
111Codex Personal CODEX_HOME path: ~/.codex, Shadow home path: ~/.codex_p
112```
113
114Those two providers are considered compatible for continuation, so the locked model picker can show
115both.
116
117If you add a third Codex provider with a completely different `CODEX_HOME path`, T3 Code treats it
118as a different workspace. It will not be offered for existing threads created under `~/.codex`.
119
120## If Both Accounts Look The Same
121
122If two Codex providers show the same account or the same unexpected model list:
123
1241. Check the email in Settings.
1252. Refresh provider status.
1263. Confirm the second provider has `Shadow home path` set.
1274. Confirm the shadow directory has its own `auth.json`.
1285. If you copied `~/.codex` into the shadow directory, remove everything except `auth.json`.
129
130Example cleanup:
131
132```bash
133find ~/.codex_p -mindepth 1 ! -name auth.json -exec rm -rf {} +
134```
135
136## When To Use A Separate CODEX_HOME
137
138Use a totally separate `CODEX_HOME path` only when you want a separate Codex workspace.
139
140That means separate sessions and less account switching inside old threads. Most dual-account users
141should 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 +14,15 @@ That gives you:
1414
15## Enabling Network Access15## Enabling Network Access
1616
17There are two ways to expose your server for remote connections: from the desktop app or from the CLI.17There are three ways to reach your server from another device: expose the desktop app's backend,
18run a headless server from the CLI, or have the desktop app launch T3 Code over SSH.
1819
19### Option 1: Desktop App20### Option 1: Desktop App
2021
21If you are already running the desktop app and want to make it reachable from other devices:22If you are already running the desktop app and want to make it reachable from other devices:
2223
231. Open **Settings** → **Connections**.241. Open **Settings** → **Connections**.
242. Under **Manage Local Backend**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces.252. Under **This environment**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces.
253. 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.263. 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.
264. Use **Create Link** to generate a pairing link you can share with another device.274. Use **Create Link** to generate a pairing link you can share with another device.
2728
...@@ -48,10 +49,10 @@ Depending on your Tailscale setup, this may include:...@@ -48,10 +49,10 @@ Depending on your Tailscale setup, this may include:
48- an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend49- an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend
4950
50The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as51The 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 this52`https://machine.tailnet.ts.net/`, and is off until you opt in. Turn on **Enable Tailscale HTTPS**
52backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend53on the **Tailscale HTTPS** row in **Settings** → **Connections**. The desktop app restarts the
53with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale54backend with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks
54Serve to proxy HTTPS traffic to the local backend.55Tailscale Serve to proxy HTTPS traffic to the local backend. Turn the same switch off to stop it.
5556
56The 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.57The 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.
5758
...@@ -96,10 +97,8 @@ By default this configures Tailscale Serve on HTTPS port 443 and advertises...@@ -96,10 +97,8 @@ By default this configures Tailscale Serve on HTTPS port 443 and advertises
96npx t3 serve --tailscale-serve --tailscale-serve-port 844397npx t3 serve --tailscale-serve --tailscale-serve-port 8443
97```98```
9899
99> Note100Once paired, add projects normally: open the Command Palette and choose **Add Project**, then pick
100> The GUIs do not currently support adding projects on remote environments.101the environment the project lives on. Every saved environment is offered, not only the local one.
101> For now, use `t3 project ...` on the server machine instead.
102> Full GUI support for remote project management is coming soon.
103102
104### Option 3: Desktop-Managed SSH Launch103### Option 3: Desktop-Managed SSH Launch
105104
...@@ -125,16 +124,10 @@ The remote host must have a compatible Node.js runtime. T3 Code uses the server...@@ -125,16 +124,10 @@ The remote host must have a compatible Node.js runtime. T3 Code uses the server
125^22.16 || ^23.11 || >=24.10124^22.16 || ^23.11 || >=24.10
126```125```
127126
128During 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:127During SSH launch, T3 Code first checks whether `node` is on `PATH`. If it is missing, the launcher
129128looks in the usual install directories and tries to activate a version manager if it finds one
130- `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`129(Volta, asdf, mise, fnm, nodenv, nvm). That covers most setups, but a version manager that only
131- Volta via `~/.volta/bin`130initializes from an interactive shell profile will not be picked up.
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`
138131
139If 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:132If 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:
140133
...@@ -148,7 +141,7 @@ If that does not print a compatible Node version, configure your version manager...@@ -148,7 +141,7 @@ If that does not print a compatible Node version, configure your version manager
148nvm alias default 24141nvm alias default 24
149```142```
150143
151With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above.144With 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.
152145
153If 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.146If 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.
154147
...@@ -160,7 +153,7 @@ be able to update and reconnect the server for you, or it may ask you to update...@@ -160,7 +153,7 @@ be able to update and reconnect the server for you, or it may ask you to update
160run a copied command on the server machine.153run a copied command on the server machine.
161154
162Finish active work before updating because the server restarts briefly. For step-by-step guidance,155Finish active work before updating because the server restarts briefly. For step-by-step guidance,
163see [Keeping T3 Code in Sync](./server-updates.md).156see [Keeping T3 Code in Sync](./updating.md).
164157
165On a Linux host, you can keep the server running after logout and manage it independently of the158On a Linux host, you can keep the server running after logout and manage it independently of the
166connection method. See [Running T3 Code in the Background](./background-service.md).159connection 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
3The T3 Code web or desktop app and the server it connects to work best when they use the same
4version. 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
8You 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
13Dismissing the conversation warning only hides that reminder for those two versions. It does not
14update the server, and the version difference remains visible in Connections.
15
16## Before You Update
17
18Let active agent work and terminal commands finish first. Updating restarts the server, so the
19connection will disappear briefly and work that is still running may be interrupted.
20
21The 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
31The available action depends on how that server was started. T3 Code does not update connected
32servers silently in the background.
33
34After 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
36Connections, so navigating between them does not lose the update. A failed step remains visible
37with its error and an option to retry.
38
39If the server uses the T3 Code background service, you can also update it directly on the host:
40
41```sh
42npx t3@latest service update
43```
44
45See [Running T3 Code in the Background](./background-service.md) for install, status, and removal
46commands.
47
48## After the Update
49
50Keep the web or desktop app open while the server restarts. The update completes only after the
51replacement server reports the requested version and is ready to accept commands. The warning and
52progress rail then disappear.
53
54If a step fails:
55
561. Retry the offered action once.
572. Make sure you updated the machine named in the warning, not only the device you are using.
583. 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
61For 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
3T3 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
7T3 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
46The **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
52Run 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
581. Install the GitHub CLI on the machine running T3 Code:
59 ```bash
60 brew install gh
61 ```
622. Sign in:
63 ```bash
64 gh auth login
65 ```
663. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated
67
68You can now clone, publish, and create pull requests.
69
70### For GitLab
71
721. Install the GitLab CLI:
73 ```bash
74 brew install glab
75 ```
762. Authenticate:
77 ```bash
78 glab auth login
79 ```
803. Check **Settings → Source Control** to confirm the connection
81
82### For Bitbucket
83
84Bitbucket uses tokens instead of a CLI tool. Two options, both set as environment variables on the
85machine running T3 Code.
86
87Recommended, a Bitbucket access token:
88
89```bash
90export T3CODE_BITBUCKET_ACCESS_TOKEN="your-access-token"
91```
92
93Or an Atlassian account email plus API token, with read/write access to pull requests and
94repositories:
95
96```bash
97export T3CODE_BITBUCKET_EMAIL="you@example.com"
98export T3CODE_BITBUCKET_API_TOKEN="your-token"
99```
100
101If both are set, the access token wins. Restart T3 Code and verify the connection in **Source
102Control settings**.
103
104### For Azure DevOps
105
1061. Install Azure CLI:
107 ```bash
108 brew install azure-cli
109 ```
1102. Add the DevOps extension:
111 ```bash
112 az extension add --name azure-devops
113 ```
1143. 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
3The T3 Code web or desktop app and the server it connects to work best when they use the same
4version. 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
8You 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
13Dismissing the conversation warning only hides that reminder for those two versions. It does not
14update the server, and the version difference remains visible in Connections.
15
16## Before You Update
17
18Let active agent work and terminal commands finish first. Updating restarts the server, so the
19connection will disappear briefly and work that is still running may be interrupted.
20
21The 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
31The available action depends on how that server was started. T3 Code does not update connected
32servers silently in the background.
33
34After 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
36Connections, so navigating between them does not lose the update. A failed step remains visible
37with its error and an option to retry.
38
39**Copy update command** gives you `npx t3@<client-version>`, which relaunches the server directly
40at the matching version. Add whatever startup options you normally use.
41
42If the server instead runs as the T3 Code background service, update the service on the host and
43pin the same version:
44
45```sh
46npx 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`
50only resolves the skew when your client happens to be on the latest release. The exact version from
51the warning always works.
52
53See [Running T3 Code in the Background](./background-service.md) for install, status, and removal
54commands.
55
56## After the Update
57
58Keep the web or desktop app open while the server restarts. The update completes only after the
59replacement server reports the requested version and is ready to accept commands. The warning and
60progress rail then disappear.
61
62If a step fails:
63
641. Retry the offered action once.
652. Make sure you updated the machine named in the warning, not only the device you are using.
663. 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
69For remote connection setup and access troubleshooting, see [Remote Access](./remote-access.md).
infra/relay/README.md+7-7
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1# T3 Connect Relay1# T3 Connect Relay
22
3> [!WARNING]3> [!NOTE]
4> T3 Connect is currently in private beta. Join the waitlist in the app under Settings > T3 Connect.4> Sign in to T3 Connect from the app under Settings > Connections.
55
6The relay is the hosted control plane for T3 Connect. It helps clients discover and connect to6The relay is the hosted control plane for T3 Connect. It helps clients discover and connect to
7remote environments, manages the cloud-side records needed for those connections, and delivers7remote environments, manages the cloud-side records needed for those connections, and delivers
...@@ -9,7 +9,7 @@ optional mobile notifications and Live Activities....@@ -9,7 +9,7 @@ optional mobile notifications and Live Activities.
99
10The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects,10The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects,
11regular API and WebSocket traffic goes directly between that client and the selected environment.11regular API and WebSocket traffic goes directly between that client and the selected environment.
12See the [T3 Connect architecture overview](../../docs/cloud/t3-code-connect-auth-flow.html) for the larger system12See the [T3 Connect architecture overview](../../docs/internals/t3-code-connect-auth-flow.html) for the larger system
13design.13design.
1414
15## Responsibilities15## Responsibilities
...@@ -25,7 +25,7 @@ The relay currently owns:...@@ -25,7 +25,7 @@ The relay currently owns:
25- Persisting relay state and exposing relay-specific traces for diagnostics.25- Persisting relay state and exposing relay-specific traces for diagnostics.
2626
27The environment server and relay have separate credentials and trust boundaries. Read27The 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,
29credential, or authorization behavior.29credential, or authorization behavior.
3030
31## Code Map31## Code Map
...@@ -159,8 +159,8 @@ and hosted web builds....@@ -159,8 +159,8 @@ and hosted web builds.
159159
160See:160See:
161161
162- [T3 Connect Clerk Setup](../../docs/cloud/t3-connect-clerk.md) for Clerk keys, JWT templates, and waitlist162- [T3 Connect Clerk Setup](../../docs/internals/t3-connect.md) for Clerk keys, JWT templates, and sign-up restrictions
163 setup.163 setup.
164- [Relay Observability](../../docs/relay-observability.md) for deployment tracing and diagnostics.164- [Relay Observability](../../docs/operations/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,165- [T3 Connect Architecture Overview](../../docs/internals/t3-code-connect-auth-flow.html) for the full link,
166 connect, endpoint, and notification flows.166 connect, endpoint, and notification flows.