authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-04 20:03:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-04 20:03:12-07:00
logec18938bf9be0a4ab682484812dc85395bfed133
treeed18082c75f5947b98fc42b6ae94862c6a05d2f6
parent5ae77c0d63ece1a3e3fa5a70bc7717aa7c342756
signature Signed by PGP key B5690EEEBB952194

Restructure documentation into topical folders (#2963)


48 files changed, 3918 insertions(+), 3899 deletions(-)

.docs/architecture.md deleted-143
......@@ -1,143 +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## Event Lifecycle
41
42### Startup and client connect
43
44```mermaid
45sequenceDiagram
46 participant Browser
47 participant Transport as WsTransport
48 participant Server as wsServer
49 participant Layers as serverLayers
50 participant Ready as ServerReadiness
51 participant Push as ServerPushBus
52
53 Browser->>Transport: Load app and open WebSocket
54 Transport->>Server: Connect
55 Server->>Layers: Start runtime services
56 Server->>Ready: Wait for startup barriers
57 Ready-->>Server: Ready
58 Server->>Push: Publish server.welcome
59 Push-->>Transport: Ordered welcome push
60 Transport-->>Browser: Hydrate initial state
61```
62
631. The browser boots [`WsTransport`][1] and registers typed listeners in [`wsNativeApi`][2].
642. The server accepts the connection in [`wsServer`][3] and brings up the runtime graph defined in [`serverLayers`][7].
653. [`ServerReadiness`][4] waits until the key startup barriers are complete.
664. Once the server is ready, [`wsServer`][3] sends `server.welcome` from the contracts in [`ws.ts`][6] through [`ServerPushBus`][5].
675. The browser receives that ordered push through [`WsTransport`][1], and [`wsNativeApi`][2] uses it to seed local client state.
68
69### User turn flow
70
71```mermaid
72sequenceDiagram
73 participant Browser
74 participant Transport as WsTransport
75 participant Server as wsServer
76 participant Provider as ProviderService
77 participant Codex as codex app-server
78 participant Ingest as ProviderRuntimeIngestion
79 participant Engine as OrchestrationEngine
80 participant Push as ServerPushBus
81
82 Browser->>Transport: Send user action
83 Transport->>Server: Typed WebSocket request
84 Server->>Provider: Route request
85 Provider->>Codex: JSON-RPC over stdio
86 Codex-->>Ingest: Provider runtime events
87 Ingest->>Engine: Normalize into orchestration events
88 Engine-->>Server: Domain events
89 Server->>Push: Publish orchestration.domainEvent
90 Push-->>Browser: Typed push
91```
92
931. A user action in the browser becomes a typed request through [`WsTransport`][1] and the browser API layer in [`nativeApi`][12].
942. [`wsServer`][3] decodes that request using the shared WebSocket contracts in [`ws.ts`][6] and routes it to the right service.
953. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio.
964. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events.
975. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events.
986. [`wsServer`][3] pushes those updates to the browser through [`ServerPushBus`][5] on channels defined in [`orchestration.ts`][11].
99
100### Async completion flow
101
102```mermaid
103sequenceDiagram
104 participant Server as wsServer
105 participant Worker as Queue-backed workers
106 participant Cmd as ProviderCommandReactor
107 participant Checkpoint as CheckpointReactor
108 participant Receipt as RuntimeReceiptBus
109 participant Push as ServerPushBus
110 participant Browser
111
112 Server->>Worker: Enqueue follow-up work
113 Worker->>Cmd: Process provider commands
114 Worker->>Checkpoint: Process checkpoint tasks
115 Checkpoint->>Receipt: Publish completion receipt
116 Cmd-->>Server: Produce orchestration changes
117 Checkpoint-->>Server: Produce orchestration changes
118 Server->>Push: Publish resulting state updates
119 Push-->>Browser: User-visible push
120```
121
1221. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14].
1232. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic.
1243. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence.
1254. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers.
1265. Any user-visible state changes produced by that async work still go back through [`wsServer`][3] and [`ServerPushBus`][5].
127
128[1]: ../apps/web/src/wsTransport.ts
129[2]: ../apps/web/src/wsNativeApi.ts
130[3]: ../apps/server/src/wsServer.ts
131[4]: ../apps/server/src/wsServer/readiness.ts
132[5]: ../apps/server/src/wsServer/pushBus.ts
133[6]: ../packages/contracts/src/ws.ts
134[7]: ../apps/server/src/serverLayers.ts
135[8]: ../apps/server/src/provider/Layers/ProviderService.ts
136[9]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
137[10]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts
138[11]: ../packages/contracts/src/orchestration.ts
139[12]: ../apps/web/src/nativeApi.ts
140[13]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
141[14]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts
142[15]: ../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts
143[16]: ../packages/shared/src/DrainableWorker.ts
.docs/ci.md deleted-6
......@@ -1,6 +0,0 @@
1# CI quality gates
2
3- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun 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 secrets are present: Apple credentials for macOS and Azure Trusted Signing credentials for Windows. Without secrets, it still releases unsigned artifacts.
6- See `docs/release.md` for full release/signing setup checklist.
.docs/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/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/Services/CheckpointStore.ts
176[20]: ../apps/server/src/checkpointing/Services/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/provider-architecture.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/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/remote-architecture.md deleted-382
......@@ -1,382 +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## Security model
335
336Remote support must assume that some environments will be reachable over untrusted networks.
337
338That means:
339
340- remote-capable environments should require explicit authentication
341- tunnel exposure should not rely on obscurity
342- client-saved endpoints should carry enough auth metadata to reconnect safely
343
344T3 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.
345
346For publicly reachable environments, authenticated access should be treated as required.
347
348Hosted 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.
349
350## Relationship to Zed
351
352Zed is a useful reference implementation for managed remote launch and reconnect behavior.
353
354The relevant lessons are:
355
356- remote bootstrap should be explicit
357- reconnect should be first-class
358- connection UX belongs in the client shell
359- runtime ownership should stay clearly on the remote host
360
361The important mismatch is transport shape.
362
363Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime.
364
365T3 should not copy that part.
366
367T3 already has the right runtime boundary:
368
369- one T3 server per environment
370- ordinary HTTP/WebSocket between client and environment
371
372So T3 should borrow Zed's launch discipline, not its transport protocol.
373
374## Recommended rollout
375
3761. First-class known environments and access endpoints.
3772. Direct `ws` / `wss` remote environments.
3783. Authenticated tunnel-backed environments.
3794. Desktop-managed SSH launch and forwarding.
3805. Multi-environment UI improvements after the base runtime path is proven.
381
382This ordering keeps the architecture network-first and transport-agnostic while still leaving room for richer managed remote flows.
.docs/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/scripts.md deleted-42
......@@ -1,42 +0,0 @@
1# Scripts
2
3- `bun run dev` — Starts contracts, server, and web in `turbo watch` mode.
4- `bun run dev:server` — Starts just the WebSocket server (uses Bun TypeScript execution).
5- `bun run dev:web` — Starts just the Vite dev server for the web app.
6- Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state.
7- Override server CLI-equivalent flags from root dev commands with `--`, for example:
8 `bun run dev -- --base-dir ~/.t3-2`
9- `bun run start` — Runs the production server (serves built web app as static files).
10- `bun run build` — Builds contracts, web app, and server through Turbo.
11- `bun run typecheck` — Strict TypeScript checks for all packages.
12- `bun run test` — Runs workspace tests.
13- `bun run dist:desktop:artifact -- --platform <mac|linux|win> --target <target> --arch <arch>` — Builds a desktop artifact for a specific platform/target/arch.
14- `bun run dist:desktop:dmg` — Builds a shareable macOS `.dmg` into `./release`.
15- `bun run dist:desktop:dmg:x64` — Builds an Intel macOS `.dmg`.
16- `bun run dist:desktop:linux` — Builds a Linux AppImage into `./release`.
17- `bun run dist:desktop:win` — Builds a Windows NSIS installer into `./release`.
18
19## Desktop `.dmg` packaging notes
20
21- Default build is unsigned/not notarized for local sharing.
22- The DMG build uses `assets/macos-icon-1024.png` as the production app icon source.
23- Desktop production windows load the bundled UI from `t3://app/index.html` (not a `127.0.0.1` document URL).
24- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an auth token for WebSocket/API traffic.
25- Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first launch.
26- To keep staging files for debugging package contents, run: `bun run dist:desktop:dmg -- --keep-stage`
27- To allow code-signing/notarization when configured in CI/secrets, add: `--signed`.
28- Windows `--signed` uses Azure Trusted Signing and expects:
29 `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`,
30 `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`.
31- Azure authentication env vars are also required (for example service principal with secret):
32 `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`.
33
34## Running multiple dev instances
35
36Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together.
37
38- Default ports: server `3773`, web `5733`
39- Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`)
40- Example: `T3CODE_DEV_INSTANCE=branch-a bun run dev:desktop`
41
42If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset.
.docs/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.
KEYBINDINGS.md deleted-103
......@@ -1,103 +0,0 @@
1# Keybindings
2
3T3 Code reads keybindings from:
4
5- `~/.t3/keybindings.json`
6
7The file must be a JSON array of rules:
8
9```json
10[
11 { "key": "mod+g", "command": "terminal.toggle" },
12 { "key": "mod+shift+g", "command": "terminal.new", "when": "terminalFocus" }
13]
14```
15
16See the full schema for more details: [`packages/contracts/src/keybindings.ts`](packages/contracts/src/keybindings.ts)
17
18## Defaults
19
20```json
21[
22 { "key": "mod+j", "command": "terminal.toggle" },
23 { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" },
24 { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
25 { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" },
26 { "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" },
27 { "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
28 { "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
29 { "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },
30 { "key": "mod+o", "command": "editor.openFavorite" }
31]
32```
33
34For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](apps/server/src/keybindings.ts)
35
36## Configuration
37
38### Rule Shape
39
40Each entry supports:
41
42- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d`
43- `command` (required): action ID
44- `when` (optional): boolean expression controlling when the shortcut is active
45
46Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server.
47
48### Available Commands
49
50- `terminal.toggle`: open/close terminal drawer
51- `terminal.split`: split terminal (in focused terminal context by default)
52- `terminal.new`: create new terminal (in focused terminal context by default)
53- `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
54- `commandPalette.toggle`: open or close the global command palette
55- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
56- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
57- `editor.openFavorite`: open current project/worktree in the last-used editor
58- `script.{id}.run`: run a project script by id (for example `script.test.run`)
59
60### Key Syntax
61
62Supported modifiers:
63
64- `mod` (`cmd` on macOS, `ctrl` on non-macOS)
65- `cmd` / `meta`
66- `ctrl` / `control`
67- `shift`
68- `alt` / `option`
69
70Examples:
71
72- `mod+j`
73- `mod+shift+d`
74- `ctrl+l`
75- `cmd+k`
76
77### `when` Conditions
78
79Currently available context keys:
80
81- `terminalFocus`
82- `terminalOpen`
83
84Supported operators:
85
86- `!` (not)
87- `&&` (and)
88- `||` (or)
89- parentheses: `(` `)`
90
91Examples:
92
93- `"when": "terminalFocus"`
94- `"when": "terminalOpen && !terminalFocus"`
95- `"when": "terminalFocus || terminalOpen"`
96
97Unknown condition keys evaluate to `false`.
98
99### Precedence
100
101- Rules are evaluated in array order.
102- For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins.
103- That means precedence is across commands, not only within the same command.
README.md+11-3
......@@ -46,11 +46,19 @@ We are very very early in this project. Expect bugs.
4646
4747We are not accepting contributions yet.
4848
49Observability guide: [docs/observability.md](./docs/observability.md)
49Observability guide: [docs/operations/observability.md](./docs/operations/observability.md)
5050
51Relay observability: [docs/relay-observability.md](./docs/relay-observability.md)
51Relay observability: [docs/operations/relay-observability.md](./docs/operations/relay-observability.md)
5252
53T3 Cloud Clerk setup: [docs/t3-cloud-clerk.md](./docs/t3-cloud-clerk.md)
53T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk.md)
54
55## Documentation
56
57- [Getting started](./docs/getting-started/quick-start.md)
58- [Architecture overview](./docs/architecture/overview.md)
59- [Provider guides](./docs/providers/codex.md)
60- [Operations](./docs/operations/ci.md)
61- [Reference](./docs/reference/encyclopedia.md)
5462
5563## If you REALLY want to contribute still.... read this first
5664
REMOTE.md deleted-199
......@@ -1,199 +0,0 @@
1# Remote Access
2
3Use this when you want to connect to a T3 Code server from another device such as a phone, tablet, or separate desktop app.
4
5## Recommended Setup
6
7Use a trusted private network that meshes your devices together, such as a tailnet.
8
9That gives you:
10
11- a stable address to connect to
12- transport security at the network layer
13- less exposure than opening the server to the public internet
14
15## Enabling Network Access
16
17There are two ways to expose your server for remote connections: from the desktop app or from the CLI.
18
19### Option 1: Desktop App
20
21If you are already running the desktop app and want to make it reachable from other devices:
22
231. 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.
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.
264. Use **Create Link** to generate a pairing link you can share with another device.
27
28The default endpoint controls the QR code and primary copy action for pairing links. You can change it from the expanded endpoint list. The preference is stored by endpoint type, so choosing the local LAN endpoint survives normal IP address changes when you move between networks.
29
30When no user default is saved, the app uses the built-in LAN endpoint for pairing links when
31available. You can set another endpoint as the default from the expanded endpoint list.
32
33- HTTPS/WSS-compatible endpoints work from `https://app.t3.codes`, but are not made the default
34 automatically.
35- Non-loopback HTTP endpoints are useful for direct LAN pairing.
36- Loopback-only endpoints are not useful for another device unless that device is the same machine.
37
38If the copied link points directly at `http://192.168.x.y:3773`, open it from a client that can reach that LAN address. If it points at `https://app.t3.codes/pair?...`, the hosted web app will save the environment and connect directly to the backend URL in the link.
39
40### Tailscale Endpoints
41
42When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list.
43
44Depending on your Tailscale setup, this may include:
45
46- the machine's `100.x.y.z` Tailnet IP
47- a MagicDNS name
48- an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend
49
50The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as
51`https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this
52backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend
53with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale
54Serve to proxy HTTPS traffic to the local backend.
55
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.
57
58For `https://app.t3.codes`, prefer an HTTPS Tailnet or other HTTPS endpoint. A plain `http://100.x.y.z:3773` endpoint can still work from a desktop client or another browser page served over HTTP, but it will not work from the hosted HTTPS app because of browser mixed-content rules.
59
60### Option 2: Headless Server (CLI)
61
62Use this when you want to run the server without a GUI, for example on a remote machine over SSH.
63
64Run the server with `t3 serve`.
65
66```bash
67npx t3 serve --host "$(tailscale ip -4)"
68```
69
70`t3 serve` starts the server without opening a browser and prints:
71
72- a connection string
73- a pairing token
74- a pairing URL
75- a QR code for the pairing URL
76
77From there, connect from another device in either of these ways:
78
79- scan the QR code on your phone
80- in the desktop app, enter the full pairing URL
81- in the desktop app, enter the host and token separately
82- in the hosted web app, open a hosted pairing URL when the backend is reachable over HTTPS
83
84Use `t3 serve --help` for the full flag reference. It supports the same general startup options as the normal server command, including an optional `cwd` argument.
85
86For hosted web pairing over Tailscale HTTPS, opt in to Tailscale Serve:
87
88```bash
89npx t3 serve --tailscale-serve
90```
91
92By default this configures Tailscale Serve on HTTPS port 443 and advertises
93`https://machine.tailnet.ts.net/`. Advanced users can choose a different HTTPS port:
94
95```bash
96npx t3 serve --tailscale-serve --tailscale-serve-port 8443
97```
98
99> Note
100> The GUIs do not currently support adding projects on remote environments.
101> For now, use `t3 project ...` on the server machine instead.
102> Full GUI support for remote project management is coming soon.
103
104### Option 3: Desktop-Managed SSH Launch
105
106Use this when you want the desktop app to start or reuse T3 Code on another machine over SSH.
107
1081. Open **Settings** → **Connections**.
1092. Under **Remote Environments**, choose **Add environment**.
1103. Select the SSH launch flow.
1114. Enter the SSH target, such as `user@example.com`.
1125. Confirm the launch. The desktop app probes the host, starts or reuses a remote T3 server, opens a local port forward, and saves the environment.
113
114After setup, the renderer connects to a local forwarded HTTP/WebSocket endpoint. The remote host still owns the actual T3 server, projects, files, git state, terminals, and provider sessions.
115
116SSH launch is a desktop feature because it needs local process and SSH access. Once the environment is paired and saved, it uses the same environment list and connection model as direct LAN, Tailscale, HTTPS, or future tunnel-backed environments.
117
118#### SSH Launch Troubleshooting
119
120The desktop SSH launcher connects with a non-interactive `sh` session, writes a small launcher script under `~/.t3/ssh-launch/<host-key>/`, starts or reuses a remote T3 server, and forwards the remote loopback port back to your desktop.
121
122The remote host must have a compatible Node.js runtime. T3 Code uses the server package's `engines.node` requirement:
123
124```text
125^22.16 || ^23.11 || >=24.10
126```
127
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:
129
130- `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
131- Volta via `~/.volta/bin`
132- asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh`
133- mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh`
134- fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh`
135- nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -`
136- nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts`
137- installed nvm versions under `$NVM_DIR/versions/node/*/bin`
138
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:
140
141```bash
142ssh user@example.com 'sh -lc "command -v node && node --version"'
143```
144
145If that does not print a compatible Node version, configure your version manager for non-interactive shells or install a compatible Node binary in one of the searched locations. For example, with nvm you may need a default alias:
146
147```bash
148nvm alias default 24
149```
150
151With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above.
152
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.
154
155## How Pairing Works
156
157The remote device does not need a long-lived secret up front.
158
159Instead:
160
1611. `t3 serve` issues a one-time owner pairing token.
1622. The remote device exchanges that token with the server.
1633. The server creates an authenticated session for that device.
164
165After pairing, future access is session-based. You do not need to keep reusing the original token unless you are pairing a new device.
166
167## Hosted Web App Pairing
168
169The hosted web app at `https://app.t3.codes` can save a remote backend in browser local storage from a URL like:
170
171```text
172https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE
173```
174
175Use hosted pairing when the backend is reachable from the browser over HTTPS/WSS. This includes a backend behind a trusted HTTPS tunnel or another HTTPS endpoint you operate.
176
177Do not use hosted pairing for plain HTTP LAN URLs such as `http://192.168.x.y:3773`. Browsers block an HTTPS page from connecting to an insecure HTTP or WS backend. For those endpoints, use the direct pairing URL shown by the desktop app or CLI from a client that can open that HTTP URL directly.
178
179Hosted pairing does not proxy traffic through T3 Code. The browser still connects directly to the backend URL in the pairing link.
180
181## Managing Access Later
182
183Use `t3 auth` to manage access after the initial pairing flow.
184
185Typical uses:
186
187- issue additional pairing credentials
188- inspect active sessions
189- revoke old pairing links or sessions
190
191Use `t3 auth --help` and the nested subcommand help pages for the full reference.
192
193## Security Notes
194
195- Treat pairing URLs and pairing tokens like passwords.
196- Prefer binding `--host` to a trusted private address, such as a Tailnet IP, instead of exposing the server broadly.
197- Anyone with a valid pairing credential can create a session until that credential expires or is revoked.
198- Hosted pairing links keep the credential in the URL hash so it is not sent to the hosted app server, but it can still be exposed through browser history, screenshots, logs, or copy/paste.
199- Use `t3 auth` to revoke credentials or sessions you no longer trust.
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
apps/mobile/README.md deleted-98
......@@ -1,98 +0,0 @@
1# T3 Code Mobile
2
3> [!WARNING]
4> T3 Code Mobile is currently in development and is not distributed yet. If you want to try it out, you can build it from source.
5
6## Quickstart
7
8> [!NOTE]
9> Uses native modules so using Expo Go is not supported. You need to use the Expo Dev Client.
10
11This app has three variants:
12
13- `development`: Expo dev client, installable side-by-side as `T3 Code Dev`
14- `preview`: persistent internal preview build, installable side-by-side as `T3 Code Preview`
15- `production`: store/release build as `T3 Code`
16
17Run commands from `apps/mobile`.
18
19T3 Cloud is optional and disabled in a fresh clone. Public configuration belongs in the
20repository-root `.env` or `.env.local`, not an `apps/mobile/.env` file. See
21[`../../.env.example`](../../.env.example).
22
23## Development
24
25Start Metro for the dev client:
26
27```bash
28vp run dev:client
29```
30
31Build and run the local iOS dev client:
32
33```bash
34vp run ios:dev
35```
36
37Build and run the local iOS preview app:
38
39```bash
40vp run ios:preview
41```
42
43Force the review diff highlighter engine:
44
45```bash
46EXPO_PUBLIC_REVIEW_HIGHLIGHTER_ENGINE=javascript vp run ios:dev
47```
48
49`javascript` is the default and recommended setting for the review diff screen. Set `EXPO_PUBLIC_REVIEW_HIGHLIGHTER_ENGINE=native` only when you explicitly want to test the native Shiki engine.
50
51Inspect the resolved Expo config for a variant:
52
53```bash
54vp run config:dev
55vp run config:preview
56```
57
58Run static checks for mobile native code:
59
60```bash
61node ../../scripts/mobile-native-static-check.ts
62```
63
64The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. Missing native tools are reported as warnings and skipped locally. CI installs the default toolset from `apps/mobile/Brewfile` before running the native checks.
65
66## EAS Builds
67
68CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy.
69
70For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`,
71`T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL`
72as EAS environment variables. Expo config maps the canonical values into the mobile build.
73
74Create a PR preview dev-client build manually:
75
76```bash
77vp run eas:ios:preview:dev
78```
79
80Create a cloud dev-client build:
81
82```bash
83vp run eas:ios:dev
84```
85
86Create a persistent preview build:
87
88```bash
89vp run eas:ios:preview
90```
91
92Android equivalents:
93
94```bash
95vp run eas:android:dev
96vp run eas:android:preview:dev
97vp run eas:android:preview
98```
apps/mobile/modules/t3-terminal/README.md deleted-38
......@@ -1,38 +0,0 @@
1# T3 Mobile Terminal Native Module
2
3This local Expo module owns the native terminal surface for the mobile app.
4
5The JavaScript contract is intentionally small:
6
7- input from the native surface is emitted as `{ data: string }`
8- resize from the native surface is emitted as `{ cols: number, rows: number }`
9- remote PTY output is delivered by the existing `WsRpcClient.terminal` RPC stream
10
11The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the Ghostty custom-I/O
12fork, with T3's iOS 16 compatibility patch applied. `T3TerminalView` owns a `libghostty` surface and
13uses that callback I/O model:
14
151. initialize libghostty once for the process
162. create one Ghostty app and surface per native view
173. feed remote output into the surface with `ghostty_surface_feed_data`
184. send user input back to JS with the write callback
195. emit Ghostty's measured terminal size through `onResize`
20
21Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the
22React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a
23future Android Ghostty build without changing JS.
24
25Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`.
26
27## Rebuilding GhosttyKit
28
29The checked-in `GhosttyKit.xcframework` is built from the Ghostty custom-I/O fork (https://github.com/Yash-Singh1/ghostty/tree/custom-io).
30Set the directory to the cloned repository checked out on the `custom-io` branch to `GHOSTTY_SOURCE_DIR`.
31
32```bash
33apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh
34```
35
36The script builds Ghostty with Zig 0.15.2, strips the iOS archives, and replaces only the
37`ios-arm64` and `ios-arm64-simulator` slices. Xcode's Metal toolchain must be installed; if `metal`
38fails, run `xcodebuild -downloadComponent MetalToolchain`.
docs/README.md created+11
......@@ -0,0 +1,11 @@
1# Documentation
2
3- [Getting started](./getting-started/quick-start.md)
4- [Architecture](./architecture/overview.md)
5- [Cloud](./cloud/t3-cloud-clerk.md)
6- [Integrations](./integrations/source-control-providers.md)
7- [Mobile](./mobile/app.md)
8- [Operations](./operations/ci.md)
9- [Providers](./providers/codex.md)
10- [Reference](./reference/encyclopedia.md)
11- [User guides](./user/keybindings.md)
docs/architecture/overview.md created+143
......@@ -0,0 +1,143 @@
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## Event Lifecycle
41
42### Startup and client connect
43
44```mermaid
45sequenceDiagram
46 participant Browser
47 participant Transport as WsTransport
48 participant Server as wsServer
49 participant Layers as serverLayers
50 participant Ready as ServerReadiness
51 participant Push as ServerPushBus
52
53 Browser->>Transport: Load app and open WebSocket
54 Transport->>Server: Connect
55 Server->>Layers: Start runtime services
56 Server->>Ready: Wait for startup barriers
57 Ready-->>Server: Ready
58 Server->>Push: Publish server.welcome
59 Push-->>Transport: Ordered welcome push
60 Transport-->>Browser: Hydrate initial state
61```
62
631. The browser boots [`WsTransport`][1] and registers typed listeners in [`wsNativeApi`][2].
642. The server accepts the connection in [`wsServer`][3] and brings up the runtime graph defined in [`serverLayers`][7].
653. [`ServerReadiness`][4] waits until the key startup barriers are complete.
664. Once the server is ready, [`wsServer`][3] sends `server.welcome` from the contracts in [`ws.ts`][6] through [`ServerPushBus`][5].
675. The browser receives that ordered push through [`WsTransport`][1], and [`wsNativeApi`][2] uses it to seed local client state.
68
69### User turn flow
70
71```mermaid
72sequenceDiagram
73 participant Browser
74 participant Transport as WsTransport
75 participant Server as wsServer
76 participant Provider as ProviderService
77 participant Codex as codex app-server
78 participant Ingest as ProviderRuntimeIngestion
79 participant Engine as OrchestrationEngine
80 participant Push as ServerPushBus
81
82 Browser->>Transport: Send user action
83 Transport->>Server: Typed WebSocket request
84 Server->>Provider: Route request
85 Provider->>Codex: JSON-RPC over stdio
86 Codex-->>Ingest: Provider runtime events
87 Ingest->>Engine: Normalize into orchestration events
88 Engine-->>Server: Domain events
89 Server->>Push: Publish orchestration.domainEvent
90 Push-->>Browser: Typed push
91```
92
931. A user action in the browser becomes a typed request through [`WsTransport`][1] and the browser API layer in [`nativeApi`][12].
942. [`wsServer`][3] decodes that request using the shared WebSocket contracts in [`ws.ts`][6] and routes it to the right service.
953. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio.
964. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events.
975. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events.
986. [`wsServer`][3] pushes those updates to the browser through [`ServerPushBus`][5] on channels defined in [`orchestration.ts`][11].
99
100### Async completion flow
101
102```mermaid
103sequenceDiagram
104 participant Server as wsServer
105 participant Worker as Queue-backed workers
106 participant Cmd as ProviderCommandReactor
107 participant Checkpoint as CheckpointReactor
108 participant Receipt as RuntimeReceiptBus
109 participant Push as ServerPushBus
110 participant Browser
111
112 Server->>Worker: Enqueue follow-up work
113 Worker->>Cmd: Process provider commands
114 Worker->>Checkpoint: Process checkpoint tasks
115 Checkpoint->>Receipt: Publish completion receipt
116 Cmd-->>Server: Produce orchestration changes
117 Checkpoint-->>Server: Produce orchestration changes
118 Server->>Push: Publish resulting state updates
119 Push-->>Browser: User-visible push
120```
121
1221. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14].
1232. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic.
1243. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence.
1254. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers.
1265. Any user-visible state changes produced by that async work still go back through [`wsServer`][3] and [`ServerPushBus`][5].
127
128[1]: ../apps/web/src/wsTransport.ts
129[2]: ../apps/web/src/wsNativeApi.ts
130[3]: ../apps/server/src/wsServer.ts
131[4]: ../apps/server/src/wsServer/readiness.ts
132[5]: ../apps/server/src/wsServer/pushBus.ts
133[6]: ../packages/contracts/src/ws.ts
134[7]: ../apps/server/src/serverLayers.ts
135[8]: ../apps/server/src/provider/Layers/ProviderService.ts
136[9]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
137[10]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts
138[11]: ../packages/contracts/src/orchestration.ts
139[12]: ../apps/web/src/nativeApi.ts
140[13]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
141[14]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts
142[15]: ../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts
143[16]: ../packages/shared/src/DrainableWorker.ts
docs/architecture/providers.md created+30
......@@ -0,0 +1,30 @@
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 created+382
......@@ -0,0 +1,382 @@
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## Security model
335
336Remote support must assume that some environments will be reachable over untrusted networks.
337
338That means:
339
340- remote-capable environments should require explicit authentication
341- tunnel exposure should not rely on obscurity
342- client-saved endpoints should carry enough auth metadata to reconnect safely
343
344T3 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.
345
346For publicly reachable environments, authenticated access should be treated as required.
347
348Hosted 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.
349
350## Relationship to Zed
351
352Zed is a useful reference implementation for managed remote launch and reconnect behavior.
353
354The relevant lessons are:
355
356- remote bootstrap should be explicit
357- reconnect should be first-class
358- connection UX belongs in the client shell
359- runtime ownership should stay clearly on the remote host
360
361The important mismatch is transport shape.
362
363Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime.
364
365T3 should not copy that part.
366
367T3 already has the right runtime boundary:
368
369- one T3 server per environment
370- ordinary HTTP/WebSocket between client and environment
371
372So T3 should borrow Zed's launch discipline, not its transport protocol.
373
374## Recommended rollout
375
3761. First-class known environments and access endpoints.
3772. Direct `ws` / `wss` remote environments.
3783. Authenticated tunnel-backed environments.
3794. Desktop-managed SSH launch and forwarding.
3805. Multi-environment UI improvements after the base runtime path is proven.
381
382This ordering keeps the architecture network-first and transport-agnostic while still leaving room for richer managed remote flows.
docs/architecture/runtime-modes.md created+6
......@@ -0,0 +1,6 @@
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/cloud/environment-auth.md created+113
......@@ -0,0 +1,113 @@
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-cloud-clerk.md created+174
......@@ -0,0 +1,174 @@
1# T3 Cloud Clerk Setup
2
3T3 Cloud 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 Cloud 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 cloud` 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 cloud` 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 cloud login
79t3 cloud link
80t3 cloud status
81t3 cloud unlink
82t3 cloud logout
83t3 serve
84```
85
86`t3 cloud login` opens the Clerk authorization flow and stores the CLI credential without enabling
87cloud exposure. `t3 cloud 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 cloud 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 cloud link` can re-enable exposure without another browser flow. `t3 cloud
93logout` performs the same cleanup and removes the stored CLI authorization.
94
95The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH,
96forward that port before running `t3 cloud login` or `t3 cloud link`:
97
98```sh
99ssh -L 34338:127.0.0.1:34338 <host>
100```
101
102A relay-hosted callback broker can remove this port-forward requirement later without changing the
103stored PKCE token model.
104
105## JWT Template
106
107In **Clerk Dashboard > JWT templates**, create a template with:
108
109| Setting | Value |
110| ------- | ---------------------------- |
111| Name | `t3-relay` |
112| Claims | `{ "aud": "t3-code-relay" }` |
113
114Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set
115`CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and
116`CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value
117is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still
118selects the concrete relay deployment, but changing that URL does not require a JWT template change.
119
120## Desktop OAuth Redirect Allowlist
121
122The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme.
123In **Clerk Dashboard > Native applications**, enable native application support and add these
124entries under the mobile SSO redirect allowlist:
125
126```text
127t3code-dev://auth/callback
128t3code://auth/callback
129```
130
131The first entry is for local desktop development. The second is for packaged desktop builds.
132The app also adds a request-scoped `t3_state` query parameter and validates it on callback. Initial
133sign-in and linked-account OAuth flows both return through this bridge. The desktop provider keeps
134Clerk's stock profile component, replaces its renderer-page callback with the custom-scheme callback,
135and opens the provider URL in the system browser. Do not add the local renderer URL as an OAuth
136redirect: an external browser cannot use it to reopen the packaged app.
137
138The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth
139flow uses a custom redirect URI, add that exact URI to the same allowlist.
140
141## Enable Waitlist Access
142
143For a private beta where people should request access, use **Clerk Dashboard > Waitlist**:
144
1451. Toggle on **Enable waitlist** and save.
1462. Review requests on the same page and select **Invite** or **Deny**.
147
148Signed-out web and desktop users see Clerk's waitlist enrollment as the T3 Cloud page content,
149while approved signed-in users see cloud settings. The browser app also uses `/settings/cloud` as
150its Clerk waitlist URL.
151
152On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the
153Settings form sheet. It submits enrollment through Clerk's `useWaitlist()` flow because the prebuilt
154`<Waitlist />` component is web-only in the Expo SDK. Approved users can use **Sign in** from that
155screen.
156
157## Alternative: Known-User Allowlist
158
159For a closed beta where all permitted users are known in advance, use an allowlist instead of a
160request-and-approval waitlist:
161
162To restrict the beta to permitted email addresses or domains:
163
1641. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email
165 domain.
1662. Enable the allowlist and save.
1673. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or
168 manually created without a waitlist request flow.
169
170Do not enable an empty allowlist: it blocks all new sign-ups.
171
172Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud
173access. To remove an already-created user's access, ban that user in Clerk so their active
174sessions are ended and future sign-ins are rejected.
docs/cloud/t3-code-cloud-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 Code Cloud 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 Code Cloud Control Plane and Managed Endpoint Flow</h1>
257 <p class="lede">
258 T3 Cloud links a locally authorized T3 environment to a signed-in cloud user, provisions a
259 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 Cloud 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 Cloud 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/cloud/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/cloud/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 cloud 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 cloud 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 cloud login</code> stores authorization without enabling exposure.
611 <code>t3 cloud unlink</code> disables exposure while retaining authorization.
612 <code>t3 cloud 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-cloud/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/cloud/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/cloud/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/cloud/link-state</code></td>
955 <td>Local environment token with <code>relay:read</code></td>
956 <td>Read the environment's current T3 Cloud link state.</td>
957 </tr>
958 <tr>
959 <td><code>POST /api/cloud/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/cloud/unlink</code></td>
965 <td>Local environment token with <code>relay:write</code></td>
966 <td>Stop the relay client and clear local T3 Cloud configuration.</td>
967 </tr>
968 <tr>
969 <td><code>POST /api/t3-cloud/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-cloud/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/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L554)
60- [x] [makeClaudeAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L913)
61- [x] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414)
62- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887)
63- [x] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975)
64- [x] [readThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984)
65- [x] [rollbackThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990)
66- [x] [stopSession](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L513)
72- [x] [handleTraceLine](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L324)
73- [x] [emitCompleteLines](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L455)
74- [x] [commit](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1190)
75- [x] [pushCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1223)
76- [x] [pullCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1323)
77- [x] [checkoutBranch](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L387)
83- [x] [materializePullRequestHeadBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L428)
84- [x] [findOpenPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L576)
85- [x] [findLatestPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L602)
86- [x] [runCommitStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L728)
87- [x] [runPrStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L842)
88- [x] [runFeatureBranchStep](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161)
94- [x] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357)
95- [x] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L134)
104- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L196)
105- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L255)
106- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L284)
107- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L347)
108- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L393)
109- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L411)
110- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L430)
111- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L445)
112- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L466)
113- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L516)
114- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L538)
115
116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)
117
118- [x] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)
119- [x] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)
120- [x] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)
121- [x] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)
122- [x] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1317)
128- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1399)
129- [x] [writeNativeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1546)
130- [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555)
131- [x] Remaining nested callback wrappers in this file
132
133### `apps/server/src/checkpointing/Layers/CheckpointStore.ts` (`10`)
134
135- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L89)
136- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L183)
137- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L220)
138- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L252)
139- [ ] Nested callback wrappers in this file
140
141### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`)
142
143- [ ] [toLogMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77)
144- [ ] [makeThreadWriter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102)
145- [ ] [makeEventNdjsonLogger](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174)
146- [ ] [write](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231)
147- [ ] [close](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L125)
153- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L170)
154- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L221)
155- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L239)
156
157### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`)
158
159- [ ] [processEnvelope](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64)
160- [ ] [dispatch](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218)
161- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162)
162- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L242)
167- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L336)
168- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L397)
169- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L446)
170- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L478)
171
172### Smaller clusters
173
174- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`)
175- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`)
176- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`)
177- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`)
178- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`)
179- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`)
180- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`)
181- [ ] [apps/server/src/keybindings.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/keybindings.ts) (`4`)
182- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`)
183- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`)
184- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`)
185- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`)
186- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`)
187- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`)
188- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`)
189- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`)
190- [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`)
191- [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`)
192- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`)
193- [ ] [apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts) (`2`)
194- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`)
195
196```
197
198```
docs/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/getting-started/codex-prerequisites.md created+5
......@@ -0,0 +1,5 @@
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 created+22
......@@ -0,0 +1,22 @@
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 created+127
......@@ -0,0 +1,127 @@
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/mobile/app.md created+98
......@@ -0,0 +1,98 @@
1# T3 Code Mobile
2
3> [!WARNING]
4> T3 Code Mobile is currently in development and is not distributed yet. If you want to try it out, you can build it from source.
5
6## Quickstart
7
8> [!NOTE]
9> Uses native modules so using Expo Go is not supported. You need to use the Expo Dev Client.
10
11This app has three variants:
12
13- `development`: Expo dev client, installable side-by-side as `T3 Code Dev`
14- `preview`: persistent internal preview build, installable side-by-side as `T3 Code Preview`
15- `production`: store/release build as `T3 Code`
16
17Run commands from `apps/mobile`.
18
19T3 Cloud is optional and disabled in a fresh clone. Public configuration belongs in the
20repository-root `.env` or `.env.local`, not an `apps/mobile/.env` file. See
21[`../../.env.example`](../../.env.example).
22
23## Development
24
25Start Metro for the dev client:
26
27```bash
28vp run dev:client
29```
30
31Build and run the local iOS dev client:
32
33```bash
34vp run ios:dev
35```
36
37Build and run the local iOS preview app:
38
39```bash
40vp run ios:preview
41```
42
43Force the review diff highlighter engine:
44
45```bash
46EXPO_PUBLIC_REVIEW_HIGHLIGHTER_ENGINE=javascript vp run ios:dev
47```
48
49`javascript` is the default and recommended setting for the review diff screen. Set `EXPO_PUBLIC_REVIEW_HIGHLIGHTER_ENGINE=native` only when you explicitly want to test the native Shiki engine.
50
51Inspect the resolved Expo config for a variant:
52
53```bash
54vp run config:dev
55vp run config:preview
56```
57
58Run static checks for mobile native code:
59
60```bash
61node ../../scripts/mobile-native-static-check.ts
62```
63
64The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. Missing native tools are reported as warnings and skipped locally. CI installs the default toolset from `apps/mobile/Brewfile` before running the native checks.
65
66## EAS Builds
67
68CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy.
69
70For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`,
71`T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL`
72as EAS environment variables. Expo config maps the canonical values into the mobile build.
73
74Create a PR preview dev-client build manually:
75
76```bash
77vp run eas:ios:preview:dev
78```
79
80Create a cloud dev-client build:
81
82```bash
83vp run eas:ios:dev
84```
85
86Create a persistent preview build:
87
88```bash
89vp run eas:ios:preview
90```
91
92Android equivalents:
93
94```bash
95vp run eas:android:dev
96vp run eas:android:preview:dev
97vp run eas:android:preview
98```
docs/mobile/terminal-native-module.md created+38
......@@ -0,0 +1,38 @@
1# T3 Mobile Terminal Native Module
2
3This local Expo module owns the native terminal surface for the mobile app.
4
5The JavaScript contract is intentionally small:
6
7- input from the native surface is emitted as `{ data: string }`
8- resize from the native surface is emitted as `{ cols: number, rows: number }`
9- remote PTY output is delivered by the existing `WsRpcClient.terminal` RPC stream
10
11The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the Ghostty custom-I/O
12fork, with T3's iOS 16 compatibility patch applied. `T3TerminalView` owns a `libghostty` surface and
13uses that callback I/O model:
14
151. initialize libghostty once for the process
162. create one Ghostty app and surface per native view
173. feed remote output into the surface with `ghostty_surface_feed_data`
184. send user input back to JS with the write callback
195. emit Ghostty's measured terminal size through `onResize`
20
21Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the
22React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a
23future Android Ghostty build without changing JS.
24
25Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`.
26
27## Rebuilding GhosttyKit
28
29The checked-in `GhosttyKit.xcframework` is built from the Ghostty custom-I/O fork (https://github.com/Yash-Singh1/ghostty/tree/custom-io).
30Set the directory to the cloned repository checked out on the `custom-io` branch to `GHOSTTY_SOURCE_DIR`.
31
32```bash
33apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh
34```
35
36The script builds Ghostty with Zig 0.15.2, strips the iOS archives, and replaces only the
37`ios-arm64` and `ios-arm64-simulator` slices. Xcode's Metal toolchain must be installed; if `metal`
38fails, run `xcodebuild -downloadComponent MetalToolchain`.
docs/observability.md deleted-519
......@@ -1,519 +0,0 @@
1# Observability
2
3T3 Code has one server-side observability model:
4
5- pretty logs go to stdout for humans
6- 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 LGTM
8
9The local trace file is the persisted source of truth. There is no separate persisted server log file anymore.
10
11## Where To Find Things
12
13### Logs
14
15Logs are human-facing only:
16
17- destination: stdout
18- format: `Logger.consolePretty()`
19- persistence: none
20
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.
22
23### Traces
24
25Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`).
26
27Important fields in each record:
28
29- `name`: span name
30- `traceId`, `spanId`, `parentSpanId`: correlation
31- `durationMs`: elapsed time
32- `attributes`: structured context
33- `events`: embedded logs and custom events
34- `exit`: `Success`, `Failure`, or `Interrupted`
35
36The schema lives in `apps/server/src/observability/TraceRecord.ts`.
37
38### Metrics
39
40Metrics are not written to a local file.
41
42- local persistence: none
43- remote export: OTLP only, when configured
44- current definitions: `apps/server/src/observability/Metrics.ts`
45
46If OTLP is not configured, metrics still exist in-process, but you will not have a local artifact to inspect.
47
48### Related Artifacts
49
50Provider event NDJSON files still exist for provider runtime streams. Those are separate from the main server trace file.
51
52## Run The Server In Instrumented Mode
53
54There are two useful modes:
55
56- local-only: stdout + local `server.trace.ndjson`
57- full local observability: stdout + local trace file + OTLP export to Grafana/Tempo/Prometheus
58
59The local trace file is always on. OTLP export is opt-in.
60
61### Option 1: Local Traces Only
62
63You do not need any extra env vars. Just run the app normally and inspect `server.trace.ndjson`.
64
65Examples:
66
67```bash
68npx t3
69```
70
71```bash
72node --run dev
73```
74
75```bash
76node --run dev:desktop
77```
78
79### Option 2: Run With A Local LGTM Stack
80
81#### 1. Start Grafana LGTM
82
83```bash
84docker run --name lgtm \
85 -p 3000:3000 \
86 -p 4317:4317 \
87 -p 4318:4318 \
88 --rm -ti \
89 grafana/otel-lgtm
90```
91
92Then open `http://localhost:3000`.
93
94Default Grafana login:
95
96- username: `admin`
97- password: `admin`
98
99#### 2. Export OTLP env vars
100
101```bash
102export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
103export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
104export T3CODE_OTLP_SERVICE_NAME=t3-local
105```
106
107Optional:
108
109```bash
110export T3CODE_TRACE_MIN_LEVEL=Info
111export T3CODE_TRACE_TIMING_ENABLED=true
112```
113
114#### 3. Launch the app from that same shell
115
116CLI:
117
118```bash
119npx t3
120```
121
122Monorepo web/server dev:
123
124```bash
125node --run dev
126```
127
128Monorepo desktop dev:
129
130```bash
131node --run dev:desktop
132```
133
134Packaged desktop app:
135
136Launch the actual app executable from the same shell so the desktop app and embedded backend inherit `T3CODE_OTLP_*`.
137
138macOS app bundle example:
139
140```bash
141T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
142T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
143T3CODE_OTLP_SERVICE_NAME=t3-desktop \
144"/Applications/T3 Code.app/Contents/MacOS/T3 Code"
145```
146
147Direct binary example:
148
149```bash
150T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
151T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
152T3CODE_OTLP_SERVICE_NAME=t3-desktop \
153./path/to/your/desktop-app-binary
154```
155
156Do not rely on launching from Finder, Spotlight, the dock, or the Start menu after setting shell env vars. Those launches usually will not pick them up.
157
158#### 4. Fully restart after changing env
159
160The backend reads observability config at process start. If you change OTLP env vars, stop the app completely and start it again.
161
162## How To Use Traces And Metrics To Debug The Server
163
164### Start With The Local Trace File
165
166The trace file is the fastest way to inspect raw span data.
167
168Tail it:
169
170```bash
171tail -f "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
172```
173
174In monorepo dev, use:
175
176```bash
177tail -f ./dev/logs/server.trace.ndjson
178```
179
180Show failed spans:
181
182```bash
183jq -c 'select(.exit._tag != "Success") | {
184 name,
185 durationMs,
186 exit,
187 attributes
188}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
189```
190
191Show slow spans:
192
193```bash
194jq -c 'select(.durationMs > 1000) | {
195 name,
196 durationMs,
197 traceId,
198 spanId
199}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
200```
201
202Inspect embedded log events:
203
204```bash
205jq -c 'select(any(.events[]?; .attributes["effect.logLevel"] != null)) | {
206 name,
207 durationMs,
208 events: [
209 .events[]
210 | select(.attributes["effect.logLevel"] != null)
211 | {
212 message: .name,
213 level: .attributes["effect.logLevel"]
214 }
215 ]
216}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
217```
218
219Follow one trace:
220
221```bash
222jq -r 'select(.traceId == "TRACE_ID_HERE") | [
223 .name,
224 .spanId,
225 (.parentSpanId // "-"),
226 .durationMs
227] | @tsv' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
228```
229
230Filter orchestration commands:
231
232```bash
233jq -c 'select(.attributes["orchestration.command_type"] != null) | {
234 name,
235 durationMs,
236 commandType: .attributes["orchestration.command_type"],
237 aggregateKind: .attributes["orchestration.aggregate_kind"]
238}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
239```
240
241Filter git activity:
242
243```bash
244jq -c 'select(.attributes["git.operation"] != null) | {
245 name,
246 durationMs,
247 operation: .attributes["git.operation"],
248 cwd: .attributes["git.cwd"],
249 hookEvents: [
250 .events[]
251 | select(.name == "git.hook.started" or .name == "git.hook.finished")
252 ]
253}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
254```
255
256### Use Tempo When You Need A Real Trace Viewer
257
258Tempo is better than raw NDJSON when you want to:
259
260- search across many traces
261- inspect parent/child relationships visually
262- compare many slow traces
263- drill into one failing request without hand-joining by `traceId`
264
265Recommended flow in Grafana:
266
2671. Open `Explore`.
2682. Pick the `Tempo` data source.
2693. Set the time range to something recent like `Last 15 minutes`.
2704. Start broad. Do not begin with a very narrow query.
2715. Look for spans from your configured service name, then narrow by span name or attributes.
272
273Good first searches:
274
275- service name such as `t3-local`, `t3-dev`, or `t3-desktop`
276- span names like `sql.execute`, `git.runCommand`, `provider.sendTurn`
277- orchestration spans with attributes like `orchestration.command_type`
278
279Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful.
280
281### Use Metrics To See Systemic Problems
282
283Traces are best for one request. Metrics are best for trends.
284
285Good metric families to watch:
286
287- `t3_rpc_request_duration`
288- `t3_orchestration_command_duration`
289- `t3_orchestration_command_ack_duration`
290- `t3_provider_turn_duration`
291- `t3_git_command_duration`
292- `t3_db_query_duration`
293
294Counters tell you volume and failure rate:
295
296- `t3_rpc_requests_total`
297- `t3_orchestration_commands_total`
298- `t3_provider_turns_total`
299- `t3_git_commands_total`
300- `t3_db_queries_total`
301
302Use metrics when the question is:
303
304- "is this always slow?"
305- "did this get worse after a change?"
306- "which command type is failing most often?"
307
308Use traces when the question is:
309
310- "what happened in this specific request?"
311- "which child span caused this one slow interaction?"
312- "what logs were emitted inside the failing flow?"
313
314### What The New Ack Metric Means
315
316`t3_orchestration_command_ack_duration` measures:
317
318- start: command dispatch enters the orchestration engine
319- end: the first committed domain event for that command is published by the server
320
321That is a server-side acknowledgment metric. It does not measure:
322
323- websocket transit to the browser
324- client receipt
325- React render time
326
327If you need those later, add client-side instrumentation or a dedicated server fanout metric.
328
329## Common Workflows
330
331### "Why did this request fail?"
332
3331. Start with the local NDJSON file.
3342. Find spans where `exit._tag != "Success"`.
3353. Group by `traceId`.
3364. Inspect sibling spans and span events.
3375. If needed, move to Tempo for the full trace tree.
338
339### "Why is the UI feeling slow?"
340
3411. Search for slow top-level spans in the trace file or Tempo.
3422. Check child spans for sqlite, git, provider, or terminal work.
3433. Look at the matching duration metrics to see whether the slowness is systemic.
344
345### "Did this command take too long to acknowledge?"
346
3471. Check `t3_orchestration_command_ack_duration` by `commandType`.
3482. If it is high, inspect the corresponding orchestration trace.
3493. Look at child spans for projection, sqlite, provider, or git work.
350
351### "Are git hooks causing latency?"
352
3531. Filter `git.operation` spans.
3542. Inspect `git.hook.started` and `git.hook.finished` events.
3553. Compare hook timing to the enclosing git span duration.
356
357### "Why do I have spans locally but nothing in Grafana?"
358
359Usually one of these is true:
360
361- `T3CODE_OTLP_TRACES_URL` was not set
362- the app was launched from a different environment than the one where you exported the vars
363- the app was not fully restarted after changing env
364- Grafana is looking at the wrong time range or service name
365
366If the local NDJSON file is updating, local tracing is working. The problem is almost always OTLP export configuration or process startup.
367
368## How To Think About Adding Tracing To Future Code
369
370### Prefer Boundaries Over Tiny Helpers
371
372Good span boundaries:
373
374- RPC methods
375- orchestration command handling
376- provider adapter calls
377- external process calls
378- persistence writes
379- queue handoffs
380
381Avoid tracing every tiny helper. Most helpers should inherit the active span rather than create a new one.
382
383### Reuse `Effect.fn(...)` Where It Already Exists
384
385The codebase already uses `Effect.fn("name")` heavily. That should usually be your first tracing boundary.
386
387For ad hoc work:
388
389```ts
390import { Effect } from "effect";
391
392const runThing = Effect.gen(function* () {
393 yield* Effect.annotateCurrentSpan({
394 "thing.id": "abc123",
395 "thing.kind": "example",
396 });
397
398 yield* Effect.logInfo("starting thing");
399 return yield* doWork();
400}).pipe(Effect.withSpan("thing.run"));
401```
402
403### Put High-Cardinality Detail On Spans
404
405Use span annotations for IDs, paths, and other detailed context:
406
407```ts
408yield *
409 Effect.annotateCurrentSpan({
410 "provider.thread_id": input.threadId,
411 "provider.request_id": input.requestId,
412 "git.cwd": input.cwd,
413 });
414```
415
416### Keep Metric Labels Low Cardinality
417
418Good metric labels:
419
420- operation kind
421- method name
422- provider kind
423- aggregate kind
424- outcome
425
426Bad metric labels:
427
428- raw thread IDs
429- command IDs
430- file paths
431- cwd
432- full prompts
433- full model strings when a normalized family label would do
434
435Detailed context belongs on spans, not metrics.
436
437### Use Logs As Span Events
438
439Logs inside a span become part of the trace story:
440
441```ts
442yield * Effect.logInfo("starting provider turn");
443yield * Effect.logDebug("waiting for approval response");
444```
445
446Those messages show up as span events because `Logger.tracerLogger` is installed.
447
448### Use The Pipeable Metrics API
449
450`withMetrics(...)` is the default way to attach a counter and timer to an effect:
451
452```ts
453import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts";
454
455const program = doWork().pipe(
456 withMetrics({
457 counter: someCounter,
458 timer: someDuration,
459 attributes: {
460 operation: "work",
461 },
462 }),
463);
464```
465
466## Detailed API Reference
467
468### Runtime Wiring
469
470The server observability layer is assembled in `apps/server/src/observability/Layers/Observability.ts`.
471
472It provides:
473
474- pretty stdout logger
475- `Logger.tracerLogger`
476- local NDJSON tracer
477- optional OTLP trace exporter
478- optional OTLP metrics exporter
479- Effect trace-level and timing refs
480
481### Env Vars
482
483Local trace file:
484
485- `T3CODE_TRACE_FILE`: override trace file path
486- `T3CODE_TRACE_MAX_BYTES`: per-file rotation size, default `10485760`
487- `T3CODE_TRACE_MAX_FILES`: rotated file count, default `10`
488- `T3CODE_TRACE_BATCH_WINDOW_MS`: flush window, default `200`
489- `T3CODE_TRACE_MIN_LEVEL`: minimum trace level, default `Info`
490- `T3CODE_TRACE_TIMING_ENABLED`: enable timing metadata, default `true`
491
492OTLP export:
493
494- `T3CODE_OTLP_TRACES_URL`: OTLP trace endpoint
495- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint
496- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`
497- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server`
498
499If the OTLP URLs are unset, local tracing still works and metrics stay in-process only.
500
501### What Is Instrumented Today
502
503Current high-value span and metric boundaries include:
504
505- Effect RPC websocket request spans from `effect/rpc`
506- RPC request metrics in `apps/server/src/observability/RpcInstrumentation.ts`
507- startup phases
508- orchestration command processing
509- orchestration command acknowledgment latency
510- provider session and turn operations
511- git command execution and git hook events
512- terminal session lifecycle
513- sqlite query execution
514
515### Current Constraints
516
517- logs outside spans are not persisted
518- metrics are not snapshotted locally
519- the old `serverLogPath` still exists in config for compatibility, but the trace file is the persisted artifact that matters
docs/operations/ci.md created+6
......@@ -0,0 +1,6 @@
1# CI quality gates
2
3- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun 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 secrets are present: Apple credentials for macOS and Azure Trusted Signing credentials for Windows. Without secrets, it still releases unsigned artifacts.
6- See [Release Checklist](./release.md) for the full release/signing setup checklist.
docs/operations/effect-fn-checklist.md created+198
......@@ -0,0 +1,198 @@
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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L554)
60- [x] [makeClaudeAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L913)
61- [x] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414)
62- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887)
63- [x] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975)
64- [x] [readThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984)
65- [x] [rollbackThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990)
66- [x] [stopSession](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L513)
72- [x] [handleTraceLine](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L324)
73- [x] [emitCompleteLines](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L455)
74- [x] [commit](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1190)
75- [x] [pushCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1223)
76- [x] [pullCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1323)
77- [x] [checkoutBranch](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L387)
83- [x] [materializePullRequestHeadBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L428)
84- [x] [findOpenPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L576)
85- [x] [findLatestPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L602)
86- [x] [runCommitStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L728)
87- [x] [runPrStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L842)
88- [x] [runFeatureBranchStep](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161)
94- [x] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357)
95- [x] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L134)
104- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L196)
105- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L255)
106- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L284)
107- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L347)
108- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L393)
109- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L411)
110- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L430)
111- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L445)
112- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L466)
113- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L516)
114- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L538)
115
116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)
117
118- [x] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)
119- [x] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)
120- [x] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)
121- [x] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)
122- [x] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1317)
128- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1399)
129- [x] [writeNativeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1546)
130- [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555)
131- [x] Remaining nested callback wrappers in this file
132
133### `apps/server/src/checkpointing/Layers/CheckpointStore.ts` (`10`)
134
135- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L89)
136- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L183)
137- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L220)
138- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L252)
139- [ ] Nested callback wrappers in this file
140
141### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`)
142
143- [ ] [toLogMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77)
144- [ ] [makeThreadWriter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102)
145- [ ] [makeEventNdjsonLogger](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174)
146- [ ] [write](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231)
147- [ ] [close](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L125)
153- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L170)
154- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L221)
155- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L239)
156
157### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`)
158
159- [ ] [processEnvelope](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64)
160- [ ] [dispatch](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218)
161- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162)
162- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/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](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L242)
167- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L336)
168- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L397)
169- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L446)
170- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L478)
171
172### Smaller clusters
173
174- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`)
175- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`)
176- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`)
177- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`)
178- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`)
179- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`)
180- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`)
181- [ ] [apps/server/src/keybindings.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/keybindings.ts) (`4`)
182- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`)
183- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`)
184- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`)
185- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`)
186- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`)
187- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`)
188- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`)
189- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`)
190- [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`)
191- [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`)
192- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`)
193- [ ] [apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts) (`2`)
194- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`)
195
196```
197
198```
docs/operations/observability.md created+519
......@@ -0,0 +1,519 @@
1# Observability
2
3T3 Code has one server-side observability model:
4
5- pretty logs go to stdout for humans
6- 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 LGTM
8
9The local trace file is the persisted source of truth. There is no separate persisted server log file anymore.
10
11## Where To Find Things
12
13### Logs
14
15Logs are human-facing only:
16
17- destination: stdout
18- format: `Logger.consolePretty()`
19- persistence: none
20
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.
22
23### Traces
24
25Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`).
26
27Important fields in each record:
28
29- `name`: span name
30- `traceId`, `spanId`, `parentSpanId`: correlation
31- `durationMs`: elapsed time
32- `attributes`: structured context
33- `events`: embedded logs and custom events
34- `exit`: `Success`, `Failure`, or `Interrupted`
35
36The schema lives in `apps/server/src/observability/TraceRecord.ts`.
37
38### Metrics
39
40Metrics are not written to a local file.
41
42- local persistence: none
43- remote export: OTLP only, when configured
44- current definitions: `apps/server/src/observability/Metrics.ts`
45
46If OTLP is not configured, metrics still exist in-process, but you will not have a local artifact to inspect.
47
48### Related Artifacts
49
50Provider event NDJSON files still exist for provider runtime streams. Those are separate from the main server trace file.
51
52## Run The Server In Instrumented Mode
53
54There are two useful modes:
55
56- local-only: stdout + local `server.trace.ndjson`
57- full local observability: stdout + local trace file + OTLP export to Grafana/Tempo/Prometheus
58
59The local trace file is always on. OTLP export is opt-in.
60
61### Option 1: Local Traces Only
62
63You do not need any extra env vars. Just run the app normally and inspect `server.trace.ndjson`.
64
65Examples:
66
67```bash
68npx t3
69```
70
71```bash
72node --run dev
73```
74
75```bash
76node --run dev:desktop
77```
78
79### Option 2: Run With A Local LGTM Stack
80
81#### 1. Start Grafana LGTM
82
83```bash
84docker run --name lgtm \
85 -p 3000:3000 \
86 -p 4317:4317 \
87 -p 4318:4318 \
88 --rm -ti \
89 grafana/otel-lgtm
90```
91
92Then open `http://localhost:3000`.
93
94Default Grafana login:
95
96- username: `admin`
97- password: `admin`
98
99#### 2. Export OTLP env vars
100
101```bash
102export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
103export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
104export T3CODE_OTLP_SERVICE_NAME=t3-local
105```
106
107Optional:
108
109```bash
110export T3CODE_TRACE_MIN_LEVEL=Info
111export T3CODE_TRACE_TIMING_ENABLED=true
112```
113
114#### 3. Launch the app from that same shell
115
116CLI:
117
118```bash
119npx t3
120```
121
122Monorepo web/server dev:
123
124```bash
125node --run dev
126```
127
128Monorepo desktop dev:
129
130```bash
131node --run dev:desktop
132```
133
134Packaged desktop app:
135
136Launch the actual app executable from the same shell so the desktop app and embedded backend inherit `T3CODE_OTLP_*`.
137
138macOS app bundle example:
139
140```bash
141T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
142T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
143T3CODE_OTLP_SERVICE_NAME=t3-desktop \
144"/Applications/T3 Code.app/Contents/MacOS/T3 Code"
145```
146
147Direct binary example:
148
149```bash
150T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
151T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
152T3CODE_OTLP_SERVICE_NAME=t3-desktop \
153./path/to/your/desktop-app-binary
154```
155
156Do not rely on launching from Finder, Spotlight, the dock, or the Start menu after setting shell env vars. Those launches usually will not pick them up.
157
158#### 4. Fully restart after changing env
159
160The backend reads observability config at process start. If you change OTLP env vars, stop the app completely and start it again.
161
162## How To Use Traces And Metrics To Debug The Server
163
164### Start With The Local Trace File
165
166The trace file is the fastest way to inspect raw span data.
167
168Tail it:
169
170```bash
171tail -f "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
172```
173
174In monorepo dev, use:
175
176```bash
177tail -f ./dev/logs/server.trace.ndjson
178```
179
180Show failed spans:
181
182```bash
183jq -c 'select(.exit._tag != "Success") | {
184 name,
185 durationMs,
186 exit,
187 attributes
188}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
189```
190
191Show slow spans:
192
193```bash
194jq -c 'select(.durationMs > 1000) | {
195 name,
196 durationMs,
197 traceId,
198 spanId
199}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
200```
201
202Inspect embedded log events:
203
204```bash
205jq -c 'select(any(.events[]?; .attributes["effect.logLevel"] != null)) | {
206 name,
207 durationMs,
208 events: [
209 .events[]
210 | select(.attributes["effect.logLevel"] != null)
211 | {
212 message: .name,
213 level: .attributes["effect.logLevel"]
214 }
215 ]
216}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
217```
218
219Follow one trace:
220
221```bash
222jq -r 'select(.traceId == "TRACE_ID_HERE") | [
223 .name,
224 .spanId,
225 (.parentSpanId // "-"),
226 .durationMs
227] | @tsv' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
228```
229
230Filter orchestration commands:
231
232```bash
233jq -c 'select(.attributes["orchestration.command_type"] != null) | {
234 name,
235 durationMs,
236 commandType: .attributes["orchestration.command_type"],
237 aggregateKind: .attributes["orchestration.aggregate_kind"]
238}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
239```
240
241Filter git activity:
242
243```bash
244jq -c 'select(.attributes["git.operation"] != null) | {
245 name,
246 durationMs,
247 operation: .attributes["git.operation"],
248 cwd: .attributes["git.cwd"],
249 hookEvents: [
250 .events[]
251 | select(.name == "git.hook.started" or .name == "git.hook.finished")
252 ]
253}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson"
254```
255
256### Use Tempo When You Need A Real Trace Viewer
257
258Tempo is better than raw NDJSON when you want to:
259
260- search across many traces
261- inspect parent/child relationships visually
262- compare many slow traces
263- drill into one failing request without hand-joining by `traceId`
264
265Recommended flow in Grafana:
266
2671. Open `Explore`.
2682. Pick the `Tempo` data source.
2693. Set the time range to something recent like `Last 15 minutes`.
2704. Start broad. Do not begin with a very narrow query.
2715. Look for spans from your configured service name, then narrow by span name or attributes.
272
273Good first searches:
274
275- service name such as `t3-local`, `t3-dev`, or `t3-desktop`
276- span names like `sql.execute`, `git.runCommand`, `provider.sendTurn`
277- orchestration spans with attributes like `orchestration.command_type`
278
279Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful.
280
281### Use Metrics To See Systemic Problems
282
283Traces are best for one request. Metrics are best for trends.
284
285Good metric families to watch:
286
287- `t3_rpc_request_duration`
288- `t3_orchestration_command_duration`
289- `t3_orchestration_command_ack_duration`
290- `t3_provider_turn_duration`
291- `t3_git_command_duration`
292- `t3_db_query_duration`
293
294Counters tell you volume and failure rate:
295
296- `t3_rpc_requests_total`
297- `t3_orchestration_commands_total`
298- `t3_provider_turns_total`
299- `t3_git_commands_total`
300- `t3_db_queries_total`
301
302Use metrics when the question is:
303
304- "is this always slow?"
305- "did this get worse after a change?"
306- "which command type is failing most often?"
307
308Use traces when the question is:
309
310- "what happened in this specific request?"
311- "which child span caused this one slow interaction?"
312- "what logs were emitted inside the failing flow?"
313
314### What The New Ack Metric Means
315
316`t3_orchestration_command_ack_duration` measures:
317
318- start: command dispatch enters the orchestration engine
319- end: the first committed domain event for that command is published by the server
320
321That is a server-side acknowledgment metric. It does not measure:
322
323- websocket transit to the browser
324- client receipt
325- React render time
326
327If you need those later, add client-side instrumentation or a dedicated server fanout metric.
328
329## Common Workflows
330
331### "Why did this request fail?"
332
3331. Start with the local NDJSON file.
3342. Find spans where `exit._tag != "Success"`.
3353. Group by `traceId`.
3364. Inspect sibling spans and span events.
3375. If needed, move to Tempo for the full trace tree.
338
339### "Why is the UI feeling slow?"
340
3411. Search for slow top-level spans in the trace file or Tempo.
3422. Check child spans for sqlite, git, provider, or terminal work.
3433. Look at the matching duration metrics to see whether the slowness is systemic.
344
345### "Did this command take too long to acknowledge?"
346
3471. Check `t3_orchestration_command_ack_duration` by `commandType`.
3482. If it is high, inspect the corresponding orchestration trace.
3493. Look at child spans for projection, sqlite, provider, or git work.
350
351### "Are git hooks causing latency?"
352
3531. Filter `git.operation` spans.
3542. Inspect `git.hook.started` and `git.hook.finished` events.
3553. Compare hook timing to the enclosing git span duration.
356
357### "Why do I have spans locally but nothing in Grafana?"
358
359Usually one of these is true:
360
361- `T3CODE_OTLP_TRACES_URL` was not set
362- the app was launched from a different environment than the one where you exported the vars
363- the app was not fully restarted after changing env
364- Grafana is looking at the wrong time range or service name
365
366If the local NDJSON file is updating, local tracing is working. The problem is almost always OTLP export configuration or process startup.
367
368## How To Think About Adding Tracing To Future Code
369
370### Prefer Boundaries Over Tiny Helpers
371
372Good span boundaries:
373
374- RPC methods
375- orchestration command handling
376- provider adapter calls
377- external process calls
378- persistence writes
379- queue handoffs
380
381Avoid tracing every tiny helper. Most helpers should inherit the active span rather than create a new one.
382
383### Reuse `Effect.fn(...)` Where It Already Exists
384
385The codebase already uses `Effect.fn("name")` heavily. That should usually be your first tracing boundary.
386
387For ad hoc work:
388
389```ts
390import { Effect } from "effect";
391
392const runThing = Effect.gen(function* () {
393 yield* Effect.annotateCurrentSpan({
394 "thing.id": "abc123",
395 "thing.kind": "example",
396 });
397
398 yield* Effect.logInfo("starting thing");
399 return yield* doWork();
400}).pipe(Effect.withSpan("thing.run"));
401```
402
403### Put High-Cardinality Detail On Spans
404
405Use span annotations for IDs, paths, and other detailed context:
406
407```ts
408yield *
409 Effect.annotateCurrentSpan({
410 "provider.thread_id": input.threadId,
411 "provider.request_id": input.requestId,
412 "git.cwd": input.cwd,
413 });
414```
415
416### Keep Metric Labels Low Cardinality
417
418Good metric labels:
419
420- operation kind
421- method name
422- provider kind
423- aggregate kind
424- outcome
425
426Bad metric labels:
427
428- raw thread IDs
429- command IDs
430- file paths
431- cwd
432- full prompts
433- full model strings when a normalized family label would do
434
435Detailed context belongs on spans, not metrics.
436
437### Use Logs As Span Events
438
439Logs inside a span become part of the trace story:
440
441```ts
442yield * Effect.logInfo("starting provider turn");
443yield * Effect.logDebug("waiting for approval response");
444```
445
446Those messages show up as span events because `Logger.tracerLogger` is installed.
447
448### Use The Pipeable Metrics API
449
450`withMetrics(...)` is the default way to attach a counter and timer to an effect:
451
452```ts
453import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts";
454
455const program = doWork().pipe(
456 withMetrics({
457 counter: someCounter,
458 timer: someDuration,
459 attributes: {
460 operation: "work",
461 },
462 }),
463);
464```
465
466## Detailed API Reference
467
468### Runtime Wiring
469
470The server observability layer is assembled in `apps/server/src/observability/Layers/Observability.ts`.
471
472It provides:
473
474- pretty stdout logger
475- `Logger.tracerLogger`
476- local NDJSON tracer
477- optional OTLP trace exporter
478- optional OTLP metrics exporter
479- Effect trace-level and timing refs
480
481### Env Vars
482
483Local trace file:
484
485- `T3CODE_TRACE_FILE`: override trace file path
486- `T3CODE_TRACE_MAX_BYTES`: per-file rotation size, default `10485760`
487- `T3CODE_TRACE_MAX_FILES`: rotated file count, default `10`
488- `T3CODE_TRACE_BATCH_WINDOW_MS`: flush window, default `200`
489- `T3CODE_TRACE_MIN_LEVEL`: minimum trace level, default `Info`
490- `T3CODE_TRACE_TIMING_ENABLED`: enable timing metadata, default `true`
491
492OTLP export:
493
494- `T3CODE_OTLP_TRACES_URL`: OTLP trace endpoint
495- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint
496- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`
497- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server`
498
499If the OTLP URLs are unset, local tracing still works and metrics stay in-process only.
500
501### What Is Instrumented Today
502
503Current high-value span and metric boundaries include:
504
505- Effect RPC websocket request spans from `effect/rpc`
506- RPC request metrics in `apps/server/src/observability/RpcInstrumentation.ts`
507- startup phases
508- orchestration command processing
509- orchestration command acknowledgment latency
510- provider session and turn operations
511- git command execution and git hook events
512- terminal session lifecycle
513- sqlite query execution
514
515### Current Constraints
516
517- logs outside spans are not persisted
518- metrics are not snapshotted locally
519- the old `serverLogPath` still exists in config for compatibility, but the trace file is the persisted artifact that matters
docs/operations/relay-observability.md created+42
......@@ -0,0 +1,42 @@
1# Relay observability
2
3The relay Alchemy stack owns a focused Axiom trace setup:
4
5- `t3-code-relay-traces-prod`, an OpenTelemetry trace dataset for Worker requests
6- `t3-code-relay-otel-ingest-prod`, a dataset-scoped ingest token bound to the Worker
7- `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans
8
9Alchemy stages append their sanitized stage name to isolate resources, for example
10`t3-code-relay-traces-dev-julius` for a personal stage.
11
12Deploy from `infra/relay` with the normal Alchemy workflow:
13
14```sh
15vp run deploy
16```
17
18Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker
19receives only the scoped ingest token; it does not receive the diagnostics query token.
20
21The 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.
23For example:
24
25```apl
26['t3-code-relay-traces-prod']
27| where name startswith 'http.server'
28| project _time, name, trace_id, duration,
29 ['attributes.http.request.method'],
30 ['attributes.url.path'],
31 ['attributes.http.response.status_code']
32| order by _time desc
33| limit 200
34```
35
36Endpoint failure annotations and other relay-specific attributes are also emitted in the
37`attributes.custom` map when present on a span, for example
38`['attributes.custom']['relay.endpoint']`.
39
40Agents 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;
42keep the ingest token reserved for the Worker.
docs/operations/release.md created+289
......@@ -0,0 +1,289 @@
1# Release Checklist
2
3This document covers the unified release workflow for stable and nightly desktop releases.
4
5## What the workflow does
6
7- Workflow: `.github/workflows/release.yml`
8- Triggers:
9 - push tag matching `v*.*.*` for stable releases
10 - scheduled nightly check every three hours
11 - manual `workflow_dispatch` for either channel
12- Runs quality gates first: lint, typecheck, test.
13- Reads the shared production T3 Cloud relay URL and Clerk client configuration before packaging clients.
14- Builds four artifacts in parallel for both channels:
15 - macOS `arm64` DMG
16 - macOS `x64` DMG
17 - Linux `x64` AppImage
18 - Windows `x64` NSIS installer
19- Publishes one GitHub Release with all produced files.
20 - Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases.
21 - Only plain stable `X.Y.Z` releases are marked as the repository's latest release.
22 - Nightly runs are always GitHub prereleases and never marked latest.
23 - Automatically generated release notes are pinned to the previous tag in the same channel, so stable compares to the previous stable tag and nightly compares to the previous nightly tag.
24- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
25- Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file:
26 - stable releases publish npm dist-tag `latest`
27 - nightly releases publish npm dist-tag `nightly`
28- Deploys the hosted web app to Vercel only after a release is published:
29 - stable releases are aliased to the `latest` hosted app channel
30 - nightly releases are aliased to the `nightly` hosted app channel
31- Signing is optional and auto-detected per platform from secrets.
32
33## T3 Cloud relay deployment
34
35The relay is a shared control plane versioned separately from client releases. Stable and nightly
36client builds must point at the same relay so users see the same linked environments when switching
37release channels.
38
39`.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. The
40release workflow reads the relay URL and Clerk client configuration from the existing `production`
41GitHub Actions environment before building desktop, CLI, or hosted web artifacts.
42
43Required repository variables shared by relay deployments:
44
45- `CLOUDFLARE_ACCOUNT_ID`
46- `PLANETSCALE_ORGANIZATION`
47- `AXIOM_ORG_ID`
48
49Required repository secrets shared by relay deployments:
50
51- `CLOUDFLARE_API_TOKEN`
52- `PLANETSCALE_API_TOKEN_ID`
53- `PLANETSCALE_API_TOKEN`
54- `AXIOM_TOKEN`
55
56Required `production` environment variables:
57
58- `RELAY_API_ZONE_NAME`
59- `RELAY_TUNNEL_ZONE_NAME`
60- `CLERK_PUBLISHABLE_KEY`
61- `CLERK_JWT_AUDIENCE`
62- `CLERK_JWT_TEMPLATE`
63- `CLERK_CLI_OAUTH_CLIENT_ID`
64- `APNS_ENVIRONMENT`
65- `APNS_TEAM_ID`
66- `APNS_KEY_ID`
67- `APNS_BUNDLE_ID`
68
69Optional `production` environment variables:
70
71- `RELAY_DOMAIN` when overriding the derived `relay.<RELAY_API_ZONE_NAME>` domain
72
73Required `production` environment secrets:
74
75- `CLERK_SECRET_KEY`
76- `APNS_PRIVATE_KEY`
77
78The account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they
79are not bound into the relay Worker. The production deployment uses an Axiom personal access token,
80so `AXIOM_ORG_ID` must accompany `AXIOM_TOKEN`. The `prod` stage owns the retained PlanetScale
81database. Local personal stages provision isolated branches from it and are never deployed by CI.
82Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources.
83Personal stages reference the production-owned zones.
84
85Developers deploy personal stages locally rather than through pull-request automation:
86
87```sh
88vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local
89```
90
91## Hosted web app release deployment
92
93The hosted app is intentionally not deployed by Vercel's Git integration. The
94web project disables automatic Git deployments in `apps/web/vercel.ts` via
95`git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the
96web app with Vercel CLI after the GitHub Release succeeds.
97
98Required GitHub Actions secrets:
99
100- `VERCEL_TOKEN`
101- `VERCEL_ORG_ID`
102- `VERCEL_PROJECT_ID`
103
104Optional GitHub Actions variables:
105
106- `VERCEL_TEAM_SLUG`: overrides the Vercel CLI scope when the team slug is preferred over the `VERCEL_ORG_ID` secret.
107- `T3CODE_WEB_ROUTER_URL`: defaults to `https://app.t3.codes`.
108- `T3CODE_WEB_LATEST_DOMAIN`: defaults to `latest.app.t3.codes`.
109- `T3CODE_WEB_NIGHTLY_DOMAIN`: defaults to `nightly.app.t3.codes`.
110
111Required Vercel domains:
112
113- `app.t3.codes`: the router domain users open, updated by stable releases.
114- `latest.app.t3.codes`: channel alias updated by stable releases.
115- `nightly.app.t3.codes`: channel alias updated by nightly releases.
116
117The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by
118visiting `/__t3code/channel?channel=latest` or
119`/__t3code/channel?channel=nightly`; the router stores the
120`t3code_web_channel` cookie and rewrites future requests on `app.t3.codes` to
121the matching channel alias.
122
123The release deploy job rewrites release package versions before upload so the
124hosted app's About panel renders the release version. Stable deploys alias the
125same deployment to both the `latest` channel and the router domain so the router
126rules stay current. Nightly deploys only alias the `nightly` channel. The job
127also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted
128update track selector in the About panel. Changing the selector navigates
129through `/__t3code/channel` on the router domain so the user's channel cookie is
130updated before redirecting to the hosted app root.
131
132One-time Vercel dashboard setup:
133
1341. Confirm the web project root directory remains `apps/web`.
1352. Add the three domains above to the web project.
1363. Disable automatic Git deployments in the dashboard if desired; the committed
137 `vercel.ts` setting is the source-of-truth, but disconnecting Git in the
138 dashboard is also safe.
1394. Run one stable release deployment, or manually alias the current stable
140 deployment, so `app.t3.codes` points at a deployment containing the router
141 rules in `apps/web/vercel.ts`. Future stable releases keep this alias current.
142
143## Nightly builds
144
145- Workflow: `.github/workflows/release.yml`
146- Triggers:
147 - scheduled check every three hours
148 - manual `workflow_dispatch` with `channel=nightly`
149- Runs the same desktop quality gates and artifact matrix as the tagged release flow.
150- Publishes a GitHub prerelease only:
151 - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>`
152 - release name includes the short commit SHA
153 - `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.*`.
155- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.
156- Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version.
157- Does not commit version bumps back to `main`.
158
159## Desktop auto-update notes
160
161- Runtime updater: `electron-updater` in `apps/desktop/src/main.ts`.
162- Update UX:
163 - Background checks run on startup delay + interval.
164 - No automatic download or install.
165 - The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install.
166- Provider: GitHub Releases (`provider: github`) configured at build time.
167- Repository slug source:
168 - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set.
169 - otherwise `GITHUB_REPOSITORY` from GitHub Actions.
170- Temporary private-repo auth workaround:
171 - set `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment.
172 - the app forwards it as an `Authorization: Bearer <token>` request header for updater HTTP calls.
173- Required release assets for updater:
174 - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads)
175 - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases
176 - `*.blockmap` files (used for differential downloads)
177- macOS metadata note:
178 - `electron-updater` reads `latest-mac.yml` on stable and `nightly-mac.yml` on nightly, for both Intel and Apple Silicon.
179 - The workflow merges the per-arch mac manifests into one channel-specific mac manifest before publishing the GitHub Release.
180
181## 0) npm OIDC trusted publishing setup (CLI)
182
183The workflow publishes the CLI with `npm publish` from `apps/server` after bumping
184the package version to the release tag version.
185
186Checklist:
187
1881. Confirm npm org/user owns package `t3` (or rename package first if needed).
1892. In npm package settings, configure Trusted Publisher:
190 - Provider: GitHub Actions
191 - Repository: this repo
192 - Workflow file: `.github/workflows/release.yml`
193 - Environment (if used): match your npm trusted publishing config
1943. Ensure npm account and org policies allow trusted publishing for the package.
1954. Create release tag `vX.Y.Z` and push; workflow will:
196 - set `apps/server/package.json` version to `X.Y.Z`
197 - build web + server
198 - run `npm publish --access public --tag latest`
1995. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`.
200
201## 1) Dry-run release without signing
202
203Use this first to validate the release pipeline.
204
2051. Confirm no signing secrets are required for this test.
2062. Create a test tag:
207 - `git tag v0.0.0-test.1`
208 - `git push origin v0.0.0-test.1`
2093. Wait for `.github/workflows/release.yml` to finish.
2104. Verify the GitHub Release contains all platform artifacts.
2115. Download each artifact and sanity-check installation on each OS.
212
213## 2) Apple signing + notarization setup (macOS)
214
215Required secrets used by the workflow:
216
217- `CSC_LINK`
218- `CSC_KEY_PASSWORD`
219- `APPLE_API_KEY`
220- `APPLE_API_KEY_ID`
221- `APPLE_API_ISSUER`
222
223Checklist:
224
2251. Apple Developer account access:
226 - Team has rights to create Developer ID certificates.
2272. Create `Developer ID Application` certificate.
2283. Export certificate + private key as `.p12` from Keychain.
2294. Base64-encode the `.p12` and store as `CSC_LINK`.
2305. Store the `.p12` export password as `CSC_KEY_PASSWORD`.
2316. In App Store Connect, create an API key (Team key).
2327. Add API key values:
233 - `APPLE_API_KEY`: contents of the downloaded `.p8`
234 - `APPLE_API_KEY_ID`: Key ID
235 - `APPLE_API_ISSUER`: Issuer ID
2368. Re-run a tag release and confirm macOS artifacts are signed/notarized.
237
238Notes:
239
240- `APPLE_API_KEY` is stored as raw key text in secrets.
241- The workflow writes it to a temporary `AuthKey_<id>.p8` file at runtime.
242
243## 3) Azure Trusted Signing setup (Windows)
244
245Required secrets used by the workflow:
246
247- `AZURE_TENANT_ID`
248- `AZURE_CLIENT_ID`
249- `AZURE_CLIENT_SECRET`
250- `AZURE_TRUSTED_SIGNING_ENDPOINT`
251- `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`
252- `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`
253- `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`
254
255Checklist:
256
2571. Create Azure Trusted Signing account and certificate profile.
2582. Record ATS values:
259 - Endpoint
260 - Account name
261 - Certificate profile name
262 - Publisher name
2633. Create/choose an Entra app registration (service principal).
2644. Grant service principal permissions required by Trusted Signing.
2655. Create a client secret for the service principal.
2666. Add Azure secrets listed above in GitHub Actions secrets.
2677. Re-run a tag release and confirm Windows installer is signed.
268
269## 4) Ongoing release checklist
270
2711. Ensure `main` is green in CI.
2722. Bump app version as needed.
2733. Create release tag: `vX.Y.Z`.
2744. Push tag.
2755. Verify workflow steps:
276 - preflight passes
277 - all matrix builds pass
278 - release job uploads expected files
2796. Smoke test downloaded artifacts.
280
281## 5) Troubleshooting
282
283- macOS build unsigned when expected signed:
284 - Check all Apple secrets are populated and non-empty.
285- Windows build unsigned when expected signed:
286 - Check all Azure ATS and auth secrets are populated and non-empty.
287- Build fails with signing error:
288 - Retry with secrets removed to confirm unsigned path still works.
289 - Re-check certificate/profile names and tenant/client credentials.
docs/project/todo.md created+13
......@@ -0,0 +1,13 @@
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/reference/encyclopedia.md created+180
......@@ -0,0 +1,180 @@
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/Services/CheckpointStore.ts
176[20]: ../apps/server/src/checkpointing/Services/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 created+42
......@@ -0,0 +1,42 @@
1# Scripts
2
3- `bun run dev` — Starts contracts, server, and web in `turbo watch` mode.
4- `bun run dev:server` — Starts just the WebSocket server (uses Bun TypeScript execution).
5- `bun run dev:web` — Starts just the Vite dev server for the web app.
6- Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state.
7- Override server CLI-equivalent flags from root dev commands with `--`, for example:
8 `bun run dev -- --base-dir ~/.t3-2`
9- `bun run start` — Runs the production server (serves built web app as static files).
10- `bun run build` — Builds contracts, web app, and server through Turbo.
11- `bun run typecheck` — Strict TypeScript checks for all packages.
12- `bun run test` — Runs workspace tests.
13- `bun run dist:desktop:artifact -- --platform <mac|linux|win> --target <target> --arch <arch>` — Builds a desktop artifact for a specific platform/target/arch.
14- `bun run dist:desktop:dmg` — Builds a shareable macOS `.dmg` into `./release`.
15- `bun run dist:desktop:dmg:x64` — Builds an Intel macOS `.dmg`.
16- `bun run dist:desktop:linux` — Builds a Linux AppImage into `./release`.
17- `bun run dist:desktop:win` — Builds a Windows NSIS installer into `./release`.
18
19## Desktop `.dmg` packaging notes
20
21- Default build is unsigned/not notarized for local sharing.
22- The DMG build uses `assets/macos-icon-1024.png` as the production app icon source.
23- Desktop production windows load the bundled UI from `t3://app/index.html` (not a `127.0.0.1` document URL).
24- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an auth token for WebSocket/API traffic.
25- Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first launch.
26- To keep staging files for debugging package contents, run: `bun run dist:desktop:dmg -- --keep-stage`
27- To allow code-signing/notarization when configured in CI/secrets, add: `--signed`.
28- Windows `--signed` uses Azure Trusted Signing and expects:
29 `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`,
30 `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`.
31- Azure authentication env vars are also required (for example service principal with secret):
32 `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`.
33
34## Running multiple dev instances
35
36Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together.
37
38- Default ports: server `3773`, web `5733`
39- Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`)
40- Example: `T3CODE_DEV_INSTANCE=branch-a bun run dev:desktop`
41
42If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset.
docs/reference/workspace-layout.md created+7
......@@ -0,0 +1,7 @@
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/relay-observability.md deleted-42
......@@ -1,42 +0,0 @@
1# Relay observability
2
3The relay Alchemy stack owns a focused Axiom trace setup:
4
5- `t3-code-relay-traces-prod`, an OpenTelemetry trace dataset for Worker requests
6- `t3-code-relay-otel-ingest-prod`, a dataset-scoped ingest token bound to the Worker
7- `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans
8
9Alchemy stages append their sanitized stage name to isolate resources, for example
10`t3-code-relay-traces-dev-julius` for a personal stage.
11
12Deploy from `infra/relay` with the normal Alchemy workflow:
13
14```sh
15vp run deploy
16```
17
18Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker
19receives only the scoped ingest token; it does not receive the diagnostics query token.
20
21The 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.
23For example:
24
25```apl
26['t3-code-relay-traces-prod']
27| where name startswith 'http.server'
28| project _time, name, trace_id, duration,
29 ['attributes.http.request.method'],
30 ['attributes.url.path'],
31 ['attributes.http.response.status_code']
32| order by _time desc
33| limit 200
34```
35
36Endpoint failure annotations and other relay-specific attributes are also emitted in the
37`attributes.custom` map when present on a span, for example
38`['attributes.custom']['relay.endpoint']`.
39
40Agents 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;
42keep the ingest token reserved for the Worker.
docs/release.md deleted-289
......@@ -1,289 +0,0 @@
1# Release Checklist
2
3This document covers the unified release workflow for stable and nightly desktop releases.
4
5## What the workflow does
6
7- Workflow: `.github/workflows/release.yml`
8- Triggers:
9 - push tag matching `v*.*.*` for stable releases
10 - scheduled nightly check every three hours
11 - manual `workflow_dispatch` for either channel
12- Runs quality gates first: lint, typecheck, test.
13- Reads the shared production T3 Cloud relay URL and Clerk client configuration before packaging clients.
14- Builds four artifacts in parallel for both channels:
15 - macOS `arm64` DMG
16 - macOS `x64` DMG
17 - Linux `x64` AppImage
18 - Windows `x64` NSIS installer
19- Publishes one GitHub Release with all produced files.
20 - Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases.
21 - Only plain stable `X.Y.Z` releases are marked as the repository's latest release.
22 - Nightly runs are always GitHub prereleases and never marked latest.
23 - Automatically generated release notes are pinned to the previous tag in the same channel, so stable compares to the previous stable tag and nightly compares to the previous nightly tag.
24- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
25- Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file:
26 - stable releases publish npm dist-tag `latest`
27 - nightly releases publish npm dist-tag `nightly`
28- Deploys the hosted web app to Vercel only after a release is published:
29 - stable releases are aliased to the `latest` hosted app channel
30 - nightly releases are aliased to the `nightly` hosted app channel
31- Signing is optional and auto-detected per platform from secrets.
32
33## T3 Cloud relay deployment
34
35The relay is a shared control plane versioned separately from client releases. Stable and nightly
36client builds must point at the same relay so users see the same linked environments when switching
37release channels.
38
39`.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. The
40release workflow reads the relay URL and Clerk client configuration from the existing `production`
41GitHub Actions environment before building desktop, CLI, or hosted web artifacts.
42
43Required repository variables shared by relay deployments:
44
45- `CLOUDFLARE_ACCOUNT_ID`
46- `PLANETSCALE_ORGANIZATION`
47- `AXIOM_ORG_ID`
48
49Required repository secrets shared by relay deployments:
50
51- `CLOUDFLARE_API_TOKEN`
52- `PLANETSCALE_API_TOKEN_ID`
53- `PLANETSCALE_API_TOKEN`
54- `AXIOM_TOKEN`
55
56Required `production` environment variables:
57
58- `RELAY_API_ZONE_NAME`
59- `RELAY_TUNNEL_ZONE_NAME`
60- `CLERK_PUBLISHABLE_KEY`
61- `CLERK_JWT_AUDIENCE`
62- `CLERK_JWT_TEMPLATE`
63- `CLERK_CLI_OAUTH_CLIENT_ID`
64- `APNS_ENVIRONMENT`
65- `APNS_TEAM_ID`
66- `APNS_KEY_ID`
67- `APNS_BUNDLE_ID`
68
69Optional `production` environment variables:
70
71- `RELAY_DOMAIN` when overriding the derived `relay.<RELAY_API_ZONE_NAME>` domain
72
73Required `production` environment secrets:
74
75- `CLERK_SECRET_KEY`
76- `APNS_PRIVATE_KEY`
77
78The account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they
79are not bound into the relay Worker. The production deployment uses an Axiom personal access token,
80so `AXIOM_ORG_ID` must accompany `AXIOM_TOKEN`. The `prod` stage owns the retained PlanetScale
81database. Local personal stages provision isolated branches from it and are never deployed by CI.
82Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources.
83Personal stages reference the production-owned zones.
84
85Developers deploy personal stages locally rather than through pull-request automation:
86
87```sh
88vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local
89```
90
91## Hosted web app release deployment
92
93The hosted app is intentionally not deployed by Vercel's Git integration. The
94web project disables automatic Git deployments in `apps/web/vercel.ts` via
95`git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the
96web app with Vercel CLI after the GitHub Release succeeds.
97
98Required GitHub Actions secrets:
99
100- `VERCEL_TOKEN`
101- `VERCEL_ORG_ID`
102- `VERCEL_PROJECT_ID`
103
104Optional GitHub Actions variables:
105
106- `VERCEL_TEAM_SLUG`: overrides the Vercel CLI scope when the team slug is preferred over the `VERCEL_ORG_ID` secret.
107- `T3CODE_WEB_ROUTER_URL`: defaults to `https://app.t3.codes`.
108- `T3CODE_WEB_LATEST_DOMAIN`: defaults to `latest.app.t3.codes`.
109- `T3CODE_WEB_NIGHTLY_DOMAIN`: defaults to `nightly.app.t3.codes`.
110
111Required Vercel domains:
112
113- `app.t3.codes`: the router domain users open, updated by stable releases.
114- `latest.app.t3.codes`: channel alias updated by stable releases.
115- `nightly.app.t3.codes`: channel alias updated by nightly releases.
116
117The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by
118visiting `/__t3code/channel?channel=latest` or
119`/__t3code/channel?channel=nightly`; the router stores the
120`t3code_web_channel` cookie and rewrites future requests on `app.t3.codes` to
121the matching channel alias.
122
123The release deploy job rewrites release package versions before upload so the
124hosted app's About panel renders the release version. Stable deploys alias the
125same deployment to both the `latest` channel and the router domain so the router
126rules stay current. Nightly deploys only alias the `nightly` channel. The job
127also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted
128update track selector in the About panel. Changing the selector navigates
129through `/__t3code/channel` on the router domain so the user's channel cookie is
130updated before redirecting to the hosted app root.
131
132One-time Vercel dashboard setup:
133
1341. Confirm the web project root directory remains `apps/web`.
1352. Add the three domains above to the web project.
1363. Disable automatic Git deployments in the dashboard if desired; the committed
137 `vercel.ts` setting is the source-of-truth, but disconnecting Git in the
138 dashboard is also safe.
1394. Run one stable release deployment, or manually alias the current stable
140 deployment, so `app.t3.codes` points at a deployment containing the router
141 rules in `apps/web/vercel.ts`. Future stable releases keep this alias current.
142
143## Nightly builds
144
145- Workflow: `.github/workflows/release.yml`
146- Triggers:
147 - scheduled check every three hours
148 - manual `workflow_dispatch` with `channel=nightly`
149- Runs the same desktop quality gates and artifact matrix as the tagged release flow.
150- Publishes a GitHub prerelease only:
151 - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>`
152 - release name includes the short commit SHA
153 - `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.*`.
155- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.
156- Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version.
157- Does not commit version bumps back to `main`.
158
159## Desktop auto-update notes
160
161- Runtime updater: `electron-updater` in `apps/desktop/src/main.ts`.
162- Update UX:
163 - Background checks run on startup delay + interval.
164 - No automatic download or install.
165 - The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install.
166- Provider: GitHub Releases (`provider: github`) configured at build time.
167- Repository slug source:
168 - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set.
169 - otherwise `GITHUB_REPOSITORY` from GitHub Actions.
170- Temporary private-repo auth workaround:
171 - set `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment.
172 - the app forwards it as an `Authorization: Bearer <token>` request header for updater HTTP calls.
173- Required release assets for updater:
174 - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads)
175 - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases
176 - `*.blockmap` files (used for differential downloads)
177- macOS metadata note:
178 - `electron-updater` reads `latest-mac.yml` on stable and `nightly-mac.yml` on nightly, for both Intel and Apple Silicon.
179 - The workflow merges the per-arch mac manifests into one channel-specific mac manifest before publishing the GitHub Release.
180
181## 0) npm OIDC trusted publishing setup (CLI)
182
183The workflow publishes the CLI with `npm publish` from `apps/server` after bumping
184the package version to the release tag version.
185
186Checklist:
187
1881. Confirm npm org/user owns package `t3` (or rename package first if needed).
1892. In npm package settings, configure Trusted Publisher:
190 - Provider: GitHub Actions
191 - Repository: this repo
192 - Workflow file: `.github/workflows/release.yml`
193 - Environment (if used): match your npm trusted publishing config
1943. Ensure npm account and org policies allow trusted publishing for the package.
1954. Create release tag `vX.Y.Z` and push; workflow will:
196 - set `apps/server/package.json` version to `X.Y.Z`
197 - build web + server
198 - run `npm publish --access public --tag latest`
1995. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`.
200
201## 1) Dry-run release without signing
202
203Use this first to validate the release pipeline.
204
2051. Confirm no signing secrets are required for this test.
2062. Create a test tag:
207 - `git tag v0.0.0-test.1`
208 - `git push origin v0.0.0-test.1`
2093. Wait for `.github/workflows/release.yml` to finish.
2104. Verify the GitHub Release contains all platform artifacts.
2115. Download each artifact and sanity-check installation on each OS.
212
213## 2) Apple signing + notarization setup (macOS)
214
215Required secrets used by the workflow:
216
217- `CSC_LINK`
218- `CSC_KEY_PASSWORD`
219- `APPLE_API_KEY`
220- `APPLE_API_KEY_ID`
221- `APPLE_API_ISSUER`
222
223Checklist:
224
2251. Apple Developer account access:
226 - Team has rights to create Developer ID certificates.
2272. Create `Developer ID Application` certificate.
2283. Export certificate + private key as `.p12` from Keychain.
2294. Base64-encode the `.p12` and store as `CSC_LINK`.
2305. Store the `.p12` export password as `CSC_KEY_PASSWORD`.
2316. In App Store Connect, create an API key (Team key).
2327. Add API key values:
233 - `APPLE_API_KEY`: contents of the downloaded `.p8`
234 - `APPLE_API_KEY_ID`: Key ID
235 - `APPLE_API_ISSUER`: Issuer ID
2368. Re-run a tag release and confirm macOS artifacts are signed/notarized.
237
238Notes:
239
240- `APPLE_API_KEY` is stored as raw key text in secrets.
241- The workflow writes it to a temporary `AuthKey_<id>.p8` file at runtime.
242
243## 3) Azure Trusted Signing setup (Windows)
244
245Required secrets used by the workflow:
246
247- `AZURE_TENANT_ID`
248- `AZURE_CLIENT_ID`
249- `AZURE_CLIENT_SECRET`
250- `AZURE_TRUSTED_SIGNING_ENDPOINT`
251- `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`
252- `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`
253- `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`
254
255Checklist:
256
2571. Create Azure Trusted Signing account and certificate profile.
2582. Record ATS values:
259 - Endpoint
260 - Account name
261 - Certificate profile name
262 - Publisher name
2633. Create/choose an Entra app registration (service principal).
2644. Grant service principal permissions required by Trusted Signing.
2655. Create a client secret for the service principal.
2666. Add Azure secrets listed above in GitHub Actions secrets.
2677. Re-run a tag release and confirm Windows installer is signed.
268
269## 4) Ongoing release checklist
270
2711. Ensure `main` is green in CI.
2722. Bump app version as needed.
2733. Create release tag: `vX.Y.Z`.
2744. Push tag.
2755. Verify workflow steps:
276 - preflight passes
277 - all matrix builds pass
278 - release job uploads expected files
2796. Smoke test downloaded artifacts.
280
281## 5) Troubleshooting
282
283- macOS build unsigned when expected signed:
284 - Check all Apple secrets are populated and non-empty.
285- Windows build unsigned when expected signed:
286 - Check all Azure ATS and auth secrets are populated and non-empty.
287- Build fails with signing error:
288 - Retry with secrets removed to confirm unsigned path still works.
289 - Re-check certificate/profile names and tenant/client credentials.
docs/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/t3-cloud-clerk.md deleted-174
......@@ -1,174 +0,0 @@
1# T3 Cloud Clerk Setup
2
3T3 Cloud 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 Cloud 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 cloud` 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 cloud` 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 cloud login
79t3 cloud link
80t3 cloud status
81t3 cloud unlink
82t3 cloud logout
83t3 serve
84```
85
86`t3 cloud login` opens the Clerk authorization flow and stores the CLI credential without enabling
87cloud exposure. `t3 cloud 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 cloud 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 cloud link` can re-enable exposure without another browser flow. `t3 cloud
93logout` performs the same cleanup and removes the stored CLI authorization.
94
95The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH,
96forward that port before running `t3 cloud login` or `t3 cloud link`:
97
98```sh
99ssh -L 34338:127.0.0.1:34338 <host>
100```
101
102A relay-hosted callback broker can remove this port-forward requirement later without changing the
103stored PKCE token model.
104
105## JWT Template
106
107In **Clerk Dashboard > JWT templates**, create a template with:
108
109| Setting | Value |
110| ------- | ---------------------------- |
111| Name | `t3-relay` |
112| Claims | `{ "aud": "t3-code-relay" }` |
113
114Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set
115`CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and
116`CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value
117is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still
118selects the concrete relay deployment, but changing that URL does not require a JWT template change.
119
120## Desktop OAuth Redirect Allowlist
121
122The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme.
123In **Clerk Dashboard > Native applications**, enable native application support and add these
124entries under the mobile SSO redirect allowlist:
125
126```text
127t3code-dev://auth/callback
128t3code://auth/callback
129```
130
131The first entry is for local desktop development. The second is for packaged desktop builds.
132The app also adds a request-scoped `t3_state` query parameter and validates it on callback. Initial
133sign-in and linked-account OAuth flows both return through this bridge. The desktop provider keeps
134Clerk's stock profile component, replaces its renderer-page callback with the custom-scheme callback,
135and opens the provider URL in the system browser. Do not add the local renderer URL as an OAuth
136redirect: an external browser cannot use it to reopen the packaged app.
137
138The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth
139flow uses a custom redirect URI, add that exact URI to the same allowlist.
140
141## Enable Waitlist Access
142
143For a private beta where people should request access, use **Clerk Dashboard > Waitlist**:
144
1451. Toggle on **Enable waitlist** and save.
1462. Review requests on the same page and select **Invite** or **Deny**.
147
148Signed-out web and desktop users see Clerk's waitlist enrollment as the T3 Cloud page content,
149while approved signed-in users see cloud settings. The browser app also uses `/settings/cloud` as
150its Clerk waitlist URL.
151
152On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the
153Settings form sheet. It submits enrollment through Clerk's `useWaitlist()` flow because the prebuilt
154`<Waitlist />` component is web-only in the Expo SDK. Approved users can use **Sign in** from that
155screen.
156
157## Alternative: Known-User Allowlist
158
159For a closed beta where all permitted users are known in advance, use an allowlist instead of a
160request-and-approval waitlist:
161
162To restrict the beta to permitted email addresses or domains:
163
1641. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email
165 domain.
1662. Enable the allowlist and save.
1673. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or
168 manually created without a waitlist request flow.
169
170Do not enable an empty allowlist: it blocks all new sign-ups.
171
172Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud
173access. To remove an already-created user's access, ban that user in Clerk so their active
174sessions are ended and future sign-ins are rejected.
docs/t3-code-cloud-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 Code Cloud 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 Code Cloud Control Plane and Managed Endpoint Flow</h1>
257 <p class="lede">
258 T3 Cloud links a locally authorized T3 environment to a signed-in cloud user, provisions a
259 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 Cloud 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 Cloud 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/cloud/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/cloud/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 cloud 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 cloud 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 cloud login</code> stores authorization without enabling exposure.
611 <code>t3 cloud unlink</code> disables exposure while retaining authorization.
612 <code>t3 cloud 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-cloud/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/cloud/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/cloud/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/cloud/link-state</code></td>
955 <td>Local environment token with <code>relay:read</code></td>
956 <td>Read the environment's current T3 Cloud link state.</td>
957 </tr>
958 <tr>
959 <td><code>POST /api/cloud/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/cloud/unlink</code></td>
965 <td>Local environment token with <code>relay:write</code></td>
966 <td>Stop the relay client and clear local T3 Cloud configuration.</td>
967 </tr>
968 <tr>
969 <td><code>POST /api/t3-cloud/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-cloud/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/user/keybindings.md created+103
......@@ -0,0 +1,103 @@
1# Keybindings
2
3T3 Code reads keybindings from:
4
5- `~/.t3/keybindings.json`
6
7The file must be a JSON array of rules:
8
9```json
10[
11 { "key": "mod+g", "command": "terminal.toggle" },
12 { "key": "mod+shift+g", "command": "terminal.new", "when": "terminalFocus" }
13]
14```
15
16See the full schema for more details: [`packages/contracts/src/keybindings.ts`](../../packages/contracts/src/keybindings.ts)
17
18## Defaults
19
20```json
21[
22 { "key": "mod+j", "command": "terminal.toggle" },
23 { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" },
24 { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
25 { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" },
26 { "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" },
27 { "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
28 { "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
29 { "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },
30 { "key": "mod+o", "command": "editor.openFavorite" }
31]
32```
33
34For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](../../apps/server/src/keybindings.ts)
35
36## Configuration
37
38### Rule Shape
39
40Each entry supports:
41
42- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d`
43- `command` (required): action ID
44- `when` (optional): boolean expression controlling when the shortcut is active
45
46Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server.
47
48### Available Commands
49
50- `terminal.toggle`: open/close terminal drawer
51- `terminal.split`: split terminal (in focused terminal context by default)
52- `terminal.new`: create new terminal (in focused terminal context by default)
53- `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
54- `commandPalette.toggle`: open or close the global command palette
55- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
56- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
57- `editor.openFavorite`: open current project/worktree in the last-used editor
58- `script.{id}.run`: run a project script by id (for example `script.test.run`)
59
60### Key Syntax
61
62Supported modifiers:
63
64- `mod` (`cmd` on macOS, `ctrl` on non-macOS)
65- `cmd` / `meta`
66- `ctrl` / `control`
67- `shift`
68- `alt` / `option`
69
70Examples:
71
72- `mod+j`
73- `mod+shift+d`
74- `ctrl+l`
75- `cmd+k`
76
77### `when` Conditions
78
79Currently available context keys:
80
81- `terminalFocus`
82- `terminalOpen`
83
84Supported operators:
85
86- `!` (not)
87- `&&` (and)
88- `||` (or)
89- parentheses: `(` `)`
90
91Examples:
92
93- `"when": "terminalFocus"`
94- `"when": "terminalOpen && !terminalFocus"`
95- `"when": "terminalFocus || terminalOpen"`
96
97Unknown condition keys evaluate to `false`.
98
99### Precedence
100
101- Rules are evaluated in array order.
102- For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins.
103- That means precedence is across commands, not only within the same command.
docs/user/remote-access.md created+199
......@@ -0,0 +1,199 @@
1# Remote Access
2
3Use this when you want to connect to a T3 Code server from another device such as a phone, tablet, or separate desktop app.
4
5## Recommended Setup
6
7Use a trusted private network that meshes your devices together, such as a tailnet.
8
9That gives you:
10
11- a stable address to connect to
12- transport security at the network layer
13- less exposure than opening the server to the public internet
14
15## Enabling Network Access
16
17There are two ways to expose your server for remote connections: from the desktop app or from the CLI.
18
19### Option 1: Desktop App
20
21If you are already running the desktop app and want to make it reachable from other devices:
22
231. 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.
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.
264. Use **Create Link** to generate a pairing link you can share with another device.
27
28The default endpoint controls the QR code and primary copy action for pairing links. You can change it from the expanded endpoint list. The preference is stored by endpoint type, so choosing the local LAN endpoint survives normal IP address changes when you move between networks.
29
30When no user default is saved, the app uses the built-in LAN endpoint for pairing links when
31available. You can set another endpoint as the default from the expanded endpoint list.
32
33- HTTPS/WSS-compatible endpoints work from `https://app.t3.codes`, but are not made the default
34 automatically.
35- Non-loopback HTTP endpoints are useful for direct LAN pairing.
36- Loopback-only endpoints are not useful for another device unless that device is the same machine.
37
38If the copied link points directly at `http://192.168.x.y:3773`, open it from a client that can reach that LAN address. If it points at `https://app.t3.codes/pair?...`, the hosted web app will save the environment and connect directly to the backend URL in the link.
39
40### Tailscale Endpoints
41
42When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list.
43
44Depending on your Tailscale setup, this may include:
45
46- the machine's `100.x.y.z` Tailnet IP
47- a MagicDNS name
48- an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend
49
50The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as
51`https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this
52backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend
53with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale
54Serve to proxy HTTPS traffic to the local backend.
55
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.
57
58For `https://app.t3.codes`, prefer an HTTPS Tailnet or other HTTPS endpoint. A plain `http://100.x.y.z:3773` endpoint can still work from a desktop client or another browser page served over HTTP, but it will not work from the hosted HTTPS app because of browser mixed-content rules.
59
60### Option 2: Headless Server (CLI)
61
62Use this when you want to run the server without a GUI, for example on a remote machine over SSH.
63
64Run the server with `t3 serve`.
65
66```bash
67npx t3 serve --host "$(tailscale ip -4)"
68```
69
70`t3 serve` starts the server without opening a browser and prints:
71
72- a connection string
73- a pairing token
74- a pairing URL
75- a QR code for the pairing URL
76
77From there, connect from another device in either of these ways:
78
79- scan the QR code on your phone
80- in the desktop app, enter the full pairing URL
81- in the desktop app, enter the host and token separately
82- in the hosted web app, open a hosted pairing URL when the backend is reachable over HTTPS
83
84Use `t3 serve --help` for the full flag reference. It supports the same general startup options as the normal server command, including an optional `cwd` argument.
85
86For hosted web pairing over Tailscale HTTPS, opt in to Tailscale Serve:
87
88```bash
89npx t3 serve --tailscale-serve
90```
91
92By default this configures Tailscale Serve on HTTPS port 443 and advertises
93`https://machine.tailnet.ts.net/`. Advanced users can choose a different HTTPS port:
94
95```bash
96npx t3 serve --tailscale-serve --tailscale-serve-port 8443
97```
98
99> Note
100> The GUIs do not currently support adding projects on remote environments.
101> For now, use `t3 project ...` on the server machine instead.
102> Full GUI support for remote project management is coming soon.
103
104### Option 3: Desktop-Managed SSH Launch
105
106Use this when you want the desktop app to start or reuse T3 Code on another machine over SSH.
107
1081. Open **Settings** → **Connections**.
1092. Under **Remote Environments**, choose **Add environment**.
1103. Select the SSH launch flow.
1114. Enter the SSH target, such as `user@example.com`.
1125. Confirm the launch. The desktop app probes the host, starts or reuses a remote T3 server, opens a local port forward, and saves the environment.
113
114After setup, the renderer connects to a local forwarded HTTP/WebSocket endpoint. The remote host still owns the actual T3 server, projects, files, git state, terminals, and provider sessions.
115
116SSH launch is a desktop feature because it needs local process and SSH access. Once the environment is paired and saved, it uses the same environment list and connection model as direct LAN, Tailscale, HTTPS, or future tunnel-backed environments.
117
118#### SSH Launch Troubleshooting
119
120The desktop SSH launcher connects with a non-interactive `sh` session, writes a small launcher script under `~/.t3/ssh-launch/<host-key>/`, starts or reuses a remote T3 server, and forwards the remote loopback port back to your desktop.
121
122The remote host must have a compatible Node.js runtime. T3 Code uses the server package's `engines.node` requirement:
123
124```text
125^22.16 || ^23.11 || >=24.10
126```
127
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:
129
130- `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
131- Volta via `~/.volta/bin`
132- asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh`
133- mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh`
134- fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh`
135- nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -`
136- nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts`
137- installed nvm versions under `$NVM_DIR/versions/node/*/bin`
138
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:
140
141```bash
142ssh user@example.com 'sh -lc "command -v node && node --version"'
143```
144
145If that does not print a compatible Node version, configure your version manager for non-interactive shells or install a compatible Node binary in one of the searched locations. For example, with nvm you may need a default alias:
146
147```bash
148nvm alias default 24
149```
150
151With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above.
152
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.
154
155## How Pairing Works
156
157The remote device does not need a long-lived secret up front.
158
159Instead:
160
1611. `t3 serve` issues a one-time owner pairing token.
1622. The remote device exchanges that token with the server.
1633. The server creates an authenticated session for that device.
164
165After pairing, future access is session-based. You do not need to keep reusing the original token unless you are pairing a new device.
166
167## Hosted Web App Pairing
168
169The hosted web app at `https://app.t3.codes` can save a remote backend in browser local storage from a URL like:
170
171```text
172https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE
173```
174
175Use hosted pairing when the backend is reachable from the browser over HTTPS/WSS. This includes a backend behind a trusted HTTPS tunnel or another HTTPS endpoint you operate.
176
177Do not use hosted pairing for plain HTTP LAN URLs such as `http://192.168.x.y:3773`. Browsers block an HTTPS page from connecting to an insecure HTTP or WS backend. For those endpoints, use the direct pairing URL shown by the desktop app or CLI from a client that can open that HTTP URL directly.
178
179Hosted pairing does not proxy traffic through T3 Code. The browser still connects directly to the backend URL in the pairing link.
180
181## Managing Access Later
182
183Use `t3 auth` to manage access after the initial pairing flow.
184
185Typical uses:
186
187- issue additional pairing credentials
188- inspect active sessions
189- revoke old pairing links or sessions
190
191Use `t3 auth --help` and the nested subcommand help pages for the full reference.
192
193## Security Notes
194
195- Treat pairing URLs and pairing tokens like passwords.
196- Prefer binding `--host` to a trusted private address, such as a Tailnet IP, instead of exposing the server broadly.
197- Anyone with a valid pairing credential can create a session until that credential expires or is revoked.
198- Hosted pairing links keep the credential in the URL hash so it is not sent to the hosted app server, but it can still be exposed through browser history, screenshots, logs, or copy/paste.
199- Use `t3 auth` to revoke credentials or sessions you no longer trust.