| author | |
| committer | |
| log | ec18938bf9be0a4ab682484812dc85395bfed133 |
| tree | ed18082c75f5947b98fc42b6ae94862c6a05d2f6 |
| parent | 5ae77c0d63ece1a3e3fa5a70bc7717aa7c342756 |
| signature | Signed by PGP key B5690EEEBB952194 |
48 files changed, 3918 insertions(+), 3899 deletions(-)
.docs/architecture.md deleted-143| ... | @@ -1,143 +0,0 @@ | ||
| 1 | # Architecture | ||
| 2 | |||
| 3 | T3 Code runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app. | ||
| 4 | |||
| 5 | ``` | ||
| 6 | ┌─────────────────────────────────┐ | ||
| 7 | │ Browser (React + Vite) │ | ||
| 8 | │ wsTransport (state machine) │ | ||
| 9 | │ Typed push decode at boundary │ | ||
| 10 | └──────────┬──────────────────────┘ | ||
| 11 | │ ws://localhost:3773 | ||
| 12 | ┌──────────▼──────────────────────┐ | ||
| 13 | │ apps/server (Node.js) │ | ||
| 14 | │ WebSocket + HTTP static server │ | ||
| 15 | │ ServerPushBus (ordered pushes) │ | ||
| 16 | │ ServerReadiness (startup gate) │ | ||
| 17 | │ OrchestrationEngine │ | ||
| 18 | │ ProviderService │ | ||
| 19 | │ CheckpointReactor │ | ||
| 20 | │ RuntimeReceiptBus │ | ||
| 21 | └──────────┬──────────────────────┘ | ||
| 22 | │ JSON-RPC over stdio | ||
| 23 | ┌──────────▼──────────────────────┐ | ||
| 24 | │ codex app-server │ | ||
| 25 | └─────────────────────────────────┘ | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Components | ||
| 29 | |||
| 30 | - **Browser app**: The React app renders session state, owns the client-side WebSocket transport, and treats typed push events as the boundary between server runtime details and UI state. | ||
| 31 | |||
| 32 | - **Server**: `apps/server` is the main coordinator. It serves the web app, accepts WebSocket requests, waits for startup readiness before welcoming clients, and sends all outbound pushes through a single ordered push path. | ||
| 33 | |||
| 34 | - **Provider runtime**: `codex app-server` does the actual provider/session work. The server talks to it over JSON-RPC on stdio and translates those runtime events into the app's orchestration model. | ||
| 35 | |||
| 36 | - **Background workers**: Long-running async flows such as runtime ingestion, command reaction, and checkpoint processing run as queue-backed workers. This keeps work ordered, reduces timing races, and gives tests a deterministic way to wait for the system to go idle. | ||
| 37 | |||
| 38 | - **Runtime signals**: The server emits lightweight typed receipts when important async milestones finish, such as checkpoint capture, diff finalization, or a turn becoming fully quiescent. Tests and orchestration code wait on these signals instead of polling internal state. | ||
| 39 | |||
| 40 | ## Event Lifecycle | ||
| 41 | |||
| 42 | ### Startup and client connect | ||
| 43 | |||
| 44 | ```mermaid | ||
| 45 | sequenceDiagram | ||
| 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 | |||
| 63 | 1. The browser boots [`WsTransport`][1] and registers typed listeners in [`wsNativeApi`][2]. | ||
| 64 | 2. The server accepts the connection in [`wsServer`][3] and brings up the runtime graph defined in [`serverLayers`][7]. | ||
| 65 | 3. [`ServerReadiness`][4] waits until the key startup barriers are complete. | ||
| 66 | 4. Once the server is ready, [`wsServer`][3] sends `server.welcome` from the contracts in [`ws.ts`][6] through [`ServerPushBus`][5]. | ||
| 67 | 5. 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 | ||
| 72 | sequenceDiagram | ||
| 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 | |||
| 93 | 1. A user action in the browser becomes a typed request through [`WsTransport`][1] and the browser API layer in [`nativeApi`][12]. | ||
| 94 | 2. [`wsServer`][3] decodes that request using the shared WebSocket contracts in [`ws.ts`][6] and routes it to the right service. | ||
| 95 | 3. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio. | ||
| 96 | 4. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events. | ||
| 97 | 5. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events. | ||
| 98 | 6. [`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 | ||
| 103 | sequenceDiagram | ||
| 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 | |||
| 122 | 1. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14]. | ||
| 123 | 2. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic. | ||
| 124 | 3. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence. | ||
| 125 | 4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers. | ||
| 126 | 5. 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 | |||
| 3 | This is a living glossary for T3 Code. It explains what common terms mean in this codebase. | ||
| 4 | |||
| 5 | ## Table of contents | ||
| 6 | |||
| 7 | - [Project and workspace](#project-and-workspace) | ||
| 8 | - [Thread timeline](#thread-timeline) | ||
| 9 | - [Orchestration](#orchestration) | ||
| 10 | - [Provider runtime](#provider-runtime) | ||
| 11 | - [Checkpointing](#checkpointing) | ||
| 12 | |||
| 13 | ## Concepts | ||
| 14 | |||
| 15 | ### Project and workspace | ||
| 16 | |||
| 17 | #### Project | ||
| 18 | |||
| 19 | The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot`, a title, and one or more threads. See [workspace-layout.md][2]. | ||
| 20 | |||
| 21 | #### Workspace root | ||
| 22 | |||
| 23 | The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2]. | ||
| 24 | |||
| 25 | #### Worktree | ||
| 26 | |||
| 27 | A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live in [GitCore.ts][3]. | ||
| 28 | |||
| 29 | ### Thread timeline | ||
| 30 | |||
| 31 | #### Thread | ||
| 32 | |||
| 33 | The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4]. | ||
| 34 | |||
| 35 | #### Turn | ||
| 36 | |||
| 37 | A single user-to-assistant work cycle inside a thread. It starts with user input and ends when follow-up work like checkpointing settles. See [the contracts][1], [ProviderRuntimeIngestion.ts][5], and [CheckpointReactor.ts][6]. | ||
| 38 | |||
| 39 | #### Activity | ||
| 40 | |||
| 41 | A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4]. | ||
| 42 | |||
| 43 | ### Orchestration | ||
| 44 | |||
| 45 | Orchestration is the server-side domain layer that turns runtime activity into stable app state. The main entry point is [OrchestrationEngine.ts][7], with core logic in [decider.ts][8] and [projector.ts][4]. | ||
| 46 | |||
| 47 | #### Aggregate | ||
| 48 | |||
| 49 | The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8]. | ||
| 50 | |||
| 51 | #### Command | ||
| 52 | |||
| 53 | A typed request to change domain state. In [the contracts][1], commands are validated in [commandInvariants.ts][9] and turned into events by [decider.ts][8]. | ||
| 54 | Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`. | ||
| 55 | |||
| 56 | #### Domain Event | ||
| 57 | |||
| 58 | A persisted fact that something already happened. In [the contracts][1], events are the source of truth, and [projector.ts][4] shows how they are applied. | ||
| 59 | Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`. | ||
| 60 | |||
| 61 | #### Decider | ||
| 62 | |||
| 63 | The pure orchestration logic that turns commands plus current state into events. The core implementation is in [decider.ts][8], with preconditions in [commandInvariants.ts][9]. | ||
| 64 | |||
| 65 | #### Projection | ||
| 66 | |||
| 67 | A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10]. | ||
| 68 | |||
| 69 | #### Projector | ||
| 70 | |||
| 71 | The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11]. | ||
| 72 | |||
| 73 | #### Read model | ||
| 74 | |||
| 75 | The current materialized view of orchestration state. In [the contracts][1], it holds projects, threads, messages, activities, checkpoints, and session state. See [ProjectionSnapshotQuery.ts][10] and [OrchestrationEngine.ts][7]. | ||
| 76 | |||
| 77 | #### Reactor | ||
| 78 | |||
| 79 | A side-effecting service that handles follow-up work after events or runtime signals. Examples include [CheckpointReactor.ts][6], [ProviderCommandReactor.ts][12], and [ProviderRuntimeIngestion.ts][5]. | ||
| 80 | |||
| 81 | #### Receipt | ||
| 82 | |||
| 83 | A lightweight typed runtime signal emitted when an async milestone completes. See [RuntimeReceiptBus.ts][13]. | ||
| 84 | Examples include `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, and `turn.processing.quiesced`, which are emitted by flows such as [CheckpointReactor.ts][6]. | ||
| 85 | |||
| 86 | #### Quiesced | ||
| 87 | |||
| 88 | "Quiesced" means a turn has gone quiet and stable. In [the receipt schema][13], it means the follow-up work has settled, including work in [CheckpointReactor.ts][6]. | ||
| 89 | |||
| 90 | ### Provider runtime | ||
| 91 | |||
| 92 | The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [provider-architecture.md][16]. | ||
| 93 | |||
| 94 | #### Provider | ||
| 95 | |||
| 96 | The backend agent runtime that actually performs work. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17]. | ||
| 97 | |||
| 98 | #### Session | ||
| 99 | |||
| 100 | The live provider-backed runtime attached to a thread. Session shape is in [the orchestration contracts][1], and lifecycle is managed in [ProviderService.ts][14]. | ||
| 101 | |||
| 102 | #### Runtime mode | ||
| 103 | |||
| 104 | The safety/access mode for a thread or session. In [the contracts][1], the main values are `approval-required` and `full-access`. See [runtime-modes.md][18]. | ||
| 105 | |||
| 106 | #### Interaction mode | ||
| 107 | |||
| 108 | The agent interaction style for a thread. In [the contracts][1], the main values are `default` and `plan`. See [runtime-modes.md][18]. | ||
| 109 | |||
| 110 | #### Assistant delivery mode | ||
| 111 | |||
| 112 | Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` delivers a completed result. See [ProviderService.ts][14]. | ||
| 113 | |||
| 114 | #### Snapshot | ||
| 115 | |||
| 116 | A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19]. | ||
| 117 | |||
| 118 | ### Checkpointing | ||
| 119 | |||
| 120 | Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6]. | ||
| 121 | |||
| 122 | #### Checkpoint | ||
| 123 | |||
| 124 | A saved snapshot of a thread workspace at a particular turn. In practice it is a hidden Git ref in [CheckpointStore.ts][19] plus a projected summary from [ProjectionCheckpoints.ts][21]. Capture and lifecycle work happen in [CheckpointReactor.ts][6]. | ||
| 125 | |||
| 126 | #### Checkpoint ref | ||
| 127 | |||
| 128 | The durable identifier for a filesystem checkpoint, stored as a Git ref. It is typed in [the contracts][1], constructed in [Utils.ts][22], and used by [CheckpointStore.ts][19]. | ||
| 129 | |||
| 130 | #### Checkpoint baseline | ||
| 131 | |||
| 132 | The starting checkpoint for diffing a thread timeline. This flow is surfaced through [RuntimeReceiptBus.ts][13], coordinated in [CheckpointReactor.ts][6], and supported by [Utils.ts][22]. | ||
| 133 | |||
| 134 | #### Checkpoint diff | ||
| 135 | |||
| 136 | The patch difference between two checkpoints. Query logic lives in [CheckpointDiffQuery.ts][20], diff parsing lives in [Diffs.ts][23], and finalization is coordinated by [CheckpointReactor.ts][6]. | ||
| 137 | |||
| 138 | #### Turn diff | ||
| 139 | |||
| 140 | The file patch and changed-file summary for one turn. It is usually computed in [CheckpointDiffQuery.ts][20], represented in [the contracts][1], and recorded into thread state by [projector.ts][4]. | ||
| 141 | |||
| 142 | ## Practical Shortcuts | ||
| 143 | |||
| 144 | - If you see `requested`, think "intent recorded". | ||
| 145 | - If you see `completed`, think "result applied". | ||
| 146 | - If you see `receipt`, think "async milestone signal". | ||
| 147 | - If you see `checkpoint`, think "workspace snapshot for diff/restore". | ||
| 148 | - If you see `quiesced`, think "all relevant follow-up work has gone idle". | ||
| 149 | |||
| 150 | ## Related Docs | ||
| 151 | |||
| 152 | - [architecture.md][24] | ||
| 153 | - [provider-architecture.md][16] | ||
| 154 | - [runtime-modes.md][18] | ||
| 155 | - [workspace-layout.md][2] | ||
| 156 | |||
| 157 | [1]: ../packages/contracts/src/orchestration.ts | ||
| 158 | [2]: ./workspace-layout.md | ||
| 159 | [3]: ../apps/server/src/git/Layers/GitCore.ts | ||
| 160 | [4]: ../apps/server/src/orchestration/projector.ts | ||
| 161 | [5]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | ||
| 162 | [6]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts | ||
| 163 | [7]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | ||
| 164 | [8]: ../apps/server/src/orchestration/decider.ts | ||
| 165 | [9]: ../apps/server/src/orchestration/commandInvariants.ts | ||
| 166 | [10]: ../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts | ||
| 167 | [11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts | ||
| 168 | [12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | ||
| 169 | [13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts | ||
| 170 | [14]: ../apps/server/src/provider/Layers/ProviderService.ts | ||
| 171 | [15]: ../apps/server/src/provider/Services/ProviderAdapter.ts | ||
| 172 | [16]: ./provider-architecture.md | ||
| 173 | [17]: ../apps/server/src/provider/Layers/CodexAdapter.ts | ||
| 174 | [18]: ./runtime-modes.md | ||
| 175 | [19]: ../apps/server/src/checkpointing/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 | |||
| 3 | The web app communicates with the server via WebSocket using a simple JSON-RPC-style protocol: | ||
| 4 | |||
| 5 | - **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }` | ||
| 6 | - **Push events**: typed envelopes with `channel`, `sequence` (monotonic per connection), and channel-specific `data` | ||
| 7 | |||
| 8 | Push channels: `server.welcome`, `server.configUpdated`, `terminal.event`, `orchestration.domainEvent`. Payloads are schema-validated at the transport boundary (`wsTransport.ts`). Decode failures produce structured `WsDecodeDiagnostic` with `code`, `reason`, and path info. | ||
| 9 | |||
| 10 | Methods mirror the `NativeApi` interface defined in `@t3tools/contracts`: | ||
| 11 | |||
| 12 | - `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn` | ||
| 13 | - `providers.respondToRequest`, `providers.stopSession` | ||
| 14 | - `shell.openInEditor`, `server.getConfig` | ||
| 15 | |||
| 16 | Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. | ||
| 17 | |||
| 18 | ## Client transport | ||
| 19 | |||
| 20 | `wsTransport.ts` manages connection state: `connecting` → `open` → `reconnecting` → `closed` → `disposed`. Outbound requests are queued while disconnected and flushed on reconnect. Inbound pushes are decoded and validated at the boundary, then cached per channel. Subscribers can opt into `replayLatest` to receive the last push on subscribe. | ||
| 21 | |||
| 22 | ## Server-side orchestration layers | ||
| 23 | |||
| 24 | Provider runtime events flow through queue-based workers: | ||
| 25 | |||
| 26 | 1. **ProviderRuntimeIngestion** — consumes provider runtime streams, emits orchestration commands | ||
| 27 | 2. **ProviderCommandReactor** — reacts to orchestration intent events, dispatches provider calls | ||
| 28 | 3. **CheckpointReactor** — captures git checkpoints on turn start/complete, publishes runtime receipts | ||
| 29 | |||
| 30 | All three use `DrainableWorker` internally and expose `drain()` for deterministic test synchronization. | ||
.docs/quick-start.md deleted-22| ... | @@ -1,22 +0,0 @@ | ||
| 1 | # Quick start | ||
| 2 | |||
| 3 | ```bash | ||
| 4 | # Development (with hot reload) | ||
| 5 | bun run dev | ||
| 6 | |||
| 7 | # Desktop development | ||
| 8 | bun run dev:desktop | ||
| 9 | |||
| 10 | # Desktop development on an isolated port set | ||
| 11 | T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop | ||
| 12 | |||
| 13 | # Production | ||
| 14 | bun run build | ||
| 15 | bun run start | ||
| 16 | |||
| 17 | # Build a shareable macOS .dmg (arm64 by default) | ||
| 18 | bun run dist:desktop:dmg | ||
| 19 | |||
| 20 | # Or from any project directory after publishing: | ||
| 21 | npx t3 | ||
| 22 | ``` | ||
.docs/remote-architecture.md deleted-382| ... | @@ -1,382 +0,0 @@ | ||
| 1 | # Remote Architecture | ||
| 2 | |||
| 3 | This document describes the target architecture for first-class remote environments in T3 Code. | ||
| 4 | |||
| 5 | It is intentionally architecture-first. It does not define a complete implementation plan or user-facing rollout checklist. The goal is to establish the core model so remote support can be added without another broad rewrite. | ||
| 6 | |||
| 7 | ## Goals | ||
| 8 | |||
| 9 | - Treat remote environments as first-class product primitives, not special cases. | ||
| 10 | - Support multiple ways to reach the same environment. | ||
| 11 | - Keep the T3 server as the execution boundary. | ||
| 12 | - Let desktop, mobile, and web all share the same conceptual model. | ||
| 13 | - Avoid introducing a local control plane unless product pressure proves it is necessary. | ||
| 14 | |||
| 15 | ## Non-goals | ||
| 16 | |||
| 17 | - Replacing the existing WebSocket server boundary with a custom transport protocol. | ||
| 18 | - Making SSH the only remote story. | ||
| 19 | - Syncing provider auth across machines. | ||
| 20 | - Shipping every access method in the first iteration. | ||
| 21 | |||
| 22 | ## High-level architecture | ||
| 23 | |||
| 24 | T3 already has a clean runtime boundary: the client talks to a T3 server over HTTP/WebSocket, and the server owns orchestration, providers, terminals, git, and filesystem operations. | ||
| 25 | |||
| 26 | Remote support should preserve that boundary. | ||
| 27 | |||
| 28 | ```text | ||
| 29 | ┌──────────────────────────────────────────────┐ | ||
| 30 | │ Client (desktop / mobile / web) │ | ||
| 31 | │ │ | ||
| 32 | │ - known environments │ | ||
| 33 | │ - connection manager │ | ||
| 34 | │ - environment-aware routing │ | ||
| 35 | └───────────────┬──────────────────────────────┘ | ||
| 36 | │ | ||
| 37 | │ resolves one access endpoint | ||
| 38 | │ | ||
| 39 | ┌───────────────▼──────────────────────────────┐ | ||
| 40 | │ Access method │ | ||
| 41 | │ │ | ||
| 42 | │ - direct ws / wss │ | ||
| 43 | │ - tunneled ws / wss │ | ||
| 44 | │ - desktop-managed ssh bootstrap + forward │ | ||
| 45 | └───────────────┬──────────────────────────────┘ | ||
| 46 | │ | ||
| 47 | │ connects to one T3 server | ||
| 48 | │ | ||
| 49 | ┌───────────────▼──────────────────────────────┐ | ||
| 50 | │ Execution environment = one T3 server │ | ||
| 51 | │ │ | ||
| 52 | │ - environment identity │ | ||
| 53 | │ - provider state │ | ||
| 54 | │ - projects / threads / terminals │ | ||
| 55 | │ - git / filesystem / process runtime │ | ||
| 56 | └──────────────────────────────────────────────┘ | ||
| 57 | ``` | ||
| 58 | |||
| 59 | The important decision is that remoteness is expressed at the environment connection layer, not by splitting the T3 runtime itself. | ||
| 60 | |||
| 61 | ## Domain model | ||
| 62 | |||
| 63 | ### ExecutionEnvironment | ||
| 64 | |||
| 65 | An `ExecutionEnvironment` is one running T3 server instance. | ||
| 66 | |||
| 67 | It is the unit that owns: | ||
| 68 | |||
| 69 | - provider availability and auth state | ||
| 70 | - model availability | ||
| 71 | - projects and threads | ||
| 72 | - terminal processes | ||
| 73 | - filesystem access | ||
| 74 | - git operations | ||
| 75 | - server settings | ||
| 76 | |||
| 77 | It is identified by a stable `environmentId`. | ||
| 78 | |||
| 79 | This is the shared cross-client primitive. Desktop, mobile, and web should all reason about the same concept here. | ||
| 80 | |||
| 81 | ### KnownEnvironment | ||
| 82 | |||
| 83 | A `KnownEnvironment` is a client-side saved entry for an environment the client knows how to reach. | ||
| 84 | |||
| 85 | It is not server-authored. It is local to a device or client profile. | ||
| 86 | |||
| 87 | Examples: | ||
| 88 | |||
| 89 | - a saved LAN URL | ||
| 90 | - a saved public `wss://` endpoint | ||
| 91 | - a desktop-managed SSH host entry | ||
| 92 | - a saved tunneled environment | ||
| 93 | |||
| 94 | A known environment may or may not know the target `environmentId` before first successful connect. | ||
| 95 | |||
| 96 | In the hosted web app, known environments are browser-local. A hosted pairing URL can create the saved entry, but it does not give the hosted app a server-side control plane or a copy of the session state. | ||
| 97 | |||
| 98 | ### AccessEndpoint | ||
| 99 | |||
| 100 | An `AccessEndpoint` is one concrete way to reach a known environment. | ||
| 101 | |||
| 102 | This is the key abstraction that keeps SSH from taking over the model. | ||
| 103 | |||
| 104 | A single environment may have many endpoints: | ||
| 105 | |||
| 106 | - `wss://t3.example.com` | ||
| 107 | - `ws://10.0.0.25:3773` | ||
| 108 | - a tunneled relay URL | ||
| 109 | - a desktop-managed SSH tunnel that resolves to a local forwarded WebSocket URL | ||
| 110 | |||
| 111 | The environment stays the same. Only the access path changes. | ||
| 112 | |||
| 113 | ### AdvertisedEndpoint | ||
| 114 | |||
| 115 | An `AdvertisedEndpoint` is a server or desktop-authored candidate endpoint for an environment. It is how the backend tells the client which URLs may be useful for pairing and reconnecting. | ||
| 116 | |||
| 117 | `AdvertisedEndpoint` is deliberately narrower than the full access model: | ||
| 118 | |||
| 119 | - it describes a concrete HTTP and WebSocket base URL pair | ||
| 120 | - it can mark the endpoint as default, available, or unavailable | ||
| 121 | - it includes reachability hints such as loopback, LAN, private, public, or tunnel | ||
| 122 | - it includes compatibility hints such as whether the endpoint can be used from the hosted HTTPS app | ||
| 123 | |||
| 124 | Clients should treat advertised endpoints as hints, not as proof that a route works from the current device. The final connection attempt still decides whether the endpoint is reachable. | ||
| 125 | |||
| 126 | The UI presents one default advertised endpoint in the network-access summary and keeps the rest behind an expandable advanced list. The default controls pairing QR codes and primary copy actions. Users can override it, but that override is a UI preference, not backend configuration. | ||
| 127 | |||
| 128 | Persist the override by stable endpoint kind rather than raw URL whenever possible. For example, a LAN endpoint should be stored as the desktop LAN endpoint preference, not as `192.168.x.y`, because the address can change when the user switches networks. Provider endpoints should use provider-specific stable keys such as Tailscale IP or Tailscale MagicDNS HTTPS. Custom endpoints may fall back to their concrete identity. | ||
| 129 | |||
| 130 | When no user default is saved, endpoint selection should prefer: | ||
| 131 | |||
| 132 | 1. endpoints compatible with the hosted HTTPS app | ||
| 133 | 2. explicitly default endpoints | ||
| 134 | 3. non-loopback endpoints | ||
| 135 | 4. loopback endpoints only for same-machine clients | ||
| 136 | |||
| 137 | This keeps endpoint discovery centralized without making any one provider, such as Tailscale or a future tunnel service, part of the core environment model. | ||
| 138 | |||
| 139 | ### Endpoint providers | ||
| 140 | |||
| 141 | Endpoint providers are add-ons that contribute advertised endpoints for the current environment. | ||
| 142 | |||
| 143 | The provider boundary is intentionally outside the core environment model: | ||
| 144 | |||
| 145 | - core owns `ExecutionEnvironment`, saved environments, pairing, and connection lifecycle | ||
| 146 | - providers discover or synthesize endpoints | ||
| 147 | - providers return normalized `AdvertisedEndpoint` records | ||
| 148 | - the UI and pairing logic select from those records without knowing provider-specific commands | ||
| 149 | |||
| 150 | The first provider is Tailscale. It can discover Tailnet IP and MagicDNS addresses from the local machine and publish them as additional endpoint candidates. Future providers, such as a hosted tunnel service, should plug into the same shape rather than adding a separate remote environment path. | ||
| 151 | |||
| 152 | Provider-specific confidence should remain a hint. A Tailscale endpoint still needs a successful browser or desktop connection before the client treats it as connected. | ||
| 153 | |||
| 154 | ### Hosted pairing request | ||
| 155 | |||
| 156 | A hosted pairing request is a bootstrap URL for the static web app, not a transport. | ||
| 157 | |||
| 158 | Example: | ||
| 159 | |||
| 160 | ```text | ||
| 161 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | ||
| 162 | ``` | ||
| 163 | |||
| 164 | The hosted app reads the `host` parameter and pairing token, exchanges the token directly with that backend, then saves the resulting environment record in browser local storage. | ||
| 165 | |||
| 166 | Important constraints: | ||
| 167 | |||
| 168 | - the hosted app does not proxy HTTP or WebSocket traffic | ||
| 169 | - the backend must still be reachable directly from the browser | ||
| 170 | - HTTPS pages can only connect to HTTPS/WSS backends | ||
| 171 | - HTTP LAN endpoints should keep using direct desktop or CLI pairing URLs | ||
| 172 | - the token belongs in the URL hash so it is not sent to the hosted app origin | ||
| 173 | |||
| 174 | ### RepositoryIdentity | ||
| 175 | |||
| 176 | `RepositoryIdentity` remains a best-effort logical repo grouping mechanism across environments. | ||
| 177 | |||
| 178 | It is not used for routing. It is only used for UI grouping and correlation between local and remote clones of the same repository. | ||
| 179 | |||
| 180 | ### Workspace / Project | ||
| 181 | |||
| 182 | The current `Project` model remains environment-local. | ||
| 183 | |||
| 184 | That means: | ||
| 185 | |||
| 186 | - a local clone and a remote clone are different projects | ||
| 187 | - they may share a `RepositoryIdentity` | ||
| 188 | - threads still bind to one project in one environment | ||
| 189 | |||
| 190 | ## Access methods | ||
| 191 | |||
| 192 | Access methods answer one question: | ||
| 193 | |||
| 194 | How does the client speak WebSocket to a T3 server? | ||
| 195 | |||
| 196 | They do not answer: | ||
| 197 | |||
| 198 | - how the server got started | ||
| 199 | - who manages the server process | ||
| 200 | - whether the environment is local or remote | ||
| 201 | |||
| 202 | ### 1. Direct WebSocket access | ||
| 203 | |||
| 204 | Examples: | ||
| 205 | |||
| 206 | - `ws://10.0.0.15:3773` | ||
| 207 | - `wss://t3.example.com` | ||
| 208 | |||
| 209 | This is the base model and should be the first-class default. | ||
| 210 | |||
| 211 | Benefits: | ||
| 212 | |||
| 213 | - works for desktop, mobile, and web | ||
| 214 | - no client-specific process management required | ||
| 215 | - best fit for hosted or self-managed remote T3 deployments | ||
| 216 | |||
| 217 | Browser security rules are part of this access method. A hosted HTTPS web client can connect to `wss://` backends, but it cannot connect to plain `ws://` or `http://` LAN backends because that would be mixed content. | ||
| 218 | |||
| 219 | ### 2. Tunneled WebSocket access | ||
| 220 | |||
| 221 | Examples: | ||
| 222 | |||
| 223 | - public relay URLs | ||
| 224 | - private network relay URLs | ||
| 225 | - local tunnel products such as pipenet | ||
| 226 | |||
| 227 | This is still direct WebSocket access from the client's perspective. The difference is that the route is mediated by a tunnel or relay. | ||
| 228 | |||
| 229 | For T3, tunnels are best modeled as another `AccessEndpoint`, not as a different kind of environment. | ||
| 230 | |||
| 231 | This is especially useful when: | ||
| 232 | |||
| 233 | - the host is behind NAT | ||
| 234 | - inbound ports are unavailable | ||
| 235 | - mobile must reach a desktop-hosted environment | ||
| 236 | - a machine should be reachable without exposing raw LAN or public ports | ||
| 237 | |||
| 238 | Tailscale-backed access sits here architecturally even though the current implementation is endpoint discovery rather than a T3-managed tunnel. It contributes private-network endpoints and lets the existing HTTP/WebSocket client path do the actual connection. | ||
| 239 | |||
| 240 | ### 3. Desktop-managed SSH access | ||
| 241 | |||
| 242 | SSH is an access and launch helper, not a separate environment type. | ||
| 243 | |||
| 244 | The desktop main process can use SSH to: | ||
| 245 | |||
| 246 | - reach a machine | ||
| 247 | - probe it | ||
| 248 | - launch or reuse a remote T3 server | ||
| 249 | - establish a local port forward | ||
| 250 | |||
| 251 | After that, the renderer should still connect using an ordinary WebSocket URL against the forwarded local port. | ||
| 252 | |||
| 253 | This keeps the renderer transport model consistent with every other access method. | ||
| 254 | |||
| 255 | The desktop main process owns the SSH bridge because it can spawn local SSH processes, manage askpass prompts, write temporary launch scripts, and clean up forwards. The renderer receives a saved environment record and connects through the forwarded URL; it should not need SSH-specific RPC paths for normal environment traffic. | ||
| 256 | |||
| 257 | ## Launch methods | ||
| 258 | |||
| 259 | Launch methods answer a different question: | ||
| 260 | |||
| 261 | How does a T3 server come to exist on the target machine? | ||
| 262 | |||
| 263 | Launch and access should stay separate in the design. | ||
| 264 | |||
| 265 | ### 1. Pre-existing server | ||
| 266 | |||
| 267 | The simplest launch method is no launch at all. | ||
| 268 | |||
| 269 | The user or operator already runs T3 on the target machine, and the client connects through a direct or tunneled WebSocket endpoint. | ||
| 270 | |||
| 271 | This should be the first remote mode shipped because it validates the environment model with minimal extra machinery. | ||
| 272 | |||
| 273 | ### 2. Desktop-managed remote launch over SSH | ||
| 274 | |||
| 275 | This is the main place where Zed is a useful reference. | ||
| 276 | |||
| 277 | Useful ideas to borrow from Zed: | ||
| 278 | |||
| 279 | - remote probing | ||
| 280 | - platform detection | ||
| 281 | - session directories with pid/log metadata | ||
| 282 | - reconnect-friendly launcher behavior | ||
| 283 | - desktop-owned connection UX | ||
| 284 | |||
| 285 | What should be different in T3: | ||
| 286 | |||
| 287 | - no custom stdio/socket proxy protocol between renderer and remote runtime | ||
| 288 | - no attempt to make the remote runtime look like an editor transport | ||
| 289 | - keep the final client-to-server connection as WebSocket | ||
| 290 | |||
| 291 | The recommended T3 flow is: | ||
| 292 | |||
| 293 | 1. Desktop connects over SSH. | ||
| 294 | 2. Desktop probes the remote machine and verifies T3 availability. | ||
| 295 | 3. Desktop launches or reuses a remote T3 server. | ||
| 296 | 4. Desktop establishes local port forwarding. | ||
| 297 | 5. Renderer connects to the forwarded WebSocket endpoint as a normal environment. | ||
| 298 | |||
| 299 | The saved environment should remember that it was created by desktop SSH launch only for reconnect and lifecycle UX. That metadata should not change the server protocol or the environment identity model. | ||
| 300 | |||
| 301 | Failure handling should be explicit: | ||
| 302 | |||
| 303 | - SSH authentication failure should surface before any environment is saved | ||
| 304 | - remote launch failure should include remote logs or the launcher command output when available | ||
| 305 | - forwarded-port failure should leave the saved environment disconnected rather than falling back to an unrelated endpoint | ||
| 306 | - reconnect should attempt to restore the SSH bridge before reconnecting the normal WebSocket client | ||
| 307 | |||
| 308 | ### 3. Client-managed local publish | ||
| 309 | |||
| 310 | This is the inverse of remote launch: a local T3 server is already running, and the client publishes it through a tunnel. | ||
| 311 | |||
| 312 | This is useful for: | ||
| 313 | |||
| 314 | - exposing a desktop-hosted environment to mobile | ||
| 315 | - temporary remote access without changing router or firewall settings | ||
| 316 | |||
| 317 | This is still a launch concern, not a new environment kind. | ||
| 318 | |||
| 319 | ## Why access and launch must stay separate | ||
| 320 | |||
| 321 | These concerns are easy to conflate, but separating them prevents architectural drift. | ||
| 322 | |||
| 323 | Examples: | ||
| 324 | |||
| 325 | - A manually hosted T3 server might be reached through direct `wss`. | ||
| 326 | - The same server might also be reachable through a tunnel. | ||
| 327 | - An SSH-managed server might be launched over SSH but then reached through forwarded WebSocket. | ||
| 328 | - A local desktop server might be published through a tunnel for mobile. | ||
| 329 | |||
| 330 | In all of those cases, the `ExecutionEnvironment` is the same kind of thing. | ||
| 331 | |||
| 332 | Only the launch and access paths differ. | ||
| 333 | |||
| 334 | ## Security model | ||
| 335 | |||
| 336 | Remote support must assume that some environments will be reachable over untrusted networks. | ||
| 337 | |||
| 338 | That 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 | |||
| 344 | T3 already supports a WebSocket auth token on the server. That should become a first-class part of environment access rather than remaining an incidental query parameter convention. | ||
| 345 | |||
| 346 | For publicly reachable environments, authenticated access should be treated as required. | ||
| 347 | |||
| 348 | Hosted pairing should be treated as a client-side convenience only. The hosted app must not receive pairing tokens through query parameters, must not store pairing state server-side, and must not imply that an HTTP backend is safe or reachable from an HTTPS browser context. | ||
| 349 | |||
| 350 | ## Relationship to Zed | ||
| 351 | |||
| 352 | Zed is a useful reference implementation for managed remote launch and reconnect behavior. | ||
| 353 | |||
| 354 | The 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 | |||
| 361 | The important mismatch is transport shape. | ||
| 362 | |||
| 363 | Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime. | ||
| 364 | |||
| 365 | T3 should not copy that part. | ||
| 366 | |||
| 367 | T3 already has the right runtime boundary: | ||
| 368 | |||
| 369 | - one T3 server per environment | ||
| 370 | - ordinary HTTP/WebSocket between client and environment | ||
| 371 | |||
| 372 | So T3 should borrow Zed's launch discipline, not its transport protocol. | ||
| 373 | |||
| 374 | ## Recommended rollout | ||
| 375 | |||
| 376 | 1. First-class known environments and access endpoints. | ||
| 377 | 2. Direct `ws` / `wss` remote environments. | ||
| 378 | 3. Authenticated tunnel-backed environments. | ||
| 379 | 4. Desktop-managed SSH launch and forwarding. | ||
| 380 | 5. Multi-environment UI improvements after the base runtime path is proven. | ||
| 381 | |||
| 382 | This 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 | |||
| 3 | T3 Code has a global runtime mode switch in the chat toolbar: | ||
| 4 | |||
| 5 | - **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. | ||
| 6 | - **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. | ||
.docs/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 | |||
| 36 | Set `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 | |||
| 42 | If 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 | |||
| 3 | T3 Code reads keybindings from: | ||
| 4 | |||
| 5 | - `~/.t3/keybindings.json` | ||
| 6 | |||
| 7 | The 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 | |||
| 16 | See 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 | |||
| 34 | For 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 | |||
| 40 | Each 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 | |||
| 46 | Invalid 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 | |||
| 62 | Supported modifiers: | ||
| 63 | |||
| 64 | - `mod` (`cmd` on macOS, `ctrl` on non-macOS) | ||
| 65 | - `cmd` / `meta` | ||
| 66 | - `ctrl` / `control` | ||
| 67 | - `shift` | ||
| 68 | - `alt` / `option` | ||
| 69 | |||
| 70 | Examples: | ||
| 71 | |||
| 72 | - `mod+j` | ||
| 73 | - `mod+shift+d` | ||
| 74 | - `ctrl+l` | ||
| 75 | - `cmd+k` | ||
| 76 | |||
| 77 | ### `when` Conditions | ||
| 78 | |||
| 79 | Currently available context keys: | ||
| 80 | |||
| 81 | - `terminalFocus` | ||
| 82 | - `terminalOpen` | ||
| 83 | |||
| 84 | Supported operators: | ||
| 85 | |||
| 86 | - `!` (not) | ||
| 87 | - `&&` (and) | ||
| 88 | - `||` (or) | ||
| 89 | - parentheses: `(` `)` | ||
| 90 | |||
| 91 | Examples: | ||
| 92 | |||
| 93 | - `"when": "terminalFocus"` | ||
| 94 | - `"when": "terminalOpen && !terminalFocus"` | ||
| 95 | - `"when": "terminalFocus || terminalOpen"` | ||
| 96 | |||
| 97 | Unknown 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. | ... | @@ -46,11 +46,19 @@ We are very very early in this project. Expect bugs. |
| 46 | 46 | ||
| 47 | We are not accepting contributions yet. | 47 | We are not accepting contributions yet. |
| 48 | 48 | ||
| 49 | Observability guide: [docs/observability.md](./docs/observability.md) | 49 | Observability guide: [docs/operations/observability.md](./docs/operations/observability.md) |
| 50 | 50 | ||
| 51 | Relay observability: [docs/relay-observability.md](./docs/relay-observability.md) | 51 | Relay observability: [docs/operations/relay-observability.md](./docs/operations/relay-observability.md) |
| 52 | 52 | ||
| 53 | T3 Cloud Clerk setup: [docs/t3-cloud-clerk.md](./docs/t3-cloud-clerk.md) | 53 | T3 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) | ||
| 54 | 62 | ||
| 55 | ## If you REALLY want to contribute still.... read this first | 63 | ## If you REALLY want to contribute still.... read this first |
| 56 | 64 |
REMOTE.md deleted-199| ... | @@ -1,199 +0,0 @@ | ||
| 1 | # Remote Access | ||
| 2 | |||
| 3 | Use 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 | |||
| 7 | Use a trusted private network that meshes your devices together, such as a tailnet. | ||
| 8 | |||
| 9 | That 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 | |||
| 17 | There 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 | |||
| 21 | If you are already running the desktop app and want to make it reachable from other devices: | ||
| 22 | |||
| 23 | 1. Open **Settings** → **Connections**. | ||
| 24 | 2. Under **Manage Local Backend**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. | ||
| 25 | 3. The settings panel will show the default reachable endpoint, with a `+N` control when more endpoints are available. Expand it to inspect alternatives such as loopback, LAN, private-network, or HTTPS endpoints. | ||
| 26 | 4. Use **Create Link** to generate a pairing link you can share with another device. | ||
| 27 | |||
| 28 | The 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 | |||
| 30 | When no user default is saved, the app uses the built-in LAN endpoint for pairing links when | ||
| 31 | available. 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 | |||
| 38 | If 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 | |||
| 42 | When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list. | ||
| 43 | |||
| 44 | Depending 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 | |||
| 50 | The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as | ||
| 51 | `https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this | ||
| 52 | backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend | ||
| 53 | with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale | ||
| 54 | Serve to proxy HTTPS traffic to the local backend. | ||
| 55 | |||
| 56 | The Tailscale support is an endpoint provider add-on. The core remote model still works without Tailscale: LAN HTTP endpoints, custom HTTPS endpoints, future tunnels, and SSH-launched environments all use the same saved environment and pairing flow. | ||
| 57 | |||
| 58 | For `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 | |||
| 62 | Use this when you want to run the server without a GUI, for example on a remote machine over SSH. | ||
| 63 | |||
| 64 | Run the server with `t3 serve`. | ||
| 65 | |||
| 66 | ```bash | ||
| 67 | npx 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 | |||
| 77 | From 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 | |||
| 84 | Use `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 | |||
| 86 | For hosted web pairing over Tailscale HTTPS, opt in to Tailscale Serve: | ||
| 87 | |||
| 88 | ```bash | ||
| 89 | npx t3 serve --tailscale-serve | ||
| 90 | ``` | ||
| 91 | |||
| 92 | By 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 | ||
| 96 | npx 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 | |||
| 106 | Use this when you want the desktop app to start or reuse T3 Code on another machine over SSH. | ||
| 107 | |||
| 108 | 1. Open **Settings** → **Connections**. | ||
| 109 | 2. Under **Remote Environments**, choose **Add environment**. | ||
| 110 | 3. Select the SSH launch flow. | ||
| 111 | 4. Enter the SSH target, such as `user@example.com`. | ||
| 112 | 5. 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 | |||
| 114 | After 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 | |||
| 116 | SSH 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 | |||
| 120 | The 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 | |||
| 122 | The 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 | |||
| 128 | During SSH launch, T3 Code first checks whether `node` is already available on `PATH`. If it is missing, the launcher tries common non-interactive shell locations and version-manager shims/activation hooks: | ||
| 129 | |||
| 130 | - `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` | ||
| 131 | - Volta via `~/.volta/bin` | ||
| 132 | - asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh` | ||
| 133 | - mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh` | ||
| 134 | - fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh` | ||
| 135 | - nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -` | ||
| 136 | - nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts` | ||
| 137 | - installed nvm versions under `$NVM_DIR/versions/node/*/bin` | ||
| 138 | |||
| 139 | If launch fails with `node: command not found`, a port-scan failure, or a message that the remote Node version does not satisfy the required range, SSH into the host and check the same non-interactive shell path T3 Code uses: | ||
| 140 | |||
| 141 | ```bash | ||
| 142 | ssh user@example.com 'sh -lc "command -v node && node --version"' | ||
| 143 | ``` | ||
| 144 | |||
| 145 | If 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 | ||
| 148 | nvm alias default 24 | ||
| 149 | ``` | ||
| 150 | |||
| 151 | With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above. | ||
| 152 | |||
| 153 | If reconnecting after an app update fails, retry the SSH launch once. The launcher now compares its generated runner script, stops stale launcher-managed remote servers, clears the SSH launch PID/port state, and starts a fresh remote server. You should not normally need to delete `~/.t3/ssh-launch` or kill `t3` processes manually. | ||
| 154 | |||
| 155 | ## How Pairing Works | ||
| 156 | |||
| 157 | The remote device does not need a long-lived secret up front. | ||
| 158 | |||
| 159 | Instead: | ||
| 160 | |||
| 161 | 1. `t3 serve` issues a one-time owner pairing token. | ||
| 162 | 2. The remote device exchanges that token with the server. | ||
| 163 | 3. The server creates an authenticated session for that device. | ||
| 164 | |||
| 165 | After 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 | |||
| 169 | The hosted web app at `https://app.t3.codes` can save a remote backend in browser local storage from a URL like: | ||
| 170 | |||
| 171 | ```text | ||
| 172 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | ||
| 173 | ``` | ||
| 174 | |||
| 175 | Use 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 | |||
| 177 | Do 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 | |||
| 179 | Hosted 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 | |||
| 183 | Use `t3 auth` to manage access after the initial pairing flow. | ||
| 184 | |||
| 185 | Typical uses: | ||
| 186 | |||
| 187 | - issue additional pairing credentials | ||
| 188 | - inspect active sessions | ||
| 189 | - revoke old pairing links or sessions | ||
| 190 | |||
| 191 | Use `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 | |||
| 11 | This 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 | |||
| 17 | Run commands from `apps/mobile`. | ||
| 18 | |||
| 19 | T3 Cloud is optional and disabled in a fresh clone. Public configuration belongs in the | ||
| 20 | repository-root `.env` or `.env.local`, not an `apps/mobile/.env` file. See | ||
| 21 | [`../../.env.example`](../../.env.example). | ||
| 22 | |||
| 23 | ## Development | ||
| 24 | |||
| 25 | Start Metro for the dev client: | ||
| 26 | |||
| 27 | ```bash | ||
| 28 | vp run dev:client | ||
| 29 | ``` | ||
| 30 | |||
| 31 | Build and run the local iOS dev client: | ||
| 32 | |||
| 33 | ```bash | ||
| 34 | vp run ios:dev | ||
| 35 | ``` | ||
| 36 | |||
| 37 | Build and run the local iOS preview app: | ||
| 38 | |||
| 39 | ```bash | ||
| 40 | vp run ios:preview | ||
| 41 | ``` | ||
| 42 | |||
| 43 | Force the review diff highlighter engine: | ||
| 44 | |||
| 45 | ```bash | ||
| 46 | EXPO_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 | |||
| 51 | Inspect the resolved Expo config for a variant: | ||
| 52 | |||
| 53 | ```bash | ||
| 54 | vp run config:dev | ||
| 55 | vp run config:preview | ||
| 56 | ``` | ||
| 57 | |||
| 58 | Run static checks for mobile native code: | ||
| 59 | |||
| 60 | ```bash | ||
| 61 | node ../../scripts/mobile-native-static-check.ts | ||
| 62 | ``` | ||
| 63 | |||
| 64 | The 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 | |||
| 68 | CI 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 | |||
| 70 | For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`, | ||
| 71 | `T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL` | ||
| 72 | as EAS environment variables. Expo config maps the canonical values into the mobile build. | ||
| 73 | |||
| 74 | Create a PR preview dev-client build manually: | ||
| 75 | |||
| 76 | ```bash | ||
| 77 | vp run eas:ios:preview:dev | ||
| 78 | ``` | ||
| 79 | |||
| 80 | Create a cloud dev-client build: | ||
| 81 | |||
| 82 | ```bash | ||
| 83 | vp run eas:ios:dev | ||
| 84 | ``` | ||
| 85 | |||
| 86 | Create a persistent preview build: | ||
| 87 | |||
| 88 | ```bash | ||
| 89 | vp run eas:ios:preview | ||
| 90 | ``` | ||
| 91 | |||
| 92 | Android equivalents: | ||
| 93 | |||
| 94 | ```bash | ||
| 95 | vp run eas:android:dev | ||
| 96 | vp run eas:android:preview:dev | ||
| 97 | vp 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 | |||
| 3 | This local Expo module owns the native terminal surface for the mobile app. | ||
| 4 | |||
| 5 | The 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 | |||
| 11 | The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the Ghostty custom-I/O | ||
| 12 | fork, with T3's iOS 16 compatibility patch applied. `T3TerminalView` owns a `libghostty` surface and | ||
| 13 | uses that callback I/O model: | ||
| 14 | |||
| 15 | 1. initialize libghostty once for the process | ||
| 16 | 2. create one Ghostty app and surface per native view | ||
| 17 | 3. feed remote output into the surface with `ghostty_surface_feed_data` | ||
| 18 | 4. send user input back to JS with the write callback | ||
| 19 | 5. emit Ghostty's measured terminal size through `onResize` | ||
| 20 | |||
| 21 | Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the | ||
| 22 | React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a | ||
| 23 | future Android Ghostty build without changing JS. | ||
| 24 | |||
| 25 | Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`. | ||
| 26 | |||
| 27 | ## Rebuilding GhosttyKit | ||
| 28 | |||
| 29 | The checked-in `GhosttyKit.xcframework` is built from the Ghostty custom-I/O fork (https://github.com/Yash-Singh1/ghostty/tree/custom-io). | ||
| 30 | Set the directory to the cloned repository checked out on the `custom-io` branch to `GHOSTTY_SOURCE_DIR`. | ||
| 31 | |||
| 32 | ```bash | ||
| 33 | apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh | ||
| 34 | ``` | ||
| 35 | |||
| 36 | The 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` | ||
| 38 | fails, 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 | |||
| 3 | T3 Code runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app. | ||
| 4 | |||
| 5 | ``` | ||
| 6 | ┌─────────────────────────────────┐ | ||
| 7 | │ Browser (React + Vite) │ | ||
| 8 | │ wsTransport (state machine) │ | ||
| 9 | │ Typed push decode at boundary │ | ||
| 10 | └──────────┬──────────────────────┘ | ||
| 11 | │ ws://localhost:3773 | ||
| 12 | ┌──────────▼──────────────────────┐ | ||
| 13 | │ apps/server (Node.js) │ | ||
| 14 | │ WebSocket + HTTP static server │ | ||
| 15 | │ ServerPushBus (ordered pushes) │ | ||
| 16 | │ ServerReadiness (startup gate) │ | ||
| 17 | │ OrchestrationEngine │ | ||
| 18 | │ ProviderService │ | ||
| 19 | │ CheckpointReactor │ | ||
| 20 | │ RuntimeReceiptBus │ | ||
| 21 | └──────────┬──────────────────────┘ | ||
| 22 | │ JSON-RPC over stdio | ||
| 23 | ┌──────────▼──────────────────────┐ | ||
| 24 | │ codex app-server │ | ||
| 25 | └─────────────────────────────────┘ | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Components | ||
| 29 | |||
| 30 | - **Browser app**: The React app renders session state, owns the client-side WebSocket transport, and treats typed push events as the boundary between server runtime details and UI state. | ||
| 31 | |||
| 32 | - **Server**: `apps/server` is the main coordinator. It serves the web app, accepts WebSocket requests, waits for startup readiness before welcoming clients, and sends all outbound pushes through a single ordered push path. | ||
| 33 | |||
| 34 | - **Provider runtime**: `codex app-server` does the actual provider/session work. The server talks to it over JSON-RPC on stdio and translates those runtime events into the app's orchestration model. | ||
| 35 | |||
| 36 | - **Background workers**: Long-running async flows such as runtime ingestion, command reaction, and checkpoint processing run as queue-backed workers. This keeps work ordered, reduces timing races, and gives tests a deterministic way to wait for the system to go idle. | ||
| 37 | |||
| 38 | - **Runtime signals**: The server emits lightweight typed receipts when important async milestones finish, such as checkpoint capture, diff finalization, or a turn becoming fully quiescent. Tests and orchestration code wait on these signals instead of polling internal state. | ||
| 39 | |||
| 40 | ## Event Lifecycle | ||
| 41 | |||
| 42 | ### Startup and client connect | ||
| 43 | |||
| 44 | ```mermaid | ||
| 45 | sequenceDiagram | ||
| 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 | |||
| 63 | 1. The browser boots [`WsTransport`][1] and registers typed listeners in [`wsNativeApi`][2]. | ||
| 64 | 2. The server accepts the connection in [`wsServer`][3] and brings up the runtime graph defined in [`serverLayers`][7]. | ||
| 65 | 3. [`ServerReadiness`][4] waits until the key startup barriers are complete. | ||
| 66 | 4. Once the server is ready, [`wsServer`][3] sends `server.welcome` from the contracts in [`ws.ts`][6] through [`ServerPushBus`][5]. | ||
| 67 | 5. 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 | ||
| 72 | sequenceDiagram | ||
| 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 | |||
| 93 | 1. A user action in the browser becomes a typed request through [`WsTransport`][1] and the browser API layer in [`nativeApi`][12]. | ||
| 94 | 2. [`wsServer`][3] decodes that request using the shared WebSocket contracts in [`ws.ts`][6] and routes it to the right service. | ||
| 95 | 3. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio. | ||
| 96 | 4. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events. | ||
| 97 | 5. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events. | ||
| 98 | 6. [`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 | ||
| 103 | sequenceDiagram | ||
| 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 | |||
| 122 | 1. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14]. | ||
| 123 | 2. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic. | ||
| 124 | 3. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence. | ||
| 125 | 4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers. | ||
| 126 | 5. 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 | |||
| 3 | The web app communicates with the server via WebSocket using a simple JSON-RPC-style protocol: | ||
| 4 | |||
| 5 | - **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }` | ||
| 6 | - **Push events**: typed envelopes with `channel`, `sequence` (monotonic per connection), and channel-specific `data` | ||
| 7 | |||
| 8 | Push channels: `server.welcome`, `server.configUpdated`, `terminal.event`, `orchestration.domainEvent`. Payloads are schema-validated at the transport boundary (`wsTransport.ts`). Decode failures produce structured `WsDecodeDiagnostic` with `code`, `reason`, and path info. | ||
| 9 | |||
| 10 | Methods mirror the `NativeApi` interface defined in `@t3tools/contracts`: | ||
| 11 | |||
| 12 | - `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn` | ||
| 13 | - `providers.respondToRequest`, `providers.stopSession` | ||
| 14 | - `shell.openInEditor`, `server.getConfig` | ||
| 15 | |||
| 16 | Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. | ||
| 17 | |||
| 18 | ## Client transport | ||
| 19 | |||
| 20 | `wsTransport.ts` manages connection state: `connecting` → `open` → `reconnecting` → `closed` → `disposed`. Outbound requests are queued while disconnected and flushed on reconnect. Inbound pushes are decoded and validated at the boundary, then cached per channel. Subscribers can opt into `replayLatest` to receive the last push on subscribe. | ||
| 21 | |||
| 22 | ## Server-side orchestration layers | ||
| 23 | |||
| 24 | Provider runtime events flow through queue-based workers: | ||
| 25 | |||
| 26 | 1. **ProviderRuntimeIngestion** — consumes provider runtime streams, emits orchestration commands | ||
| 27 | 2. **ProviderCommandReactor** — reacts to orchestration intent events, dispatches provider calls | ||
| 28 | 3. **CheckpointReactor** — captures git checkpoints on turn start/complete, publishes runtime receipts | ||
| 29 | |||
| 30 | All three use `DrainableWorker` internally and expose `drain()` for deterministic test synchronization. | ||
docs/architecture/remote.md created+382| ... | @@ -0,0 +1,382 @@ | ||
| 1 | # Remote Architecture | ||
| 2 | |||
| 3 | This document describes the target architecture for first-class remote environments in T3 Code. | ||
| 4 | |||
| 5 | It is intentionally architecture-first. It does not define a complete implementation plan or user-facing rollout checklist. The goal is to establish the core model so remote support can be added without another broad rewrite. | ||
| 6 | |||
| 7 | ## Goals | ||
| 8 | |||
| 9 | - Treat remote environments as first-class product primitives, not special cases. | ||
| 10 | - Support multiple ways to reach the same environment. | ||
| 11 | - Keep the T3 server as the execution boundary. | ||
| 12 | - Let desktop, mobile, and web all share the same conceptual model. | ||
| 13 | - Avoid introducing a local control plane unless product pressure proves it is necessary. | ||
| 14 | |||
| 15 | ## Non-goals | ||
| 16 | |||
| 17 | - Replacing the existing WebSocket server boundary with a custom transport protocol. | ||
| 18 | - Making SSH the only remote story. | ||
| 19 | - Syncing provider auth across machines. | ||
| 20 | - Shipping every access method in the first iteration. | ||
| 21 | |||
| 22 | ## High-level architecture | ||
| 23 | |||
| 24 | T3 already has a clean runtime boundary: the client talks to a T3 server over HTTP/WebSocket, and the server owns orchestration, providers, terminals, git, and filesystem operations. | ||
| 25 | |||
| 26 | Remote support should preserve that boundary. | ||
| 27 | |||
| 28 | ```text | ||
| 29 | ┌──────────────────────────────────────────────┐ | ||
| 30 | │ Client (desktop / mobile / web) │ | ||
| 31 | │ │ | ||
| 32 | │ - known environments │ | ||
| 33 | │ - connection manager │ | ||
| 34 | │ - environment-aware routing │ | ||
| 35 | └───────────────┬──────────────────────────────┘ | ||
| 36 | │ | ||
| 37 | │ resolves one access endpoint | ||
| 38 | │ | ||
| 39 | ┌───────────────▼──────────────────────────────┐ | ||
| 40 | │ Access method │ | ||
| 41 | │ │ | ||
| 42 | │ - direct ws / wss │ | ||
| 43 | │ - tunneled ws / wss │ | ||
| 44 | │ - desktop-managed ssh bootstrap + forward │ | ||
| 45 | └───────────────┬──────────────────────────────┘ | ||
| 46 | │ | ||
| 47 | │ connects to one T3 server | ||
| 48 | │ | ||
| 49 | ┌───────────────▼──────────────────────────────┐ | ||
| 50 | │ Execution environment = one T3 server │ | ||
| 51 | │ │ | ||
| 52 | │ - environment identity │ | ||
| 53 | │ - provider state │ | ||
| 54 | │ - projects / threads / terminals │ | ||
| 55 | │ - git / filesystem / process runtime │ | ||
| 56 | └──────────────────────────────────────────────┘ | ||
| 57 | ``` | ||
| 58 | |||
| 59 | The important decision is that remoteness is expressed at the environment connection layer, not by splitting the T3 runtime itself. | ||
| 60 | |||
| 61 | ## Domain model | ||
| 62 | |||
| 63 | ### ExecutionEnvironment | ||
| 64 | |||
| 65 | An `ExecutionEnvironment` is one running T3 server instance. | ||
| 66 | |||
| 67 | It is the unit that owns: | ||
| 68 | |||
| 69 | - provider availability and auth state | ||
| 70 | - model availability | ||
| 71 | - projects and threads | ||
| 72 | - terminal processes | ||
| 73 | - filesystem access | ||
| 74 | - git operations | ||
| 75 | - server settings | ||
| 76 | |||
| 77 | It is identified by a stable `environmentId`. | ||
| 78 | |||
| 79 | This is the shared cross-client primitive. Desktop, mobile, and web should all reason about the same concept here. | ||
| 80 | |||
| 81 | ### KnownEnvironment | ||
| 82 | |||
| 83 | A `KnownEnvironment` is a client-side saved entry for an environment the client knows how to reach. | ||
| 84 | |||
| 85 | It is not server-authored. It is local to a device or client profile. | ||
| 86 | |||
| 87 | Examples: | ||
| 88 | |||
| 89 | - a saved LAN URL | ||
| 90 | - a saved public `wss://` endpoint | ||
| 91 | - a desktop-managed SSH host entry | ||
| 92 | - a saved tunneled environment | ||
| 93 | |||
| 94 | A known environment may or may not know the target `environmentId` before first successful connect. | ||
| 95 | |||
| 96 | In the hosted web app, known environments are browser-local. A hosted pairing URL can create the saved entry, but it does not give the hosted app a server-side control plane or a copy of the session state. | ||
| 97 | |||
| 98 | ### AccessEndpoint | ||
| 99 | |||
| 100 | An `AccessEndpoint` is one concrete way to reach a known environment. | ||
| 101 | |||
| 102 | This is the key abstraction that keeps SSH from taking over the model. | ||
| 103 | |||
| 104 | A single environment may have many endpoints: | ||
| 105 | |||
| 106 | - `wss://t3.example.com` | ||
| 107 | - `ws://10.0.0.25:3773` | ||
| 108 | - a tunneled relay URL | ||
| 109 | - a desktop-managed SSH tunnel that resolves to a local forwarded WebSocket URL | ||
| 110 | |||
| 111 | The environment stays the same. Only the access path changes. | ||
| 112 | |||
| 113 | ### AdvertisedEndpoint | ||
| 114 | |||
| 115 | An `AdvertisedEndpoint` is a server or desktop-authored candidate endpoint for an environment. It is how the backend tells the client which URLs may be useful for pairing and reconnecting. | ||
| 116 | |||
| 117 | `AdvertisedEndpoint` is deliberately narrower than the full access model: | ||
| 118 | |||
| 119 | - it describes a concrete HTTP and WebSocket base URL pair | ||
| 120 | - it can mark the endpoint as default, available, or unavailable | ||
| 121 | - it includes reachability hints such as loopback, LAN, private, public, or tunnel | ||
| 122 | - it includes compatibility hints such as whether the endpoint can be used from the hosted HTTPS app | ||
| 123 | |||
| 124 | Clients should treat advertised endpoints as hints, not as proof that a route works from the current device. The final connection attempt still decides whether the endpoint is reachable. | ||
| 125 | |||
| 126 | The UI presents one default advertised endpoint in the network-access summary and keeps the rest behind an expandable advanced list. The default controls pairing QR codes and primary copy actions. Users can override it, but that override is a UI preference, not backend configuration. | ||
| 127 | |||
| 128 | Persist the override by stable endpoint kind rather than raw URL whenever possible. For example, a LAN endpoint should be stored as the desktop LAN endpoint preference, not as `192.168.x.y`, because the address can change when the user switches networks. Provider endpoints should use provider-specific stable keys such as Tailscale IP or Tailscale MagicDNS HTTPS. Custom endpoints may fall back to their concrete identity. | ||
| 129 | |||
| 130 | When no user default is saved, endpoint selection should prefer: | ||
| 131 | |||
| 132 | 1. endpoints compatible with the hosted HTTPS app | ||
| 133 | 2. explicitly default endpoints | ||
| 134 | 3. non-loopback endpoints | ||
| 135 | 4. loopback endpoints only for same-machine clients | ||
| 136 | |||
| 137 | This keeps endpoint discovery centralized without making any one provider, such as Tailscale or a future tunnel service, part of the core environment model. | ||
| 138 | |||
| 139 | ### Endpoint providers | ||
| 140 | |||
| 141 | Endpoint providers are add-ons that contribute advertised endpoints for the current environment. | ||
| 142 | |||
| 143 | The provider boundary is intentionally outside the core environment model: | ||
| 144 | |||
| 145 | - core owns `ExecutionEnvironment`, saved environments, pairing, and connection lifecycle | ||
| 146 | - providers discover or synthesize endpoints | ||
| 147 | - providers return normalized `AdvertisedEndpoint` records | ||
| 148 | - the UI and pairing logic select from those records without knowing provider-specific commands | ||
| 149 | |||
| 150 | The first provider is Tailscale. It can discover Tailnet IP and MagicDNS addresses from the local machine and publish them as additional endpoint candidates. Future providers, such as a hosted tunnel service, should plug into the same shape rather than adding a separate remote environment path. | ||
| 151 | |||
| 152 | Provider-specific confidence should remain a hint. A Tailscale endpoint still needs a successful browser or desktop connection before the client treats it as connected. | ||
| 153 | |||
| 154 | ### Hosted pairing request | ||
| 155 | |||
| 156 | A hosted pairing request is a bootstrap URL for the static web app, not a transport. | ||
| 157 | |||
| 158 | Example: | ||
| 159 | |||
| 160 | ```text | ||
| 161 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | ||
| 162 | ``` | ||
| 163 | |||
| 164 | The hosted app reads the `host` parameter and pairing token, exchanges the token directly with that backend, then saves the resulting environment record in browser local storage. | ||
| 165 | |||
| 166 | Important constraints: | ||
| 167 | |||
| 168 | - the hosted app does not proxy HTTP or WebSocket traffic | ||
| 169 | - the backend must still be reachable directly from the browser | ||
| 170 | - HTTPS pages can only connect to HTTPS/WSS backends | ||
| 171 | - HTTP LAN endpoints should keep using direct desktop or CLI pairing URLs | ||
| 172 | - the token belongs in the URL hash so it is not sent to the hosted app origin | ||
| 173 | |||
| 174 | ### RepositoryIdentity | ||
| 175 | |||
| 176 | `RepositoryIdentity` remains a best-effort logical repo grouping mechanism across environments. | ||
| 177 | |||
| 178 | It is not used for routing. It is only used for UI grouping and correlation between local and remote clones of the same repository. | ||
| 179 | |||
| 180 | ### Workspace / Project | ||
| 181 | |||
| 182 | The current `Project` model remains environment-local. | ||
| 183 | |||
| 184 | That means: | ||
| 185 | |||
| 186 | - a local clone and a remote clone are different projects | ||
| 187 | - they may share a `RepositoryIdentity` | ||
| 188 | - threads still bind to one project in one environment | ||
| 189 | |||
| 190 | ## Access methods | ||
| 191 | |||
| 192 | Access methods answer one question: | ||
| 193 | |||
| 194 | How does the client speak WebSocket to a T3 server? | ||
| 195 | |||
| 196 | They do not answer: | ||
| 197 | |||
| 198 | - how the server got started | ||
| 199 | - who manages the server process | ||
| 200 | - whether the environment is local or remote | ||
| 201 | |||
| 202 | ### 1. Direct WebSocket access | ||
| 203 | |||
| 204 | Examples: | ||
| 205 | |||
| 206 | - `ws://10.0.0.15:3773` | ||
| 207 | - `wss://t3.example.com` | ||
| 208 | |||
| 209 | This is the base model and should be the first-class default. | ||
| 210 | |||
| 211 | Benefits: | ||
| 212 | |||
| 213 | - works for desktop, mobile, and web | ||
| 214 | - no client-specific process management required | ||
| 215 | - best fit for hosted or self-managed remote T3 deployments | ||
| 216 | |||
| 217 | Browser security rules are part of this access method. A hosted HTTPS web client can connect to `wss://` backends, but it cannot connect to plain `ws://` or `http://` LAN backends because that would be mixed content. | ||
| 218 | |||
| 219 | ### 2. Tunneled WebSocket access | ||
| 220 | |||
| 221 | Examples: | ||
| 222 | |||
| 223 | - public relay URLs | ||
| 224 | - private network relay URLs | ||
| 225 | - local tunnel products such as pipenet | ||
| 226 | |||
| 227 | This is still direct WebSocket access from the client's perspective. The difference is that the route is mediated by a tunnel or relay. | ||
| 228 | |||
| 229 | For T3, tunnels are best modeled as another `AccessEndpoint`, not as a different kind of environment. | ||
| 230 | |||
| 231 | This is especially useful when: | ||
| 232 | |||
| 233 | - the host is behind NAT | ||
| 234 | - inbound ports are unavailable | ||
| 235 | - mobile must reach a desktop-hosted environment | ||
| 236 | - a machine should be reachable without exposing raw LAN or public ports | ||
| 237 | |||
| 238 | Tailscale-backed access sits here architecturally even though the current implementation is endpoint discovery rather than a T3-managed tunnel. It contributes private-network endpoints and lets the existing HTTP/WebSocket client path do the actual connection. | ||
| 239 | |||
| 240 | ### 3. Desktop-managed SSH access | ||
| 241 | |||
| 242 | SSH is an access and launch helper, not a separate environment type. | ||
| 243 | |||
| 244 | The desktop main process can use SSH to: | ||
| 245 | |||
| 246 | - reach a machine | ||
| 247 | - probe it | ||
| 248 | - launch or reuse a remote T3 server | ||
| 249 | - establish a local port forward | ||
| 250 | |||
| 251 | After that, the renderer should still connect using an ordinary WebSocket URL against the forwarded local port. | ||
| 252 | |||
| 253 | This keeps the renderer transport model consistent with every other access method. | ||
| 254 | |||
| 255 | The desktop main process owns the SSH bridge because it can spawn local SSH processes, manage askpass prompts, write temporary launch scripts, and clean up forwards. The renderer receives a saved environment record and connects through the forwarded URL; it should not need SSH-specific RPC paths for normal environment traffic. | ||
| 256 | |||
| 257 | ## Launch methods | ||
| 258 | |||
| 259 | Launch methods answer a different question: | ||
| 260 | |||
| 261 | How does a T3 server come to exist on the target machine? | ||
| 262 | |||
| 263 | Launch and access should stay separate in the design. | ||
| 264 | |||
| 265 | ### 1. Pre-existing server | ||
| 266 | |||
| 267 | The simplest launch method is no launch at all. | ||
| 268 | |||
| 269 | The user or operator already runs T3 on the target machine, and the client connects through a direct or tunneled WebSocket endpoint. | ||
| 270 | |||
| 271 | This should be the first remote mode shipped because it validates the environment model with minimal extra machinery. | ||
| 272 | |||
| 273 | ### 2. Desktop-managed remote launch over SSH | ||
| 274 | |||
| 275 | This is the main place where Zed is a useful reference. | ||
| 276 | |||
| 277 | Useful ideas to borrow from Zed: | ||
| 278 | |||
| 279 | - remote probing | ||
| 280 | - platform detection | ||
| 281 | - session directories with pid/log metadata | ||
| 282 | - reconnect-friendly launcher behavior | ||
| 283 | - desktop-owned connection UX | ||
| 284 | |||
| 285 | What should be different in T3: | ||
| 286 | |||
| 287 | - no custom stdio/socket proxy protocol between renderer and remote runtime | ||
| 288 | - no attempt to make the remote runtime look like an editor transport | ||
| 289 | - keep the final client-to-server connection as WebSocket | ||
| 290 | |||
| 291 | The recommended T3 flow is: | ||
| 292 | |||
| 293 | 1. Desktop connects over SSH. | ||
| 294 | 2. Desktop probes the remote machine and verifies T3 availability. | ||
| 295 | 3. Desktop launches or reuses a remote T3 server. | ||
| 296 | 4. Desktop establishes local port forwarding. | ||
| 297 | 5. Renderer connects to the forwarded WebSocket endpoint as a normal environment. | ||
| 298 | |||
| 299 | The saved environment should remember that it was created by desktop SSH launch only for reconnect and lifecycle UX. That metadata should not change the server protocol or the environment identity model. | ||
| 300 | |||
| 301 | Failure handling should be explicit: | ||
| 302 | |||
| 303 | - SSH authentication failure should surface before any environment is saved | ||
| 304 | - remote launch failure should include remote logs or the launcher command output when available | ||
| 305 | - forwarded-port failure should leave the saved environment disconnected rather than falling back to an unrelated endpoint | ||
| 306 | - reconnect should attempt to restore the SSH bridge before reconnecting the normal WebSocket client | ||
| 307 | |||
| 308 | ### 3. Client-managed local publish | ||
| 309 | |||
| 310 | This is the inverse of remote launch: a local T3 server is already running, and the client publishes it through a tunnel. | ||
| 311 | |||
| 312 | This is useful for: | ||
| 313 | |||
| 314 | - exposing a desktop-hosted environment to mobile | ||
| 315 | - temporary remote access without changing router or firewall settings | ||
| 316 | |||
| 317 | This is still a launch concern, not a new environment kind. | ||
| 318 | |||
| 319 | ## Why access and launch must stay separate | ||
| 320 | |||
| 321 | These concerns are easy to conflate, but separating them prevents architectural drift. | ||
| 322 | |||
| 323 | Examples: | ||
| 324 | |||
| 325 | - A manually hosted T3 server might be reached through direct `wss`. | ||
| 326 | - The same server might also be reachable through a tunnel. | ||
| 327 | - An SSH-managed server might be launched over SSH but then reached through forwarded WebSocket. | ||
| 328 | - A local desktop server might be published through a tunnel for mobile. | ||
| 329 | |||
| 330 | In all of those cases, the `ExecutionEnvironment` is the same kind of thing. | ||
| 331 | |||
| 332 | Only the launch and access paths differ. | ||
| 333 | |||
| 334 | ## Security model | ||
| 335 | |||
| 336 | Remote support must assume that some environments will be reachable over untrusted networks. | ||
| 337 | |||
| 338 | That 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 | |||
| 344 | T3 already supports a WebSocket auth token on the server. That should become a first-class part of environment access rather than remaining an incidental query parameter convention. | ||
| 345 | |||
| 346 | For publicly reachable environments, authenticated access should be treated as required. | ||
| 347 | |||
| 348 | Hosted pairing should be treated as a client-side convenience only. The hosted app must not receive pairing tokens through query parameters, must not store pairing state server-side, and must not imply that an HTTP backend is safe or reachable from an HTTPS browser context. | ||
| 349 | |||
| 350 | ## Relationship to Zed | ||
| 351 | |||
| 352 | Zed is a useful reference implementation for managed remote launch and reconnect behavior. | ||
| 353 | |||
| 354 | The 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 | |||
| 361 | The important mismatch is transport shape. | ||
| 362 | |||
| 363 | Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime. | ||
| 364 | |||
| 365 | T3 should not copy that part. | ||
| 366 | |||
| 367 | T3 already has the right runtime boundary: | ||
| 368 | |||
| 369 | - one T3 server per environment | ||
| 370 | - ordinary HTTP/WebSocket between client and environment | ||
| 371 | |||
| 372 | So T3 should borrow Zed's launch discipline, not its transport protocol. | ||
| 373 | |||
| 374 | ## Recommended rollout | ||
| 375 | |||
| 376 | 1. First-class known environments and access endpoints. | ||
| 377 | 2. Direct `ws` / `wss` remote environments. | ||
| 378 | 3. Authenticated tunnel-backed environments. | ||
| 379 | 4. Desktop-managed SSH launch and forwarding. | ||
| 380 | 5. Multi-environment UI improvements after the base runtime path is proven. | ||
| 381 | |||
| 382 | This 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 | |||
| 3 | T3 Code has a global runtime mode switch in the chat toolbar: | ||
| 4 | |||
| 5 | - **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. | ||
| 6 | - **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. | ||
docs/cloud/environment-auth.md created+113| ... | @@ -0,0 +1,113 @@ | ||
| 1 | # Environment Authentication Profile | ||
| 2 | |||
| 3 | The environment server and the relay use separate credentials, issuers, and trust | ||
| 4 | boundaries. They intentionally use a similar OAuth-shaped model so that permission | ||
| 5 | checks and token exchange behavior can be audited against established concepts. | ||
| 6 | |||
| 7 | ## Authorization Model | ||
| 8 | |||
| 9 | Environment authorization is capability-based. A session carries zero or more | ||
| 10 | OAuth-style scope strings: | ||
| 11 | |||
| 12 | | Scope | Permission | | ||
| 13 | | ----------------------- | ------------------------------------------------------------------------ | | ||
| 14 | | `orchestration:read` | Read snapshots, status, events, configuration, and filesystem/VCS state. | | ||
| 15 | | `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state. | | ||
| 16 | | `terminal:operate` | Create, attach, input, resize, clear, restart, and terminate terminals. | | ||
| 17 | | `review:write` | Read review diff previews used to compose review feedback. | | ||
| 18 | | `access:read` | Inspect pairing links and client sessions. | | ||
| 19 | | `access:write` | Create or revoke pairing links and client sessions. | | ||
| 20 | | `relay:read` | Inspect managed relay connectivity. | | ||
| 21 | | `relay:write` | Link, configure, or unlink managed relay connectivity. | | ||
| 22 | |||
| 23 | Ordinary pairing links grant the four client-operation scopes and read access to | ||
| 24 | managed relay connectivity: | ||
| 25 | `orchestration:read orchestration:operate terminal:operate review:write relay:read`. | ||
| 26 | The desktop bootstrap credential and command-line administrative bootstrap | ||
| 27 | credentials additionally grant `access:read access:write relay:write`. | ||
| 28 | |||
| 29 | ## Authentication Flows | ||
| 30 | |||
| 31 | ### Browser Session | ||
| 32 | |||
| 33 | `POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a | ||
| 34 | browser session cookie. The cookie is an HTTP transport adapter for the same | ||
| 35 | scoped session model; the response never exposes the session secret to browser | ||
| 36 | JavaScript. | ||
| 37 | |||
| 38 | ### Bearer Access Token | ||
| 39 | |||
| 40 | Non-browser clients use `POST /oauth/token` with an | ||
| 41 | `application/x-www-form-urlencoded` body: | ||
| 42 | |||
| 43 | ```text | ||
| 44 | grant_type=urn:ietf:params:oauth:grant-type:token-exchange | ||
| 45 | subject_token=<bootstrap credential> | ||
| 46 | subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap | ||
| 47 | requested_token_type=urn:ietf:params:oauth:token-type:access_token | ||
| 48 | scope=orchestration:read orchestration:operate terminal:operate review:write relay:read | ||
| 49 | ``` | ||
| 50 | |||
| 51 | Clients may additionally submit `client_label`, `client_device_type`, and | ||
| 52 | `client_os` extension parameters so the authorized-clients UI can identify the | ||
| 53 | device that established the session. These are presentation hints only; the | ||
| 54 | environment derives transport metadata such as IP address and user agent from | ||
| 55 | the request and does not use these fields for authorization. | ||
| 56 | |||
| 57 | The response has the token-exchange shape: | ||
| 58 | |||
| 59 | ```json | ||
| 60 | { | ||
| 61 | "access_token": "<opaque session token>", | ||
| 62 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", | ||
| 63 | "token_type": "Bearer", | ||
| 64 | "expires_in": 3600, | ||
| 65 | "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" | ||
| 66 | } | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Requested scopes must be a subset of the one-time bootstrap credential grant. | ||
| 70 | An ordinary paired client therefore cannot exchange its grant for | ||
| 71 | `access:read`, `access:write`, or `relay:write`. | ||
| 72 | |||
| 73 | ### WebSocket Ticket | ||
| 74 | |||
| 75 | `POST /api/auth/websocket-ticket` accepts any authenticated session and returns | ||
| 76 | a short-lived, single-purpose WebSocket ticket. This keeps bearer tokens and | ||
| 77 | browser cookies out of WebSocket URLs while allowing the socket handshake to | ||
| 78 | authenticate. The ticket carries its session's scopes; each RPC method then | ||
| 79 | enforces `orchestration:read`, `orchestration:operate`, `terminal:operate`, | ||
| 80 | `review:write`, or `access:read` as appropriate. Review feedback submission | ||
| 81 | currently dispatches an orchestration operation, so clients performing it also | ||
| 82 | need `orchestration:operate`. Creating a ticket is not | ||
| 83 | authorization to call every RPC method. | ||
| 84 | |||
| 85 | ## Standards Alignment | ||
| 86 | |||
| 87 | - Bearer access tokens are used through the `Authorization: Bearer` scheme from | ||
| 88 | RFC 6750. | ||
| 89 | - The token endpoint profiles the request and response vocabulary from OAuth 2.0 | ||
| 90 | Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`, | ||
| 91 | `access_token`, `issued_token_type`, and `token_type`. | ||
| 92 | - Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited, | ||
| 93 | unordered capabilities with subset checking during exchange. | ||
| 94 | |||
| 95 | This is intentionally not a general-purpose OAuth authorization server. The | ||
| 96 | environment bootstrap token type is private, the bootstrap cookie and WebSocket | ||
| 97 | connection-token routes are product-specific adapters, and the API returns its | ||
| 98 | typed `HttpApi` errors rather than implementing every OAuth error response | ||
| 99 | surface. | ||
| 100 | |||
| 101 | ## Upgrade Behavior | ||
| 102 | |||
| 103 | Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth | ||
| 104 | records to scoped records. It deletes existing pairing links and sessions while | ||
| 105 | leaving non-authentication environment state unchanged. Upgraded clients must | ||
| 106 | pair again; old `owner` or `client` credentials are never silently mapped to new | ||
| 107 | capabilities. | ||
| 108 | |||
| 109 | ## Relay Boundary | ||
| 110 | |||
| 111 | Relay-managed tunnels use their own tokens and keys. The relay can reuse scope | ||
| 112 | parsing and token-exchange conventions, but an environment access token is not a | ||
| 113 | relay token and cannot be presented to the relay. | ||
docs/cloud/t3-cloud-clerk.md created+174| ... | @@ -0,0 +1,174 @@ | ||
| 1 | # T3 Cloud Clerk Setup | ||
| 2 | |||
| 3 | T3 Cloud uses one Clerk application for web, desktop, and mobile authentication. The relay accepts | ||
| 4 | Clerk JWTs only when they are generated from the `t3-relay` template with the shared | ||
| 5 | `t3-code-relay` audience. | ||
| 6 | |||
| 7 | ## Application Keys | ||
| 8 | |||
| 9 | T3 Cloud is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env` | ||
| 10 | or `.env.local` file: | ||
| 11 | |||
| 12 | ```dotenv | ||
| 13 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | ||
| 14 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | ||
| 15 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | ||
| 16 | T3CODE_RELAY_URL=https://relay.example.com | ||
| 17 | ``` | ||
| 18 | |||
| 19 | The shared client loader projects these canonical values into framework-specific `VITE_*` and | ||
| 20 | `EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new | ||
| 21 | client configuration should use the canonical names. | ||
| 22 | |||
| 23 | Configuration precedence is: | ||
| 24 | |||
| 25 | 1. Process or CI environment variables. | ||
| 26 | 2. Repository-root `.env.local`. | ||
| 27 | 3. Repository-root `.env`. | ||
| 28 | |||
| 29 | The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public | ||
| 30 | identifiers, not secrets. | ||
| 31 | Web, desktop, mobile, and bundled server builds statically inject the values they consume during | ||
| 32 | their build step. A built artifact does not need an environment file at runtime. CI release builds | ||
| 33 | should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, | ||
| 34 | `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and | ||
| 35 | production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS | ||
| 36 | environment. | ||
| 37 | |||
| 38 | When any client-facing public value is absent, cloud UI is omitted. When the CLI public values are | ||
| 39 | absent, the `t3 cloud` CLI command group is omitted. The bundled server still accepts runtime | ||
| 40 | overrides for self-hosted or operator-managed | ||
| 41 | deployments. | ||
| 42 | |||
| 43 | For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay | ||
| 44 | deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, | ||
| 45 | `CLERK_PUBLISHABLE_KEY`, and `CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in | ||
| 46 | deployment defaults. | ||
| 47 | `vp run --filter t3code-relay deploy` invokes Alchemy from the relay directory, so Alchemy loads | ||
| 48 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` | ||
| 49 | with the deployed HTTPS relay URL. The relay still requires | ||
| 50 | `CLERK_SECRET_KEY` as an Alchemy secret. Never put `CLERK_SECRET_KEY` in a client application | ||
| 51 | environment or commit it to the repository. | ||
| 52 | |||
| 53 | The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference | ||
| 54 | that database and provision isolated PlanetScale branches, so deploy `prod` before creating a | ||
| 55 | personal developer stage. | ||
| 56 | |||
| 57 | ## Headless CLI OAuth Application | ||
| 58 | |||
| 59 | The `t3 cloud` commands authorize a headless environment with a separate Clerk OAuth application. | ||
| 60 | This uses an OAuth public client with PKCE, so the CLI stores no client secret. | ||
| 61 | |||
| 62 | In **Clerk Dashboard > OAuth applications**: | ||
| 63 | |||
| 64 | 1. Create an OAuth application for the T3 CLI. | ||
| 65 | 2. Enable the **Public** option so authorization-code exchange uses PKCE. | ||
| 66 | 3. Add `http://127.0.0.1:34338/callback` as an allowed redirect URI. | ||
| 67 | 4. Enable the `openid`, `profile`, and `email` scopes. | ||
| 68 | 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build | ||
| 69 | environment to the generated public client ID. | ||
| 70 | |||
| 71 | The CLI derives Clerk's frontend API URL from the publishable key and calls Clerk's | ||
| 72 | `/oauth/authorize` and `/oauth/token` endpoints directly. The relay is not involved in the OAuth | ||
| 73 | handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link. | ||
| 74 | |||
| 75 | The CLI supports these headless operations: | ||
| 76 | |||
| 77 | ```sh | ||
| 78 | t3 cloud login | ||
| 79 | t3 cloud link | ||
| 80 | t3 cloud status | ||
| 81 | t3 cloud unlink | ||
| 82 | t3 cloud logout | ||
| 83 | t3 serve | ||
| 84 | ``` | ||
| 85 | |||
| 86 | `t3 cloud login` opens the Clerk authorization flow and stores the CLI credential without enabling | ||
| 87 | cloud exposure. `t3 cloud link` installs the pinned managed `cloudflared` binary when needed, | ||
| 88 | authorizes when needed, and records durable intent to expose the environment. It works without a | ||
| 89 | running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the | ||
| 90 | managed tunnel. `t3 cloud unlink` records disabled intent immediately, stops a reachable running | ||
| 91 | connector, and attempts to revoke the relay-side environment record. It retains the stored CLI | ||
| 92 | authorization so `t3 cloud link` can re-enable exposure without another browser flow. `t3 cloud | ||
| 93 | logout` performs the same cleanup and removes the stored CLI authorization. | ||
| 94 | |||
| 95 | The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH, | ||
| 96 | forward that port before running `t3 cloud login` or `t3 cloud link`: | ||
| 97 | |||
| 98 | ```sh | ||
| 99 | ssh -L 34338:127.0.0.1:34338 <host> | ||
| 100 | ``` | ||
| 101 | |||
| 102 | A relay-hosted callback broker can remove this port-forward requirement later without changing the | ||
| 103 | stored PKCE token model. | ||
| 104 | |||
| 105 | ## JWT Template | ||
| 106 | |||
| 107 | In **Clerk Dashboard > JWT templates**, create a template with: | ||
| 108 | |||
| 109 | | Setting | Value | | ||
| 110 | | ------- | ---------------------------- | | ||
| 111 | | Name | `t3-relay` | | ||
| 112 | | Claims | `{ "aud": "t3-code-relay" }` | | ||
| 113 | |||
| 114 | Set `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 | ||
| 117 | is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still | ||
| 118 | selects the concrete relay deployment, but changing that URL does not require a JWT template change. | ||
| 119 | |||
| 120 | ## Desktop OAuth Redirect Allowlist | ||
| 121 | |||
| 122 | The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme. | ||
| 123 | In **Clerk Dashboard > Native applications**, enable native application support and add these | ||
| 124 | entries under the mobile SSO redirect allowlist: | ||
| 125 | |||
| 126 | ```text | ||
| 127 | t3code-dev://auth/callback | ||
| 128 | t3code://auth/callback | ||
| 129 | ``` | ||
| 130 | |||
| 131 | The first entry is for local desktop development. The second is for packaged desktop builds. | ||
| 132 | The app also adds a request-scoped `t3_state` query parameter and validates it on callback. Initial | ||
| 133 | sign-in and linked-account OAuth flows both return through this bridge. The desktop provider keeps | ||
| 134 | Clerk's stock profile component, replaces its renderer-page callback with the custom-scheme callback, | ||
| 135 | and opens the provider URL in the system browser. Do not add the local renderer URL as an OAuth | ||
| 136 | redirect: an external browser cannot use it to reopen the packaged app. | ||
| 137 | |||
| 138 | The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth | ||
| 139 | flow uses a custom redirect URI, add that exact URI to the same allowlist. | ||
| 140 | |||
| 141 | ## Enable Waitlist Access | ||
| 142 | |||
| 143 | For a private beta where people should request access, use **Clerk Dashboard > Waitlist**: | ||
| 144 | |||
| 145 | 1. Toggle on **Enable waitlist** and save. | ||
| 146 | 2. Review requests on the same page and select **Invite** or **Deny**. | ||
| 147 | |||
| 148 | Signed-out web and desktop users see Clerk's waitlist enrollment as the T3 Cloud page content, | ||
| 149 | while approved signed-in users see cloud settings. The browser app also uses `/settings/cloud` as | ||
| 150 | its Clerk waitlist URL. | ||
| 151 | |||
| 152 | On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the | ||
| 153 | Settings 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 | ||
| 155 | screen. | ||
| 156 | |||
| 157 | ## Alternative: Known-User Allowlist | ||
| 158 | |||
| 159 | For a closed beta where all permitted users are known in advance, use an allowlist instead of a | ||
| 160 | request-and-approval waitlist: | ||
| 161 | |||
| 162 | To restrict the beta to permitted email addresses or domains: | ||
| 163 | |||
| 164 | 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email | ||
| 165 | domain. | ||
| 166 | 2. Enable the allowlist and save. | ||
| 167 | 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or | ||
| 168 | manually created without a waitlist request flow. | ||
| 169 | |||
| 170 | Do not enable an empty allowlist: it blocks all new sign-ups. | ||
| 171 | |||
| 172 | Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud | ||
| 173 | access. To remove an already-created user's access, ban that user in Clerk so their active | ||
| 174 | sessions 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"> | ||
| 347 | flowchart LR | ||
| 348 | subgraph Clients["User-facing clients"] | ||
| 349 | Web["Desktop / hosted web UI"] | ||
| 350 | Mobile["Mobile app"] | ||
| 351 | CLI["Headless CLI"] | ||
| 352 | end | ||
| 353 | |||
| 354 | Clerk["Clerk<br/>user identity and OAuth"] | ||
| 355 | |||
| 356 | subgraph Relay["T3 Code Relay on Cloudflare"] | ||
| 357 | Worker["Relay Worker<br/>HTTP API"] | ||
| 358 | Queue["APNs delivery queue<br/>with dead-letter queue"] | ||
| 359 | Hyperdrive["Hyperdrive"] | ||
| 360 | CF["Cloudflare tunnel + DNS APIs"] | ||
| 361 | end | ||
| 362 | |||
| 363 | DB["PlanetScale Postgres<br/>prod database + stage branches"] | ||
| 364 | APNs["Apple Push Notification service"] | ||
| 365 | Traces["Axiom OTLP traces"] | ||
| 366 | |||
| 367 | subgraph Local["Linked T3 environment"] | ||
| 368 | Env["Environment server<br/>local auth authority"] | ||
| 369 | RelayClient["Relay client<br/>cloudflared implementation"] | ||
| 370 | Secrets["Server secret store<br/>keys + relay config"] | ||
| 371 | end | ||
| 372 | |||
| 373 | Endpoint["Managed HTTPS/WSS endpoint"] | ||
| 374 | |||
| 375 | Web -->|"Clerk bearer / relay DPoP"| Worker | ||
| 376 | Mobile -->|"Clerk bearer / relay DPoP"| Worker | ||
| 377 | CLI -->|"Clerk OAuth bearer"| Worker | ||
| 378 | Web --> Clerk | ||
| 379 | Mobile --> Clerk | ||
| 380 | CLI -->|"PKCE authorization code flow"| Clerk | ||
| 381 | Worker --> Hyperdrive --> DB | ||
| 382 | Worker --> Traces | ||
| 383 | Worker --> CF | ||
| 384 | Worker --> Queue --> APNs | ||
| 385 | Worker --> Endpoint | ||
| 386 | Endpoint --> RelayClient -->|"http://127.0.0.1:port"| Env | ||
| 387 | Env --> Secrets | ||
| 388 | Web -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 389 | Mobile -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 390 | Env -->|"environment bearer + signed activity proof"| Worker | ||
| 391 | </pre> | ||
| 392 | </section> | ||
| 393 | |||
| 394 | <section class="panel" aria-label="Authentication matrix"> | ||
| 395 | <h2>Authentication and Transport Matrix</h2> | ||
| 396 | <table> | ||
| 397 | <thead> | ||
| 398 | <tr> | ||
| 399 | <th>Boundary</th> | ||
| 400 | <th>Transport</th> | ||
| 401 | <th>Authentication</th> | ||
| 402 | <th>Purpose</th> | ||
| 403 | </tr> | ||
| 404 | </thead> | ||
| 405 | <tbody> | ||
| 406 | <tr> | ||
| 407 | <td>Web or mobile -> relay link management</td> | ||
| 408 | <td>HTTPS</td> | ||
| 409 | <td>Clerk session-template bearer token</td> | ||
| 410 | <td>Create a link challenge, submit an environment proof, list links, or unlink.</td> | ||
| 411 | </tr> | ||
| 412 | <tr> | ||
| 413 | <td>CLI -> relay link management</td> | ||
| 414 | <td>HTTPS</td> | ||
| 415 | <td>Clerk OAuth access token obtained with PKCE</td> | ||
| 416 | <td>Reconcile the desired headless cloud link when the server starts.</td> | ||
| 417 | </tr> | ||
| 418 | <tr> | ||
| 419 | <td>Web or mobile -> relay protected client endpoints</td> | ||
| 420 | <td>HTTPS</td> | ||
| 421 | <td>Relay-issued DPoP token plus per-request DPoP proof</td> | ||
| 422 | <td> | ||
| 423 | Check environment status, request a remote connection, and register mobile devices. | ||
| 424 | </td> | ||
| 425 | </tr> | ||
| 426 | <tr> | ||
| 427 | <td>Relay -> managed environment endpoint</td> | ||
| 428 | <td>HTTPS over the managed tunnel</td> | ||
| 429 | <td>Short-lived relay-signed JWT request proof</td> | ||
| 430 | <td> | ||
| 431 | Request a signed health response or ask the local environment to mint a credential. | ||
| 432 | </td> | ||
| 433 | </tr> | ||
| 434 | <tr> | ||
| 435 | <td>Environment -> relay activity publication</td> | ||
| 436 | <td>HTTPS</td> | ||
| 437 | <td>Relay-issued environment bearer credential plus environment-signed JWT proof</td> | ||
| 438 | <td> | ||
| 439 | Publish redacted agent-activity state for push notifications and Live Activities. | ||
| 440 | </td> | ||
| 441 | </tr> | ||
| 442 | <tr> | ||
| 443 | <td>Remote client -> environment</td> | ||
| 444 | <td>HTTPS and WSS over the managed tunnel</td> | ||
| 445 | <td>Environment-issued access token bound to the client DPoP key</td> | ||
| 446 | <td>Use the normal environment HTTP APIs and request a one-time WebSocket ticket.</td> | ||
| 447 | </tr> | ||
| 448 | </tbody> | ||
| 449 | </table> | ||
| 450 | </section> | ||
| 451 | |||
| 452 | <section class="panel" aria-label="Credential ownership"> | ||
| 453 | <h2>Credential Ownership</h2> | ||
| 454 | <table> | ||
| 455 | <thead> | ||
| 456 | <tr> | ||
| 457 | <th>Credential</th> | ||
| 458 | <th>Created by</th> | ||
| 459 | <th>Stored by</th> | ||
| 460 | <th>Use</th> | ||
| 461 | </tr> | ||
| 462 | </thead> | ||
| 463 | <tbody> | ||
| 464 | <tr> | ||
| 465 | <td>Clerk session-template JWT</td> | ||
| 466 | <td>Clerk</td> | ||
| 467 | <td>Web or mobile client</td> | ||
| 468 | <td> | ||
| 469 | Authenticate cloud-user relay requests and bootstrap relay DPoP token exchange. | ||
| 470 | </td> | ||
| 471 | </tr> | ||
| 472 | <tr> | ||
| 473 | <td>CLI OAuth access and refresh token</td> | ||
| 474 | <td>Clerk OAuth</td> | ||
| 475 | <td>Local server secret store</td> | ||
| 476 | <td>Authorize headless CLI link reconciliation.</td> | ||
| 477 | </tr> | ||
| 478 | <tr> | ||
| 479 | <td>Environment Ed25519 key pair</td> | ||
| 480 | <td>Local environment server</td> | ||
| 481 | <td>Local server secret store</td> | ||
| 482 | <td> | ||
| 483 | Sign link proofs, health responses, mint responses, and activity publication proofs. | ||
| 484 | </td> | ||
| 485 | </tr> | ||
| 486 | <tr> | ||
| 487 | <td>Relay cloud-mint key pair</td> | ||
| 488 | <td>Relay deployment</td> | ||
| 489 | <td>Private key in relay Worker config; public key installed into the environment</td> | ||
| 490 | <td>Sign relay-to-environment health and mint requests.</td> | ||
| 491 | </tr> | ||
| 492 | <tr> | ||
| 493 | <td>Relay environment bearer credential</td> | ||
| 494 | <td>Relay</td> | ||
| 495 | <td>Hashed in relay Postgres; plaintext in local server secret store</td> | ||
| 496 | <td>Authenticate environment-to-relay agent-activity publication.</td> | ||
| 497 | </tr> | ||
| 498 | <tr> | ||
| 499 | <td>Relay DPoP access token</td> | ||
| 500 | <td>Relay</td> | ||
| 501 | <td>Remote client memory cache</td> | ||
| 502 | <td> | ||
| 503 | Authorize status, connect, and mobile registration endpoints with proof of | ||
| 504 | possession. | ||
| 505 | </td> | ||
| 506 | </tr> | ||
| 507 | <tr> | ||
| 508 | <td>Environment bootstrap credential</td> | ||
| 509 | <td>Local environment server</td> | ||
| 510 | <td>Passed relay -> remote client</td> | ||
| 511 | <td>One-time exchange at the environment <code>/oauth/token</code> endpoint.</td> | ||
| 512 | </tr> | ||
| 513 | <tr> | ||
| 514 | <td>Environment access token</td> | ||
| 515 | <td>Local environment server</td> | ||
| 516 | <td>Remote client</td> | ||
| 517 | <td>Authorize environment HTTP operations and WebSocket ticket issuance.</td> | ||
| 518 | </tr> | ||
| 519 | <tr> | ||
| 520 | <td>WebSocket ticket</td> | ||
| 521 | <td>Local environment server</td> | ||
| 522 | <td>Remote client until consumed</td> | ||
| 523 | <td>Short-lived one-time credential for the WSS upgrade.</td> | ||
| 524 | </tr> | ||
| 525 | <tr> | ||
| 526 | <td>Relay client connector token</td> | ||
| 527 | <td>Cloudflare tunnel API</td> | ||
| 528 | <td>Local server secret store</td> | ||
| 529 | <td> | ||
| 530 | Start the local relay client. It is passed through <code>TUNNEL_TOKEN</code>, not a | ||
| 531 | shell argument. | ||
| 532 | </td> | ||
| 533 | </tr> | ||
| 534 | </tbody> | ||
| 535 | </table> | ||
| 536 | </section> | ||
| 537 | |||
| 538 | <section class="grid" aria-label="Core flows"> | ||
| 539 | <article class="panel"> | ||
| 540 | <h2>Flow 1: Link From the Desktop Web UI</h2> | ||
| 541 | <pre class="diagram mermaid"> | ||
| 542 | sequenceDiagram | ||
| 543 | participant U as Signed-in user | ||
| 544 | participant C as Desktop web UI | ||
| 545 | participant E as Local environment | ||
| 546 | participant R as Relay Worker | ||
| 547 | participant CF as Cloudflare APIs | ||
| 548 | participant RC as Local relay client | ||
| 549 | |||
| 550 | U->>C: Enable T3 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"> | ||
| 586 | sequenceDiagram | ||
| 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"> | ||
| 619 | sequenceDiagram | ||
| 620 | participant C as Web or mobile client | ||
| 621 | participant Clerk as Clerk | ||
| 622 | participant R as Relay Worker | ||
| 623 | participant E as Managed environment endpoint | ||
| 624 | |||
| 625 | C->>Clerk: Get Clerk session-template JWT | ||
| 626 | C->>R: POST /v1/client/dpop-token (Clerk JWT + DPoP proof) | ||
| 627 | R-->>C: Relay DPoP access token | ||
| 628 | C->>R: POST /v1/environments/:id/connect (relay DPoP) | ||
| 629 | R->>E: POST /api/t3-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"> | ||
| 650 | sequenceDiagram | ||
| 651 | participant E as Local environment | ||
| 652 | participant R as Relay Worker | ||
| 653 | participant DB as Relay Postgres | ||
| 654 | participant Q as APNs queue | ||
| 655 | participant APNs as Apple Push Notification service | ||
| 656 | participant M as Mobile app | ||
| 657 | |||
| 658 | E->>E: Project publishable thread state and redact failure detail | ||
| 659 | E->>R: POST /v1/environments/:environmentId/threads/:threadId/agent-activity | ||
| 660 | Note over E,R: Environment bearer credential + environment-signed per-state proof | ||
| 661 | R->>R: Verify credential, signature, scope, expiry, and nonce | ||
| 662 | R->>DB: Store current activity row | ||
| 663 | R->>Q: Enqueue push or Live Activity delivery jobs | ||
| 664 | Q->>APNs: Deliver signed APNs request | ||
| 665 | APNs-->>M: Push notification or Live Activity update | ||
| 666 | </pre> | ||
| 667 | <p> | ||
| 668 | The local server publishes a deliberately narrow projection. Failed runs use a fixed | ||
| 669 | redacted summary, and detail strings are capped before publication. | ||
| 670 | </p> | ||
| 671 | </article> | ||
| 672 | </section> | ||
| 673 | |||
| 674 | <section class="grid" aria-label="Managed endpoint lifecycle"> | ||
| 675 | <article class="panel half"> | ||
| 676 | <h2>Managed Endpoint Reconciliation</h2> | ||
| 677 | <p> | ||
| 678 | Managed endpoint resources are keyed by <code>(userId, environmentId)</code>. The relay | ||
| 679 | reserves a stable hostname and tunnel name before provisioning, then checkpoints the | ||
| 680 | tunnel ID, DNS record ID, and ready state in Postgres. | ||
| 681 | </p> | ||
| 682 | <div class="flow"> | ||
| 683 | <div class="step"> | ||
| 684 | <strong>1. Reserve deterministic allocation</strong> | ||
| 685 | Hostname and tunnel name derive from relay stage, cloud user, and environment ID. | ||
| 686 | </div> | ||
| 687 | <div class="step"> | ||
| 688 | <strong>2. Reuse or create tunnel</strong> | ||
| 689 | Existing named tunnels are reused. Tunnel ingress is rewritten to the validated | ||
| 690 | loopback origin. | ||
| 691 | </div> | ||
| 692 | <div class="step"> | ||
| 693 | <strong>3. Reconcile DNS</strong> | ||
| 694 | Existing CNAMEs are updated and duplicate records are removed. Create conflicts are | ||
| 695 | recovered by listing and reconciling the winning record. | ||
| 696 | </div> | ||
| 697 | <div class="step"> | ||
| 698 | <strong>4. Mark ready</strong> | ||
| 699 | Connect and status requests use only ready allocations whose hostname still matches | ||
| 700 | the relay-managed namespace. | ||
| 701 | </div> | ||
| 702 | </div> | ||
| 703 | </article> | ||
| 704 | |||
| 705 | <article class="panel half"> | ||
| 706 | <h2>Unlink Cleanup</h2> | ||
| 707 | <p> | ||
| 708 | Relay unlink removes the user's managed endpoint allocation before revoking the | ||
| 709 | user/environment link: | ||
| 710 | </p> | ||
| 711 | <ul> | ||
| 712 | <li>delete the managed DNS record if present;</li> | ||
| 713 | <li>delete the Cloudflare tunnel if present;</li> | ||
| 714 | <li>remove the allocation row;</li> | ||
| 715 | <li>revoke the user's environment link; and</li> | ||
| 716 | <li> | ||
| 717 | revoke the environment publication credential only after the final active link for | ||
| 718 | that environment key disappears. | ||
| 719 | </li> | ||
| 720 | </ul> | ||
| 721 | <p> | ||
| 722 | Local unlink stops the relay client and removes the locally persisted relay URL, issuer, | ||
| 723 | linked user, environment publication credential, cloud-mint public key, connector | ||
| 724 | config, and agent-activity preference. | ||
| 725 | </p> | ||
| 726 | </article> | ||
| 727 | </section> | ||
| 728 | |||
| 729 | <section class="panel" aria-label="Cloudflare tunnel operational profile"> | ||
| 730 | <h2>Cloudflare Tunnel Operational Profile</h2> | ||
| 731 | <p> | ||
| 732 | Managed tunnels are private application backends, not general-purpose public hosting. The | ||
| 733 | relay provisions one named Cloudflare tunnel and one DNS record per active | ||
| 734 | <code>(userId, environmentId)</code> allocation. | ||
| 735 | </p> | ||
| 736 | <ul> | ||
| 737 | <li> | ||
| 738 | <strong>Lifecycle:</strong> the tunnel is created when an environment is linked, reused | ||
| 739 | across retry-safe reconciliation, kept for the lifetime of that link, and deleted on | ||
| 740 | unlink. If teardown fails, the allocation checkpoint remains so cleanup can be retried. | ||
| 741 | </li> | ||
| 742 | <li> | ||
| 743 | <strong>Origin:</strong> tunnel ingress is restricted to the linked environment server's | ||
| 744 | validated loopback HTTP origin. Arbitrary upstream hosts and raw TCP origins are | ||
| 745 | rejected. | ||
| 746 | </li> | ||
| 747 | <li> | ||
| 748 | <strong>Protocols:</strong> managed endpoints carry HTTPS request/response traffic and | ||
| 749 | WSS connections only. Remote clients use the normal environment HTTP APIs and | ||
| 750 | authenticated WebSocket RPC transport through the tunnel. | ||
| 751 | </li> | ||
| 752 | <li> | ||
| 753 | <strong>Relay-generated traffic:</strong> the hosted relay sends sparse HTTPS health | ||
| 754 | checks and credential-mint requests during status checks and remote connection | ||
| 755 | bootstrap. It does not proxy steady-state remote sessions. | ||
| 756 | </li> | ||
| 757 | <li> | ||
| 758 | <strong>Traffic shape:</strong> expected load is interactive and user-driven. Active | ||
| 759 | remote sessions may keep WebSockets open and exchange terminal, agent, and UI updates; | ||
| 760 | inactive linked environments should carry little or no tunnel traffic. | ||
| 761 | </li> | ||
| 762 | <li> | ||
| 763 | <strong>Capacity planning:</strong> the project is pre-launch, so there is not yet a | ||
| 764 | production bandwidth baseline. Tunnel count grows with active linked user/environment | ||
| 765 | allocations, while bandwidth and concurrent WebSockets grow with active remote use. | ||
| 766 | </li> | ||
| 767 | </ul> | ||
| 768 | </section> | ||
| 769 | |||
| 770 | <section class="panel" aria-label="SSRF and tunnel hardening"> | ||
| 771 | <h2>SSRF and Tunnel Hardening</h2> | ||
| 772 | <table> | ||
| 773 | <thead> | ||
| 774 | <tr> | ||
| 775 | <th>Risk</th> | ||
| 776 | <th>Implemented control</th> | ||
| 777 | </tr> | ||
| 778 | </thead> | ||
| 779 | <tbody> | ||
| 780 | <tr> | ||
| 781 | <td>A client asks the relay to tunnel an arbitrary host.</td> | ||
| 782 | <td> | ||
| 783 | The environment and relay both accept managed tunnel origins only for loopback hosts | ||
| 784 | and valid ports. | ||
| 785 | </td> | ||
| 786 | </tr> | ||
| 787 | <tr> | ||
| 788 | <td>A forwarded request tricks local origin validation.</td> | ||
| 789 | <td> | ||
| 790 | The local link-proof handler requires an exact loopback request URL and rejects | ||
| 791 | forwarded host or protocol headers. | ||
| 792 | </td> | ||
| 793 | </tr> | ||
| 794 | <tr> | ||
| 795 | <td>A stored endpoint is replaced with an arbitrary external URL.</td> | ||
| 796 | <td> | ||
| 797 | Connect and status resolve a ready allocation from Postgres and require a hostname | ||
| 798 | under the configured managed endpoint zone. | ||
| 799 | </td> | ||
| 800 | </tr> | ||
| 801 | <tr> | ||
| 802 | <td>A managed endpoint redirects relay egress elsewhere.</td> | ||
| 803 | <td> | ||
| 804 | Relay-to-environment status and mint requests set redirect handling to | ||
| 805 | <code>manual</code>. | ||
| 806 | </td> | ||
| 807 | </tr> | ||
| 808 | <tr> | ||
| 809 | <td>A process behind the tunnel returns attacker-controlled data.</td> | ||
| 810 | <td> | ||
| 811 | The relay verifies environment-signed health and mint responses against the linked | ||
| 812 | environment public key, expected nonce, and request binding. | ||
| 813 | </td> | ||
| 814 | </tr> | ||
| 815 | <tr> | ||
| 816 | <td>A stolen remote access token is replayed.</td> | ||
| 817 | <td> | ||
| 818 | Relay and environment access tokens are DPoP-bound; per-request proofs and replay | ||
| 819 | guards are required. | ||
| 820 | </td> | ||
| 821 | </tr> | ||
| 822 | <tr> | ||
| 823 | <td>The relay client token leaks through process listings or shell parsing.</td> | ||
| 824 | <td> | ||
| 825 | The relay client is spawned without a shell and receives its connector token in | ||
| 826 | <code>TUNNEL_TOKEN</code>. | ||
| 827 | </td> | ||
| 828 | </tr> | ||
| 829 | </tbody> | ||
| 830 | </table> | ||
| 831 | <div class="callout warning"> | ||
| 832 | Managed tunnel hostnames live under the configured tunnel DNS zone. Serving them from a | ||
| 833 | dedicated registrable domain with a Public Suffix List entry remains an operational | ||
| 834 | isolation requirement before broad untrusted use. | ||
| 835 | </div> | ||
| 836 | </section> | ||
| 837 | |||
| 838 | <section class="panel" aria-label="Relay HTTP API"> | ||
| 839 | <h2>Relay HTTP API</h2> | ||
| 840 | <p> | ||
| 841 | The relay publishes an OpenAPI document at <code>/openapi.json</code>, interactive API | ||
| 842 | docs at <code>/docs</code>, and redirects <code>/</code> to the docs. | ||
| 843 | </p> | ||
| 844 | <table> | ||
| 845 | <thead> | ||
| 846 | <tr> | ||
| 847 | <th>Endpoint</th> | ||
| 848 | <th>Authentication</th> | ||
| 849 | <th>Purpose</th> | ||
| 850 | </tr> | ||
| 851 | </thead> | ||
| 852 | <tbody> | ||
| 853 | <tr> | ||
| 854 | <td><code>GET /health</code></td> | ||
| 855 | <td>None</td> | ||
| 856 | <td>Relay health check.</td> | ||
| 857 | </tr> | ||
| 858 | <tr> | ||
| 859 | <td><code>GET /.well-known/oauth-authorization-server</code></td> | ||
| 860 | <td>None</td> | ||
| 861 | <td>Relay OAuth token-exchange discovery metadata.</td> | ||
| 862 | </tr> | ||
| 863 | <tr> | ||
| 864 | <td><code>GET /.well-known/oauth-protected-resource</code></td> | ||
| 865 | <td>None</td> | ||
| 866 | <td>Relay DPoP protected-resource discovery metadata.</td> | ||
| 867 | </tr> | ||
| 868 | <tr> | ||
| 869 | <td><code>GET /v1/environments</code></td> | ||
| 870 | <td>Clerk bearer</td> | ||
| 871 | <td>List environments linked to the signed-in user.</td> | ||
| 872 | </tr> | ||
| 873 | <tr> | ||
| 874 | <td><code>POST /v1/client/environment-link-challenges</code></td> | ||
| 875 | <td>Clerk bearer</td> | ||
| 876 | <td>Create a short-lived user-bound environment link challenge.</td> | ||
| 877 | </tr> | ||
| 878 | <tr> | ||
| 879 | <td><code>POST /v1/client/environment-links</code></td> | ||
| 880 | <td>Clerk bearer</td> | ||
| 881 | <td> | ||
| 882 | Verify a local environment proof, reconcile a managed endpoint, and upsert the user | ||
| 883 | link. | ||
| 884 | </td> | ||
| 885 | </tr> | ||
| 886 | <tr> | ||
| 887 | <td><code>DELETE /v1/client/environment-links/:environmentId</code></td> | ||
| 888 | <td>Clerk bearer</td> | ||
| 889 | <td>Remove managed endpoint resources and revoke the user's link.</td> | ||
| 890 | </tr> | ||
| 891 | <tr> | ||
| 892 | <td><code>POST /v1/client/dpop-token</code></td> | ||
| 893 | <td>Clerk token in token-exchange payload plus DPoP proof</td> | ||
| 894 | <td>Issue a relay DPoP access token for the requested supported scopes.</td> | ||
| 895 | </tr> | ||
| 896 | <tr> | ||
| 897 | <td><code>POST /v1/environments/:environmentId/status</code></td> | ||
| 898 | <td>Relay DPoP</td> | ||
| 899 | <td>Request and validate a signed environment health response.</td> | ||
| 900 | </tr> | ||
| 901 | <tr> | ||
| 902 | <td><code>POST /v1/environments/:environmentId/connect</code></td> | ||
| 903 | <td>Relay DPoP</td> | ||
| 904 | <td>Request and validate an environment bootstrap credential.</td> | ||
| 905 | </tr> | ||
| 906 | <tr> | ||
| 907 | <td><code>POST /v1/mobile/devices</code></td> | ||
| 908 | <td>Relay DPoP</td> | ||
| 909 | <td>Register or update a mobile device for notifications.</td> | ||
| 910 | </tr> | ||
| 911 | <tr> | ||
| 912 | <td><code>POST /v1/mobile/live-activities</code></td> | ||
| 913 | <td>Relay DPoP</td> | ||
| 914 | <td>Register a Live Activity push token.</td> | ||
| 915 | </tr> | ||
| 916 | <tr> | ||
| 917 | <td><code>DELETE /v1/mobile/devices/:deviceId</code></td> | ||
| 918 | <td>Relay DPoP</td> | ||
| 919 | <td>Unregister a mobile device.</td> | ||
| 920 | </tr> | ||
| 921 | <tr> | ||
| 922 | <td> | ||
| 923 | <code>POST /v1/environments/:environmentId/threads/:threadId/agent-activity</code> | ||
| 924 | </td> | ||
| 925 | <td>Environment bearer plus signed publish proof</td> | ||
| 926 | <td>Publish redacted current agent activity for notification delivery.</td> | ||
| 927 | </tr> | ||
| 928 | </tbody> | ||
| 929 | </table> | ||
| 930 | </section> | ||
| 931 | |||
| 932 | <section class="panel" aria-label="Environment HTTP API"> | ||
| 933 | <h2>Environment Cloud HTTP API</h2> | ||
| 934 | <table> | ||
| 935 | <thead> | ||
| 936 | <tr> | ||
| 937 | <th>Endpoint</th> | ||
| 938 | <th>Authentication</th> | ||
| 939 | <th>Purpose</th> | ||
| 940 | </tr> | ||
| 941 | </thead> | ||
| 942 | <tbody> | ||
| 943 | <tr> | ||
| 944 | <td><code>POST /api/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 | |||
| 3 | Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`. | ||
| 4 | |||
| 5 | Refactor Method: | ||
| 6 | |||
| 7 | ```ts | ||
| 8 | // Old | ||
| 9 | function old () { | ||
| 10 | return Effect.gen(function* () { | ||
| 11 | ... | ||
| 12 | }); | ||
| 13 | } | ||
| 14 | |||
| 15 | const old2 = () => Effect.gen(function* () { | ||
| 16 | ... | ||
| 17 | }); | ||
| 18 | ``` | ||
| 19 | |||
| 20 | ```ts | ||
| 21 | // New | ||
| 22 | const new = Effect.fn('functionName')(function* () { | ||
| 23 | ... | ||
| 24 | }) | ||
| 25 | ``` | ||
| 26 | |||
| 27 | - Use `Effect.fn('name')(function* (input: Input): Effect.fn.Return<A, E, R> {})` to annotate the return type of the function if needed. | ||
| 28 | |||
| 29 | - The 2nd argument works as a pipe, and it gets the effect and input as arguments: | ||
| 30 | |||
| 31 | ```ts | ||
| 32 | Effect.fn("name")( | ||
| 33 | function* (input: Input): Effect.fn.Return<A, E, R> {}, | ||
| 34 | (effect, input) => Effect.catch(effect, (reason) => Effect.logWarning("Err", { input, reason })), | ||
| 35 | ); | ||
| 36 | ``` | ||
| 37 | |||
| 38 | ## Summary | ||
| 39 | |||
| 40 | - Total non-test candidates: `322` | ||
| 41 | |||
| 42 | ## Suggested Order | ||
| 43 | |||
| 44 | - [ ] `apps/server/src/provider/Layers/ProviderService.ts` | ||
| 45 | - [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts` | ||
| 46 | - [x] `apps/server/src/provider/Layers/CodexAdapter.ts` | ||
| 47 | - [x] `apps/server/src/git/Layers/GitCore.ts` | ||
| 48 | - [x] `apps/server/src/git/Layers/GitManager.ts` | ||
| 49 | - [x] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` | ||
| 50 | - [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` | ||
| 51 | - [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` | ||
| 52 | - [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts` | ||
| 53 | - [ ] `Everything else` | ||
| 54 | |||
| 55 | ## Checklist | ||
| 56 | |||
| 57 | ### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`) | ||
| 58 | |||
| 59 | - [x] [buildUserMessageEffect](/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 | |||
| 3 | The environment server and the relay use separate credentials, issuers, and trust | ||
| 4 | boundaries. They intentionally use a similar OAuth-shaped model so that permission | ||
| 5 | checks and token exchange behavior can be audited against established concepts. | ||
| 6 | |||
| 7 | ## Authorization Model | ||
| 8 | |||
| 9 | Environment authorization is capability-based. A session carries zero or more | ||
| 10 | OAuth-style scope strings: | ||
| 11 | |||
| 12 | | Scope | Permission | | ||
| 13 | | ----------------------- | ------------------------------------------------------------------------ | | ||
| 14 | | `orchestration:read` | Read snapshots, status, events, configuration, and filesystem/VCS state. | | ||
| 15 | | `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state. | | ||
| 16 | | `terminal:operate` | Create, attach, input, resize, clear, restart, and terminate terminals. | | ||
| 17 | | `review:write` | Read review diff previews used to compose review feedback. | | ||
| 18 | | `access:read` | Inspect pairing links and client sessions. | | ||
| 19 | | `access:write` | Create or revoke pairing links and client sessions. | | ||
| 20 | | `relay:read` | Inspect managed relay connectivity. | | ||
| 21 | | `relay:write` | Link, configure, or unlink managed relay connectivity. | | ||
| 22 | |||
| 23 | Ordinary pairing links grant the four client-operation scopes and read access to | ||
| 24 | managed relay connectivity: | ||
| 25 | `orchestration:read orchestration:operate terminal:operate review:write relay:read`. | ||
| 26 | The desktop bootstrap credential and command-line administrative bootstrap | ||
| 27 | credentials additionally grant `access:read access:write relay:write`. | ||
| 28 | |||
| 29 | ## Authentication Flows | ||
| 30 | |||
| 31 | ### Browser Session | ||
| 32 | |||
| 33 | `POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a | ||
| 34 | browser session cookie. The cookie is an HTTP transport adapter for the same | ||
| 35 | scoped session model; the response never exposes the session secret to browser | ||
| 36 | JavaScript. | ||
| 37 | |||
| 38 | ### Bearer Access Token | ||
| 39 | |||
| 40 | Non-browser clients use `POST /oauth/token` with an | ||
| 41 | `application/x-www-form-urlencoded` body: | ||
| 42 | |||
| 43 | ```text | ||
| 44 | grant_type=urn:ietf:params:oauth:grant-type:token-exchange | ||
| 45 | subject_token=<bootstrap credential> | ||
| 46 | subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap | ||
| 47 | requested_token_type=urn:ietf:params:oauth:token-type:access_token | ||
| 48 | scope=orchestration:read orchestration:operate terminal:operate review:write relay:read | ||
| 49 | ``` | ||
| 50 | |||
| 51 | Clients may additionally submit `client_label`, `client_device_type`, and | ||
| 52 | `client_os` extension parameters so the authorized-clients UI can identify the | ||
| 53 | device that established the session. These are presentation hints only; the | ||
| 54 | environment derives transport metadata such as IP address and user agent from | ||
| 55 | the request and does not use these fields for authorization. | ||
| 56 | |||
| 57 | The response has the token-exchange shape: | ||
| 58 | |||
| 59 | ```json | ||
| 60 | { | ||
| 61 | "access_token": "<opaque session token>", | ||
| 62 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", | ||
| 63 | "token_type": "Bearer", | ||
| 64 | "expires_in": 3600, | ||
| 65 | "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" | ||
| 66 | } | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Requested scopes must be a subset of the one-time bootstrap credential grant. | ||
| 70 | An ordinary paired client therefore cannot exchange its grant for | ||
| 71 | `access:read`, `access:write`, or `relay:write`. | ||
| 72 | |||
| 73 | ### WebSocket Ticket | ||
| 74 | |||
| 75 | `POST /api/auth/websocket-ticket` accepts any authenticated session and returns | ||
| 76 | a short-lived, single-purpose WebSocket ticket. This keeps bearer tokens and | ||
| 77 | browser cookies out of WebSocket URLs while allowing the socket handshake to | ||
| 78 | authenticate. The ticket carries its session's scopes; each RPC method then | ||
| 79 | enforces `orchestration:read`, `orchestration:operate`, `terminal:operate`, | ||
| 80 | `review:write`, or `access:read` as appropriate. Review feedback submission | ||
| 81 | currently dispatches an orchestration operation, so clients performing it also | ||
| 82 | need `orchestration:operate`. Creating a ticket is not | ||
| 83 | authorization to call every RPC method. | ||
| 84 | |||
| 85 | ## Standards Alignment | ||
| 86 | |||
| 87 | - Bearer access tokens are used through the `Authorization: Bearer` scheme from | ||
| 88 | RFC 6750. | ||
| 89 | - The token endpoint profiles the request and response vocabulary from OAuth 2.0 | ||
| 90 | Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`, | ||
| 91 | `access_token`, `issued_token_type`, and `token_type`. | ||
| 92 | - Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited, | ||
| 93 | unordered capabilities with subset checking during exchange. | ||
| 94 | |||
| 95 | This is intentionally not a general-purpose OAuth authorization server. The | ||
| 96 | environment bootstrap token type is private, the bootstrap cookie and WebSocket | ||
| 97 | connection-token routes are product-specific adapters, and the API returns its | ||
| 98 | typed `HttpApi` errors rather than implementing every OAuth error response | ||
| 99 | surface. | ||
| 100 | |||
| 101 | ## Upgrade Behavior | ||
| 102 | |||
| 103 | Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth | ||
| 104 | records to scoped records. It deletes existing pairing links and sessions while | ||
| 105 | leaving non-authentication environment state unchanged. Upgraded clients must | ||
| 106 | pair again; old `owner` or `client` credentials are never silently mapped to new | ||
| 107 | capabilities. | ||
| 108 | |||
| 109 | ## Relay Boundary | ||
| 110 | |||
| 111 | Relay-managed tunnels use their own tokens and keys. The relay can reuse scope | ||
| 112 | parsing and token-exchange conventions, but an environment access token is not a | ||
| 113 | relay token and cannot be presented to the relay. | ||
docs/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) | ||
| 5 | bun run dev | ||
| 6 | |||
| 7 | # Desktop development | ||
| 8 | bun run dev:desktop | ||
| 9 | |||
| 10 | # Desktop development on an isolated port set | ||
| 11 | T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop | ||
| 12 | |||
| 13 | # Production | ||
| 14 | bun run build | ||
| 15 | bun run start | ||
| 16 | |||
| 17 | # Build a shareable macOS .dmg (arm64 by default) | ||
| 18 | bun run dist:desktop:dmg | ||
| 19 | |||
| 20 | # Or from any project directory after publishing: | ||
| 21 | npx t3 | ||
| 22 | ``` | ||
docs/integrations/source-control-providers.md created+127| ... | @@ -0,0 +1,127 @@ | ||
| 1 | # Source Control Integrations | ||
| 2 | |||
| 3 | T3 Code connects directly to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving your editor. Work stays in flow—no more jumping between browser tabs and terminal windows. | ||
| 4 | |||
| 5 | ## Supported Providers | ||
| 6 | |||
| 7 | T3 Code works with the platforms your team already uses: | ||
| 8 | |||
| 9 | - **GitHub** – Pull requests, repository creation, and clone integration | ||
| 10 | - **GitLab** – Merge requests, repository publishing, and hosted clones | ||
| 11 | - **Bitbucket** – Pull request workflows (via API token authentication) | ||
| 12 | - **Azure DevOps** – Pull request support for Microsoft-hosted repositories | ||
| 13 | |||
| 14 | ## What You Can Do | ||
| 15 | |||
| 16 | ### Start Projects from Anywhere | ||
| 17 | |||
| 18 | **Clone repositories directly** | ||
| 19 | |||
| 20 | - Open the Command Palette (`Cmd/Ctrl + K`) → **Add Project** | ||
| 21 | - Choose **GitHub repository**, **GitLab repository**, **Bitbucket repository**, **Azure DevOps repository**, or paste any **Git URL** | ||
| 22 | - Enter the repository path (`owner/repo`, `group/project`, `workspace/repository`, or `project/repository`) or a full Git URL, pick a destination, and start coding | ||
| 23 | |||
| 24 | **Publish local projects to the cloud** | ||
| 25 | |||
| 26 | - Have a local Git repository without a remote? | ||
| 27 | - Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push—all in one flow | ||
| 28 | - Perfect for turning a weekend prototype into a real project | ||
| 29 | |||
| 30 | ### Manage Code Reviews Without Context Switching | ||
| 31 | |||
| 32 | **Create pull requests while you work** | ||
| 33 | |||
| 34 | - Push a branch and create a pull request from the Git panel | ||
| 35 | - T3 Code can suggest titles and descriptions based on your commits | ||
| 36 | - Supports GitHub Pull Requests, GitLab Merge Requests, and Bitbucket Pull Requests | ||
| 37 | |||
| 38 | **Stay on top of open reviews** | ||
| 39 | |||
| 40 | - See if your current branch already has an open PR/MR | ||
| 41 | - Open the review directly in your browser with one click | ||
| 42 | - Check out a teammate's branch to review code locally | ||
| 43 | |||
| 44 | ### Know Your Setup at a Glance | ||
| 45 | |||
| 46 | The **Source Control settings** page shows you exactly what's connected: | ||
| 47 | |||
| 48 | - ✅ Which providers are authenticated and ready | ||
| 49 | - ⚠️ What's missing and how to fix it | ||
| 50 | - 👤 Which account is signed in (when available) | ||
| 51 | |||
| 52 | Run a quick **Rescan** after setting up a new machine or changing credentials. | ||
| 53 | |||
| 54 | ## Getting Started | ||
| 55 | |||
| 56 | ### For GitHub (Recommended for most users) | ||
| 57 | |||
| 58 | 1. Install the GitHub CLI on the machine running T3 Code: | ||
| 59 | ```bash | ||
| 60 | brew install gh | ||
| 61 | ``` | ||
| 62 | 2. Sign in: | ||
| 63 | ```bash | ||
| 64 | gh auth login | ||
| 65 | ``` | ||
| 66 | 3. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated | ||
| 67 | |||
| 68 | That's it—you can now clone, publish, and create pull requests. | ||
| 69 | |||
| 70 | ### For GitLab | ||
| 71 | |||
| 72 | 1. Install the GitLab CLI: | ||
| 73 | ```bash | ||
| 74 | brew install glab | ||
| 75 | ``` | ||
| 76 | 2. Authenticate: | ||
| 77 | ```bash | ||
| 78 | glab auth login | ||
| 79 | ``` | ||
| 80 | 3. Check **Settings → Source Control** to confirm the connection | ||
| 81 | |||
| 82 | ### For Bitbucket | ||
| 83 | |||
| 84 | Bitbucket uses API tokens instead of a CLI tool: | ||
| 85 | |||
| 86 | 1. Create an API token in your Atlassian account with read/write access to pull requests and repositories | ||
| 87 | 2. Add these environment variables to the environment running T3 Code: | ||
| 88 | ```bash | ||
| 89 | export T3CODE_BITBUCKET_EMAIL="you@example.com" | ||
| 90 | export T3CODE_BITBUCKET_API_TOKEN="your-token" | ||
| 91 | ``` | ||
| 92 | 3. Restart T3 Code and verify the connection in **Source Control settings** | ||
| 93 | |||
| 94 | ### For Azure DevOps | ||
| 95 | |||
| 96 | 1. Install Azure CLI: | ||
| 97 | ```bash | ||
| 98 | brew install azure-cli | ||
| 99 | ``` | ||
| 100 | 2. Add the DevOps extension: | ||
| 101 | ```bash | ||
| 102 | az extension add --name azure-devops | ||
| 103 | ``` | ||
| 104 | 3. Sign in: | ||
| 105 | ```bash | ||
| 106 | az login | ||
| 107 | ``` | ||
| 108 | |||
| 109 | --- | ||
| 110 | |||
| 111 | ## Requirements & Troubleshooting | ||
| 112 | |||
| 113 | **Git is required** – T3 Code uses Git for all local operations. Ensure `git` is installed on your server. | ||
| 114 | |||
| 115 | **Server-side setup** – Authentication happens on the machine running T3 Code (the server), not your local browser. If you're using a hosted or team instance, your administrator may have already configured providers. | ||
| 116 | |||
| 117 | **Common issues:** | ||
| 118 | |||
| 119 | - **Provider shows "Not authenticated"** – Run the login command for that provider (e.g., `gh auth login`) in a terminal on the server, then rescan in Settings | ||
| 120 | - **Bitbucket not connecting** – Double-check your environment variables are set in the correct shell profile and the server was restarted | ||
| 121 | - **Can't push to a remote** – Verify your Git remote URL matches the provider you've authenticated with (SSH vs HTTPS remotes may need different credentials) | ||
| 122 | |||
| 123 | **Need more help?** Check your provider's CLI documentation: | ||
| 124 | |||
| 125 | - [GitHub CLI](https://cli.github.com/) | ||
| 126 | - [GitLab CLI](https://gitlab.com/gitlab-org/cli) | ||
| 127 | - [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) | ||
docs/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 | |||
| 11 | This 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 | |||
| 17 | Run commands from `apps/mobile`. | ||
| 18 | |||
| 19 | T3 Cloud is optional and disabled in a fresh clone. Public configuration belongs in the | ||
| 20 | repository-root `.env` or `.env.local`, not an `apps/mobile/.env` file. See | ||
| 21 | [`../../.env.example`](../../.env.example). | ||
| 22 | |||
| 23 | ## Development | ||
| 24 | |||
| 25 | Start Metro for the dev client: | ||
| 26 | |||
| 27 | ```bash | ||
| 28 | vp run dev:client | ||
| 29 | ``` | ||
| 30 | |||
| 31 | Build and run the local iOS dev client: | ||
| 32 | |||
| 33 | ```bash | ||
| 34 | vp run ios:dev | ||
| 35 | ``` | ||
| 36 | |||
| 37 | Build and run the local iOS preview app: | ||
| 38 | |||
| 39 | ```bash | ||
| 40 | vp run ios:preview | ||
| 41 | ``` | ||
| 42 | |||
| 43 | Force the review diff highlighter engine: | ||
| 44 | |||
| 45 | ```bash | ||
| 46 | EXPO_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 | |||
| 51 | Inspect the resolved Expo config for a variant: | ||
| 52 | |||
| 53 | ```bash | ||
| 54 | vp run config:dev | ||
| 55 | vp run config:preview | ||
| 56 | ``` | ||
| 57 | |||
| 58 | Run static checks for mobile native code: | ||
| 59 | |||
| 60 | ```bash | ||
| 61 | node ../../scripts/mobile-native-static-check.ts | ||
| 62 | ``` | ||
| 63 | |||
| 64 | The 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 | |||
| 68 | CI 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 | |||
| 70 | For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`, | ||
| 71 | `T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL` | ||
| 72 | as EAS environment variables. Expo config maps the canonical values into the mobile build. | ||
| 73 | |||
| 74 | Create a PR preview dev-client build manually: | ||
| 75 | |||
| 76 | ```bash | ||
| 77 | vp run eas:ios:preview:dev | ||
| 78 | ``` | ||
| 79 | |||
| 80 | Create a cloud dev-client build: | ||
| 81 | |||
| 82 | ```bash | ||
| 83 | vp run eas:ios:dev | ||
| 84 | ``` | ||
| 85 | |||
| 86 | Create a persistent preview build: | ||
| 87 | |||
| 88 | ```bash | ||
| 89 | vp run eas:ios:preview | ||
| 90 | ``` | ||
| 91 | |||
| 92 | Android equivalents: | ||
| 93 | |||
| 94 | ```bash | ||
| 95 | vp run eas:android:dev | ||
| 96 | vp run eas:android:preview:dev | ||
| 97 | vp run eas:android:preview | ||
| 98 | ``` | ||
docs/mobile/terminal-native-module.md created+38| ... | @@ -0,0 +1,38 @@ | ||
| 1 | # T3 Mobile Terminal Native Module | ||
| 2 | |||
| 3 | This local Expo module owns the native terminal surface for the mobile app. | ||
| 4 | |||
| 5 | The 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 | |||
| 11 | The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the Ghostty custom-I/O | ||
| 12 | fork, with T3's iOS 16 compatibility patch applied. `T3TerminalView` owns a `libghostty` surface and | ||
| 13 | uses that callback I/O model: | ||
| 14 | |||
| 15 | 1. initialize libghostty once for the process | ||
| 16 | 2. create one Ghostty app and surface per native view | ||
| 17 | 3. feed remote output into the surface with `ghostty_surface_feed_data` | ||
| 18 | 4. send user input back to JS with the write callback | ||
| 19 | 5. emit Ghostty's measured terminal size through `onResize` | ||
| 20 | |||
| 21 | Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the | ||
| 22 | React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a | ||
| 23 | future Android Ghostty build without changing JS. | ||
| 24 | |||
| 25 | Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`. | ||
| 26 | |||
| 27 | ## Rebuilding GhosttyKit | ||
| 28 | |||
| 29 | The checked-in `GhosttyKit.xcframework` is built from the Ghostty custom-I/O fork (https://github.com/Yash-Singh1/ghostty/tree/custom-io). | ||
| 30 | Set the directory to the cloned repository checked out on the `custom-io` branch to `GHOSTTY_SOURCE_DIR`. | ||
| 31 | |||
| 32 | ```bash | ||
| 33 | apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh | ||
| 34 | ``` | ||
| 35 | |||
| 36 | The 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` | ||
| 38 | fails, run `xcodebuild -downloadComponent MetalToolchain`. | ||
docs/observability.md deleted-519| ... | @@ -1,519 +0,0 @@ | ||
| 1 | # Observability | ||
| 2 | |||
| 3 | T3 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 | |||
| 9 | The 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 | |||
| 15 | Logs are human-facing only: | ||
| 16 | |||
| 17 | - destination: stdout | ||
| 18 | - format: `Logger.consolePretty()` | ||
| 19 | - persistence: none | ||
| 20 | |||
| 21 | If you want a log message to show up in the trace file, emit it inside an active span with `Effect.log...`. `Logger.tracerLogger` will attach it as a span event. | ||
| 22 | |||
| 23 | ### Traces | ||
| 24 | |||
| 25 | Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`). | ||
| 26 | |||
| 27 | Important 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 | |||
| 36 | The schema lives in `apps/server/src/observability/TraceRecord.ts`. | ||
| 37 | |||
| 38 | ### Metrics | ||
| 39 | |||
| 40 | Metrics 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 | |||
| 46 | If OTLP is not configured, metrics still exist in-process, but you will not have a local artifact to inspect. | ||
| 47 | |||
| 48 | ### Related Artifacts | ||
| 49 | |||
| 50 | Provider 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 | |||
| 54 | There 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 | |||
| 59 | The local trace file is always on. OTLP export is opt-in. | ||
| 60 | |||
| 61 | ### Option 1: Local Traces Only | ||
| 62 | |||
| 63 | You do not need any extra env vars. Just run the app normally and inspect `server.trace.ndjson`. | ||
| 64 | |||
| 65 | Examples: | ||
| 66 | |||
| 67 | ```bash | ||
| 68 | npx t3 | ||
| 69 | ``` | ||
| 70 | |||
| 71 | ```bash | ||
| 72 | node --run dev | ||
| 73 | ``` | ||
| 74 | |||
| 75 | ```bash | ||
| 76 | node --run dev:desktop | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ### Option 2: Run With A Local LGTM Stack | ||
| 80 | |||
| 81 | #### 1. Start Grafana LGTM | ||
| 82 | |||
| 83 | ```bash | ||
| 84 | docker run --name lgtm \ | ||
| 85 | -p 3000:3000 \ | ||
| 86 | -p 4317:4317 \ | ||
| 87 | -p 4318:4318 \ | ||
| 88 | --rm -ti \ | ||
| 89 | grafana/otel-lgtm | ||
| 90 | ``` | ||
| 91 | |||
| 92 | Then open `http://localhost:3000`. | ||
| 93 | |||
| 94 | Default Grafana login: | ||
| 95 | |||
| 96 | - username: `admin` | ||
| 97 | - password: `admin` | ||
| 98 | |||
| 99 | #### 2. Export OTLP env vars | ||
| 100 | |||
| 101 | ```bash | ||
| 102 | export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces | ||
| 103 | export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics | ||
| 104 | export T3CODE_OTLP_SERVICE_NAME=t3-local | ||
| 105 | ``` | ||
| 106 | |||
| 107 | Optional: | ||
| 108 | |||
| 109 | ```bash | ||
| 110 | export T3CODE_TRACE_MIN_LEVEL=Info | ||
| 111 | export T3CODE_TRACE_TIMING_ENABLED=true | ||
| 112 | ``` | ||
| 113 | |||
| 114 | #### 3. Launch the app from that same shell | ||
| 115 | |||
| 116 | CLI: | ||
| 117 | |||
| 118 | ```bash | ||
| 119 | npx t3 | ||
| 120 | ``` | ||
| 121 | |||
| 122 | Monorepo web/server dev: | ||
| 123 | |||
| 124 | ```bash | ||
| 125 | node --run dev | ||
| 126 | ``` | ||
| 127 | |||
| 128 | Monorepo desktop dev: | ||
| 129 | |||
| 130 | ```bash | ||
| 131 | node --run dev:desktop | ||
| 132 | ``` | ||
| 133 | |||
| 134 | Packaged desktop app: | ||
| 135 | |||
| 136 | Launch the actual app executable from the same shell so the desktop app and embedded backend inherit `T3CODE_OTLP_*`. | ||
| 137 | |||
| 138 | macOS app bundle example: | ||
| 139 | |||
| 140 | ```bash | ||
| 141 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ | ||
| 142 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ | ||
| 143 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ | ||
| 144 | "/Applications/T3 Code.app/Contents/MacOS/T3 Code" | ||
| 145 | ``` | ||
| 146 | |||
| 147 | Direct binary example: | ||
| 148 | |||
| 149 | ```bash | ||
| 150 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ | ||
| 151 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ | ||
| 152 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ | ||
| 153 | ./path/to/your/desktop-app-binary | ||
| 154 | ``` | ||
| 155 | |||
| 156 | Do 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 | |||
| 160 | The 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 | |||
| 166 | The trace file is the fastest way to inspect raw span data. | ||
| 167 | |||
| 168 | Tail it: | ||
| 169 | |||
| 170 | ```bash | ||
| 171 | tail -f "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 172 | ``` | ||
| 173 | |||
| 174 | In monorepo dev, use: | ||
| 175 | |||
| 176 | ```bash | ||
| 177 | tail -f ./dev/logs/server.trace.ndjson | ||
| 178 | ``` | ||
| 179 | |||
| 180 | Show failed spans: | ||
| 181 | |||
| 182 | ```bash | ||
| 183 | jq -c 'select(.exit._tag != "Success") | { | ||
| 184 | name, | ||
| 185 | durationMs, | ||
| 186 | exit, | ||
| 187 | attributes | ||
| 188 | }' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 189 | ``` | ||
| 190 | |||
| 191 | Show slow spans: | ||
| 192 | |||
| 193 | ```bash | ||
| 194 | jq -c 'select(.durationMs > 1000) | { | ||
| 195 | name, | ||
| 196 | durationMs, | ||
| 197 | traceId, | ||
| 198 | spanId | ||
| 199 | }' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 200 | ``` | ||
| 201 | |||
| 202 | Inspect embedded log events: | ||
| 203 | |||
| 204 | ```bash | ||
| 205 | jq -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 | |||
| 219 | Follow one trace: | ||
| 220 | |||
| 221 | ```bash | ||
| 222 | jq -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 | |||
| 230 | Filter orchestration commands: | ||
| 231 | |||
| 232 | ```bash | ||
| 233 | jq -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 | |||
| 241 | Filter git activity: | ||
| 242 | |||
| 243 | ```bash | ||
| 244 | jq -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 | |||
| 258 | Tempo 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 | |||
| 265 | Recommended flow in Grafana: | ||
| 266 | |||
| 267 | 1. Open `Explore`. | ||
| 268 | 2. Pick the `Tempo` data source. | ||
| 269 | 3. Set the time range to something recent like `Last 15 minutes`. | ||
| 270 | 4. Start broad. Do not begin with a very narrow query. | ||
| 271 | 5. Look for spans from your configured service name, then narrow by span name or attributes. | ||
| 272 | |||
| 273 | Good 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 | |||
| 279 | Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful. | ||
| 280 | |||
| 281 | ### Use Metrics To See Systemic Problems | ||
| 282 | |||
| 283 | Traces are best for one request. Metrics are best for trends. | ||
| 284 | |||
| 285 | Good 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 | |||
| 294 | Counters 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 | |||
| 302 | Use 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 | |||
| 308 | Use 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 | |||
| 321 | That 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 | |||
| 327 | If 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 | |||
| 333 | 1. Start with the local NDJSON file. | ||
| 334 | 2. Find spans where `exit._tag != "Success"`. | ||
| 335 | 3. Group by `traceId`. | ||
| 336 | 4. Inspect sibling spans and span events. | ||
| 337 | 5. If needed, move to Tempo for the full trace tree. | ||
| 338 | |||
| 339 | ### "Why is the UI feeling slow?" | ||
| 340 | |||
| 341 | 1. Search for slow top-level spans in the trace file or Tempo. | ||
| 342 | 2. Check child spans for sqlite, git, provider, or terminal work. | ||
| 343 | 3. Look at the matching duration metrics to see whether the slowness is systemic. | ||
| 344 | |||
| 345 | ### "Did this command take too long to acknowledge?" | ||
| 346 | |||
| 347 | 1. Check `t3_orchestration_command_ack_duration` by `commandType`. | ||
| 348 | 2. If it is high, inspect the corresponding orchestration trace. | ||
| 349 | 3. Look at child spans for projection, sqlite, provider, or git work. | ||
| 350 | |||
| 351 | ### "Are git hooks causing latency?" | ||
| 352 | |||
| 353 | 1. Filter `git.operation` spans. | ||
| 354 | 2. Inspect `git.hook.started` and `git.hook.finished` events. | ||
| 355 | 3. Compare hook timing to the enclosing git span duration. | ||
| 356 | |||
| 357 | ### "Why do I have spans locally but nothing in Grafana?" | ||
| 358 | |||
| 359 | Usually 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 | |||
| 366 | If 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 | |||
| 372 | Good 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 | |||
| 381 | Avoid 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 | |||
| 385 | The codebase already uses `Effect.fn("name")` heavily. That should usually be your first tracing boundary. | ||
| 386 | |||
| 387 | For ad hoc work: | ||
| 388 | |||
| 389 | ```ts | ||
| 390 | import { Effect } from "effect"; | ||
| 391 | |||
| 392 | const 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 | |||
| 405 | Use span annotations for IDs, paths, and other detailed context: | ||
| 406 | |||
| 407 | ```ts | ||
| 408 | yield * | ||
| 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 | |||
| 418 | Good metric labels: | ||
| 419 | |||
| 420 | - operation kind | ||
| 421 | - method name | ||
| 422 | - provider kind | ||
| 423 | - aggregate kind | ||
| 424 | - outcome | ||
| 425 | |||
| 426 | Bad 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 | |||
| 435 | Detailed context belongs on spans, not metrics. | ||
| 436 | |||
| 437 | ### Use Logs As Span Events | ||
| 438 | |||
| 439 | Logs inside a span become part of the trace story: | ||
| 440 | |||
| 441 | ```ts | ||
| 442 | yield * Effect.logInfo("starting provider turn"); | ||
| 443 | yield * Effect.logDebug("waiting for approval response"); | ||
| 444 | ``` | ||
| 445 | |||
| 446 | Those 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 | ||
| 453 | import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts"; | ||
| 454 | |||
| 455 | const 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 | |||
| 470 | The server observability layer is assembled in `apps/server/src/observability/Layers/Observability.ts`. | ||
| 471 | |||
| 472 | It 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 | |||
| 483 | Local 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 | |||
| 492 | OTLP 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 | |||
| 499 | If the OTLP URLs are unset, local tracing still works and metrics stay in-process only. | ||
| 500 | |||
| 501 | ### What Is Instrumented Today | ||
| 502 | |||
| 503 | Current 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 | |||
| 3 | Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`. | ||
| 4 | |||
| 5 | Refactor Method: | ||
| 6 | |||
| 7 | ```ts | ||
| 8 | // Old | ||
| 9 | function old () { | ||
| 10 | return Effect.gen(function* () { | ||
| 11 | ... | ||
| 12 | }); | ||
| 13 | } | ||
| 14 | |||
| 15 | const old2 = () => Effect.gen(function* () { | ||
| 16 | ... | ||
| 17 | }); | ||
| 18 | ``` | ||
| 19 | |||
| 20 | ```ts | ||
| 21 | // New | ||
| 22 | const new = Effect.fn('functionName')(function* () { | ||
| 23 | ... | ||
| 24 | }) | ||
| 25 | ``` | ||
| 26 | |||
| 27 | - Use `Effect.fn('name')(function* (input: Input): Effect.fn.Return<A, E, R> {})` to annotate the return type of the function if needed. | ||
| 28 | |||
| 29 | - The 2nd argument works as a pipe, and it gets the effect and input as arguments: | ||
| 30 | |||
| 31 | ```ts | ||
| 32 | Effect.fn("name")( | ||
| 33 | function* (input: Input): Effect.fn.Return<A, E, R> {}, | ||
| 34 | (effect, input) => Effect.catch(effect, (reason) => Effect.logWarning("Err", { input, reason })), | ||
| 35 | ); | ||
| 36 | ``` | ||
| 37 | |||
| 38 | ## Summary | ||
| 39 | |||
| 40 | - Total non-test candidates: `322` | ||
| 41 | |||
| 42 | ## Suggested Order | ||
| 43 | |||
| 44 | - [ ] `apps/server/src/provider/Layers/ProviderService.ts` | ||
| 45 | - [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts` | ||
| 46 | - [x] `apps/server/src/provider/Layers/CodexAdapter.ts` | ||
| 47 | - [x] `apps/server/src/git/Layers/GitCore.ts` | ||
| 48 | - [x] `apps/server/src/git/Layers/GitManager.ts` | ||
| 49 | - [x] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` | ||
| 50 | - [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` | ||
| 51 | - [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` | ||
| 52 | - [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts` | ||
| 53 | - [ ] `Everything else` | ||
| 54 | |||
| 55 | ## Checklist | ||
| 56 | |||
| 57 | ### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`) | ||
| 58 | |||
| 59 | - [x] [buildUserMessageEffect](/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 | |||
| 3 | T3 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 | |||
| 9 | The 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 | |||
| 15 | Logs are human-facing only: | ||
| 16 | |||
| 17 | - destination: stdout | ||
| 18 | - format: `Logger.consolePretty()` | ||
| 19 | - persistence: none | ||
| 20 | |||
| 21 | If you want a log message to show up in the trace file, emit it inside an active span with `Effect.log...`. `Logger.tracerLogger` will attach it as a span event. | ||
| 22 | |||
| 23 | ### Traces | ||
| 24 | |||
| 25 | Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`). | ||
| 26 | |||
| 27 | Important 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 | |||
| 36 | The schema lives in `apps/server/src/observability/TraceRecord.ts`. | ||
| 37 | |||
| 38 | ### Metrics | ||
| 39 | |||
| 40 | Metrics 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 | |||
| 46 | If OTLP is not configured, metrics still exist in-process, but you will not have a local artifact to inspect. | ||
| 47 | |||
| 48 | ### Related Artifacts | ||
| 49 | |||
| 50 | Provider 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 | |||
| 54 | There 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 | |||
| 59 | The local trace file is always on. OTLP export is opt-in. | ||
| 60 | |||
| 61 | ### Option 1: Local Traces Only | ||
| 62 | |||
| 63 | You do not need any extra env vars. Just run the app normally and inspect `server.trace.ndjson`. | ||
| 64 | |||
| 65 | Examples: | ||
| 66 | |||
| 67 | ```bash | ||
| 68 | npx t3 | ||
| 69 | ``` | ||
| 70 | |||
| 71 | ```bash | ||
| 72 | node --run dev | ||
| 73 | ``` | ||
| 74 | |||
| 75 | ```bash | ||
| 76 | node --run dev:desktop | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ### Option 2: Run With A Local LGTM Stack | ||
| 80 | |||
| 81 | #### 1. Start Grafana LGTM | ||
| 82 | |||
| 83 | ```bash | ||
| 84 | docker run --name lgtm \ | ||
| 85 | -p 3000:3000 \ | ||
| 86 | -p 4317:4317 \ | ||
| 87 | -p 4318:4318 \ | ||
| 88 | --rm -ti \ | ||
| 89 | grafana/otel-lgtm | ||
| 90 | ``` | ||
| 91 | |||
| 92 | Then open `http://localhost:3000`. | ||
| 93 | |||
| 94 | Default Grafana login: | ||
| 95 | |||
| 96 | - username: `admin` | ||
| 97 | - password: `admin` | ||
| 98 | |||
| 99 | #### 2. Export OTLP env vars | ||
| 100 | |||
| 101 | ```bash | ||
| 102 | export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces | ||
| 103 | export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics | ||
| 104 | export T3CODE_OTLP_SERVICE_NAME=t3-local | ||
| 105 | ``` | ||
| 106 | |||
| 107 | Optional: | ||
| 108 | |||
| 109 | ```bash | ||
| 110 | export T3CODE_TRACE_MIN_LEVEL=Info | ||
| 111 | export T3CODE_TRACE_TIMING_ENABLED=true | ||
| 112 | ``` | ||
| 113 | |||
| 114 | #### 3. Launch the app from that same shell | ||
| 115 | |||
| 116 | CLI: | ||
| 117 | |||
| 118 | ```bash | ||
| 119 | npx t3 | ||
| 120 | ``` | ||
| 121 | |||
| 122 | Monorepo web/server dev: | ||
| 123 | |||
| 124 | ```bash | ||
| 125 | node --run dev | ||
| 126 | ``` | ||
| 127 | |||
| 128 | Monorepo desktop dev: | ||
| 129 | |||
| 130 | ```bash | ||
| 131 | node --run dev:desktop | ||
| 132 | ``` | ||
| 133 | |||
| 134 | Packaged desktop app: | ||
| 135 | |||
| 136 | Launch the actual app executable from the same shell so the desktop app and embedded backend inherit `T3CODE_OTLP_*`. | ||
| 137 | |||
| 138 | macOS app bundle example: | ||
| 139 | |||
| 140 | ```bash | ||
| 141 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ | ||
| 142 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ | ||
| 143 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ | ||
| 144 | "/Applications/T3 Code.app/Contents/MacOS/T3 Code" | ||
| 145 | ``` | ||
| 146 | |||
| 147 | Direct binary example: | ||
| 148 | |||
| 149 | ```bash | ||
| 150 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ | ||
| 151 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ | ||
| 152 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ | ||
| 153 | ./path/to/your/desktop-app-binary | ||
| 154 | ``` | ||
| 155 | |||
| 156 | Do 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 | |||
| 160 | The 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 | |||
| 166 | The trace file is the fastest way to inspect raw span data. | ||
| 167 | |||
| 168 | Tail it: | ||
| 169 | |||
| 170 | ```bash | ||
| 171 | tail -f "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 172 | ``` | ||
| 173 | |||
| 174 | In monorepo dev, use: | ||
| 175 | |||
| 176 | ```bash | ||
| 177 | tail -f ./dev/logs/server.trace.ndjson | ||
| 178 | ``` | ||
| 179 | |||
| 180 | Show failed spans: | ||
| 181 | |||
| 182 | ```bash | ||
| 183 | jq -c 'select(.exit._tag != "Success") | { | ||
| 184 | name, | ||
| 185 | durationMs, | ||
| 186 | exit, | ||
| 187 | attributes | ||
| 188 | }' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 189 | ``` | ||
| 190 | |||
| 191 | Show slow spans: | ||
| 192 | |||
| 193 | ```bash | ||
| 194 | jq -c 'select(.durationMs > 1000) | { | ||
| 195 | name, | ||
| 196 | durationMs, | ||
| 197 | traceId, | ||
| 198 | spanId | ||
| 199 | }' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" | ||
| 200 | ``` | ||
| 201 | |||
| 202 | Inspect embedded log events: | ||
| 203 | |||
| 204 | ```bash | ||
| 205 | jq -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 | |||
| 219 | Follow one trace: | ||
| 220 | |||
| 221 | ```bash | ||
| 222 | jq -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 | |||
| 230 | Filter orchestration commands: | ||
| 231 | |||
| 232 | ```bash | ||
| 233 | jq -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 | |||
| 241 | Filter git activity: | ||
| 242 | |||
| 243 | ```bash | ||
| 244 | jq -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 | |||
| 258 | Tempo 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 | |||
| 265 | Recommended flow in Grafana: | ||
| 266 | |||
| 267 | 1. Open `Explore`. | ||
| 268 | 2. Pick the `Tempo` data source. | ||
| 269 | 3. Set the time range to something recent like `Last 15 minutes`. | ||
| 270 | 4. Start broad. Do not begin with a very narrow query. | ||
| 271 | 5. Look for spans from your configured service name, then narrow by span name or attributes. | ||
| 272 | |||
| 273 | Good 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 | |||
| 279 | Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful. | ||
| 280 | |||
| 281 | ### Use Metrics To See Systemic Problems | ||
| 282 | |||
| 283 | Traces are best for one request. Metrics are best for trends. | ||
| 284 | |||
| 285 | Good 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 | |||
| 294 | Counters 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 | |||
| 302 | Use 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 | |||
| 308 | Use 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 | |||
| 321 | That 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 | |||
| 327 | If 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 | |||
| 333 | 1. Start with the local NDJSON file. | ||
| 334 | 2. Find spans where `exit._tag != "Success"`. | ||
| 335 | 3. Group by `traceId`. | ||
| 336 | 4. Inspect sibling spans and span events. | ||
| 337 | 5. If needed, move to Tempo for the full trace tree. | ||
| 338 | |||
| 339 | ### "Why is the UI feeling slow?" | ||
| 340 | |||
| 341 | 1. Search for slow top-level spans in the trace file or Tempo. | ||
| 342 | 2. Check child spans for sqlite, git, provider, or terminal work. | ||
| 343 | 3. Look at the matching duration metrics to see whether the slowness is systemic. | ||
| 344 | |||
| 345 | ### "Did this command take too long to acknowledge?" | ||
| 346 | |||
| 347 | 1. Check `t3_orchestration_command_ack_duration` by `commandType`. | ||
| 348 | 2. If it is high, inspect the corresponding orchestration trace. | ||
| 349 | 3. Look at child spans for projection, sqlite, provider, or git work. | ||
| 350 | |||
| 351 | ### "Are git hooks causing latency?" | ||
| 352 | |||
| 353 | 1. Filter `git.operation` spans. | ||
| 354 | 2. Inspect `git.hook.started` and `git.hook.finished` events. | ||
| 355 | 3. Compare hook timing to the enclosing git span duration. | ||
| 356 | |||
| 357 | ### "Why do I have spans locally but nothing in Grafana?" | ||
| 358 | |||
| 359 | Usually 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 | |||
| 366 | If 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 | |||
| 372 | Good 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 | |||
| 381 | Avoid 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 | |||
| 385 | The codebase already uses `Effect.fn("name")` heavily. That should usually be your first tracing boundary. | ||
| 386 | |||
| 387 | For ad hoc work: | ||
| 388 | |||
| 389 | ```ts | ||
| 390 | import { Effect } from "effect"; | ||
| 391 | |||
| 392 | const 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 | |||
| 405 | Use span annotations for IDs, paths, and other detailed context: | ||
| 406 | |||
| 407 | ```ts | ||
| 408 | yield * | ||
| 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 | |||
| 418 | Good metric labels: | ||
| 419 | |||
| 420 | - operation kind | ||
| 421 | - method name | ||
| 422 | - provider kind | ||
| 423 | - aggregate kind | ||
| 424 | - outcome | ||
| 425 | |||
| 426 | Bad 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 | |||
| 435 | Detailed context belongs on spans, not metrics. | ||
| 436 | |||
| 437 | ### Use Logs As Span Events | ||
| 438 | |||
| 439 | Logs inside a span become part of the trace story: | ||
| 440 | |||
| 441 | ```ts | ||
| 442 | yield * Effect.logInfo("starting provider turn"); | ||
| 443 | yield * Effect.logDebug("waiting for approval response"); | ||
| 444 | ``` | ||
| 445 | |||
| 446 | Those 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 | ||
| 453 | import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts"; | ||
| 454 | |||
| 455 | const 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 | |||
| 470 | The server observability layer is assembled in `apps/server/src/observability/Layers/Observability.ts`. | ||
| 471 | |||
| 472 | It 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 | |||
| 483 | Local 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 | |||
| 492 | OTLP 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 | |||
| 499 | If the OTLP URLs are unset, local tracing still works and metrics stay in-process only. | ||
| 500 | |||
| 501 | ### What Is Instrumented Today | ||
| 502 | |||
| 503 | Current 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 | |||
| 3 | The 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 | |||
| 9 | Alchemy stages append their sanitized stage name to isolate resources, for example | ||
| 10 | `t3-code-relay-traces-dev-julius` for a personal stage. | ||
| 11 | |||
| 12 | Deploy from `infra/relay` with the normal Alchemy workflow: | ||
| 13 | |||
| 14 | ```sh | ||
| 15 | vp run deploy | ||
| 16 | ``` | ||
| 17 | |||
| 18 | Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker | ||
| 19 | receives only the scoped ingest token; it does not receive the diagnostics query token. | ||
| 20 | |||
| 21 | The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans. | ||
| 22 | Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix. | ||
| 23 | For 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 | |||
| 36 | Endpoint failure annotations and other relay-specific attributes are also emitted in the | ||
| 37 | `attributes.custom` map when present on a span, for example | ||
| 38 | `['attributes.custom']['relay.endpoint']`. | ||
| 39 | |||
| 40 | Agents should prefer the provisioned view or APL queries for completed incidents instead of | ||
| 41 | tailing the Cloudflare Worker. Use the read-only query token when scripted access is needed; | ||
| 42 | keep the ingest token reserved for the Worker. | ||
docs/operations/release.md created+289| ... | @@ -0,0 +1,289 @@ | ||
| 1 | # Release Checklist | ||
| 2 | |||
| 3 | This 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 | |||
| 35 | The relay is a shared control plane versioned separately from client releases. Stable and nightly | ||
| 36 | client builds must point at the same relay so users see the same linked environments when switching | ||
| 37 | release channels. | ||
| 38 | |||
| 39 | `.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. The | ||
| 40 | release workflow reads the relay URL and Clerk client configuration from the existing `production` | ||
| 41 | GitHub Actions environment before building desktop, CLI, or hosted web artifacts. | ||
| 42 | |||
| 43 | Required repository variables shared by relay deployments: | ||
| 44 | |||
| 45 | - `CLOUDFLARE_ACCOUNT_ID` | ||
| 46 | - `PLANETSCALE_ORGANIZATION` | ||
| 47 | - `AXIOM_ORG_ID` | ||
| 48 | |||
| 49 | Required repository secrets shared by relay deployments: | ||
| 50 | |||
| 51 | - `CLOUDFLARE_API_TOKEN` | ||
| 52 | - `PLANETSCALE_API_TOKEN_ID` | ||
| 53 | - `PLANETSCALE_API_TOKEN` | ||
| 54 | - `AXIOM_TOKEN` | ||
| 55 | |||
| 56 | Required `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 | |||
| 69 | Optional `production` environment variables: | ||
| 70 | |||
| 71 | - `RELAY_DOMAIN` when overriding the derived `relay.<RELAY_API_ZONE_NAME>` domain | ||
| 72 | |||
| 73 | Required `production` environment secrets: | ||
| 74 | |||
| 75 | - `CLERK_SECRET_KEY` | ||
| 76 | - `APNS_PRIVATE_KEY` | ||
| 77 | |||
| 78 | The account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they | ||
| 79 | are not bound into the relay Worker. The production deployment uses an Axiom personal access token, | ||
| 80 | so `AXIOM_ORG_ID` must accompany `AXIOM_TOKEN`. The `prod` stage owns the retained PlanetScale | ||
| 81 | database. Local personal stages provision isolated branches from it and are never deployed by CI. | ||
| 82 | Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources. | ||
| 83 | Personal stages reference the production-owned zones. | ||
| 84 | |||
| 85 | Developers deploy personal stages locally rather than through pull-request automation: | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local | ||
| 89 | ``` | ||
| 90 | |||
| 91 | ## Hosted web app release deployment | ||
| 92 | |||
| 93 | The hosted app is intentionally not deployed by Vercel's Git integration. The | ||
| 94 | web project disables automatic Git deployments in `apps/web/vercel.ts` via | ||
| 95 | `git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the | ||
| 96 | web app with Vercel CLI after the GitHub Release succeeds. | ||
| 97 | |||
| 98 | Required GitHub Actions secrets: | ||
| 99 | |||
| 100 | - `VERCEL_TOKEN` | ||
| 101 | - `VERCEL_ORG_ID` | ||
| 102 | - `VERCEL_PROJECT_ID` | ||
| 103 | |||
| 104 | Optional 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 | |||
| 111 | Required 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 | |||
| 117 | The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by | ||
| 118 | visiting `/__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 | ||
| 121 | the matching channel alias. | ||
| 122 | |||
| 123 | The release deploy job rewrites release package versions before upload so the | ||
| 124 | hosted app's About panel renders the release version. Stable deploys alias the | ||
| 125 | same deployment to both the `latest` channel and the router domain so the router | ||
| 126 | rules stay current. Nightly deploys only alias the `nightly` channel. The job | ||
| 127 | also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted | ||
| 128 | update track selector in the About panel. Changing the selector navigates | ||
| 129 | through `/__t3code/channel` on the router domain so the user's channel cookie is | ||
| 130 | updated before redirecting to the hosted app root. | ||
| 131 | |||
| 132 | One-time Vercel dashboard setup: | ||
| 133 | |||
| 134 | 1. Confirm the web project root directory remains `apps/web`. | ||
| 135 | 2. Add the three domains above to the web project. | ||
| 136 | 3. 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. | ||
| 139 | 4. 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 | |||
| 183 | The workflow publishes the CLI with `npm publish` from `apps/server` after bumping | ||
| 184 | the package version to the release tag version. | ||
| 185 | |||
| 186 | Checklist: | ||
| 187 | |||
| 188 | 1. Confirm npm org/user owns package `t3` (or rename package first if needed). | ||
| 189 | 2. 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 | ||
| 194 | 3. Ensure npm account and org policies allow trusted publishing for the package. | ||
| 195 | 4. 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` | ||
| 199 | 5. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`. | ||
| 200 | |||
| 201 | ## 1) Dry-run release without signing | ||
| 202 | |||
| 203 | Use this first to validate the release pipeline. | ||
| 204 | |||
| 205 | 1. Confirm no signing secrets are required for this test. | ||
| 206 | 2. Create a test tag: | ||
| 207 | - `git tag v0.0.0-test.1` | ||
| 208 | - `git push origin v0.0.0-test.1` | ||
| 209 | 3. Wait for `.github/workflows/release.yml` to finish. | ||
| 210 | 4. Verify the GitHub Release contains all platform artifacts. | ||
| 211 | 5. Download each artifact and sanity-check installation on each OS. | ||
| 212 | |||
| 213 | ## 2) Apple signing + notarization setup (macOS) | ||
| 214 | |||
| 215 | Required 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 | |||
| 223 | Checklist: | ||
| 224 | |||
| 225 | 1. Apple Developer account access: | ||
| 226 | - Team has rights to create Developer ID certificates. | ||
| 227 | 2. Create `Developer ID Application` certificate. | ||
| 228 | 3. Export certificate + private key as `.p12` from Keychain. | ||
| 229 | 4. Base64-encode the `.p12` and store as `CSC_LINK`. | ||
| 230 | 5. Store the `.p12` export password as `CSC_KEY_PASSWORD`. | ||
| 231 | 6. In App Store Connect, create an API key (Team key). | ||
| 232 | 7. 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 | ||
| 236 | 8. Re-run a tag release and confirm macOS artifacts are signed/notarized. | ||
| 237 | |||
| 238 | Notes: | ||
| 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 | |||
| 245 | Required 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 | |||
| 255 | Checklist: | ||
| 256 | |||
| 257 | 1. Create Azure Trusted Signing account and certificate profile. | ||
| 258 | 2. Record ATS values: | ||
| 259 | - Endpoint | ||
| 260 | - Account name | ||
| 261 | - Certificate profile name | ||
| 262 | - Publisher name | ||
| 263 | 3. Create/choose an Entra app registration (service principal). | ||
| 264 | 4. Grant service principal permissions required by Trusted Signing. | ||
| 265 | 5. Create a client secret for the service principal. | ||
| 266 | 6. Add Azure secrets listed above in GitHub Actions secrets. | ||
| 267 | 7. Re-run a tag release and confirm Windows installer is signed. | ||
| 268 | |||
| 269 | ## 4) Ongoing release checklist | ||
| 270 | |||
| 271 | 1. Ensure `main` is green in CI. | ||
| 272 | 2. Bump app version as needed. | ||
| 273 | 3. Create release tag: `vX.Y.Z`. | ||
| 274 | 4. Push tag. | ||
| 275 | 5. Verify workflow steps: | ||
| 276 | - preflight passes | ||
| 277 | - all matrix builds pass | ||
| 278 | - release job uploads expected files | ||
| 279 | 6. 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 | |||
| 3 | This is a living glossary for T3 Code. It explains what common terms mean in this codebase. | ||
| 4 | |||
| 5 | ## Table of contents | ||
| 6 | |||
| 7 | - [Project and workspace](#project-and-workspace) | ||
| 8 | - [Thread timeline](#thread-timeline) | ||
| 9 | - [Orchestration](#orchestration) | ||
| 10 | - [Provider runtime](#provider-runtime) | ||
| 11 | - [Checkpointing](#checkpointing) | ||
| 12 | |||
| 13 | ## Concepts | ||
| 14 | |||
| 15 | ### Project and workspace | ||
| 16 | |||
| 17 | #### Project | ||
| 18 | |||
| 19 | The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot`, a title, and one or more threads. See [workspace-layout.md][2]. | ||
| 20 | |||
| 21 | #### Workspace root | ||
| 22 | |||
| 23 | The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2]. | ||
| 24 | |||
| 25 | #### Worktree | ||
| 26 | |||
| 27 | A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live in [GitCore.ts][3]. | ||
| 28 | |||
| 29 | ### Thread timeline | ||
| 30 | |||
| 31 | #### Thread | ||
| 32 | |||
| 33 | The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4]. | ||
| 34 | |||
| 35 | #### Turn | ||
| 36 | |||
| 37 | A single user-to-assistant work cycle inside a thread. It starts with user input and ends when follow-up work like checkpointing settles. See [the contracts][1], [ProviderRuntimeIngestion.ts][5], and [CheckpointReactor.ts][6]. | ||
| 38 | |||
| 39 | #### Activity | ||
| 40 | |||
| 41 | A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4]. | ||
| 42 | |||
| 43 | ### Orchestration | ||
| 44 | |||
| 45 | Orchestration is the server-side domain layer that turns runtime activity into stable app state. The main entry point is [OrchestrationEngine.ts][7], with core logic in [decider.ts][8] and [projector.ts][4]. | ||
| 46 | |||
| 47 | #### Aggregate | ||
| 48 | |||
| 49 | The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8]. | ||
| 50 | |||
| 51 | #### Command | ||
| 52 | |||
| 53 | A typed request to change domain state. In [the contracts][1], commands are validated in [commandInvariants.ts][9] and turned into events by [decider.ts][8]. | ||
| 54 | Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`. | ||
| 55 | |||
| 56 | #### Domain Event | ||
| 57 | |||
| 58 | A persisted fact that something already happened. In [the contracts][1], events are the source of truth, and [projector.ts][4] shows how they are applied. | ||
| 59 | Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`. | ||
| 60 | |||
| 61 | #### Decider | ||
| 62 | |||
| 63 | The pure orchestration logic that turns commands plus current state into events. The core implementation is in [decider.ts][8], with preconditions in [commandInvariants.ts][9]. | ||
| 64 | |||
| 65 | #### Projection | ||
| 66 | |||
| 67 | A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10]. | ||
| 68 | |||
| 69 | #### Projector | ||
| 70 | |||
| 71 | The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11]. | ||
| 72 | |||
| 73 | #### Read model | ||
| 74 | |||
| 75 | The current materialized view of orchestration state. In [the contracts][1], it holds projects, threads, messages, activities, checkpoints, and session state. See [ProjectionSnapshotQuery.ts][10] and [OrchestrationEngine.ts][7]. | ||
| 76 | |||
| 77 | #### Reactor | ||
| 78 | |||
| 79 | A side-effecting service that handles follow-up work after events or runtime signals. Examples include [CheckpointReactor.ts][6], [ProviderCommandReactor.ts][12], and [ProviderRuntimeIngestion.ts][5]. | ||
| 80 | |||
| 81 | #### Receipt | ||
| 82 | |||
| 83 | A lightweight typed runtime signal emitted when an async milestone completes. See [RuntimeReceiptBus.ts][13]. | ||
| 84 | Examples include `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, and `turn.processing.quiesced`, which are emitted by flows such as [CheckpointReactor.ts][6]. | ||
| 85 | |||
| 86 | #### Quiesced | ||
| 87 | |||
| 88 | "Quiesced" means a turn has gone quiet and stable. In [the receipt schema][13], it means the follow-up work has settled, including work in [CheckpointReactor.ts][6]. | ||
| 89 | |||
| 90 | ### Provider runtime | ||
| 91 | |||
| 92 | The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [provider-architecture.md][16]. | ||
| 93 | |||
| 94 | #### Provider | ||
| 95 | |||
| 96 | The backend agent runtime that actually performs work. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17]. | ||
| 97 | |||
| 98 | #### Session | ||
| 99 | |||
| 100 | The live provider-backed runtime attached to a thread. Session shape is in [the orchestration contracts][1], and lifecycle is managed in [ProviderService.ts][14]. | ||
| 101 | |||
| 102 | #### Runtime mode | ||
| 103 | |||
| 104 | The safety/access mode for a thread or session. In [the contracts][1], the main values are `approval-required` and `full-access`. See [runtime-modes.md][18]. | ||
| 105 | |||
| 106 | #### Interaction mode | ||
| 107 | |||
| 108 | The agent interaction style for a thread. In [the contracts][1], the main values are `default` and `plan`. See [runtime-modes.md][18]. | ||
| 109 | |||
| 110 | #### Assistant delivery mode | ||
| 111 | |||
| 112 | Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` delivers a completed result. See [ProviderService.ts][14]. | ||
| 113 | |||
| 114 | #### Snapshot | ||
| 115 | |||
| 116 | A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19]. | ||
| 117 | |||
| 118 | ### Checkpointing | ||
| 119 | |||
| 120 | Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6]. | ||
| 121 | |||
| 122 | #### Checkpoint | ||
| 123 | |||
| 124 | A saved snapshot of a thread workspace at a particular turn. In practice it is a hidden Git ref in [CheckpointStore.ts][19] plus a projected summary from [ProjectionCheckpoints.ts][21]. Capture and lifecycle work happen in [CheckpointReactor.ts][6]. | ||
| 125 | |||
| 126 | #### Checkpoint ref | ||
| 127 | |||
| 128 | The durable identifier for a filesystem checkpoint, stored as a Git ref. It is typed in [the contracts][1], constructed in [Utils.ts][22], and used by [CheckpointStore.ts][19]. | ||
| 129 | |||
| 130 | #### Checkpoint baseline | ||
| 131 | |||
| 132 | The starting checkpoint for diffing a thread timeline. This flow is surfaced through [RuntimeReceiptBus.ts][13], coordinated in [CheckpointReactor.ts][6], and supported by [Utils.ts][22]. | ||
| 133 | |||
| 134 | #### Checkpoint diff | ||
| 135 | |||
| 136 | The patch difference between two checkpoints. Query logic lives in [CheckpointDiffQuery.ts][20], diff parsing lives in [Diffs.ts][23], and finalization is coordinated by [CheckpointReactor.ts][6]. | ||
| 137 | |||
| 138 | #### Turn diff | ||
| 139 | |||
| 140 | The file patch and changed-file summary for one turn. It is usually computed in [CheckpointDiffQuery.ts][20], represented in [the contracts][1], and recorded into thread state by [projector.ts][4]. | ||
| 141 | |||
| 142 | ## Practical Shortcuts | ||
| 143 | |||
| 144 | - If you see `requested`, think "intent recorded". | ||
| 145 | - If you see `completed`, think "result applied". | ||
| 146 | - If you see `receipt`, think "async milestone signal". | ||
| 147 | - If you see `checkpoint`, think "workspace snapshot for diff/restore". | ||
| 148 | - If you see `quiesced`, think "all relevant follow-up work has gone idle". | ||
| 149 | |||
| 150 | ## Related Docs | ||
| 151 | |||
| 152 | - [architecture.md][24] | ||
| 153 | - [provider-architecture.md][16] | ||
| 154 | - [runtime-modes.md][18] | ||
| 155 | - [workspace-layout.md][2] | ||
| 156 | |||
| 157 | [1]: ../packages/contracts/src/orchestration.ts | ||
| 158 | [2]: ./workspace-layout.md | ||
| 159 | [3]: ../apps/server/src/git/Layers/GitCore.ts | ||
| 160 | [4]: ../apps/server/src/orchestration/projector.ts | ||
| 161 | [5]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | ||
| 162 | [6]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts | ||
| 163 | [7]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | ||
| 164 | [8]: ../apps/server/src/orchestration/decider.ts | ||
| 165 | [9]: ../apps/server/src/orchestration/commandInvariants.ts | ||
| 166 | [10]: ../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts | ||
| 167 | [11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts | ||
| 168 | [12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | ||
| 169 | [13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts | ||
| 170 | [14]: ../apps/server/src/provider/Layers/ProviderService.ts | ||
| 171 | [15]: ../apps/server/src/provider/Services/ProviderAdapter.ts | ||
| 172 | [16]: ./provider-architecture.md | ||
| 173 | [17]: ../apps/server/src/provider/Layers/CodexAdapter.ts | ||
| 174 | [18]: ./runtime-modes.md | ||
| 175 | [19]: ../apps/server/src/checkpointing/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 | |||
| 36 | Set `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 | |||
| 42 | If 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 | |||
| 3 | The 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 | |||
| 9 | Alchemy stages append their sanitized stage name to isolate resources, for example | ||
| 10 | `t3-code-relay-traces-dev-julius` for a personal stage. | ||
| 11 | |||
| 12 | Deploy from `infra/relay` with the normal Alchemy workflow: | ||
| 13 | |||
| 14 | ```sh | ||
| 15 | vp run deploy | ||
| 16 | ``` | ||
| 17 | |||
| 18 | Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker | ||
| 19 | receives only the scoped ingest token; it does not receive the diagnostics query token. | ||
| 20 | |||
| 21 | The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans. | ||
| 22 | Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix. | ||
| 23 | For 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 | |||
| 36 | Endpoint failure annotations and other relay-specific attributes are also emitted in the | ||
| 37 | `attributes.custom` map when present on a span, for example | ||
| 38 | `['attributes.custom']['relay.endpoint']`. | ||
| 39 | |||
| 40 | Agents should prefer the provisioned view or APL queries for completed incidents instead of | ||
| 41 | tailing the Cloudflare Worker. Use the read-only query token when scripted access is needed; | ||
| 42 | keep the ingest token reserved for the Worker. | ||
docs/release.md deleted-289| ... | @@ -1,289 +0,0 @@ | ||
| 1 | # Release Checklist | ||
| 2 | |||
| 3 | This 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 | |||
| 35 | The relay is a shared control plane versioned separately from client releases. Stable and nightly | ||
| 36 | client builds must point at the same relay so users see the same linked environments when switching | ||
| 37 | release channels. | ||
| 38 | |||
| 39 | `.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. The | ||
| 40 | release workflow reads the relay URL and Clerk client configuration from the existing `production` | ||
| 41 | GitHub Actions environment before building desktop, CLI, or hosted web artifacts. | ||
| 42 | |||
| 43 | Required repository variables shared by relay deployments: | ||
| 44 | |||
| 45 | - `CLOUDFLARE_ACCOUNT_ID` | ||
| 46 | - `PLANETSCALE_ORGANIZATION` | ||
| 47 | - `AXIOM_ORG_ID` | ||
| 48 | |||
| 49 | Required repository secrets shared by relay deployments: | ||
| 50 | |||
| 51 | - `CLOUDFLARE_API_TOKEN` | ||
| 52 | - `PLANETSCALE_API_TOKEN_ID` | ||
| 53 | - `PLANETSCALE_API_TOKEN` | ||
| 54 | - `AXIOM_TOKEN` | ||
| 55 | |||
| 56 | Required `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 | |||
| 69 | Optional `production` environment variables: | ||
| 70 | |||
| 71 | - `RELAY_DOMAIN` when overriding the derived `relay.<RELAY_API_ZONE_NAME>` domain | ||
| 72 | |||
| 73 | Required `production` environment secrets: | ||
| 74 | |||
| 75 | - `CLERK_SECRET_KEY` | ||
| 76 | - `APNS_PRIVATE_KEY` | ||
| 77 | |||
| 78 | The account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they | ||
| 79 | are not bound into the relay Worker. The production deployment uses an Axiom personal access token, | ||
| 80 | so `AXIOM_ORG_ID` must accompany `AXIOM_TOKEN`. The `prod` stage owns the retained PlanetScale | ||
| 81 | database. Local personal stages provision isolated branches from it and are never deployed by CI. | ||
| 82 | Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources. | ||
| 83 | Personal stages reference the production-owned zones. | ||
| 84 | |||
| 85 | Developers deploy personal stages locally rather than through pull-request automation: | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local | ||
| 89 | ``` | ||
| 90 | |||
| 91 | ## Hosted web app release deployment | ||
| 92 | |||
| 93 | The hosted app is intentionally not deployed by Vercel's Git integration. The | ||
| 94 | web project disables automatic Git deployments in `apps/web/vercel.ts` via | ||
| 95 | `git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the | ||
| 96 | web app with Vercel CLI after the GitHub Release succeeds. | ||
| 97 | |||
| 98 | Required GitHub Actions secrets: | ||
| 99 | |||
| 100 | - `VERCEL_TOKEN` | ||
| 101 | - `VERCEL_ORG_ID` | ||
| 102 | - `VERCEL_PROJECT_ID` | ||
| 103 | |||
| 104 | Optional 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 | |||
| 111 | Required 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 | |||
| 117 | The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by | ||
| 118 | visiting `/__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 | ||
| 121 | the matching channel alias. | ||
| 122 | |||
| 123 | The release deploy job rewrites release package versions before upload so the | ||
| 124 | hosted app's About panel renders the release version. Stable deploys alias the | ||
| 125 | same deployment to both the `latest` channel and the router domain so the router | ||
| 126 | rules stay current. Nightly deploys only alias the `nightly` channel. The job | ||
| 127 | also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted | ||
| 128 | update track selector in the About panel. Changing the selector navigates | ||
| 129 | through `/__t3code/channel` on the router domain so the user's channel cookie is | ||
| 130 | updated before redirecting to the hosted app root. | ||
| 131 | |||
| 132 | One-time Vercel dashboard setup: | ||
| 133 | |||
| 134 | 1. Confirm the web project root directory remains `apps/web`. | ||
| 135 | 2. Add the three domains above to the web project. | ||
| 136 | 3. 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. | ||
| 139 | 4. 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 | |||
| 183 | The workflow publishes the CLI with `npm publish` from `apps/server` after bumping | ||
| 184 | the package version to the release tag version. | ||
| 185 | |||
| 186 | Checklist: | ||
| 187 | |||
| 188 | 1. Confirm npm org/user owns package `t3` (or rename package first if needed). | ||
| 189 | 2. 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 | ||
| 194 | 3. Ensure npm account and org policies allow trusted publishing for the package. | ||
| 195 | 4. 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` | ||
| 199 | 5. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`. | ||
| 200 | |||
| 201 | ## 1) Dry-run release without signing | ||
| 202 | |||
| 203 | Use this first to validate the release pipeline. | ||
| 204 | |||
| 205 | 1. Confirm no signing secrets are required for this test. | ||
| 206 | 2. Create a test tag: | ||
| 207 | - `git tag v0.0.0-test.1` | ||
| 208 | - `git push origin v0.0.0-test.1` | ||
| 209 | 3. Wait for `.github/workflows/release.yml` to finish. | ||
| 210 | 4. Verify the GitHub Release contains all platform artifacts. | ||
| 211 | 5. Download each artifact and sanity-check installation on each OS. | ||
| 212 | |||
| 213 | ## 2) Apple signing + notarization setup (macOS) | ||
| 214 | |||
| 215 | Required 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 | |||
| 223 | Checklist: | ||
| 224 | |||
| 225 | 1. Apple Developer account access: | ||
| 226 | - Team has rights to create Developer ID certificates. | ||
| 227 | 2. Create `Developer ID Application` certificate. | ||
| 228 | 3. Export certificate + private key as `.p12` from Keychain. | ||
| 229 | 4. Base64-encode the `.p12` and store as `CSC_LINK`. | ||
| 230 | 5. Store the `.p12` export password as `CSC_KEY_PASSWORD`. | ||
| 231 | 6. In App Store Connect, create an API key (Team key). | ||
| 232 | 7. 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 | ||
| 236 | 8. Re-run a tag release and confirm macOS artifacts are signed/notarized. | ||
| 237 | |||
| 238 | Notes: | ||
| 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 | |||
| 245 | Required 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 | |||
| 255 | Checklist: | ||
| 256 | |||
| 257 | 1. Create Azure Trusted Signing account and certificate profile. | ||
| 258 | 2. Record ATS values: | ||
| 259 | - Endpoint | ||
| 260 | - Account name | ||
| 261 | - Certificate profile name | ||
| 262 | - Publisher name | ||
| 263 | 3. Create/choose an Entra app registration (service principal). | ||
| 264 | 4. Grant service principal permissions required by Trusted Signing. | ||
| 265 | 5. Create a client secret for the service principal. | ||
| 266 | 6. Add Azure secrets listed above in GitHub Actions secrets. | ||
| 267 | 7. Re-run a tag release and confirm Windows installer is signed. | ||
| 268 | |||
| 269 | ## 4) Ongoing release checklist | ||
| 270 | |||
| 271 | 1. Ensure `main` is green in CI. | ||
| 272 | 2. Bump app version as needed. | ||
| 273 | 3. Create release tag: `vX.Y.Z`. | ||
| 274 | 4. Push tag. | ||
| 275 | 5. Verify workflow steps: | ||
| 276 | - preflight passes | ||
| 277 | - all matrix builds pass | ||
| 278 | - release job uploads expected files | ||
| 279 | 6. 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 | |||
| 3 | T3 Code connects directly to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving your editor. Work stays in flow—no more jumping between browser tabs and terminal windows. | ||
| 4 | |||
| 5 | ## Supported Providers | ||
| 6 | |||
| 7 | T3 Code works with the platforms your team already uses: | ||
| 8 | |||
| 9 | - **GitHub** – Pull requests, repository creation, and clone integration | ||
| 10 | - **GitLab** – Merge requests, repository publishing, and hosted clones | ||
| 11 | - **Bitbucket** – Pull request workflows (via API token authentication) | ||
| 12 | - **Azure DevOps** – Pull request support for Microsoft-hosted repositories | ||
| 13 | |||
| 14 | ## What You Can Do | ||
| 15 | |||
| 16 | ### Start Projects from Anywhere | ||
| 17 | |||
| 18 | **Clone repositories directly** | ||
| 19 | |||
| 20 | - Open the Command Palette (`Cmd/Ctrl + K`) → **Add Project** | ||
| 21 | - Choose **GitHub repository**, **GitLab repository**, **Bitbucket repository**, **Azure DevOps repository**, or paste any **Git URL** | ||
| 22 | - Enter the repository path (`owner/repo`, `group/project`, `workspace/repository`, or `project/repository`) or a full Git URL, pick a destination, and start coding | ||
| 23 | |||
| 24 | **Publish local projects to the cloud** | ||
| 25 | |||
| 26 | - Have a local Git repository without a remote? | ||
| 27 | - Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push—all in one flow | ||
| 28 | - Perfect for turning a weekend prototype into a real project | ||
| 29 | |||
| 30 | ### Manage Code Reviews Without Context Switching | ||
| 31 | |||
| 32 | **Create pull requests while you work** | ||
| 33 | |||
| 34 | - Push a branch and create a pull request from the Git panel | ||
| 35 | - T3 Code can suggest titles and descriptions based on your commits | ||
| 36 | - Supports GitHub Pull Requests, GitLab Merge Requests, and Bitbucket Pull Requests | ||
| 37 | |||
| 38 | **Stay on top of open reviews** | ||
| 39 | |||
| 40 | - See if your current branch already has an open PR/MR | ||
| 41 | - Open the review directly in your browser with one click | ||
| 42 | - Check out a teammate's branch to review code locally | ||
| 43 | |||
| 44 | ### Know Your Setup at a Glance | ||
| 45 | |||
| 46 | The **Source Control settings** page shows you exactly what's connected: | ||
| 47 | |||
| 48 | - ✅ Which providers are authenticated and ready | ||
| 49 | - ⚠️ What's missing and how to fix it | ||
| 50 | - 👤 Which account is signed in (when available) | ||
| 51 | |||
| 52 | Run a quick **Rescan** after setting up a new machine or changing credentials. | ||
| 53 | |||
| 54 | ## Getting Started | ||
| 55 | |||
| 56 | ### For GitHub (Recommended for most users) | ||
| 57 | |||
| 58 | 1. Install the GitHub CLI on the machine running T3 Code: | ||
| 59 | ```bash | ||
| 60 | brew install gh | ||
| 61 | ``` | ||
| 62 | 2. Sign in: | ||
| 63 | ```bash | ||
| 64 | gh auth login | ||
| 65 | ``` | ||
| 66 | 3. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated | ||
| 67 | |||
| 68 | That's it—you can now clone, publish, and create pull requests. | ||
| 69 | |||
| 70 | ### For GitLab | ||
| 71 | |||
| 72 | 1. Install the GitLab CLI: | ||
| 73 | ```bash | ||
| 74 | brew install glab | ||
| 75 | ``` | ||
| 76 | 2. Authenticate: | ||
| 77 | ```bash | ||
| 78 | glab auth login | ||
| 79 | ``` | ||
| 80 | 3. Check **Settings → Source Control** to confirm the connection | ||
| 81 | |||
| 82 | ### For Bitbucket | ||
| 83 | |||
| 84 | Bitbucket uses API tokens instead of a CLI tool: | ||
| 85 | |||
| 86 | 1. Create an API token in your Atlassian account with read/write access to pull requests and repositories | ||
| 87 | 2. Add these environment variables to the environment running T3 Code: | ||
| 88 | ```bash | ||
| 89 | export T3CODE_BITBUCKET_EMAIL="you@example.com" | ||
| 90 | export T3CODE_BITBUCKET_API_TOKEN="your-token" | ||
| 91 | ``` | ||
| 92 | 3. Restart T3 Code and verify the connection in **Source Control settings** | ||
| 93 | |||
| 94 | ### For Azure DevOps | ||
| 95 | |||
| 96 | 1. Install Azure CLI: | ||
| 97 | ```bash | ||
| 98 | brew install azure-cli | ||
| 99 | ``` | ||
| 100 | 2. Add the DevOps extension: | ||
| 101 | ```bash | ||
| 102 | az extension add --name azure-devops | ||
| 103 | ``` | ||
| 104 | 3. Sign in: | ||
| 105 | ```bash | ||
| 106 | az login | ||
| 107 | ``` | ||
| 108 | |||
| 109 | --- | ||
| 110 | |||
| 111 | ## Requirements & Troubleshooting | ||
| 112 | |||
| 113 | **Git is required** – T3 Code uses Git for all local operations. Ensure `git` is installed on your server. | ||
| 114 | |||
| 115 | **Server-side setup** – Authentication happens on the machine running T3 Code (the server), not your local browser. If you're using a hosted or team instance, your administrator may have already configured providers. | ||
| 116 | |||
| 117 | **Common issues:** | ||
| 118 | |||
| 119 | - **Provider shows "Not authenticated"** – Run the login command for that provider (e.g., `gh auth login`) in a terminal on the server, then rescan in Settings | ||
| 120 | - **Bitbucket not connecting** – Double-check your environment variables are set in the correct shell profile and the server was restarted | ||
| 121 | - **Can't push to a remote** – Verify your Git remote URL matches the provider you've authenticated with (SSH vs HTTPS remotes may need different credentials) | ||
| 122 | |||
| 123 | **Need more help?** Check your provider's CLI documentation: | ||
| 124 | |||
| 125 | - [GitHub CLI](https://cli.github.com/) | ||
| 126 | - [GitLab CLI](https://gitlab.com/gitlab-org/cli) | ||
| 127 | - [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) | ||
docs/t3-cloud-clerk.md deleted-174| ... | @@ -1,174 +0,0 @@ | ||
| 1 | # T3 Cloud Clerk Setup | ||
| 2 | |||
| 3 | T3 Cloud uses one Clerk application for web, desktop, and mobile authentication. The relay accepts | ||
| 4 | Clerk JWTs only when they are generated from the `t3-relay` template with the shared | ||
| 5 | `t3-code-relay` audience. | ||
| 6 | |||
| 7 | ## Application Keys | ||
| 8 | |||
| 9 | T3 Cloud is disabled in a fresh clone. To enable it for source builds, add a repository-root `.env` | ||
| 10 | or `.env.local` file: | ||
| 11 | |||
| 12 | ```dotenv | ||
| 13 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | ||
| 14 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | ||
| 15 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | ||
| 16 | T3CODE_RELAY_URL=https://relay.example.com | ||
| 17 | ``` | ||
| 18 | |||
| 19 | The shared client loader projects these canonical values into framework-specific `VITE_*` and | ||
| 20 | `EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new | ||
| 21 | client configuration should use the canonical names. | ||
| 22 | |||
| 23 | Configuration precedence is: | ||
| 24 | |||
| 25 | 1. Process or CI environment variables. | ||
| 26 | 2. Repository-root `.env.local`. | ||
| 27 | 3. Repository-root `.env`. | ||
| 28 | |||
| 29 | The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public | ||
| 30 | identifiers, not secrets. | ||
| 31 | Web, desktop, mobile, and bundled server builds statically inject the values they consume during | ||
| 32 | their build step. A built artifact does not need an environment file at runtime. CI release builds | ||
| 33 | should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, | ||
| 34 | `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and | ||
| 35 | production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS | ||
| 36 | environment. | ||
| 37 | |||
| 38 | When any client-facing public value is absent, cloud UI is omitted. When the CLI public values are | ||
| 39 | absent, the `t3 cloud` CLI command group is omitted. The bundled server still accepts runtime | ||
| 40 | overrides for self-hosted or operator-managed | ||
| 41 | deployments. | ||
| 42 | |||
| 43 | For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay | ||
| 44 | deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, | ||
| 45 | `CLERK_PUBLISHABLE_KEY`, and `CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in | ||
| 46 | deployment defaults. | ||
| 47 | `vp run --filter t3code-relay deploy` invokes Alchemy from the relay directory, so Alchemy loads | ||
| 48 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` | ||
| 49 | with the deployed HTTPS relay URL. The relay still requires | ||
| 50 | `CLERK_SECRET_KEY` as an Alchemy secret. Never put `CLERK_SECRET_KEY` in a client application | ||
| 51 | environment or commit it to the repository. | ||
| 52 | |||
| 53 | The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference | ||
| 54 | that database and provision isolated PlanetScale branches, so deploy `prod` before creating a | ||
| 55 | personal developer stage. | ||
| 56 | |||
| 57 | ## Headless CLI OAuth Application | ||
| 58 | |||
| 59 | The `t3 cloud` commands authorize a headless environment with a separate Clerk OAuth application. | ||
| 60 | This uses an OAuth public client with PKCE, so the CLI stores no client secret. | ||
| 61 | |||
| 62 | In **Clerk Dashboard > OAuth applications**: | ||
| 63 | |||
| 64 | 1. Create an OAuth application for the T3 CLI. | ||
| 65 | 2. Enable the **Public** option so authorization-code exchange uses PKCE. | ||
| 66 | 3. Add `http://127.0.0.1:34338/callback` as an allowed redirect URI. | ||
| 67 | 4. Enable the `openid`, `profile`, and `email` scopes. | ||
| 68 | 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build | ||
| 69 | environment to the generated public client ID. | ||
| 70 | |||
| 71 | The CLI derives Clerk's frontend API URL from the publishable key and calls Clerk's | ||
| 72 | `/oauth/authorize` and `/oauth/token` endpoints directly. The relay is not involved in the OAuth | ||
| 73 | handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link. | ||
| 74 | |||
| 75 | The CLI supports these headless operations: | ||
| 76 | |||
| 77 | ```sh | ||
| 78 | t3 cloud login | ||
| 79 | t3 cloud link | ||
| 80 | t3 cloud status | ||
| 81 | t3 cloud unlink | ||
| 82 | t3 cloud logout | ||
| 83 | t3 serve | ||
| 84 | ``` | ||
| 85 | |||
| 86 | `t3 cloud login` opens the Clerk authorization flow and stores the CLI credential without enabling | ||
| 87 | cloud exposure. `t3 cloud link` installs the pinned managed `cloudflared` binary when needed, | ||
| 88 | authorizes when needed, and records durable intent to expose the environment. It works without a | ||
| 89 | running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the | ||
| 90 | managed tunnel. `t3 cloud unlink` records disabled intent immediately, stops a reachable running | ||
| 91 | connector, and attempts to revoke the relay-side environment record. It retains the stored CLI | ||
| 92 | authorization so `t3 cloud link` can re-enable exposure without another browser flow. `t3 cloud | ||
| 93 | logout` performs the same cleanup and removes the stored CLI authorization. | ||
| 94 | |||
| 95 | The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH, | ||
| 96 | forward that port before running `t3 cloud login` or `t3 cloud link`: | ||
| 97 | |||
| 98 | ```sh | ||
| 99 | ssh -L 34338:127.0.0.1:34338 <host> | ||
| 100 | ``` | ||
| 101 | |||
| 102 | A relay-hosted callback broker can remove this port-forward requirement later without changing the | ||
| 103 | stored PKCE token model. | ||
| 104 | |||
| 105 | ## JWT Template | ||
| 106 | |||
| 107 | In **Clerk Dashboard > JWT templates**, create a template with: | ||
| 108 | |||
| 109 | | Setting | Value | | ||
| 110 | | ------- | ---------------------------- | | ||
| 111 | | Name | `t3-relay` | | ||
| 112 | | Claims | `{ "aud": "t3-code-relay" }` | | ||
| 113 | |||
| 114 | Set `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 | ||
| 117 | is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still | ||
| 118 | selects the concrete relay deployment, but changing that URL does not require a JWT template change. | ||
| 119 | |||
| 120 | ## Desktop OAuth Redirect Allowlist | ||
| 121 | |||
| 122 | The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme. | ||
| 123 | In **Clerk Dashboard > Native applications**, enable native application support and add these | ||
| 124 | entries under the mobile SSO redirect allowlist: | ||
| 125 | |||
| 126 | ```text | ||
| 127 | t3code-dev://auth/callback | ||
| 128 | t3code://auth/callback | ||
| 129 | ``` | ||
| 130 | |||
| 131 | The first entry is for local desktop development. The second is for packaged desktop builds. | ||
| 132 | The app also adds a request-scoped `t3_state` query parameter and validates it on callback. Initial | ||
| 133 | sign-in and linked-account OAuth flows both return through this bridge. The desktop provider keeps | ||
| 134 | Clerk's stock profile component, replaces its renderer-page callback with the custom-scheme callback, | ||
| 135 | and opens the provider URL in the system browser. Do not add the local renderer URL as an OAuth | ||
| 136 | redirect: an external browser cannot use it to reopen the packaged app. | ||
| 137 | |||
| 138 | The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth | ||
| 139 | flow uses a custom redirect URI, add that exact URI to the same allowlist. | ||
| 140 | |||
| 141 | ## Enable Waitlist Access | ||
| 142 | |||
| 143 | For a private beta where people should request access, use **Clerk Dashboard > Waitlist**: | ||
| 144 | |||
| 145 | 1. Toggle on **Enable waitlist** and save. | ||
| 146 | 2. Review requests on the same page and select **Invite** or **Deny**. | ||
| 147 | |||
| 148 | Signed-out web and desktop users see Clerk's waitlist enrollment as the T3 Cloud page content, | ||
| 149 | while approved signed-in users see cloud settings. The browser app also uses `/settings/cloud` as | ||
| 150 | its Clerk waitlist URL. | ||
| 151 | |||
| 152 | On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the | ||
| 153 | Settings 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 | ||
| 155 | screen. | ||
| 156 | |||
| 157 | ## Alternative: Known-User Allowlist | ||
| 158 | |||
| 159 | For a closed beta where all permitted users are known in advance, use an allowlist instead of a | ||
| 160 | request-and-approval waitlist: | ||
| 161 | |||
| 162 | To restrict the beta to permitted email addresses or domains: | ||
| 163 | |||
| 164 | 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email | ||
| 165 | domain. | ||
| 166 | 2. Enable the allowlist and save. | ||
| 167 | 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or | ||
| 168 | manually created without a waitlist request flow. | ||
| 169 | |||
| 170 | Do not enable an empty allowlist: it blocks all new sign-ups. | ||
| 171 | |||
| 172 | Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud | ||
| 173 | access. To remove an already-created user's access, ban that user in Clerk so their active | ||
| 174 | sessions 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"> | ||
| 347 | flowchart LR | ||
| 348 | subgraph Clients["User-facing clients"] | ||
| 349 | Web["Desktop / hosted web UI"] | ||
| 350 | Mobile["Mobile app"] | ||
| 351 | CLI["Headless CLI"] | ||
| 352 | end | ||
| 353 | |||
| 354 | Clerk["Clerk<br/>user identity and OAuth"] | ||
| 355 | |||
| 356 | subgraph Relay["T3 Code Relay on Cloudflare"] | ||
| 357 | Worker["Relay Worker<br/>HTTP API"] | ||
| 358 | Queue["APNs delivery queue<br/>with dead-letter queue"] | ||
| 359 | Hyperdrive["Hyperdrive"] | ||
| 360 | CF["Cloudflare tunnel + DNS APIs"] | ||
| 361 | end | ||
| 362 | |||
| 363 | DB["PlanetScale Postgres<br/>prod database + stage branches"] | ||
| 364 | APNs["Apple Push Notification service"] | ||
| 365 | Traces["Axiom OTLP traces"] | ||
| 366 | |||
| 367 | subgraph Local["Linked T3 environment"] | ||
| 368 | Env["Environment server<br/>local auth authority"] | ||
| 369 | RelayClient["Relay client<br/>cloudflared implementation"] | ||
| 370 | Secrets["Server secret store<br/>keys + relay config"] | ||
| 371 | end | ||
| 372 | |||
| 373 | Endpoint["Managed HTTPS/WSS endpoint"] | ||
| 374 | |||
| 375 | Web -->|"Clerk bearer / relay DPoP"| Worker | ||
| 376 | Mobile -->|"Clerk bearer / relay DPoP"| Worker | ||
| 377 | CLI -->|"Clerk OAuth bearer"| Worker | ||
| 378 | Web --> Clerk | ||
| 379 | Mobile --> Clerk | ||
| 380 | CLI -->|"PKCE authorization code flow"| Clerk | ||
| 381 | Worker --> Hyperdrive --> DB | ||
| 382 | Worker --> Traces | ||
| 383 | Worker --> CF | ||
| 384 | Worker --> Queue --> APNs | ||
| 385 | Worker --> Endpoint | ||
| 386 | Endpoint --> RelayClient -->|"http://127.0.0.1:port"| Env | ||
| 387 | Env --> Secrets | ||
| 388 | Web -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 389 | Mobile -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 390 | Env -->|"environment bearer + signed activity proof"| Worker | ||
| 391 | </pre> | ||
| 392 | </section> | ||
| 393 | |||
| 394 | <section class="panel" aria-label="Authentication matrix"> | ||
| 395 | <h2>Authentication and Transport Matrix</h2> | ||
| 396 | <table> | ||
| 397 | <thead> | ||
| 398 | <tr> | ||
| 399 | <th>Boundary</th> | ||
| 400 | <th>Transport</th> | ||
| 401 | <th>Authentication</th> | ||
| 402 | <th>Purpose</th> | ||
| 403 | </tr> | ||
| 404 | </thead> | ||
| 405 | <tbody> | ||
| 406 | <tr> | ||
| 407 | <td>Web or mobile -> relay link management</td> | ||
| 408 | <td>HTTPS</td> | ||
| 409 | <td>Clerk session-template bearer token</td> | ||
| 410 | <td>Create a link challenge, submit an environment proof, list links, or unlink.</td> | ||
| 411 | </tr> | ||
| 412 | <tr> | ||
| 413 | <td>CLI -> relay link management</td> | ||
| 414 | <td>HTTPS</td> | ||
| 415 | <td>Clerk OAuth access token obtained with PKCE</td> | ||
| 416 | <td>Reconcile the desired headless cloud link when the server starts.</td> | ||
| 417 | </tr> | ||
| 418 | <tr> | ||
| 419 | <td>Web or mobile -> relay protected client endpoints</td> | ||
| 420 | <td>HTTPS</td> | ||
| 421 | <td>Relay-issued DPoP token plus per-request DPoP proof</td> | ||
| 422 | <td> | ||
| 423 | Check environment status, request a remote connection, and register mobile devices. | ||
| 424 | </td> | ||
| 425 | </tr> | ||
| 426 | <tr> | ||
| 427 | <td>Relay -> managed environment endpoint</td> | ||
| 428 | <td>HTTPS over the managed tunnel</td> | ||
| 429 | <td>Short-lived relay-signed JWT request proof</td> | ||
| 430 | <td> | ||
| 431 | Request a signed health response or ask the local environment to mint a credential. | ||
| 432 | </td> | ||
| 433 | </tr> | ||
| 434 | <tr> | ||
| 435 | <td>Environment -> relay activity publication</td> | ||
| 436 | <td>HTTPS</td> | ||
| 437 | <td>Relay-issued environment bearer credential plus environment-signed JWT proof</td> | ||
| 438 | <td> | ||
| 439 | Publish redacted agent-activity state for push notifications and Live Activities. | ||
| 440 | </td> | ||
| 441 | </tr> | ||
| 442 | <tr> | ||
| 443 | <td>Remote client -> environment</td> | ||
| 444 | <td>HTTPS and WSS over the managed tunnel</td> | ||
| 445 | <td>Environment-issued access token bound to the client DPoP key</td> | ||
| 446 | <td>Use the normal environment HTTP APIs and request a one-time WebSocket ticket.</td> | ||
| 447 | </tr> | ||
| 448 | </tbody> | ||
| 449 | </table> | ||
| 450 | </section> | ||
| 451 | |||
| 452 | <section class="panel" aria-label="Credential ownership"> | ||
| 453 | <h2>Credential Ownership</h2> | ||
| 454 | <table> | ||
| 455 | <thead> | ||
| 456 | <tr> | ||
| 457 | <th>Credential</th> | ||
| 458 | <th>Created by</th> | ||
| 459 | <th>Stored by</th> | ||
| 460 | <th>Use</th> | ||
| 461 | </tr> | ||
| 462 | </thead> | ||
| 463 | <tbody> | ||
| 464 | <tr> | ||
| 465 | <td>Clerk session-template JWT</td> | ||
| 466 | <td>Clerk</td> | ||
| 467 | <td>Web or mobile client</td> | ||
| 468 | <td> | ||
| 469 | Authenticate cloud-user relay requests and bootstrap relay DPoP token exchange. | ||
| 470 | </td> | ||
| 471 | </tr> | ||
| 472 | <tr> | ||
| 473 | <td>CLI OAuth access and refresh token</td> | ||
| 474 | <td>Clerk OAuth</td> | ||
| 475 | <td>Local server secret store</td> | ||
| 476 | <td>Authorize headless CLI link reconciliation.</td> | ||
| 477 | </tr> | ||
| 478 | <tr> | ||
| 479 | <td>Environment Ed25519 key pair</td> | ||
| 480 | <td>Local environment server</td> | ||
| 481 | <td>Local server secret store</td> | ||
| 482 | <td> | ||
| 483 | Sign link proofs, health responses, mint responses, and activity publication proofs. | ||
| 484 | </td> | ||
| 485 | </tr> | ||
| 486 | <tr> | ||
| 487 | <td>Relay cloud-mint key pair</td> | ||
| 488 | <td>Relay deployment</td> | ||
| 489 | <td>Private key in relay Worker config; public key installed into the environment</td> | ||
| 490 | <td>Sign relay-to-environment health and mint requests.</td> | ||
| 491 | </tr> | ||
| 492 | <tr> | ||
| 493 | <td>Relay environment bearer credential</td> | ||
| 494 | <td>Relay</td> | ||
| 495 | <td>Hashed in relay Postgres; plaintext in local server secret store</td> | ||
| 496 | <td>Authenticate environment-to-relay agent-activity publication.</td> | ||
| 497 | </tr> | ||
| 498 | <tr> | ||
| 499 | <td>Relay DPoP access token</td> | ||
| 500 | <td>Relay</td> | ||
| 501 | <td>Remote client memory cache</td> | ||
| 502 | <td> | ||
| 503 | Authorize status, connect, and mobile registration endpoints with proof of | ||
| 504 | possession. | ||
| 505 | </td> | ||
| 506 | </tr> | ||
| 507 | <tr> | ||
| 508 | <td>Environment bootstrap credential</td> | ||
| 509 | <td>Local environment server</td> | ||
| 510 | <td>Passed relay -> remote client</td> | ||
| 511 | <td>One-time exchange at the environment <code>/oauth/token</code> endpoint.</td> | ||
| 512 | </tr> | ||
| 513 | <tr> | ||
| 514 | <td>Environment access token</td> | ||
| 515 | <td>Local environment server</td> | ||
| 516 | <td>Remote client</td> | ||
| 517 | <td>Authorize environment HTTP operations and WebSocket ticket issuance.</td> | ||
| 518 | </tr> | ||
| 519 | <tr> | ||
| 520 | <td>WebSocket ticket</td> | ||
| 521 | <td>Local environment server</td> | ||
| 522 | <td>Remote client until consumed</td> | ||
| 523 | <td>Short-lived one-time credential for the WSS upgrade.</td> | ||
| 524 | </tr> | ||
| 525 | <tr> | ||
| 526 | <td>Relay client connector token</td> | ||
| 527 | <td>Cloudflare tunnel API</td> | ||
| 528 | <td>Local server secret store</td> | ||
| 529 | <td> | ||
| 530 | Start the local relay client. It is passed through <code>TUNNEL_TOKEN</code>, not a | ||
| 531 | shell argument. | ||
| 532 | </td> | ||
| 533 | </tr> | ||
| 534 | </tbody> | ||
| 535 | </table> | ||
| 536 | </section> | ||
| 537 | |||
| 538 | <section class="grid" aria-label="Core flows"> | ||
| 539 | <article class="panel"> | ||
| 540 | <h2>Flow 1: Link From the Desktop Web UI</h2> | ||
| 541 | <pre class="diagram mermaid"> | ||
| 542 | sequenceDiagram | ||
| 543 | participant U as Signed-in user | ||
| 544 | participant C as Desktop web UI | ||
| 545 | participant E as Local environment | ||
| 546 | participant R as Relay Worker | ||
| 547 | participant CF as Cloudflare APIs | ||
| 548 | participant RC as Local relay client | ||
| 549 | |||
| 550 | U->>C: Enable T3 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"> | ||
| 586 | sequenceDiagram | ||
| 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"> | ||
| 619 | sequenceDiagram | ||
| 620 | participant C as Web or mobile client | ||
| 621 | participant Clerk as Clerk | ||
| 622 | participant R as Relay Worker | ||
| 623 | participant E as Managed environment endpoint | ||
| 624 | |||
| 625 | C->>Clerk: Get Clerk session-template JWT | ||
| 626 | C->>R: POST /v1/client/dpop-token (Clerk JWT + DPoP proof) | ||
| 627 | R-->>C: Relay DPoP access token | ||
| 628 | C->>R: POST /v1/environments/:id/connect (relay DPoP) | ||
| 629 | R->>E: POST /api/t3-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"> | ||
| 650 | sequenceDiagram | ||
| 651 | participant E as Local environment | ||
| 652 | participant R as Relay Worker | ||
| 653 | participant DB as Relay Postgres | ||
| 654 | participant Q as APNs queue | ||
| 655 | participant APNs as Apple Push Notification service | ||
| 656 | participant M as Mobile app | ||
| 657 | |||
| 658 | E->>E: Project publishable thread state and redact failure detail | ||
| 659 | E->>R: POST /v1/environments/:environmentId/threads/:threadId/agent-activity | ||
| 660 | Note over E,R: Environment bearer credential + environment-signed per-state proof | ||
| 661 | R->>R: Verify credential, signature, scope, expiry, and nonce | ||
| 662 | R->>DB: Store current activity row | ||
| 663 | R->>Q: Enqueue push or Live Activity delivery jobs | ||
| 664 | Q->>APNs: Deliver signed APNs request | ||
| 665 | APNs-->>M: Push notification or Live Activity update | ||
| 666 | </pre> | ||
| 667 | <p> | ||
| 668 | The local server publishes a deliberately narrow projection. Failed runs use a fixed | ||
| 669 | redacted summary, and detail strings are capped before publication. | ||
| 670 | </p> | ||
| 671 | </article> | ||
| 672 | </section> | ||
| 673 | |||
| 674 | <section class="grid" aria-label="Managed endpoint lifecycle"> | ||
| 675 | <article class="panel half"> | ||
| 676 | <h2>Managed Endpoint Reconciliation</h2> | ||
| 677 | <p> | ||
| 678 | Managed endpoint resources are keyed by <code>(userId, environmentId)</code>. The relay | ||
| 679 | reserves a stable hostname and tunnel name before provisioning, then checkpoints the | ||
| 680 | tunnel ID, DNS record ID, and ready state in Postgres. | ||
| 681 | </p> | ||
| 682 | <div class="flow"> | ||
| 683 | <div class="step"> | ||
| 684 | <strong>1. Reserve deterministic allocation</strong> | ||
| 685 | Hostname and tunnel name derive from relay stage, cloud user, and environment ID. | ||
| 686 | </div> | ||
| 687 | <div class="step"> | ||
| 688 | <strong>2. Reuse or create tunnel</strong> | ||
| 689 | Existing named tunnels are reused. Tunnel ingress is rewritten to the validated | ||
| 690 | loopback origin. | ||
| 691 | </div> | ||
| 692 | <div class="step"> | ||
| 693 | <strong>3. Reconcile DNS</strong> | ||
| 694 | Existing CNAMEs are updated and duplicate records are removed. Create conflicts are | ||
| 695 | recovered by listing and reconciling the winning record. | ||
| 696 | </div> | ||
| 697 | <div class="step"> | ||
| 698 | <strong>4. Mark ready</strong> | ||
| 699 | Connect and status requests use only ready allocations whose hostname still matches | ||
| 700 | the relay-managed namespace. | ||
| 701 | </div> | ||
| 702 | </div> | ||
| 703 | </article> | ||
| 704 | |||
| 705 | <article class="panel half"> | ||
| 706 | <h2>Unlink Cleanup</h2> | ||
| 707 | <p> | ||
| 708 | Relay unlink removes the user's managed endpoint allocation before revoking the | ||
| 709 | user/environment link: | ||
| 710 | </p> | ||
| 711 | <ul> | ||
| 712 | <li>delete the managed DNS record if present;</li> | ||
| 713 | <li>delete the Cloudflare tunnel if present;</li> | ||
| 714 | <li>remove the allocation row;</li> | ||
| 715 | <li>revoke the user's environment link; and</li> | ||
| 716 | <li> | ||
| 717 | revoke the environment publication credential only after the final active link for | ||
| 718 | that environment key disappears. | ||
| 719 | </li> | ||
| 720 | </ul> | ||
| 721 | <p> | ||
| 722 | Local unlink stops the relay client and removes the locally persisted relay URL, issuer, | ||
| 723 | linked user, environment publication credential, cloud-mint public key, connector | ||
| 724 | config, and agent-activity preference. | ||
| 725 | </p> | ||
| 726 | </article> | ||
| 727 | </section> | ||
| 728 | |||
| 729 | <section class="panel" aria-label="Cloudflare tunnel operational profile"> | ||
| 730 | <h2>Cloudflare Tunnel Operational Profile</h2> | ||
| 731 | <p> | ||
| 732 | Managed tunnels are private application backends, not general-purpose public hosting. The | ||
| 733 | relay provisions one named Cloudflare tunnel and one DNS record per active | ||
| 734 | <code>(userId, environmentId)</code> allocation. | ||
| 735 | </p> | ||
| 736 | <ul> | ||
| 737 | <li> | ||
| 738 | <strong>Lifecycle:</strong> the tunnel is created when an environment is linked, reused | ||
| 739 | across retry-safe reconciliation, kept for the lifetime of that link, and deleted on | ||
| 740 | unlink. If teardown fails, the allocation checkpoint remains so cleanup can be retried. | ||
| 741 | </li> | ||
| 742 | <li> | ||
| 743 | <strong>Origin:</strong> tunnel ingress is restricted to the linked environment server's | ||
| 744 | validated loopback HTTP origin. Arbitrary upstream hosts and raw TCP origins are | ||
| 745 | rejected. | ||
| 746 | </li> | ||
| 747 | <li> | ||
| 748 | <strong>Protocols:</strong> managed endpoints carry HTTPS request/response traffic and | ||
| 749 | WSS connections only. Remote clients use the normal environment HTTP APIs and | ||
| 750 | authenticated WebSocket RPC transport through the tunnel. | ||
| 751 | </li> | ||
| 752 | <li> | ||
| 753 | <strong>Relay-generated traffic:</strong> the hosted relay sends sparse HTTPS health | ||
| 754 | checks and credential-mint requests during status checks and remote connection | ||
| 755 | bootstrap. It does not proxy steady-state remote sessions. | ||
| 756 | </li> | ||
| 757 | <li> | ||
| 758 | <strong>Traffic shape:</strong> expected load is interactive and user-driven. Active | ||
| 759 | remote sessions may keep WebSockets open and exchange terminal, agent, and UI updates; | ||
| 760 | inactive linked environments should carry little or no tunnel traffic. | ||
| 761 | </li> | ||
| 762 | <li> | ||
| 763 | <strong>Capacity planning:</strong> the project is pre-launch, so there is not yet a | ||
| 764 | production bandwidth baseline. Tunnel count grows with active linked user/environment | ||
| 765 | allocations, while bandwidth and concurrent WebSockets grow with active remote use. | ||
| 766 | </li> | ||
| 767 | </ul> | ||
| 768 | </section> | ||
| 769 | |||
| 770 | <section class="panel" aria-label="SSRF and tunnel hardening"> | ||
| 771 | <h2>SSRF and Tunnel Hardening</h2> | ||
| 772 | <table> | ||
| 773 | <thead> | ||
| 774 | <tr> | ||
| 775 | <th>Risk</th> | ||
| 776 | <th>Implemented control</th> | ||
| 777 | </tr> | ||
| 778 | </thead> | ||
| 779 | <tbody> | ||
| 780 | <tr> | ||
| 781 | <td>A client asks the relay to tunnel an arbitrary host.</td> | ||
| 782 | <td> | ||
| 783 | The environment and relay both accept managed tunnel origins only for loopback hosts | ||
| 784 | and valid ports. | ||
| 785 | </td> | ||
| 786 | </tr> | ||
| 787 | <tr> | ||
| 788 | <td>A forwarded request tricks local origin validation.</td> | ||
| 789 | <td> | ||
| 790 | The local link-proof handler requires an exact loopback request URL and rejects | ||
| 791 | forwarded host or protocol headers. | ||
| 792 | </td> | ||
| 793 | </tr> | ||
| 794 | <tr> | ||
| 795 | <td>A stored endpoint is replaced with an arbitrary external URL.</td> | ||
| 796 | <td> | ||
| 797 | Connect and status resolve a ready allocation from Postgres and require a hostname | ||
| 798 | under the configured managed endpoint zone. | ||
| 799 | </td> | ||
| 800 | </tr> | ||
| 801 | <tr> | ||
| 802 | <td>A managed endpoint redirects relay egress elsewhere.</td> | ||
| 803 | <td> | ||
| 804 | Relay-to-environment status and mint requests set redirect handling to | ||
| 805 | <code>manual</code>. | ||
| 806 | </td> | ||
| 807 | </tr> | ||
| 808 | <tr> | ||
| 809 | <td>A process behind the tunnel returns attacker-controlled data.</td> | ||
| 810 | <td> | ||
| 811 | The relay verifies environment-signed health and mint responses against the linked | ||
| 812 | environment public key, expected nonce, and request binding. | ||
| 813 | </td> | ||
| 814 | </tr> | ||
| 815 | <tr> | ||
| 816 | <td>A stolen remote access token is replayed.</td> | ||
| 817 | <td> | ||
| 818 | Relay and environment access tokens are DPoP-bound; per-request proofs and replay | ||
| 819 | guards are required. | ||
| 820 | </td> | ||
| 821 | </tr> | ||
| 822 | <tr> | ||
| 823 | <td>The relay client token leaks through process listings or shell parsing.</td> | ||
| 824 | <td> | ||
| 825 | The relay client is spawned without a shell and receives its connector token in | ||
| 826 | <code>TUNNEL_TOKEN</code>. | ||
| 827 | </td> | ||
| 828 | </tr> | ||
| 829 | </tbody> | ||
| 830 | </table> | ||
| 831 | <div class="callout warning"> | ||
| 832 | Managed tunnel hostnames live under the configured tunnel DNS zone. Serving them from a | ||
| 833 | dedicated registrable domain with a Public Suffix List entry remains an operational | ||
| 834 | isolation requirement before broad untrusted use. | ||
| 835 | </div> | ||
| 836 | </section> | ||
| 837 | |||
| 838 | <section class="panel" aria-label="Relay HTTP API"> | ||
| 839 | <h2>Relay HTTP API</h2> | ||
| 840 | <p> | ||
| 841 | The relay publishes an OpenAPI document at <code>/openapi.json</code>, interactive API | ||
| 842 | docs at <code>/docs</code>, and redirects <code>/</code> to the docs. | ||
| 843 | </p> | ||
| 844 | <table> | ||
| 845 | <thead> | ||
| 846 | <tr> | ||
| 847 | <th>Endpoint</th> | ||
| 848 | <th>Authentication</th> | ||
| 849 | <th>Purpose</th> | ||
| 850 | </tr> | ||
| 851 | </thead> | ||
| 852 | <tbody> | ||
| 853 | <tr> | ||
| 854 | <td><code>GET /health</code></td> | ||
| 855 | <td>None</td> | ||
| 856 | <td>Relay health check.</td> | ||
| 857 | </tr> | ||
| 858 | <tr> | ||
| 859 | <td><code>GET /.well-known/oauth-authorization-server</code></td> | ||
| 860 | <td>None</td> | ||
| 861 | <td>Relay OAuth token-exchange discovery metadata.</td> | ||
| 862 | </tr> | ||
| 863 | <tr> | ||
| 864 | <td><code>GET /.well-known/oauth-protected-resource</code></td> | ||
| 865 | <td>None</td> | ||
| 866 | <td>Relay DPoP protected-resource discovery metadata.</td> | ||
| 867 | </tr> | ||
| 868 | <tr> | ||
| 869 | <td><code>GET /v1/environments</code></td> | ||
| 870 | <td>Clerk bearer</td> | ||
| 871 | <td>List environments linked to the signed-in user.</td> | ||
| 872 | </tr> | ||
| 873 | <tr> | ||
| 874 | <td><code>POST /v1/client/environment-link-challenges</code></td> | ||
| 875 | <td>Clerk bearer</td> | ||
| 876 | <td>Create a short-lived user-bound environment link challenge.</td> | ||
| 877 | </tr> | ||
| 878 | <tr> | ||
| 879 | <td><code>POST /v1/client/environment-links</code></td> | ||
| 880 | <td>Clerk bearer</td> | ||
| 881 | <td> | ||
| 882 | Verify a local environment proof, reconcile a managed endpoint, and upsert the user | ||
| 883 | link. | ||
| 884 | </td> | ||
| 885 | </tr> | ||
| 886 | <tr> | ||
| 887 | <td><code>DELETE /v1/client/environment-links/:environmentId</code></td> | ||
| 888 | <td>Clerk bearer</td> | ||
| 889 | <td>Remove managed endpoint resources and revoke the user's link.</td> | ||
| 890 | </tr> | ||
| 891 | <tr> | ||
| 892 | <td><code>POST /v1/client/dpop-token</code></td> | ||
| 893 | <td>Clerk token in token-exchange payload plus DPoP proof</td> | ||
| 894 | <td>Issue a relay DPoP access token for the requested supported scopes.</td> | ||
| 895 | </tr> | ||
| 896 | <tr> | ||
| 897 | <td><code>POST /v1/environments/:environmentId/status</code></td> | ||
| 898 | <td>Relay DPoP</td> | ||
| 899 | <td>Request and validate a signed environment health response.</td> | ||
| 900 | </tr> | ||
| 901 | <tr> | ||
| 902 | <td><code>POST /v1/environments/:environmentId/connect</code></td> | ||
| 903 | <td>Relay DPoP</td> | ||
| 904 | <td>Request and validate an environment bootstrap credential.</td> | ||
| 905 | </tr> | ||
| 906 | <tr> | ||
| 907 | <td><code>POST /v1/mobile/devices</code></td> | ||
| 908 | <td>Relay DPoP</td> | ||
| 909 | <td>Register or update a mobile device for notifications.</td> | ||
| 910 | </tr> | ||
| 911 | <tr> | ||
| 912 | <td><code>POST /v1/mobile/live-activities</code></td> | ||
| 913 | <td>Relay DPoP</td> | ||
| 914 | <td>Register a Live Activity push token.</td> | ||
| 915 | </tr> | ||
| 916 | <tr> | ||
| 917 | <td><code>DELETE /v1/mobile/devices/:deviceId</code></td> | ||
| 918 | <td>Relay DPoP</td> | ||
| 919 | <td>Unregister a mobile device.</td> | ||
| 920 | </tr> | ||
| 921 | <tr> | ||
| 922 | <td> | ||
| 923 | <code>POST /v1/environments/:environmentId/threads/:threadId/agent-activity</code> | ||
| 924 | </td> | ||
| 925 | <td>Environment bearer plus signed publish proof</td> | ||
| 926 | <td>Publish redacted current agent activity for notification delivery.</td> | ||
| 927 | </tr> | ||
| 928 | </tbody> | ||
| 929 | </table> | ||
| 930 | </section> | ||
| 931 | |||
| 932 | <section class="panel" aria-label="Environment HTTP API"> | ||
| 933 | <h2>Environment Cloud HTTP API</h2> | ||
| 934 | <table> | ||
| 935 | <thead> | ||
| 936 | <tr> | ||
| 937 | <th>Endpoint</th> | ||
| 938 | <th>Authentication</th> | ||
| 939 | <th>Purpose</th> | ||
| 940 | </tr> | ||
| 941 | </thead> | ||
| 942 | <tbody> | ||
| 943 | <tr> | ||
| 944 | <td><code>POST /api/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 | |||
| 3 | T3 Code reads keybindings from: | ||
| 4 | |||
| 5 | - `~/.t3/keybindings.json` | ||
| 6 | |||
| 7 | The 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 | |||
| 16 | See 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 | |||
| 34 | For 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 | |||
| 40 | Each 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 | |||
| 46 | Invalid 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 | |||
| 62 | Supported modifiers: | ||
| 63 | |||
| 64 | - `mod` (`cmd` on macOS, `ctrl` on non-macOS) | ||
| 65 | - `cmd` / `meta` | ||
| 66 | - `ctrl` / `control` | ||
| 67 | - `shift` | ||
| 68 | - `alt` / `option` | ||
| 69 | |||
| 70 | Examples: | ||
| 71 | |||
| 72 | - `mod+j` | ||
| 73 | - `mod+shift+d` | ||
| 74 | - `ctrl+l` | ||
| 75 | - `cmd+k` | ||
| 76 | |||
| 77 | ### `when` Conditions | ||
| 78 | |||
| 79 | Currently available context keys: | ||
| 80 | |||
| 81 | - `terminalFocus` | ||
| 82 | - `terminalOpen` | ||
| 83 | |||
| 84 | Supported operators: | ||
| 85 | |||
| 86 | - `!` (not) | ||
| 87 | - `&&` (and) | ||
| 88 | - `||` (or) | ||
| 89 | - parentheses: `(` `)` | ||
| 90 | |||
| 91 | Examples: | ||
| 92 | |||
| 93 | - `"when": "terminalFocus"` | ||
| 94 | - `"when": "terminalOpen && !terminalFocus"` | ||
| 95 | - `"when": "terminalFocus || terminalOpen"` | ||
| 96 | |||
| 97 | Unknown 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 | |||
| 3 | Use 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 | |||
| 7 | Use a trusted private network that meshes your devices together, such as a tailnet. | ||
| 8 | |||
| 9 | That 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 | |||
| 17 | There 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 | |||
| 21 | If you are already running the desktop app and want to make it reachable from other devices: | ||
| 22 | |||
| 23 | 1. Open **Settings** → **Connections**. | ||
| 24 | 2. Under **Manage Local Backend**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. | ||
| 25 | 3. The settings panel will show the default reachable endpoint, with a `+N` control when more endpoints are available. Expand it to inspect alternatives such as loopback, LAN, private-network, or HTTPS endpoints. | ||
| 26 | 4. Use **Create Link** to generate a pairing link you can share with another device. | ||
| 27 | |||
| 28 | The 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 | |||
| 30 | When no user default is saved, the app uses the built-in LAN endpoint for pairing links when | ||
| 31 | available. 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 | |||
| 38 | If 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 | |||
| 42 | When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list. | ||
| 43 | |||
| 44 | Depending 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 | |||
| 50 | The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as | ||
| 51 | `https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this | ||
| 52 | backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend | ||
| 53 | with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale | ||
| 54 | Serve to proxy HTTPS traffic to the local backend. | ||
| 55 | |||
| 56 | The Tailscale support is an endpoint provider add-on. The core remote model still works without Tailscale: LAN HTTP endpoints, custom HTTPS endpoints, future tunnels, and SSH-launched environments all use the same saved environment and pairing flow. | ||
| 57 | |||
| 58 | For `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 | |||
| 62 | Use this when you want to run the server without a GUI, for example on a remote machine over SSH. | ||
| 63 | |||
| 64 | Run the server with `t3 serve`. | ||
| 65 | |||
| 66 | ```bash | ||
| 67 | npx 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 | |||
| 77 | From 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 | |||
| 84 | Use `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 | |||
| 86 | For hosted web pairing over Tailscale HTTPS, opt in to Tailscale Serve: | ||
| 87 | |||
| 88 | ```bash | ||
| 89 | npx t3 serve --tailscale-serve | ||
| 90 | ``` | ||
| 91 | |||
| 92 | By 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 | ||
| 96 | npx 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 | |||
| 106 | Use this when you want the desktop app to start or reuse T3 Code on another machine over SSH. | ||
| 107 | |||
| 108 | 1. Open **Settings** → **Connections**. | ||
| 109 | 2. Under **Remote Environments**, choose **Add environment**. | ||
| 110 | 3. Select the SSH launch flow. | ||
| 111 | 4. Enter the SSH target, such as `user@example.com`. | ||
| 112 | 5. 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 | |||
| 114 | After 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 | |||
| 116 | SSH 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 | |||
| 120 | The 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 | |||
| 122 | The 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 | |||
| 128 | During SSH launch, T3 Code first checks whether `node` is already available on `PATH`. If it is missing, the launcher tries common non-interactive shell locations and version-manager shims/activation hooks: | ||
| 129 | |||
| 130 | - `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` | ||
| 131 | - Volta via `~/.volta/bin` | ||
| 132 | - asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh` | ||
| 133 | - mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh` | ||
| 134 | - fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh` | ||
| 135 | - nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -` | ||
| 136 | - nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts` | ||
| 137 | - installed nvm versions under `$NVM_DIR/versions/node/*/bin` | ||
| 138 | |||
| 139 | If launch fails with `node: command not found`, a port-scan failure, or a message that the remote Node version does not satisfy the required range, SSH into the host and check the same non-interactive shell path T3 Code uses: | ||
| 140 | |||
| 141 | ```bash | ||
| 142 | ssh user@example.com 'sh -lc "command -v node && node --version"' | ||
| 143 | ``` | ||
| 144 | |||
| 145 | If 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 | ||
| 148 | nvm alias default 24 | ||
| 149 | ``` | ||
| 150 | |||
| 151 | With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above. | ||
| 152 | |||
| 153 | If reconnecting after an app update fails, retry the SSH launch once. The launcher now compares its generated runner script, stops stale launcher-managed remote servers, clears the SSH launch PID/port state, and starts a fresh remote server. You should not normally need to delete `~/.t3/ssh-launch` or kill `t3` processes manually. | ||
| 154 | |||
| 155 | ## How Pairing Works | ||
| 156 | |||
| 157 | The remote device does not need a long-lived secret up front. | ||
| 158 | |||
| 159 | Instead: | ||
| 160 | |||
| 161 | 1. `t3 serve` issues a one-time owner pairing token. | ||
| 162 | 2. The remote device exchanges that token with the server. | ||
| 163 | 3. The server creates an authenticated session for that device. | ||
| 164 | |||
| 165 | After 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 | |||
| 169 | The hosted web app at `https://app.t3.codes` can save a remote backend in browser local storage from a URL like: | ||
| 170 | |||
| 171 | ```text | ||
| 172 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | ||
| 173 | ``` | ||
| 174 | |||
| 175 | Use 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 | |||
| 177 | Do 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 | |||
| 179 | Hosted 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 | |||
| 183 | Use `t3 auth` to manage access after the initial pairing flow. | ||
| 184 | |||
| 185 | Typical uses: | ||
| 186 | |||
| 187 | - issue additional pairing credentials | ||
| 188 | - inspect active sessions | ||
| 189 | - revoke old pairing links or sessions | ||
| 190 | |||
| 191 | Use `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. | ||