| author | |
| committer | |
| log | 2e688a53cf8fc7a9caa9fd5070148bfcd3d5a741 |
| tree | 775a2b1d93b443074961842f71905a14d0f29321 |
| parent | 13427ecd8ba5bfa7892fc90d1e3f0ef59514870f |
| signature | Signed by PGP key B5690EEEBB952194 |
31 files changed, 1104 insertions(+), 4469 deletions(-)
AGENTS.md+13-2| ... | @@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste | ... | @@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste |
| 72 | - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow. | 72 | - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow. |
| 73 | - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug. | 73 | - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug. |
| 74 | - **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real. | 74 | - **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real. |
| 75 | - **Docs.** `docs/` splits by audience. Behavior changes that a user would notice belong in `docs/user/` (shipped-product voice, no repo tooling or source paths); architecture and contributor changes in `docs/internals/`; runbooks in `docs/operations/`; new vocabulary in `docs/internals/glossary.md`. | 75 | - **Docs.** Check whether the change makes existing guidance inaccurate. Apply the [documentation rules](#documentation) before adding anything. |
| 76 | 76 | ||
| 77 | ## Dev servers | 77 | ## Dev servers |
| 78 | 78 | ||
| ... | @@ -120,11 +120,22 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real | ... | @@ -120,11 +120,22 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real |
| 120 | - One concern per PR. If the description says "also", split it. | 120 | - One concern per PR. If the description says "also", split it. |
| 121 | - When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit. | 121 | - When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit. |
| 122 | 122 | ||
| 123 | ## Documentation | ||
| 124 | |||
| 125 | Most code changes do not need an internal documentation change. Agents can read the code. | ||
| 126 | |||
| 127 | - `docs/internals/` is for architectural decisions and their reasons, constraints that span components, and implementation traps that are hard to discover from the source. Before adding a paragraph, ask what a maintainer would get wrong without it. If reading the relevant code answers the question, leave it out. | ||
| 128 | - Do not document every feature, enumerate fields or methods, narrate control flow, maintain file catalogs, or append PR summaries. Types, tests, and code already record the implementation. The glossary defines shared vocabulary; it is not a feature index. | ||
| 129 | - Keep a local implementation explanation in a nearby code comment. Use an internal doc when the reasoning crosses boundaries or needs context the code cannot carry well. Link to the relevant source instead of copying it. | ||
| 130 | - When a documented decision or constraint changes, rewrite or remove the affected text. Do not append another account of the new behavior. A new internal page needs a distinct, durable reason to exist. | ||
| 131 | - `docs/user/` helps users accomplish tasks. Give each major feature a concise section explaining what it does, how to start, and anything unintuitive. A settings path is useful; descriptions of visible buttons, icons, layouts, animations, or every UI state are not. Before adding text, ask what task or decision it helps the user with. | ||
| 132 | - Keep user docs in the shipped product's voice, without implementation details or contributor tooling. Update the relevant feature section when how to use it changes. A UI tweak does not need a documentation entry, and a new control does not need its own page. | ||
| 133 | - `docs/operations/` holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides. | ||
| 134 | |||
| 123 | ## Plans and work artifacts | 135 | ## Plans and work artifacts |
| 124 | 136 | ||
| 125 | - Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. `.plans/` is gitignored only as a safety net for legacy tooling. | 137 | - Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. `.plans/` is gitignored only as a safety net for legacy tooling. |
| 126 | - Track active maintainer work in the GitHub issue or project item that owns it. External proposals follow `CONTRIBUTING.md` and belong in Ideas discussions. | 138 | - Track active maintainer work in the GitHub issue or project item that owns it. External proposals follow `CONTRIBUTING.md` and belong in Ideas discussions. |
| 127 | - Put durable architecture, constraints, and decisions in `docs/internals/`. Update those docs when the product changes so agents find current facts instead of abandoned intentions. | ||
| 128 | - A merged PR is the implementation record. Close or update its tracking item when the work lands; do not preserve a second checklist in the repository. | 139 | - A merged PR is the implementation record. Close or update its tracking item when the work lands; do not preserve a second checklist in the repository. |
| 129 | 140 | ||
| 130 | ## How it works | 141 | ## How it works |
CONTRIBUTING.md+5-1| ... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | ||
| 3 | ## Developer Setup | 3 | ## Developer Setup |
| 4 | 4 | ||
| 5 | See the [maintainer scripts guide](docs/internals/scripts.md#first-checkout) for the initial checkout, | 5 | See the [development runbook](docs/operations/development.md#first-checkout) for the initial checkout, |
| 6 | development commands, tests, and platform-specific desktop packaging prerequisites. | 6 | development commands, tests, and platform-specific desktop packaging prerequisites. |
| 7 | 7 | ||
| 8 | ## Read This First | 8 | ## Read This First |
| ... | @@ -49,6 +49,10 @@ Explain exactly what changed. | ... | @@ -49,6 +49,10 @@ Explain exactly what changed. |
| 49 | 49 | ||
| 50 | Explain exactly why the change should exist. | 50 | Explain exactly why the change should exist. |
| 51 | 51 | ||
| 52 | Follow the [documentation rules](AGENTS.md#documentation). Keep internal docs for decisions and | ||
| 53 | hard-to-discover constraints. Update user guides when how to use a feature changes; skip descriptions | ||
| 54 | of obvious controls and cosmetic changes. | ||
| 55 | |||
| 52 | Do not mix unrelated fixes together. | 56 | Do not mix unrelated fixes together. |
| 53 | 57 | ||
| 54 | If the PR makes anything resembling a UI change, include clear before/after images. | 58 | If the PR makes anything resembling a UI change, include clear before/after images. |
apps/mobile/README.md+6-6| ... | @@ -38,12 +38,8 @@ vp run dev:client:reset | ... | @@ -38,12 +38,8 @@ vp run dev:client:reset |
| 38 | Run that reset once after installing or changing the Uniwind dependency patch. Cached transforms | 38 | Run that reset once after installing or changing the Uniwind dependency patch. Cached transforms |
| 39 | can otherwise reference its previous pnpm package path. Ordinary Metro starts still keep the cache. | 39 | can otherwise reference its previous pnpm package path. Ordinary Metro starts still keep the cache. |
| 40 | 40 | ||
| 41 | Component edits use Fast Refresh. Connection-runtime edits replace the active Effect layer through | 41 | Component edits use Fast Refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md) |
| 42 | a stable atom runtime, preserving navigation and existing atom subscribers. Replaced registries | 42 | before changing runtime ownership or refresh behavior. |
| 43 | and managed runtimes dispose their resources; the app does not force a JavaScript reload. The Uniwind patch | ||
| 44 | skips global style invalidation when generated styles and themes are unchanged, while real style | ||
| 45 | changes still refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md) | ||
| 46 | for the lifetime boundaries. | ||
| 47 | 43 | ||
| 48 | Build and run the local iOS dev client: | 44 | Build and run the local iOS dev client: |
| 49 | 45 | ||
| ... | @@ -51,6 +47,10 @@ Build and run the local iOS dev client: | ... | @@ -51,6 +47,10 @@ Build and run the local iOS dev client: |
| 51 | vp run ios:dev | 47 | vp run ios:dev |
| 52 | ``` | 48 | ``` |
| 53 | 49 | ||
| 50 | After changing a native dependency patch, rerun CocoaPods before rebuilding an existing iOS | ||
| 51 | project. pnpm gives each patch hash a new package path; Pods can otherwise keep compiling the | ||
| 52 | previous directory. | ||
| 53 | |||
| 54 | If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the | 54 | If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the |
| 55 | reduced-capability local build. Personal Team builds omit the widget and share extensions, push | 55 | reduced-capability local build. Personal Team builds omit the widget and share extensions, push |
| 56 | entitlement, and native Sign in with Apple entitlement; builds without this opt-in are unchanged. | 56 | entitlement, and native Sign in with Apple entitlement; builds without this opt-in are unchanged. |
docs/README.md+14-7| ... | @@ -23,27 +23,34 @@ Mobile app: [apps/mobile/README.md](../apps/mobile/README.md) | ... | @@ -23,27 +23,34 @@ Mobile app: [apps/mobile/README.md](../apps/mobile/README.md) |
| 23 | 23 | ||
| 24 | ## Working on T3 Code | 24 | ## Working on T3 Code |
| 25 | 25 | ||
| 26 | Everything below is for maintainers. Setup lives in the [root README](../README.md); | 26 | Start with the [development runbook](./operations/development.md) and |
| 27 | policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../AGENTS.md). | 27 | [contribution policy](../CONTRIBUTING.md). |
| 28 | |||
| 29 | Internal notes preserve architectural decisions, constraints, and implementation traps that the | ||
| 30 | source alone does not explain. Most code changes do not need an internal documentation update. Follow the | ||
| 31 | [documentation rules](../AGENTS.md#documentation) before adding one. | ||
| 28 | 32 | ||
| 29 | - [Architecture overview](./internals/overview.md) | 33 | - [Architecture overview](./internals/overview.md) |
| 30 | - [Workspace layout](./internals/workspace-layout.md) | ||
| 31 | - [Glossary](./internals/glossary.md) | 34 | - [Glossary](./internals/glossary.md) |
| 32 | - [Scripts](./internals/scripts.md) | ||
| 33 | - [Connection runtime](./internals/connection-runtime.md) | 35 | - [Connection runtime](./internals/connection-runtime.md) |
| 34 | - [Voice input](./internals/voice-input.md) | ||
| 35 | - [Providers](./internals/providers.md) | 36 | - [Providers](./internals/providers.md) |
| 37 | - [Model classification](./internals/model-manifest.md) | ||
| 36 | - [Remote environments](./internals/remote.md) | 38 | - [Remote environments](./internals/remote.md) |
| 37 | - [Server updates](./internals/server-updates.md) | 39 | - [Server updates](./internals/server-updates.md) |
| 38 | - [Resource telemetry](./internals/resource-telemetry.md) | 40 | - [Resource telemetry](./internals/resource-telemetry.md) |
| 39 | - [Product analytics](./internals/product-analytics.md) | 41 | - [Product analytics](./internals/product-analytics.md) |
| 40 | - [Environment auth](./internals/environment-auth.md) | 42 | - [Environment auth](./internals/environment-auth.md) |
| 41 | - [T3 Connect](./internals/t3-connect.md) | 43 | - [T3 Connect](./internals/t3-connect.md) |
| 42 | - [CI gates](./internals/ci.md) | 44 | - [Assistant citations](./internals/assistant-citations.md) |
| 43 | - [Engineering work artifacts](./internals/work-artifacts.md) | 45 | - [Mobile navigation](./internals/mobile-navigation.md) |
| 46 | - [Mobile development lifecycle](./internals/mobile-development.md) | ||
| 47 | - [Terminal runtime](./internals/terminal-runtime.md) | ||
| 48 | - [Voice input](./internals/voice-input.md) | ||
| 44 | 49 | ||
| 45 | ### Runbooks | 50 | ### Runbooks |
| 46 | 51 | ||
| 52 | - [Development and local builds](./operations/development.md) | ||
| 53 | - [T3 Connect setup](./operations/connect-setup.md) | ||
| 47 | - [Release](./operations/release.md) | 54 | - [Release](./operations/release.md) |
| 48 | - [Observability](./operations/observability.md) | 55 | - [Observability](./operations/observability.md) |
| 49 | - [Relay observability](./operations/relay-observability.md) | 56 | - [Relay observability](./operations/relay-observability.md) |
docs/architecture/terminal-renderers.md deleted-42| ... | @@ -1,42 +0,0 @@ | ||
| 1 | # Terminal renderers | ||
| 2 | |||
| 3 | Terminal sessions remain server-owned PTYs. Clients receive the existing raw byte stream and send | ||
| 4 | input and resize events over the existing terminal contracts; renderer choices never cross the | ||
| 5 | wire. | ||
| 6 | |||
| 7 | ## Ghostty alignment | ||
| 8 | |||
| 9 | Android and web use the official `libghostty-vt` C ABI for parsing, terminal state, grapheme | ||
| 10 | boundaries, keyboard encoding, selection, and scrollback: | ||
| 11 | |||
| 12 | - Android links the native shared library and converts render state into a compact JNI snapshot. | ||
| 13 | - Web loads a separately cached WebAssembly build and reads render state into a Canvas 2D surface. | ||
| 14 | - Both artifacts are built from the revision in | ||
| 15 | `native/libghostty-vt/VERSION`. | ||
| 16 | |||
| 17 | The platform adapters deliberately own only platform behavior. Android owns its Kotlin Canvas and | ||
| 18 | touch integration. Web owns browser font shaping, the hidden IME textarea, clipboard and DOM input, | ||
| 19 | and its Canvas renderer. The web adapter also delegates application mouse encoding, word and line | ||
| 20 | selection, and OSC 8 hyperlink metadata to the official ABI. Browser conventions remain available: | ||
| 21 | holding Shift bypasses application mouse capture, and the platform link modifier opens hyperlinks. | ||
| 22 | React does not participate in terminal frames. | ||
| 23 | |||
| 24 | The web runtime is singleton-scoped per browser tab so split terminals share one compiled module | ||
| 25 | and memory. Each visible terminal owns and frees its own terminal, render state, row iterator, cell | ||
| 26 | iterator, key and mouse encoder, and input event handles. Restoring captured scrollback temporarily | ||
| 27 | detaches the PTY callback so historical device queries cannot emit replies into the current shell. | ||
| 28 | |||
| 29 | ## Updating Ghostty | ||
| 30 | |||
| 31 | Update and rebuild Android first, because mobile's `VERSION` file is the single source of truth for | ||
| 32 | the upstream pin (the upstream `LICENSE` lives beside it). Then run: | ||
| 33 | |||
| 34 | ```sh | ||
| 35 | pnpm --dir apps/web build:ghostty-wasm | ||
| 36 | ``` | ||
| 37 | |||
| 38 | Commit the regenerated web `wasm` artifacts. The build embeds the pinned revision into the binary as | ||
| 39 | semver build metadata, and the focused web ABI test reads it back through `ghostty_build_info` and | ||
| 40 | compares it against mobile's `VERSION` — so the web vendor directory holds only the artifacts, drift | ||
| 41 | cannot hide, and there is no second pin to keep in sync. The same test enforces the artifact budget | ||
| 42 | and exercises repeated create/write/free cycles with multi-codepoint graphemes. | ||
docs/internals/assistant-citations.md+27-133| ... | @@ -1,135 +1,29 @@ | ... | @@ -1,135 +1,29 @@ |
| 1 | # Assistant citations | 1 | # Assistant citations |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [Quote an assistant response](../user/composer.md#quote-an-assistant-response). | 3 | Citations carry the selected excerpt and source identity in ordinary message text. |
| 4 | 4 | Drafts, clipboard copies, stashes, and sent messages keep that representation | |
| 5 | Assistant citations store a selected excerpt and its source in ordinary message text. They need | 5 | without a separate citation store. The saved quote remains usable when its source |
| 6 | no database migration, citation table, or sidecar draft metadata. The saved excerpt remains | 6 | disappears or changes. The [shared format](../../packages/shared/src/assistantCitations.ts) uses |
| 7 | available even when the source response is missing or has changed. | 7 | stable environment IDs without a browser origin, so moving between local, remote, |
| 8 | 8 | and tunnel connections does not change a citation's identity. | |
| 9 | ## Serialized form | 9 | |
| 10 | 10 | Source navigation is best effort. [Text selectors](../../apps/web/src/lib/assistantTextSelection.ts) | |
| 11 | [`AssistantCitation`][contract] defines version 1. [`assistantCitations.ts`][shared] serializes it | 11 | refer to rendered text after whitespace normalization, measured in UTF-16 units. |
| 12 | as a Markdown link with the literal label `Assistant quote`: | 12 | They cannot be applied to raw Markdown. The original excerpt keeps its whitespace; |
| 13 | 13 | normalization is only for locating it. Repeated text needs an unambiguous context | |
| 14 | ```text | 14 | match, even if one occurrence still sits at the saved offsets. Guessing could |
| 15 | [Assistant quote](t3-citation://v1/<environmentId>/<threadId>/<messageId>?text=<quote>&start=<start>&end=<end>&prefix=<prefix>&suffix=<suffix>&comment=<optional-user-comment>) | 15 | highlight the wrong claim. |
| 16 | ``` | 16 | |
| 17 | 17 | Navigation owns the highlight's lifetime across virtual-row remounts. The source | |
| 18 | The formatter percent-encodes each path segment and uses `URLSearchParams` for the query. The | 18 | row must be fetched, unfolded, mounted, and measured before quote positioning |
| 19 | environment, thread, and message IDs identify the source without a browser origin, so local, | 19 | takes over from normal timeline scrolling. Remounting must not revive a dismissed |
| 20 | remote, and tunnel clients share one form. Opening the source still requires access to its | 20 | highlight. The [navigation hash](../../apps/web/src/lib/assistantCitationNavigation.ts) |
| 21 | environment. | 21 | uses base64url because router normalization otherwise decodes whitespace and |
| 22 | 22 | source IDs inside the citation URI. | |
| 23 | The parser accepts only version 1 with exactly three source IDs and one of each selector field; | 23 | |
| 24 | `comment` is optional and links without one keep their original format. It rejects credentials, | 24 | [ProviderService](../../apps/server/src/provider/Layers/ProviderService.ts) expands |
| 25 | ports, malformed encoding, and extra or duplicate query fields. IDs have a 512-character limit, | 25 | citations before dispatch to any provider adapter. It sends the saved excerpt |
| 26 | the quote and comment each fit in 8,000 UTF-16 code units, offsets are nonnegative safe integers | 26 | without looking up the source, distinguishes quoted reference material from the |
| 27 | with `end > start`, prefix and suffix each fit in 32 code units, and the URI itself is capped at | 27 | user's comment, and leaves persisted messages in their original form. Input |
| 28 | 160,000 characters. Malformed or unsupported links remain ordinary prompt text everywhere: | 28 | limits apply after expansion as well as before it. A draft that fits as encoded |
| 29 | collectors, provider expansion, and readable fallbacks all skip them. Parsing validates the | 29 | links can exceed the provider limit once expanded. |
| 30 | payload, not the existence of its source. | ||
| 31 | |||
| 32 | ## Selection and source matching | ||
| 33 | |||
| 34 | [`assistantTextSelection.ts`][selection] captures one native DOM range inside one assistant | ||
| 35 | response, including backward and paragraph selections. Empty range endpoints at neighboring blocks | ||
| 36 | are narrowed to the first and last selected text nodes before validating the source, so | ||
| 37 | triple-click selections that end at the next block still resolve. The text stream follows DOM | ||
| 38 | order, inserts line breaks between blocks and at `<br>`, and includes displayed code and link | ||
| 39 | text. Controls, hidden subtrees, CSS-generated content, and soft wrapping do not contribute. | ||
| 40 | |||
| 41 | `text` keeps the exact selected excerpt, including line breaks and indentation. `start`, `end`, | ||
| 42 | `prefix`, and `suffix` use the rendered-text stream after each JavaScript whitespace run collapses | ||
| 43 | to one space, without trimming, in UTF-16 code units. Context boundaries avoid splitting | ||
| 44 | surrogate pairs. | ||
| 45 | |||
| 46 | Resolution uses the same normalization and case-sensitive matching. It checks the saved offsets | ||
| 47 | against the quote and context, then searches for a context-matching occurrence if positions have | ||
| 48 | moved. A unique quote can still match after its context changes. Multiple context-matching | ||
| 49 | occurrences are ambiguous, even if one remains at the saved offsets; the resolver does not guess. | ||
| 50 | |||
| 51 | ## Composer and clients | ||
| 52 | |||
| 53 | [`AssistantSelectionToolbar`][toolbar] offers **Cite in composer** for a selection within one | ||
| 54 | response and disables it above the quote limit. It shares [`selectionActions.ts`][actions] with | ||
| 55 | the terminal's selection menu: the observer opens actions only after a primary-button release from | ||
| 56 | a gesture that started inside the surface, waits out the 500 ms repeat-click window so | ||
| 57 | double/triple clicks can finish, anchors to the release point (or the text end for keyboard | ||
| 58 | selections), and dismisses on outside interaction, scrolling, or `Escape`. The toolbar registers | ||
| 59 | its own element with the observer so interacting with it cannot replace the captured quote. | ||
| 60 | |||
| 61 | `ChatView` inserts the serialized link at the last composer cursor. A one-shot editor request then | ||
| 62 | opens the comment editor beside the cited source text, carrying the live DOM range for positioning | ||
| 63 | and an editing highlight; that range is transient and never becomes part of the citation. | ||
| 64 | `withAssistantCitationComment` trims outer whitespace and removes the field when blank; it never | ||
| 65 | changes the saved quote or selector. Restoring or pasting a citation does not reopen the editor. | ||
| 66 | |||
| 67 | [`ComposerCitationNode`][node] is an inline Lexical `DecoratorNode` holding the quote and comment | ||
| 68 | as one atomic chip. Its label shows the comment when present, otherwise a short quote preview; the | ||
| 69 | node retains the full serialized source and returns it from `getTextContent()`. Comment edits | ||
| 70 | update the citation and serialized source together as one undoable change. The token parser, | ||
| 71 | clipboard handling, and cursor mapping treat citations as inline tokens alongside mentions and | ||
| 72 | skills, so drafts, stashes, and pastes reconstruct chips from the prompt string alone. Sent | ||
| 73 | messages keep the same links; `ChatMarkdown` renders valid citation URIs as read-only | ||
| 74 | [`AssistantCitationChip`][chip]s, and the whole chip supplies its serialized value to Markdown | ||
| 75 | copy so the displayed label is not duplicated as text. | ||
| 76 | |||
| 77 | ## Source navigation | ||
| 78 | |||
| 79 | [`assistantCitationNavigation.ts`][navigation] routes to the source thread with | ||
| 80 | `#assistant-citation=<base64url-encoded citation URI>`; the encoding keeps router hash | ||
| 81 | normalization from decoding quote whitespace or source IDs. Each click adds a fresh activation ID | ||
| 82 | to navigation state so the same link can reveal its source again after dismissal. | ||
| 83 | |||
| 84 | [`useAssistantCitationTarget`][target] loads earlier history (bounded at 20 pages or a repeated | ||
| 85 | cursor, with a warning past that), expands a folded turn, and asks the virtual list to mount the | ||
| 86 | source row. While positioning, the timeline disables initial end-scroll and scroll maintenance and | ||
| 87 | keeps the cited row mounted until measured; the activation ID doubles as the list's data version | ||
| 88 | so pinning rows recalculates a stationary Legend list without discarding measurements. | ||
| 89 | |||
| 90 | [`AssistantCitationSource`][source] resolves the DOM range, smoothly scrolls to it, and pulses | ||
| 91 | only that text through the CSS Highlight API: two 650 ms pulses driven by a registered opacity | ||
| 92 | property, with the second settling into a held highlight before fading out. Navigation owns the | ||
| 93 | highlight's start time and completion state: range repairs keep the original clock, remounting | ||
| 94 | resumes only the remaining time, and an expired or dismissed highlight cannot revive. Reduced | ||
| 95 | motion scrolls instantly and holds the highlight without pulsing. Browsers | ||
| 96 | without the Highlight API temporarily select the range and remove only that owned selection. | ||
| 97 | User navigation or `Escape` cancels pending positioning; missing messages or unmatched text | ||
| 98 | produce warnings without changing the saved quote. The separate comment-editing highlight uses | ||
| 99 | the same range-repair observer while its editor is open. | ||
| 100 | |||
| 101 | Desktop shares the web implementation. Mobile's `ThreadFeed` uses `renderAssistantCitationsAsText` | ||
| 102 | to show sent quotes as readable Markdown blockquotes with each user comment below its quote; it | ||
| 103 | does not create citations or navigate to sources. | ||
| 104 | |||
| 105 | ## Provider input and titles | ||
| 106 | |||
| 107 | [`ProviderService.sendTurn`][provider] calls `expandAssistantCitationsForProvider` before routing | ||
| 108 | to any adapter; persisted messages keep their links. Each distinct serialized link becomes an | ||
| 109 | inline `[assistant-quote-N]` marker, and one `<assistant_citations>` block carries JSON entries | ||
| 110 | with an `id` and the complete `citation`. The block identifies `citation.text` as quoted reference | ||
| 111 | material, not new instructions, and `citation.comment` as the user's comment about that quote. | ||
| 112 | JSON encoding escapes `<`, `>`, and `&` so content cannot close the wrapper. The expansion is | ||
| 113 | shared by all providers; there is no per-adapter citation handling or source lookup at send time. | ||
| 114 | |||
| 115 | The 120,000-character provider input limit applies both before and after expansion: | ||
| 116 | [`composerSubmission.ts`][submission] checks the larger length client-side and keeps an over-limit | ||
| 117 | draft editable, and the server revalidates the expanded input before dispatch. Expansion does not | ||
| 118 | apply to pending user-input answers. | ||
| 119 | |||
| 120 | `assistantCitationsToPlainText` supplies readable input for title seeds, stash previews, and the | ||
| 121 | server's title and branch-name helpers, which see the quoted text and comment rather than an | ||
| 122 | encoded URI. | ||
| 123 | |||
| 124 | [contract]: ../../packages/contracts/src/assistantCitations.ts | ||
| 125 | [shared]: ../../packages/shared/src/assistantCitations.ts | ||
| 126 | [selection]: ../../apps/web/src/lib/assistantTextSelection.ts | ||
| 127 | [toolbar]: ../../apps/web/src/components/chat/AssistantSelectionToolbar.tsx | ||
| 128 | [actions]: ../../apps/web/src/lib/selectionActions.ts | ||
| 129 | [node]: ../../apps/web/src/components/ComposerCitationNode.tsx | ||
| 130 | [chip]: ../../apps/web/src/components/chat/AssistantCitationChip.tsx | ||
| 131 | [navigation]: ../../apps/web/src/lib/assistantCitationNavigation.ts | ||
| 132 | [target]: ../../apps/web/src/components/chat/useAssistantCitationTarget.ts | ||
| 133 | [source]: ../../apps/web/src/components/chat/AssistantCitationSource.tsx | ||
| 134 | [provider]: ../../apps/server/src/provider/Layers/ProviderService.ts | ||
| 135 | [submission]: ../../apps/web/src/components/chat/composerSubmission.ts |
docs/internals/ci.md deleted-44| ... | @@ -1,44 +0,0 @@ | ||
| 1 | # CI quality gates | ||
| 2 | |||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | ||
| 4 | |||
| 5 | [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs these quality gates on pull requests | ||
| 6 | and pushes to `main`: | ||
| 7 | |||
| 8 | - **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options), | ||
| 9 | then `vpr typecheck` for the workspace type check. The same job | ||
| 10 | builds the desktop pipeline (`vp run build:desktop`) and verifies the preload bundle exists and | ||
| 11 | uses only imports that Electron's sandbox can load. The verifier parses imports, then executes the | ||
| 12 | trusted artifact with controlled bridge stubs to confirm that its required APIs are callable. | ||
| 13 | - **Test**: `vp run test` across the workspace. | ||
| 14 | - **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping | ||
| 15 | `scripts/mobile-native-static-check.ts`. A cheap Linux **Mobile Native Changes** job gates it: | ||
| 16 | the macOS runner only boots when the diff touches `apps/mobile` Swift/Kotlin sources, the | ||
| 17 | SwiftLint/detekt/ktlint configuration, the `Brewfile`, the check script, the root `package.json` | ||
| 18 | that defines `lint:mobile`, or `ci.yml`. Otherwise the job is skipped, which GitHub reports as | ||
| 19 | success for the required check. Renames are matched on both their old and new path. The gate fails | ||
| 20 | open in every other case: if the changed-file list cannot be resolved, GitHub truncates it, or the | ||
| 21 | gate job itself fails, the lint runs. | ||
| 22 | - **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so | ||
| 23 | release breakage surfaces on PRs rather than at tag time. | ||
| 24 | |||
| 25 | [`.github/workflows/windows-tests.yml`](../../.github/workflows/windows-tests.yml) is a manual | ||
| 26 | Windows lane (`workflow_dispatch` only) on a Blacksmith Windows 2025 runner. The suite does not | ||
| 27 | pass on Windows yet, so it is not a required check; it exists so the work to get there can be | ||
| 28 | iterated against a real Windows box without one on hand. Dispatch it with `gh workflow run | ||
| 29 | windows-tests.yml --ref <branch>`, optionally with `-f package=<dir>` to run one workspace package | ||
| 30 | and `-f files="<paths>"` to run specific test files inside it. Once it is green, fold it into | ||
| 31 | `ci.yml`. | ||
| 32 | |||
| 33 | `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) | ||
| 34 | desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. It auto-enables | ||
| 35 | signing only when platform credentials are present. macOS passkey builds additionally require | ||
| 36 | `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. | ||
| 37 | Without the core signing credentials, it still releases unsigned artifacts. | ||
| 38 | |||
| 39 | Preflight shares pnpm's lockfile verification results with the desktop build jobs through a small | ||
| 40 | artifact. This avoids repeating dependency checks, especially on Windows, without transferring the | ||
| 41 | large registry metadata cache. pnpm checks the current lockfile and policy before it reuses a result. | ||
| 42 | If the artifact is unavailable, installation runs the checks again. | ||
| 43 | |||
| 44 | See [Release Checklist](../operations/release.md) for the full release/signing setup checklist. | ||
docs/internals/connection-runtime.md+60-211| ... | @@ -1,211 +1,60 @@ | ... | @@ -1,211 +1,60 @@ |
| 1 | # Connection Runtime | 1 | # Connection runtime |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | Web, the desktop renderer, and mobile share one connection owner per environment |
| 4 | 4 | in `packages/client-runtime`. Platform code supplies storage, credentials, network | |
| 5 | The connection runtime is shared by web and mobile. It owns connectivity, | 5 | signals, and application lifecycle events. React views consume the runtime. |
| 6 | authentication, retries, transport lifetime, cached environment data, and | 6 | Keeping retries and session lifetime here prevents competing reconnect loops when |
| 7 | environment-scoped operations. | 7 | several views need the same environment. |
| 8 | 8 | ||
| 9 | Web and mobile mount this runtime once at the application root and compose it | 9 | ## One retry owner |
| 10 | identically: `apps/web/src/connection/runtime.ts` and | 10 | |
| 11 | `apps/mobile/src/connection/runtime.ts` differ only in the platform layer they | 11 | The [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) owns |
| 12 | supply. There is no legacy connection owner or supported mixed mode. | 12 | retry policy; resolving an endpoint and opening an RPC session are single |
| 13 | 13 | attempts. Transient failures retry with capped backoff. Offline states and | |
| 14 | ## Composition | 14 | authentication failures wait for a wakeup instead of spending attempts on |
| 15 | 15 | unchanged conditions. | |
| 16 | [`connection/layer.ts`][layer] assembles the runtime: | 16 | |
| 17 | 17 | Foregrounding needs different treatment depending on the connection's state. | |
| 18 | - `ConnectionResolver` ([resolver.ts][resolver]) resolves a catalog entry into a | 18 | It wakes a retry immediately, leaves an ordinary in-flight attempt alone, and |
| 19 | prepared, authenticated endpoint for primary, bearer, relay, or SSH targets. | 19 | probes an established session before replacing it. A long mobile background |
| 20 | - `ConnectionDriver` ([driver.ts][driver]) prepares through the resolver, opens | 20 | suspension forces replacement because the OS can kill a socket without reporting |
| 21 | one RPC session, and reports `preparing`, `opening`, and `synchronizing`. | 21 | closure. Treating every foreground event as a reconnect delays healthy attempts; |
| 22 | - `RpcSessionFactory` ([rpc/session.ts][session]) performs one transport | 22 | treating every resume as harmless leaves suspended sockets stuck. |
| 23 | attempt. It does not retry. `RpcSession` is the interface it returns, | 23 | |
| 24 | exposing `client`, `initialConfig`, `ready`, `probe`, and `closed`. | 24 | The [registry](../../packages/client-runtime/src/connection/registry.ts) scopes |
| 25 | - `EnvironmentRegistry` ([registry.ts][registry]) owns the catalog and the | 25 | connections by environment. An involuntary disconnect retains the registration |
| 26 | per-environment scopes. | 26 | and cached data. Explicit removal closes the scope and clears credentials, |
| 27 | - `ConnectionOnboarding` and `RelayEnvironmentDiscovery` sit alongside the | 27 | projections, and platform-owned state such as drafts. Cloud-account changes apply |
| 28 | registry. Startup calls `EnvironmentRegistry.start` and streams platform | 28 | to relay registrations; they must not discard directly paired environments. |
| 29 | registrations into `reconcilePlatform`. | 29 | |
| 30 | 30 | ## Transport health and data freshness are separate | |
| 31 | The registry creates one environment-scoped supervisor per environment. | 31 | |
| 32 | `acquireSupervisor` serializes access per environment, reuses an existing | 32 | A socket opening is insufficient evidence that the environment is usable. The |
| 33 | supervisor when the catalog entry is unchanged, and closes and recreates the | 33 | [RPC session](../../packages/client-runtime/src/rpc/session.ts) waits for the |
| 34 | scope when it changed. `createServiceScope` builds an `EnvironmentSupervisor` | 34 | initial server configuration before becoming ready. Shell and thread data then |
| 35 | bound to a closeable scope and connects it; `run` and `runStream` execute caller | 35 | have their own synchronization state. A failed shell subscription can coexist |
| 36 | effects with that supervisor provided. | 36 | with a healthy connection; labeling that state "reconnecting" promises a |
| 37 | 37 | transport retry that will never happen. | |
| 38 | `EnvironmentSupervisor` owns desired state, retry scheduling, and the active | 38 | |
| 39 | session scope. React components do not create connections, transports, retry | 39 | Cached projections remain readable offline. They must neither imply a live |
| 40 | loops, or RPC clients. | 40 | connection nor overwrite newer live data during a reconnect. Loading and |
| 41 | 41 | resuming snapshots belongs to the shared state services, so every view agrees | |
| 42 | ## Connection State | 42 | on which data is current. |
| 43 | 43 | ||
| 44 | The supervisor is the only retry owner. | 44 | [Thread detail](../../packages/client-runtime/src/state/threads.ts) separates |
| 45 | 45 | subscription lifetime from cache lifetime. Mounted consumers share one live | |
| 46 | 1. A persisted or platform registration marks an environment as desired. | 46 | stream, which stops when the last consumer unmounts; hidden mounted routes still |
| 47 | 2. If the device is offline, the supervisor releases the active session and | 47 | count. A registry-local cache retains state and its replay cursor for five idle |
| 48 | waits for a signal without consuming retry attempts or running a timer. | 48 | minutes so back navigation can resume without another snapshot download. |
| 49 | 3. When online, it asks the driver for one prepared connection and one RPC | 49 | |
| 50 | session. | 50 | Retain state and cursor together only after an update finishes. Cancellation must |
| 51 | 4. Transient failures retry forever with exponential backoff capped at 16 | 51 | not advance the cached cursor beyond the applied data, and an old scope must not |
| 52 | seconds (`RETRY_DELAYS_MS`). A connection stable for 30 seconds resets | 52 | overwrite its successor's cache. Preserve pagination data on reuse, but clear |
| 53 | accumulated backoff. | 53 | canceled loading state. |
| 54 | 5. Authentication or configuration failures remain blocked until an external | 54 | |
| 55 | wakeup changes the relevant input. | 55 | The [RPC boundary](../../packages/client-runtime/src/rpc/client.ts) resolves |
| 56 | 6. An involuntary session close keeps the registration and cache, then retries. | 56 | requests against the current session at execution time. Durable subscriptions |
| 57 | 7. Explicit removal closes the session and deletes the registration, | 57 | follow replacement sessions. After a transport failure they wait for the |
| 58 | credentials, shell cache, and thread cache. | 58 | supervisor; an expected domain failure may resubscribe on the same healthy |
| 59 | 59 | session. Reconnection does not automatically replay mutations, whose retry and | |
| 60 | ### Wakeups | 60 | idempotency rules belong to the operation. |
| 61 | |||
| 62 | Wakeup handling differs by phase, in [supervisor.ts][supervisor]: | ||
| 63 | |||
| 64 | - During establishment, `waitForEstablishmentInterrupt` consumes and **ignores** | ||
| 65 | plain application activation. Restarting an in-flight attempt because the app | ||
| 66 | came to the foreground would only delay it. The exception is | ||
| 67 | `application-active-reconnect`, which mobile emits after a meaningful | ||
| 68 | background suspension; it interrupts establishment and resets the retry | ||
| 69 | ladder, because the OS may have silently killed the socket underneath the | ||
| 70 | attempt. | ||
| 71 | - Credential changes interrupt establishment only for relay targets, where a new | ||
| 72 | credential changes what is being established. | ||
| 73 | - Explicit disconnect, explicit retry, and going offline interrupt establishment | ||
| 74 | in every case. | ||
| 75 | - While waiting out backoff, application activation resets the retry ladder so a | ||
| 76 | foregrounded app reconnects immediately instead of serving the remaining | ||
| 77 | delay. | ||
| 78 | - Once connected, `monitorConnectedLease` handles plain activation by probing | ||
| 79 | the existing session (`lease.session.probe`, with a shorter timeout for | ||
| 80 | mobile's `application-active-probe`) rather than reconnecting; a healthy | ||
| 81 | session survives foregrounding. `application-active-reconnect` skips the probe | ||
| 82 | and replaces the lease outright. | ||
| 83 | |||
| 84 | The UI derives `available`, `offline`, `connecting`, `reconnecting`, | ||
| 85 | `connected`, and `error` from supervisor state plus explicit data-sync state. | ||
| 86 | It does not infer connection health from cached data or the existence of a | ||
| 87 | transport object. An environment becomes `connected` after the socket opens and | ||
| 88 | the initial config RPC succeeds, proving that the server is responsive. Shell | ||
| 89 | and thread synchronization are independent data states. A healthy RPC transport | ||
| 90 | with a failed shell subscription is shown as connected with a synchronization | ||
| 91 | error, not as a reconnect that is not actually scheduled. | ||
| 92 | |||
| 93 | ## Data Boundary | ||
| 94 | |||
| 95 | Finite requests, durable subscriptions, and commands are separate APIs: | ||
| 96 | |||
| 97 | - Query atoms revalidate when the RPC generation changes. | ||
| 98 | - Subscription atoms switch to replacement sessions. | ||
| 99 | - Subscription failure handling in [rpc/client.ts][client] distinguishes two | ||
| 100 | cases. A transport failure (`isTransportFailure`: every failure is an RPC | ||
| 101 | client error) ends the inner subscription without resubscribing, so the outer | ||
| 102 | stream waits for the supervisor to supply a replacement session. A handled | ||
| 103 | domain failure runs `onExpectedFailure` and, when | ||
| 104 | `retryExpectedFailureAfter` is set, sleeps and resubscribes on the **same** | ||
| 105 | session. A healthy transport is never torn down for a domain failure. | ||
| 106 | - Mutations resolve the current environment runtime at execution time. | ||
| 107 | - Shell and thread snapshots are available while offline. | ||
| 108 | - Sync status is explicit and independent per domain. Shell status is `empty`, | ||
| 109 | `cached`, `synchronizing`, or `live`, with a separate `error` field; there is | ||
| 110 | no `failed` status. Thread status adds `deleted`. | ||
| 111 | - Cached shell and thread projections are never allowed to overwrite newer live | ||
| 112 | data during a fast reconnect. | ||
| 113 | - Domain atom factories route effects through the environment registry and | ||
| 114 | resolve the current scoped service at execution time. Project and thread | ||
| 115 | commands are Atom factories under `src/state` | ||
| 116 | (`createProjectEnvironmentAtoms`, `createThreadEnvironmentAtoms`), as are the | ||
| 117 | shell and thread state factories (`createEnvironmentShellAtoms`, | ||
| 118 | `createEnvironmentThreadStateAtoms`). | ||
| 119 | - Web and mobile own their Atom runtimes, React hooks, and feature composition. | ||
| 120 | |||
| 121 | The Promise bridge exists only at the React/Atom boundary. Runtime and business | ||
| 122 | logic remain Effect-native. | ||
| 123 | |||
| 124 | ## Platform Layers | ||
| 125 | |||
| 126 | Web and mobile provide: | ||
| 127 | |||
| 128 | - network status and network-change streams; | ||
| 129 | - application lifecycle wakeups; | ||
| 130 | - cloud session credentials; | ||
| 131 | - device identity; | ||
| 132 | - platform registrations; | ||
| 133 | - persistent catalog, credential, shell, and thread stores; | ||
| 134 | - HTTP, crypto, and telemetry layers. | ||
| 135 | |||
| 136 | Platform layers adapt operating-system capabilities. They do not implement | ||
| 137 | connection policy. `EnvironmentOwnedDataCleanup` is part of this contract: on | ||
| 138 | removal the registry clears its cache and calls the platform implementation, so | ||
| 139 | web clears composer drafts and mobile clears drafts plus the thread outbox. | ||
| 140 | |||
| 141 | Mobile cloud sign-out first saves relay drafts and queued messages in the local | ||
| 142 | composer store under the owning account. These saved copies retain attachment | ||
| 143 | files during cleanup and remain outside the active composer and upload queue. | ||
| 144 | Signing back into that account restores them before relay credentials activate. | ||
| 145 | Directly paired environments keep their drafts and outbox when cloud sign-out runs. | ||
| 146 | |||
| 147 | Mobile composer attachments upload over HTTP while their environment is connected, | ||
| 148 | with at most three concurrent transfers. Drafts retain local image data or an owned | ||
| 149 | file URI alongside the pending upload ID. Sending verifies and reuses that ID, or | ||
| 150 | uploads the local bytes again if it expired. Disconnecting cancels active transfers | ||
| 151 | without discarding drafts; reconnecting resumes preparation. Older servers without | ||
| 152 | attachment-upload support continue to receive inline images. | ||
| 153 | |||
| 154 | ## Source Boundaries | ||
| 155 | |||
| 156 | Applications must import explicit package subpaths; the package intentionally | ||
| 157 | has no root export. The subpaths are documented in | ||
| 158 | [packages/client-runtime/README.md](../../packages/client-runtime/README.md), | ||
| 159 | with the `exports` map in that package's `package.json` as the authoritative | ||
| 160 | list. Files that are not exported are implementation details. | ||
| 161 | |||
| 162 | ## Application Boundary | ||
| 163 | |||
| 164 | The application root mounts the shared connection layer, creates its own Atom | ||
| 165 | runtime, and selects the domain atom factories required by that platform. Web | ||
| 166 | and mobile may expose different hooks and features without changing connection | ||
| 167 | ownership. | ||
| 168 | |||
| 169 | Application code must not construct RPC clients, retry loops, or raw | ||
| 170 | orchestration commands. Persistence paths belong to the platform registration | ||
| 171 | and cache stores, with explicit migration or invalidation policy. | ||
| 172 | |||
| 173 | ### Thread detail ownership | ||
| 174 | |||
| 175 | Mounted detail consumers share one live thread subscription. The subscription | ||
| 176 | ends when the last consumer leaves. Hidden routes that remain mounted still | ||
| 177 | count as consumers. | ||
| 178 | |||
| 179 | A separate registry-local atom retains the last thread state and replay cursor | ||
| 180 | for five idle minutes. A warm mount renders that state and resumes the stream | ||
| 181 | without another snapshot download. The cache holds completed state and cursor | ||
| 182 | updates together. It retains pagination data but clears canceled loading state. | ||
| 183 | An ownership token prevents an old scope from replacing its successor's cache. | ||
| 184 | Closing a thread skips snapshots that were already saved. Changed snapshots and | ||
| 185 | failed background writes still get a final save attempt. | ||
| 186 | |||
| 187 | ## Verification | ||
| 188 | |||
| 189 | Core state-machine tests use `@effect/vitest` and deterministic service layers. | ||
| 190 | Required coverage includes: | ||
| 191 | |||
| 192 | - offline startup and online wakeup; | ||
| 193 | - forever retry with the 16-second cap; | ||
| 194 | - explicit retry interrupting backoff; | ||
| 195 | - authentication wakeups; | ||
| 196 | - involuntary close and reconnect; | ||
| 197 | - explicit removal clearing all owned state; | ||
| 198 | - relay token reuse and refresh; | ||
| 199 | - progressive relay discovery; | ||
| 200 | - shell and thread cache hydration; | ||
| 201 | - thread stream shutdown, warm resume, and snapshot expiry; | ||
| 202 | - durable subscriptions switching sessions; | ||
| 203 | - command metadata and idempotent queued-command metadata. | ||
| 204 | |||
| 205 | [layer]: ../../packages/client-runtime/src/connection/layer.ts | ||
| 206 | [resolver]: ../../packages/client-runtime/src/connection/resolver.ts | ||
| 207 | [driver]: ../../packages/client-runtime/src/connection/driver.ts | ||
| 208 | [registry]: ../../packages/client-runtime/src/connection/registry.ts | ||
| 209 | [supervisor]: ../../packages/client-runtime/src/connection/supervisor.ts | ||
| 210 | [session]: ../../packages/client-runtime/src/rpc/session.ts | ||
| 211 | [client]: ../../packages/client-runtime/src/rpc/client.ts |
docs/internals/environment-auth.md+60-197| ... | @@ -1,197 +1,60 @@ | ... | @@ -1,197 +1,60 @@ |
| 1 | # Environment Authentication Profile | 1 | # Environment authentication |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | The environment issues its own sessions and enforces their capabilities. Cloud |
| 4 | 4 | identity and relay credentials belong to a separate trust boundary, described in | |
| 5 | The environment server and the relay use separate credentials, issuers, and trust | 5 | [T3 Connect](./t3-connect.md). A relay token is never an environment login. |
| 6 | boundaries. They intentionally use a similar OAuth-shaped model so that permission | 6 | |
| 7 | checks and token exchange behavior can be audited against established concepts. | 7 | ## Authority survives transport changes |
| 8 | 8 | ||
| 9 | ## Authorization Model | 9 | Pairing delegates a set of scopes. Exchanging a bootstrap credential can narrow |
| 10 | 10 | that grant but cannot widen it. Ordinary pairing does not grant access-management | |
| 11 | Environment authorization is capability-based. A session carries zero or more | 11 | or relay-management authority. Creating another pairing link requires both |
| 12 | OAuth-style scope strings: | 12 | `access:write` and every scope being delegated. The |
| 13 | 13 | [auth handlers](../../apps/server/src/auth/http.ts) enforce this at issuance; | |
| 14 | | Scope | Permission | | 14 | client labels and device metadata have no authorization role. |
| 15 | | ----------------------- | ------------------------------------------------------------------------ | | 15 | |
| 16 | | `orchestration:read` | Read snapshots, status, events, configuration, and filesystem/VCS state. | | 16 | The access read model contains pairing metadata, never recoverable pairing |
| 17 | | `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state. | | 17 | secrets. Only the creation response returns the raw credential. Otherwise read |
| 18 | | `terminal:operate` | Create, attach, input, resize, clear, restart, and terminate terminals. | | 18 | access to the connections list would become a way to acquire another client's |
| 19 | | `review:write` | Read review diff previews used to compose review feedback. | | 19 | authority. |
| 20 | | `access:read` | Inspect pairing links and client sessions. | | 20 | |
| 21 | | `access:write` | Create or revoke pairing links and client sessions. | | 21 | Browser cookies, bearer tokens, and DPoP tokens adapt the same scoped session |
| 22 | | `relay:read` | Inspect managed relay connectivity. | | 22 | model. DPoP binds a token to a client's proof key; an invalid proof must fail |
| 23 | | `relay:write` | Link, configure, or unlink managed relay connectivity. | | 23 | rather than fall back to bearer authentication. The OAuth token-exchange |
| 24 | 24 | vocabulary gives these grants a familiar meaning, but the environment does not | |
| 25 | Pairing-link lists and access-stream snapshots and updates contain metadata only. | 25 | implement a general-purpose OAuth authorization server. |
| 26 | The raw credential is returned only by the creation request, after the server checks | 26 | |
| 27 | `access:write` and the delegated scopes. Web and desktop clients keep that response | 27 | Bearer and DPoP clients obtain short-lived WebSocket tickets through authenticated |
| 28 | in memory for sharing. They do not recover credentials from access read models. | 28 | HTTP so long-lived tokens stay out of socket URLs. Browser sessions can |
| 29 | 29 | authenticate the upgrade with their cookie. A successful handshake grants no | |
| 30 | Ordinary pairing links grant the four client-operation scopes and read access to | 30 | extra authority: [every RPC declares a required |
| 31 | managed relay connectivity: | 31 | scope](../../apps/server/src/auth/RpcAuthorization.ts). |
| 32 | `orchestration:read orchestration:operate terminal:operate review:write relay:read`. | 32 | |
| 33 | The desktop bootstrap credential and command-line administrative bootstrap | 33 | Desktop restarts forget the previous local bearer token, so its reusable |
| 34 | credentials additionally grant `access:read access:write relay:write`. | 34 | bootstrap grant replaces earlier sessions for the same subject and method. |
| 35 | 35 | Revocation and insertion share a [database | |
| 36 | ## Host file access | 36 | transaction](../../apps/server/src/persistence/AuthSessions.ts); a failed |
| 37 | 37 | replacement must leave the old credential usable. Pairing and browser sessions | |
| 38 | Clients with `orchestration:read` can read files anywhere the environment's server account can | 38 | do not follow this replacement rule. |
| 39 | read, following the environment-wide authorization model rather than introducing per-project | 39 | |
| 40 | filesystem permissions. `projects.readFile` accepts an absolute path and returns the text of that | 40 | ## The environment is the filesystem boundary |
| 41 | host file; only workspace-relative paths pass its root check, and `projects.writeFile` never | 41 | |
| 42 | accepts an absolute path. Clients use this to show files an agent wrote outside the workspace, such | 42 | Projects are organizational boundaries, not filesystem sandboxes. |
| 43 | as a report in a temp directory, read-only. | 43 | `orchestration:read` permits reading files the server account can read, including |
| 44 | 44 | absolute paths outside a project. This lets clients display artifacts that an | |
| 45 | ## Media preview access | 45 | agent writes in a temporary directory. Relative paths and writes still follow |
| 46 | 46 | the [workspace path rules](../../apps/server/src/workspace/WorkspaceFileSystem.ts). | |
| 47 | Clients with `orchestration:read` can request a `media-file` URL through `assets.createUrl` for | 47 | |
| 48 | supported images, videos, HTML, and PDF files anywhere the environment's server account can read. | 48 | Signed asset URLs are bearer credentials. A URL for media on the host grants |
| 49 | A thread ID supplies the workspace for relative paths; absolute paths refer to the environment | 49 | access to one canonical file and its device/inode identity, not its containing directory. |
| 50 | host, not the client. | 50 | [Asset access](../../apps/server/src/assets/AssetAccess.ts) rechecks the opened |
| 51 | 51 | file's identity when serving it, so atomic replacement requires a new URL while | |
| 52 | [`AssetAccess.ts`](../../apps/server/src/assets/AssetAccess.ts) resolves symlinks, requires a regular | 52 | editing the same file in place does not. An HTML file authorized this way cannot |
| 53 | file, and validates the resolved file's literal extension. It opens the file and signs its canonical | 53 | load sibling assets; directory-scoped workspace previews are a separate grant. |
| 54 | path and device/inode identity for one hour. The token grants access to that exact file, not adjacent | 54 | Clients should share the authored file reference so they do not disclose the |
| 55 | files or its containing directory. Serving rechecks the canonical path, media type, and opened | 55 | temporary URL's credential. |
| 56 | descriptor's identity, then streams full or partial responses from that descriptor. Replacing a | 56 | |
| 57 | file atomically requires a freshly signed URL; editing it in place does not. Because the token names | 57 | Host videos can change in place. Their [HTTP |
| 58 | one file, an HTML document served this way cannot load sibling assets; the directory-scoped | 58 | responses](../../apps/server/src/http.ts) omit cache validators because file |
| 59 | `workspace-file` resource remains the route for HTML inside the workspace. Uploaded attachments keep | 59 | metadata cannot prove byte-for-byte identity for `If-Range`. Adding weak |
| 60 | their separate asset resource. | 60 | validators would turn native-player seeks into full downloads. |
| 61 | |||
| 62 | Signed asset URLs are bearer credentials. Anyone who obtains a URL and can reach the environment | ||
| 63 | can fetch that file until it expires. Clients should copy the authored reference, not the temporary | ||
| 64 | URL. Responses use `nosniff`; SVG responses retain their restrictive sandbox policy. Video reads | ||
| 65 | support byte ranges so playback does not require a complete download first. | ||
| 66 | |||
| 67 | Host videos can change in place, so their responses use `private, no-store` and omit `ETag` | ||
| 68 | and `Last-Modified`. File metadata cannot prove byte-for-byte identity for `If-Range`; advertising | ||
| 69 | those validators would encourage native players to send conditional seeks that require a full | ||
| 70 | response. Ordinary range requests receive partial responses. An explicitly supplied `If-Range` | ||
| 71 | still falls back to a full response because no strong validator is available. Host image previews | ||
| 72 | keep their private cache policy and weak metadata validators. | ||
| 73 | |||
| 74 | The server serves media in place without importing it into attachment storage. Deletion makes | ||
| 75 | future server reads fail, though an already loaded client or its cache can retain bytes. Native | ||
| 76 | viewers may use temporary client-side files for display or explicit sharing; those are not durable | ||
| 77 | environment copies. | ||
| 78 | |||
| 79 | ## Authentication Flows | ||
| 80 | |||
| 81 | ### Browser Session | ||
| 82 | |||
| 83 | `POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a | ||
| 84 | browser session cookie. The cookie is an HTTP transport adapter for the same | ||
| 85 | scoped session model; the response never exposes the session secret to browser | ||
| 86 | JavaScript. | ||
| 87 | |||
| 88 | ### Bearer Access Token | ||
| 89 | |||
| 90 | Non-browser clients use `POST /oauth/token` with an | ||
| 91 | `application/x-www-form-urlencoded` body: | ||
| 92 | |||
| 93 | ```text | ||
| 94 | grant_type=urn:ietf:params:oauth:grant-type:token-exchange | ||
| 95 | subject_token=<bootstrap credential> | ||
| 96 | subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap | ||
| 97 | requested_token_type=urn:ietf:params:oauth:token-type:access_token | ||
| 98 | scope=orchestration:read orchestration:operate terminal:operate review:write relay:read | ||
| 99 | ``` | ||
| 100 | |||
| 101 | Clients may additionally submit `client_label`, `client_device_type`, and | ||
| 102 | `client_os` extension parameters so the authorized-clients UI can identify the | ||
| 103 | device that established the session. These are presentation hints only; the | ||
| 104 | environment derives transport metadata such as IP address and user agent from | ||
| 105 | the request and does not use these fields for authorization. | ||
| 106 | |||
| 107 | The response has the token-exchange shape: | ||
| 108 | |||
| 109 | ```json | ||
| 110 | { | ||
| 111 | "access_token": "<opaque session token>", | ||
| 112 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", | ||
| 113 | "token_type": "Bearer", | ||
| 114 | "expires_in": 2592000, | ||
| 115 | "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" | ||
| 116 | } | ||
| 117 | ``` | ||
| 118 | |||
| 119 | Sessions issued from a plain bearer exchange use the store's | ||
| 120 | `DEFAULT_SESSION_TTL` of 30 days. The shorter one-hour `expires_in: 3600` applies | ||
| 121 | only to DPoP-bound exchanges, where the token is additionally constrained by a | ||
| 122 | proof key. See `SessionStore.ts` and `EnvironmentAuth.ts`. | ||
| 123 | |||
| 124 | The reusable `desktop-bootstrap` grant replaces active sessions with the same | ||
| 125 | subject and authentication method. Revocation and insertion share one database | ||
| 126 | transaction, so a failed insertion preserves the previous credential. This also | ||
| 127 | removes stale local desktop entries from earlier launches. Browser-cookie sessions | ||
| 128 | and sessions issued through pairing links are not replaced. | ||
| 129 | |||
| 130 | Requested scopes must be a subset of the one-time bootstrap credential grant. | ||
| 131 | An ordinary paired client therefore cannot exchange its grant for | ||
| 132 | `access:read`, `access:write`, or `relay:write`. | ||
| 133 | |||
| 134 | ### DPoP-Bound Access Token | ||
| 135 | |||
| 136 | The same `/oauth/token` exchange supports proof-of-possession tokens. A client | ||
| 137 | that sends a `DPoP` header has its proof verified by `verifyRequestDpopProof`; | ||
| 138 | the resulting JWK thumbprint is stored on the session, which is then issued with | ||
| 139 | method `dpop-access-token` and a one-hour TTL instead of the bearer default. An | ||
| 140 | invalid proof gets a DPoP challenge header and a credential error rather than a | ||
| 141 | bearer token. Newer servers include a safe `dpopFailureReason` category in that | ||
| 142 | error. When an older server omits the category, clients mention clock skew as | ||
| 143 | one possible cause rather than presenting it as confirmed. | ||
| 144 | |||
| 145 | `dpop-access-token` is advertised alongside `browser-session-cookie` and | ||
| 146 | `bearer-access-token` in the descriptor's `sessionMethods` | ||
| 147 | (`EnvironmentAuthPolicy.ts`), so clients can discover support rather than | ||
| 148 | assume it. Relay-brokered clients use this mode so that a leaked token cannot be | ||
| 149 | replayed without the corresponding key. | ||
| 150 | |||
| 151 | ### WebSocket Ticket | ||
| 152 | |||
| 153 | `POST /api/auth/websocket-ticket` accepts any authenticated session and returns | ||
| 154 | a short-lived, single-purpose WebSocket ticket, issued through | ||
| 155 | `EnvironmentAuth.issueWebSocketTicket` with a five-minute default TTL. The | ||
| 156 | client presents its bearer or DPoP credential in headers to get the ticket, then | ||
| 157 | appends only that ticket to the socket URL as `wsTicket`. This keeps long-lived | ||
| 158 | tokens and browser cookies out of WebSocket URLs while letting the handshake | ||
| 159 | authenticate. | ||
| 160 | |||
| 161 | The ticket carries its session's scopes; each RPC method then enforces | ||
| 162 | `orchestration:read`, `orchestration:operate`, `terminal:operate`, | ||
| 163 | `review:write`, `relay:write`, or `access:read` as appropriate, through | ||
| 164 | `RPC_REQUIRED_SCOPES` in `apps/server/src/auth/RpcAuthorization.ts`. Review feedback submission currently dispatches | ||
| 165 | an orchestration operation, so clients performing it also need | ||
| 166 | `orchestration:operate`. Creating a ticket is not authorization to call every | ||
| 167 | RPC method. | ||
| 168 | |||
| 169 | ## Standards Alignment | ||
| 170 | |||
| 171 | - Bearer access tokens are used through the `Authorization: Bearer` scheme from | ||
| 172 | RFC 6750. | ||
| 173 | - The token endpoint profiles the request and response vocabulary from OAuth 2.0 | ||
| 174 | Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`, | ||
| 175 | `access_token`, `issued_token_type`, and `token_type`. | ||
| 176 | - Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited, | ||
| 177 | unordered capabilities with subset checking during exchange. | ||
| 178 | |||
| 179 | This is intentionally not a general-purpose OAuth authorization server. The | ||
| 180 | environment bootstrap token type is private, the bootstrap cookie and WebSocket | ||
| 181 | connection-token routes are product-specific adapters, and the API returns its | ||
| 182 | typed `HttpApi` errors rather than implementing every OAuth error response | ||
| 183 | surface. | ||
| 184 | |||
| 185 | ## Upgrade Behavior | ||
| 186 | |||
| 187 | Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth | ||
| 188 | records to scoped records. It deletes existing pairing links and sessions while | ||
| 189 | leaving non-authentication environment state unchanged. Upgraded clients must | ||
| 190 | pair again; old `owner` or `client` credentials are never silently mapped to new | ||
| 191 | capabilities. | ||
| 192 | |||
| 193 | ## Relay Boundary | ||
| 194 | |||
| 195 | Relay-managed tunnels use their own tokens and keys. The relay can reuse scope | ||
| 196 | parsing and token-exchange conventions, but an environment access token is not a | ||
| 197 | relay token and cannot be presented to the relay. |
docs/internals/glossary.md+45-209| ... | @@ -1,211 +1,47 @@ | ... | @@ -1,211 +1,47 @@ |
| 1 | # Glossary | 1 | # Glossary |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | Terms whose meaning matters across T3 Code. Architecture and lifecycle constraints belong in the |
| 4 | 4 | [overview](./overview.md), not in these definitions. | |
| 5 | This is a living glossary for T3 Code. It explains what common terms mean in this codebase. | 5 | |
| 6 | 6 | ## Workspace and conversation | |
| 7 | ## Table of contents | 7 | |
| 8 | 8 | | Term | Meaning | | |
| 9 | - [Project and workspace](#project-and-workspace) | 9 | | -------------- | ------------------------------------------------------------------------------------------------- | |
| 10 | - [Thread timeline](#thread-timeline) | 10 | | Environment | One running server and the machine, credentials, workspace access, and state it owns. | |
| 11 | - [Orchestration](#orchestration) | 11 | | Client | A web, desktop, or mobile UI connected to an environment. The desktop app can also host a server. | |
| 12 | - [Provider runtime](#provider-runtime) | 12 | | Project | An environment-local workspace record rooted at a directory. | |
| 13 | - [Checkpointing](#checkpointing) | 13 | | Workspace root | The project's base filesystem directory on the environment. | |
| 14 | - [Appearance](#appearance) | 14 | | Worktree | A separate Git checkout a thread can use instead of the project's main checkout. | |
| 15 | 15 | | Thread | The durable conversation and work history for a project. It survives provider process exits. | | |
| 16 | ## Concepts | 16 | | Turn | One user-to-agent work cycle. Provider work can finish before checkpoint and diff work settles. | |
| 17 | 17 | | Activity | A non-message timeline item, such as a tool action, approval, or failure. | | |
| 18 | ### Project and workspace | 18 | | T3 home | The base data directory. Runtime state normally lives under its `userdata` directory. | |
| 19 | 19 | ||
| 20 | #### Project | 20 | ## Orchestration |
| 21 | 21 | ||
| 22 | The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot` and a title. It does not contain threads: `OrchestrationProject` and `OrchestrationThread` are separate arrays on the read model, and a project can have zero threads. See [workspace-layout.md][2]. | 22 | | Term | Meaning | |
| 23 | 23 | | ----------------------- | -------------------------------------------------------------------------------------------- | | |
| 24 | #### Workspace root | 24 | | Command | A request to change domain state. Accepting it does not mean its side effects have finished. | |
| 25 | 25 | | Event | A persisted fact produced by a command. | | |
| 26 | The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2]. | 26 | | Decider | The pure logic that turns a command and current state into events. | |
| 27 | 27 | | Projection / read model | A view of current state derived from persisted events. | | |
| 28 | #### Worktree | 28 | | Projector | The logic that applies events to a read model. | |
| 29 | 29 | | Reactor | A worker that performs follow-up work in response to recorded intent or runtime signals. | | |
| 30 | A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live behind the VCS driver contract in `apps/server/src/vcs/VcsDriver.ts`, implemented by [GitVcsDriverCore.ts][3]. | 30 | | Command receipt | A durable record of a command's result, used to make retries idempotent. | |
| 31 | 31 | | Runtime receipt | A test-only signal that an asynchronous milestone completed. | | |
| 32 | ### Thread timeline | 32 | | Quiesced | The relevant follow-up workers have finished, beyond the provider turn merely ending. | |
| 33 | 33 | ||
| 34 | #### Thread | 34 | ## Providers and checkpoints |
| 35 | 35 | ||
| 36 | The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4]. | 36 | | Term | Meaning | |
| 37 | 37 | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | |
| 38 | #### Turn | 38 | | Provider | The agent runtime T3 Code controls, such as Codex or Claude Code. | |
| 39 | 39 | | Driver | The integration for a provider kind. | | |
| 40 | A single user-to-assistant work cycle inside a thread. It starts with user input and ends when the session leaves `running` status, which [projector.ts][4] treats as the authoritative completion signal (`settledTurnStateForSessionStatus`). Checkpoint and diff work may settle afterward without changing when the turn ended. See [the contracts][1] and [ProviderRuntimeIngestion.ts][5]. | 40 | | Provider instance | One configured provider, with its own settings and lifecycle. Multiple instances can use the same driver. | |
| 41 | 41 | | Adapter | The boundary translating a provider's native protocol into T3 Code operations and events. | | |
| 42 | #### Activity | 42 | | Session | The provider runtime attached to a thread. A session can be stopped and resumed without deleting the thread. | |
| 43 | 43 | | Runtime mode | The thread's permission policy. See [permission modes](../user/permission-modes.md). | | |
| 44 | A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4]. | 44 | | Interaction mode | How the agent approaches the task, such as planning. Separate from permission policy. | |
| 45 | 45 | | Checkpoint | A saved workspace state used for diffs and restore, stored as a hidden Git ref. | | |
| 46 | ### Orchestration | 46 | | Checkpoint baseline | The workspace state captured before the work being compared. | |
| 47 | 47 | | Turn diff | The workspace changes attributed to one turn. | | |
| 48 | 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]. | ||
| 49 | |||
| 50 | #### Aggregate | ||
| 51 | |||
| 52 | The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8]. | ||
| 53 | |||
| 54 | #### Command | ||
| 55 | |||
| 56 | 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]. | ||
| 57 | Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`. | ||
| 58 | |||
| 59 | #### Domain Event | ||
| 60 | |||
| 61 | 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. | ||
| 62 | Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`. | ||
| 63 | |||
| 64 | #### Decider | ||
| 65 | |||
| 66 | 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]. | ||
| 67 | |||
| 68 | #### Projection | ||
| 69 | |||
| 70 | A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10]. | ||
| 71 | |||
| 72 | #### Projector | ||
| 73 | |||
| 74 | The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11]. | ||
| 75 | |||
| 76 | #### Read model | ||
| 77 | |||
| 78 | 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]. | ||
| 79 | |||
| 80 | #### Reactor | ||
| 81 | |||
| 82 | 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]. | ||
| 83 | |||
| 84 | #### Receipt | ||
| 85 | |||
| 86 | A typed signal emitted when an async milestone completes, such as `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, or `turn.processing.quiesced`. Receipts are a test-only mechanism: the production `RuntimeReceiptBusLive` publish is a no-op and only the test layer is PubSub-backed. Do not build production behavior on them. See [RuntimeReceiptBus.ts][13] and [CheckpointReactor.ts][6]. | ||
| 87 | |||
| 88 | #### Quiesced | ||
| 89 | |||
| 90 | "Quiesced" means a turn has gone quiet and stable: follow-up work such as [CheckpointReactor.ts][6] has settled. It appears in [the receipt schema][13], so in practice it is something tests wait on rather than a production signal. | ||
| 91 | |||
| 92 | ### Provider runtime | ||
| 93 | |||
| 94 | The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [providers.md][16]. | ||
| 95 | |||
| 96 | #### Provider | ||
| 97 | |||
| 98 | The backend agent runtime that actually performs work. Six drivers ship built in: Codex, Claude, Cursor, Grok, OpenCode, and Antigravity. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17] as a representative adapter. | ||
| 99 | |||
| 100 | #### Session | ||
| 101 | |||
| 102 | 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]. | ||
| 103 | |||
| 104 | #### Runtime mode | ||
| 105 | |||
| 106 | The safety/access mode for a thread or session. [The contracts][1] define four values: `approval-required`, `auto-accept-edits`, `auto`, and `full-access`. See [permission modes][18]. | ||
| 107 | |||
| 108 | #### Interaction mode | ||
| 109 | |||
| 110 | The agent interaction style for a thread. In [the contracts][1], the values are `default` and `plan`. | ||
| 111 | |||
| 112 | #### Assistant delivery mode | ||
| 113 | |||
| 114 | Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` accumulates text. Buffered delivery is not held until the turn completes: it spills once accumulated text would exceed 24,000 characters, and flushes at approval and user-input boundaries. See [ProviderRuntimeIngestion.ts][5]. | ||
| 115 | |||
| 116 | #### Snapshot | ||
| 117 | |||
| 118 | 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]. | ||
| 119 | |||
| 120 | #### Usage limits | ||
| 121 | |||
| 122 | The rolling subscription quota windows a provider reports for its signed-in account, such as Claude's five-hour and weekly windows or Codex's primary and secondary allowances. Each driver decides in its own `checkProvider` whether it has any and returns them on the snapshot as `usageLimits`; drivers with no notion of subscription usage leave the field absent. Adapters that receive rate-limit telemetry during a turn normalise it into a `ProviderUsageLimitsUpdate` at the boundary, and `ProviderUsageLimitsIngestion` folds it onto the owning instance's snapshot through `ServerProviderShape.applyUsageLimits`, so no central service needs to know a driver kind. See [providerUsageLimits.ts](../../packages/contracts/src/providerUsageLimits.ts) and [makeManagedServerProvider.ts](../../apps/server/src/provider/makeManagedServerProvider.ts). | ||
| 123 | |||
| 124 | #### Usage limit source | ||
| 125 | |||
| 126 | A read-only quota feed outside this environment's provider CLIs, configured under `settings.usageLimitSources`. The only kind today is a CLIProxyAPI hub, whose `quota-scheduler/status` reports the windows of every pooled account. `UsageLimitSources` polls each source on the provider health interval and publishes `UsageLimitSourceSnapshot`s over the config stream as `usageLimitSourcesUpdated`, gated by a client capability flag the way environment themes are. The management key round-trips through the secret store with a redaction marker on disk. See [UsageLimitSources.ts](../../apps/server/src/usage/UsageLimitSources.ts). | ||
| 127 | |||
| 128 | #### Model manifest | ||
| 129 | |||
| 130 | The per-driver list of current model slugs that decides which models land in the model picker's legacy section. Bundled at `apps/server/src/provider/model-manifest.json` and refreshed at runtime from the same file on `main`, so classification updates ship as commits instead of releases. See the [provider architecture][16] model manifest section. | ||
| 131 | |||
| 132 | ### Checkpointing | ||
| 133 | |||
| 134 | 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]. | ||
| 135 | |||
| 136 | #### Checkpoint | ||
| 137 | |||
| 138 | 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]. | ||
| 139 | |||
| 140 | #### Checkpoint ref | ||
| 141 | |||
| 142 | 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]. | ||
| 143 | |||
| 144 | #### Checkpoint baseline | ||
| 145 | |||
| 146 | 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]. | ||
| 147 | |||
| 148 | #### Checkpoint diff | ||
| 149 | |||
| 150 | The difference between two checkpoints. [CheckpointDiffQuery.ts][20] reads full patches on demand. [CheckpointReactor.ts][6] uses NUL-delimited Git numstat output for automatic file summaries, parsed by [Diffs.ts][23]. | ||
| 151 | |||
| 152 | #### Turn diff | ||
| 153 | |||
| 154 | 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]. | ||
| 155 | |||
| 156 | ### Appearance | ||
| 157 | |||
| 158 | #### Environment theme | ||
| 159 | |||
| 160 | A theme an environment's machine publishes for clients to follow, one file per theme under `themes/` in that environment's state directory; the filename is the theme id. [environmentTheme.ts][25] watches the directory and streams the set over `subscribeServerConfig`; clients render each as a library card, generating a full palette when the file carries seed colors and using the palette directly when it is a standard exported theme file. A desktop that retints its apps when the system theme changes rewrites its file, so T3 Code follows along without a restart. See [environment-theme.md][26]. | ||
| 161 | |||
| 162 | #### Default theme | ||
| 163 | |||
| 164 | The environment's theme, held in its `settings.json` as `defaultTheme` (with `defaultThemeSetAt` | ||
| 165 | as the set-generation) and set with `t3 theme set <id>`. Web and desktop clients apply each set | ||
| 166 | once — live when connected, on the next connect otherwise — so setting it switches them, while a | ||
| 167 | theme a user picks in Settings afterwards sticks until the next set; mobile keeps its own | ||
| 168 | appearance settings. Naming a published [environment theme](#environment-theme) is how a desktop | ||
| 169 | ships T3 Code already matching it. | ||
| 170 | |||
| 171 | ## Practical Shortcuts | ||
| 172 | |||
| 173 | - If you see `requested`, think "intent recorded". | ||
| 174 | - If you see `completed`, think "result applied". | ||
| 175 | - If you see `receipt`, think "async milestone signal, for tests". | ||
| 176 | - If you see `checkpoint`, think "workspace snapshot for diff/restore". | ||
| 177 | - If you see `quiesced`, think "all relevant follow-up work has gone idle". | ||
| 178 | |||
| 179 | ## Related Docs | ||
| 180 | |||
| 181 | - [Architecture overview][24] | ||
| 182 | - [Provider architecture][16] | ||
| 183 | - [Permission modes][18] | ||
| 184 | - [Workspace layout][2] | ||
| 185 | |||
| 186 | [1]: ../../packages/contracts/src/orchestration.ts | ||
| 187 | [2]: ./workspace-layout.md | ||
| 188 | [3]: ../../apps/server/src/vcs/GitVcsDriverCore.ts | ||
| 189 | [4]: ../../apps/server/src/orchestration/projector.ts | ||
| 190 | [5]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | ||
| 191 | [6]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts | ||
| 192 | [7]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | ||
| 193 | [8]: ../../apps/server/src/orchestration/decider.ts | ||
| 194 | [9]: ../../apps/server/src/orchestration/commandInvariants.ts | ||
| 195 | [10]: ../../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts | ||
| 196 | [11]: ../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts | ||
| 197 | [12]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | ||
| 198 | [13]: ../../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts | ||
| 199 | [14]: ../../apps/server/src/provider/Layers/ProviderService.ts | ||
| 200 | [15]: ../../apps/server/src/provider/Services/ProviderAdapter.ts | ||
| 201 | [16]: ./providers.md | ||
| 202 | [17]: ../../apps/server/src/provider/Layers/CodexAdapter.ts | ||
| 203 | [18]: ../user/permission-modes.md | ||
| 204 | [19]: ../../apps/server/src/checkpointing/CheckpointStore.ts | ||
| 205 | [20]: ../../apps/server/src/checkpointing/CheckpointDiffQuery.ts | ||
| 206 | [21]: ../../apps/server/src/persistence/Services/ProjectionCheckpoints.ts | ||
| 207 | [22]: ../../apps/server/src/checkpointing/Utils.ts | ||
| 208 | [23]: ../../apps/server/src/checkpointing/Diffs.ts | ||
| 209 | [24]: ./overview.md | ||
| 210 | [25]: ../../apps/server/src/environmentTheme.ts | ||
| 211 | [26]: ../user/environment-theme.md |
docs/internals/mobile-development.md+23-48| ... | @@ -1,50 +1,25 @@ | ... | @@ -1,50 +1,25 @@ |
| 1 | # Mobile development lifecycle | 1 | # Mobile development lifecycle |
| 2 | 2 | ||
| 3 | Ordinary component changes use Metro Fast Refresh. The connection runtime uses | 3 | The [connection runtime's HMR boundary](../../apps/mobile/src/lib/hot-swappable-atom-runtime.ts) |
| 4 | a stable development-only atom runtime whose writable layer atom receives the | 4 | keeps a stable atom runtime and replaces its Effect layer through a writable atom. |
| 5 | newly evaluated Effect layer. Its module accepts the update only after installing | 5 | It accepts the update only after installing the new layer. Otherwise importers |
| 6 | that layer. Existing subscribers observe the new context, and Effect releases the | 6 | retain the old behavior even though Metro reports a successful refresh. |
| 7 | previous runtime scope without restarting React Navigation. Production uses an | 7 | |
| 8 | ordinary `Atom.runtime` without this hot-update boundary. | 8 | Do not reset the shared atom registry to refresh a connection-runtime edit. Reset |
| 9 | 9 | removes listeners from mounted consumers that Metro has no reason to rerender. | |
| 10 | Replacing the atom registry itself disposes its old nodes; replacing the managed | 10 | When the registry or managed-runtime module itself changes, normal Metro propagation |
| 11 | runtime starts its asynchronous disposal. These modules use normal Metro update | 11 | disposes its old resources. This boundary does not make arbitrary module-level atom |
| 12 | propagation. The app does not call `DevSettings.reload` during their replacement. | 12 | families safe to hot-swap. Production uses an ordinary atom runtime. |
| 13 | React can still reset state when its normal Fast Refresh rules require it. | 13 | |
| 14 | 14 | [Environment supervisor scopes](../../packages/client-runtime/src/connection/registry.ts) | |
| 15 | Do not reset the shared registry to clean up a connection-runtime edit: registry | 15 | are children of the registry scope. The per-environment map supports targeted |
| 16 | reset removes listeners from unedited mounted consumers. Do not self-accept a | 16 | shutdown, but a supervisor created after its cleanup runs would escape it. A closed |
| 17 | runtime module while leaving importers attached to its old implementation. A | 17 | parent scope also closes late arrivals, preventing interrupted startup or runtime |
| 18 | hot-update boundary must install fresh behavior through the existing reactive | 18 | replacement from leaving a WebSocket alive outside the new registry. |
| 19 | dependencies. These boundaries do not make every module-level atom family in the | 19 | |
| 20 | app hot-swappable; new runtime singletons still need explicit ownership. Editing | 20 | The [Uniwind patch](../../patches/uniwind@1.11.0.patch) still compiles CSS on Metro |
| 21 | the registry or managed runtime can still rebuild a much larger dependency graph | 21 | updates so newly used classes are discovered. It skips global style invalidation |
| 22 | than an ordinary component or connection-runtime edit. | 22 | only when the generated stylesheet and theme list are unchanged. Skipping compilation |
| 23 | 23 | would lose new classes; invalidating every consumer for unchanged output makes an | |
| 24 | Environment supervisor scopes are children of the connection registry scope. | 24 | ordinary component edit refresh the whole app. The fingerprint is recorded only |
| 25 | The registry's per-environment map supports targeted shutdown, but it cannot be | 25 | after initialization succeeds. |
| 26 | the sole owner: a supervisor created after that map's finalizer has run must | ||
| 27 | still inherit the closed parent scope. Otherwise interrupted startup or runtime | ||
| 28 | replacement can leave a session and WebSocket alive outside the current registry. | ||
| 29 | |||
| 30 | The compact Home list owns its minute-based presentation clock in a focus effect. | ||
| 31 | Blur clears the interval without a render-driving focus subscription; focus | ||
| 32 | refreshes the clock immediately. Other prop or state changes can still render | ||
| 33 | the hidden list. Exact snooze-expiry timers remain active, and the visible iPad | ||
| 34 | sidebar keeps its own minute updates. | ||
| 35 | |||
| 36 | Connection and runtime projections are shared per environment. Thread selection | ||
| 37 | consumers should read those atoms instead of repeatedly parsing the same socket | ||
| 38 | URL or constructing new connection objects during each render. | ||
| 39 | |||
| 40 | The Uniwind dependency patch still recompiles CSS on Metro updates so newly used | ||
| 41 | classes are discovered. It fingerprints the generated native stylesheet and | ||
| 42 | theme list, then skips development-only global invalidation when that output is | ||
| 43 | unchanged. A changed stylesheet or theme list still resets the style caches and | ||
| 44 | notifies subscribers. The digest is committed only after initialization succeeds. | ||
| 45 | Web and production retain their existing initialization behavior. | ||
| 46 | |||
| 47 | After installing or changing the Uniwind patch, restart Metro once with | ||
| 48 | `vp run dev:client:reset` from `apps/mobile`. pnpm gives patched packages new | ||
| 49 | filesystem paths, and cached transforms can otherwise retain references to the | ||
| 50 | previous package. Ordinary development starts should retain the transform cache. |
docs/internals/mobile-navigation.md+51-133| ... | @@ -1,138 +1,56 @@ | ... | @@ -1,138 +1,56 @@ |
| 1 | # Mobile navigation | 1 | # Mobile navigation |
| 2 | 2 | ||
| 3 | The iOS Home and thread routes share the root native stack in | 3 | Home and thread routes share the [root native stack](../../apps/mobile/src/Stack.tsx) |
| 4 | [`Stack.tsx`](../../apps/mobile/src/Stack.tsx). Keeping them in one navigation | 4 | so UIKit can animate the header within one navigation controller. The iPad sidebar |
| 5 | controller lets UIKit animate the header between routes. The iPad sidebar owns | 5 | has its own stack. Splitting Home and threads between controllers loses that |
| 6 | a separate, single-screen stack; Android uses its own in-flow headers. | 6 | continuous transition. |
| 7 | 7 | ||
| 8 | Home and the iPad sidebar render their brand and connection status through | 8 | ## UIKit constraints |
| 9 | `headerTitle`, with `Threads` retained as the route title. The editor-style native | 9 | |
| 10 | bar aligns that title on the leading side. Do not model the brand as a toolbar | 10 | The [react-native-screens patch](../../patches/react-native-screens@4.26.2.patch) |
| 11 | button: on iOS 26.5 UIKit morphs a background-free custom leading item's rectangle | 11 | preserves behavior that is easy to break when changing native headers: |
| 12 | into the next screen's glass back button, even with distinct item identifiers. | 12 | |
| 13 | Using the title slot lets the brand and native back button animate separately. | 13 | - The brand belongs in `headerTitle`. On iOS 26.5, UIKit morphs a background-free |
| 14 | 14 | leading toolbar item's rectangle into the next screen's glass back button, | |
| 15 | The connection-status title has a maximum width based on its header's width and | 15 | even when the items have different identifiers. |
| 16 | trailing actions. A long environment name or larger text must not push Settings | 16 | - UIKit's scroll-edge fade does not recognize Fabric text views. An empty native |
| 17 | into UIKit's overflow menu. Keep the full status as the accessibility label | 17 | label supplies the custom title's geometry. Removing that apparently unused |
| 18 | while visually truncating it. The iPad sidebar uses its pane width, not the full | 18 | view changes the fade. |
| 19 | window width. | 19 | - Leading, trailing, and center item groups need independent caches. A button can |
| 20 | 20 | belong to only one group; rebuilding an unchanged group can pull its buttons | |
| 21 | On iOS 26, UIKit does not recognize Fabric's custom text views when shaping the | 21 | out of a group UIKit is still animating. Cache ownership also includes the |
| 22 | native scroll-edge fade. The screens patch gives custom title subviews an empty, | 22 | header config, whose event emitter changes on remount. |
| 23 | non-interactive native label matching their bounds. This supplies the fade's | 23 | - A horizontal ScrollView at its leading edge must yield to the full-screen back |
| 24 | geometry without drawing anything or turning the title back into a toolbar item. | 24 | gesture. Upstream gives every horizontal ScrollView priority, so swiping back |
| 25 | Check the top scroll fade as well as push/pop when changing these title views. | 25 | on a code block or table can only bounce its content. |
| 26 | 26 | ||
| 27 | The react-native-screens patch caches leading, trailing, and center item groups | 27 | The patch adds native header props that require code generation and a new binary. |
| 28 | independently. A button can belong to only one group: constructing another group | 28 | Patch rebuild guidance lives in the |
| 29 | with the same button removes it from the previous one. Unrelated menu updates | 29 | [mobile development README](../../apps/mobile/README.md#development). |
| 30 | must therefore preserve the other groups while UIKit may be animating them. | ||
| 31 | |||
| 32 | Each cache includes the owning header config, its item values, and custom native | ||
| 33 | item identities, so changed content or remounted event emitters still rebuild. | ||
| 34 | |||
| 35 | Custom header identifiers require native code generation and a new mobile build; | ||
| 36 | an over-the-air JavaScript update alone is insufficient. The Android view manager | ||
| 37 | implements the generated identifier setter as a no-op because this behavior is | ||
| 38 | specific to iOS 26 and later. | ||
| 39 | |||
| 40 | On iOS 26 the full-screen back swipe is UIKit's `interactiveContentPopGestureRecognizer`, | ||
| 41 | and react-native-screens makes it wait for any horizontally scrollable ScrollView to | ||
| 42 | fail first. Upstream applies that to every horizontal ScrollView regardless of | ||
| 43 | position, so a back swipe on a code block or table that is already at its leading | ||
| 44 | edge only bounces. The patch narrows the rule to ScrollViews with content still | ||
| 45 | hidden to the left; at the leading edge they yield to the pop gesture like plain | ||
| 46 | text does. | ||
| 47 | |||
| 48 | After changing a dependency patch, refresh CocoaPods before rebuilding an | ||
| 49 | existing iOS project. pnpm installs each patch hash in a different directory; | ||
| 50 | an old Pods project can keep compiling the previous directory even though Metro | ||
| 51 | and `apps/mobile/node_modules` resolve to the new patch. | ||
| 52 | |||
| 53 | `ControlPillMenu` resolves semantic icon colors through `withUniwind` and supplies them to every | ||
| 54 | iOS `MenuView` action, including nested actions. The menu library's Fabric bridge converts a missing | ||
| 55 | `imageColor` to transparent, so callers should use this wrapper instead of | ||
| 56 | rendering `MenuView` directly. Explicit colors are preserved, and destructive | ||
| 57 | actions default to the theme's danger foreground color. Native stack header menus | ||
| 58 | use a separate implementation and do not need this workaround. | ||
| 59 | 30 | ||
| 60 | ## Native media presentations | 31 | ## Native media presentations |
| 61 | 32 | ||
| 62 | `PresentationSource` in `NativePresentation` registers a thumbnail for AVKit, | 33 | iOS delegates full-screen video to [AVKit](../../apps/mobile/modules/t3-native-controls/ios/T3NativeVideoPresentation.swift) |
| 63 | image zoom transitions, and UIKit's share sheet. Wrap the thumbnail as its single child | 34 | and images and documents to [Quick Look](../../apps/mobile/modules/t3-native-controls/ios/T3NativeFilePresentation.swift). |
| 64 | and pass the stable identifier to the presentation. The registry keeps weak | 35 | Each framework owns its controls and transition. A separate UIKit zoom transition |
| 65 | references to source views; recycled or compact composer thumbnails can register | 36 | prevents AVKit's native Close action from exiting full screen and interferes with |
| 66 | the same identifier. Identifiers must distinguish simultaneously visible attachments. | 37 | Quick Look's interactive return to its thumbnail. The AVKit entry selector is |
| 67 | The source registration does not own the preview. Android uses a regular view. | 38 | guarded, with standard modal presentation as the fallback. |
| 68 | 39 | ||
| 69 | On iOS, video previews mount `AVPlayerViewController` temporarily inside the | 40 | [Thumbnail registration](../../apps/mobile/modules/t3-native-controls/ios/T3NativePresentation.swift) |
| 70 | registered source and enter full screen through AVKit. AVKit | 41 | holds weak view references for transition and share-sheet anchors. It does not own |
| 71 | owns that zoom, its playback controls, Close button, and interactive dismissal. | 42 | the preview; a source row can disappear while a native presentation is open. |
| 72 | Do not replace AVKit's transition with `preferredTransition`: in the iOS 27 | 43 | Identifiers must distinguish attachments that are visible at the same time. |
| 73 | simulator, that leaves native Close unable to exit full screen. When the source is unavailable, | 44 | Native promises complete after dismissal so callers keep local file leases until |
| 74 | the player uses a standard modal presentation. Programmatic entry uses the same | 45 | the preview or share flow has finished. |
| 75 | guarded `enterFullScreenAnimated:completionHandler:` selector as Expo Video; | 46 | |
| 76 | if that selector is unavailable, the player also falls back to a standard modal. | 47 | Treat a dismissal request as pending until UIKit finishes its current transition. |
| 77 | 48 | Starting another dismissal while presentation or an interactive dismissal is | |
| 78 | `FilePreviewModal` resolves image and PDF sources from a URI, a signed environment asset, | 49 | settling can strand the controller. Video cleanup restores the previous audio |
| 79 | or a retained composer file. On iOS, Quick Look owns image and document layout, controls, | 50 | configuration only if it still matches the preview's configuration. It must not |
| 80 | zooming, sharing, and interactive dismissal. Its delegate supplies the registered thumbnail | 51 | deactivate the shared audio session, which may belong to another player or recorder. |
| 81 | and its bounds for Quick Look's source-view zoom. Do not layer `preferredTransition` or | 52 | |
| 82 | another image scroll view over that presentation: Quick Look coordinates its image gestures | 53 | Video playback holds the first signed asset URL for the lifetime of the preview. |
| 83 | with the return to the thumbnail. Missing sources use the standard transition, and Reduce | 54 | Following credential refreshes reactively would restart playback. Quick Look copies |
| 84 | Motion disables animation. A pending programmatic Close waits until the current presentation | 55 | original bytes into its own temporary directory so preview and sharing cannot |
| 85 | or cancelled dismissal has settled before starting another transition. | 56 | mutate a draft or workspace file. |
| 86 | |||
| 87 | The shared native presenter copies original bytes into its own temporary directory and | ||
| 88 | removes that copy after dismissal. Network downloads write to disk, and sharing never edits | ||
| 89 | the source attachment. Draft images use their stored upload data rather than a potentially | ||
| 90 | expired picker URI. No React Navigation route or custom transition animator is needed. | ||
| 91 | |||
| 92 | The same viewer handles message images, markdown images, PDF attachments and links, | ||
| 93 | composer thumbnails, and workspace image previews. The workspace PDF web preview has an | ||
| 94 | Open PDF action for the native viewer. Android retains its image viewer and uses the | ||
| 95 | system chooser for PDFs. Saving images on iOS uses the add-only photo-library permission. | ||
| 96 | |||
| 97 | Every full-screen video preview on iOS is AVKit, playing directly from the signed asset | ||
| 98 | URL: sent attachments, composer drafts, and file links that point at a video. Inline | ||
| 99 | markdown embeds and the workspace file screen play in place with the Expo Video player. | ||
| 100 | AVKit handles buffering; the client does not download the entire file or show a separate | ||
| 101 | opening overlay before presentation. | ||
| 102 | The URL is captured once per preview so credential refresh does not restart playback. | ||
| 103 | Media actions (copy path, open in file viewer, save or share) belong to the surface that | ||
| 104 | opened the video, a long-press on the thumbnail, so the player carries no | ||
| 105 | menu. AVKit has no retry; a failure alerts and closes, re-mints the URL in the background, and the next open plays from the fresh one. | ||
| 106 | |||
| 107 | The native presentation promise completes after dismissal. Local draft previews | ||
| 108 | hold their file lease until that promise settles. The iOS preview component requests | ||
| 109 | native dismissal when its source screen unmounts. Playback pauses in the background. | ||
| 110 | AVPlayer activates audio as playback starts. The presenter pauses and releases | ||
| 111 | its own player on close, then restores the previous audio-session configuration | ||
| 112 | if no other component changed it during playback. It does not deactivate the | ||
| 113 | shared session, which may still serve another player or recorder. Android plays every | ||
| 114 | video, attachments included, in its React Native modal with the Expo Video player. | ||
| 115 | Composer drafts play and share from their local file; other videos stream from the | ||
| 116 | signed URL and download only for save or share. | ||
| 117 | |||
| 118 | `shareFileFromSource` uses the same source registration to anchor UIKit's activity | ||
| 119 | controller. Its promise completes when the native share flow finishes, keeping | ||
| 120 | the existing attachment lease and foreground handoff active for that duration. | ||
| 121 | Android uses Expo Sharing. On iOS, received and draft video attachments expose | ||
| 122 | Save or share through `VideoAttachmentMenu`. The attachment supplies the source | ||
| 123 | identifier, and the native share presentation inherits its appearance. AVKit's | ||
| 124 | iOS playback controls do not expose a public custom-share-action API. | ||
| 125 | |||
| 126 | Video attachment thumbnails use Expo Video's native frame extraction and Expo Image. | ||
| 127 | Received attachments use their signed asset URL; drafts retain and resolve their local file | ||
| 128 | until extraction ends. Extraction is serial; temporary players never play or change audio settings. Leaving the screen cancels | ||
| 129 | pending work; a 15-second limit prevents an unreachable source from holding up the queue. | ||
| 130 | The client keeps at most 32 native images, each bounded to 480 pixels per side, keyed by | ||
| 131 | environment and attachment identity rather than expiring URLs. Images still displayed keep | ||
| 132 | their own references when evicted from that cache. | ||
| 133 | |||
| 134 | The asset HTTP route supports single byte ranges for videos so iOS can read metadata and | ||
| 135 | frames without first downloading the whole file. Normal downloads keep their full response; | ||
| 136 | unsupported ranges and conditional `If-Range` requests also fall back to the full file. | ||
| 137 | An older environment without range support may still show the play-card fallback. Thumbnail | ||
| 138 | failure never disables playback or sharing. |
docs/internals/model-manifest.md+23-40| ... | @@ -1,42 +1,25 @@ | ... | @@ -1,42 +1,25 @@ |
| 1 | # Model manifest | 1 | # Model manifest |
| 2 | 2 | ||
| 3 | `apps/server/src/provider/model-manifest.json` is bundled for offline startup and fetched from | 3 | The [bundled manifest](../../apps/server/src/provider/model-manifest.json) allows |
| 4 | `main` at runtime. A remote fetch replaces the in-memory and on-disk cache only after generic | 4 | offline startup; fetching it from `main` lets model metadata change between |
| 5 | catalog references and provider-owned adapter data validate. A failed or invalid fetch keeps the | 5 | releases. Failed fetches or invalid data preserve the last usable manifest. |
| 6 | last successful remote manifest. The bundle is used when no valid remote cache exists, or when | 6 | Remote data must pass both catalog-reference validation and the owning provider's |
| 7 | the bundle's `updatedAt` is later than the cached manifest's, so a release that edits the manifest | 7 | adapter validation before replacing the cache. |
| 8 | takes effect before the next successful fetch. Bump `updatedAt` whenever you edit the file. | 8 | |
| 9 | 9 | A newer bundle outranks the cached remote manifest by `updatedAt`, so a release can | |
| 10 | The top-level provider catalog is generic: models contain presentation metadata, aliases, status, | 10 | correct model data before the next successful fetch. Bump `updatedAt` whenever the |
| 11 | an optional badge, and a reusable capability profile. The profile and model `adapter` fields are | 11 | file changes. Fetch time cannot establish which copy contains the newer edit. |
| 12 | opaque until the owning provider validates them with its own allowlisted schema. | 12 | |
| 13 | 13 | Generic catalog data describes presentation and capabilities. Each provider owns | |
| 14 | Claude Code uses the manifest as its complete built-in model catalog. To add a Claude model that | 14 | its adapter schema and dispatch mappings. Claude uses the manifest for its entire |
| 15 | uses an existing profile, add one object to `providers.claudeAgent.models`. Do not add a test or | 15 | built-in catalog. Adding a model with an existing capability profile is a JSON |
| 16 | change application code. Add or change a profile in the same JSON file only when the model exposes | 16 | edit; a new profile is needed only for a new capability combination. Codex still |
| 17 | a capability combination that does not already exist. | 17 | gets its model list from its app server. |
| 18 | 18 | ||
| 19 | `currentModels.claudeAgent` is retained as a frozen compatibility field for releases that predate | 19 | `currentModels.claudeAgent` is frozen for releases that predate catalog discovery. |
| 20 | catalog discovery. New Claude models do not need to be added there. Codex still discovers models | 20 | Do not extend it when adding Claude models. Codex uses `currentModels.codex` as a |
| 21 | from its app server and uses `currentModels.codex` only as a legacy-classification overlay. | 21 | legacy-classification overlay for discovered models. |
| 22 | 22 | ||
| 23 | Claude model entries support: | 23 | Model data is schema-validated configuration. Tests should cover resolver, cache, |
| 24 | 24 | and adapter semantics with synthetic model names, so adding a model never requires | |
| 25 | - `aliases`, `status`, `badge`, and `profile` for client presentation and selection. | 25 | tests that repeat the configuration. |
| 26 | - `adapter.claudeCode.minVersion` and `maxVersionExclusive` for installed-runtime compatibility. | ||
| 27 | - Profile-level effort mappings, model suffixes, and context-window metadata for dispatch. | ||
| 28 | |||
| 29 | ## Test policy | ||
| 30 | |||
| 31 | Changing model data does not require tests. Do not add or update tests for a model slug, display | ||
| 32 | name, alias, legacy status, version boundary, badge, or profile assignment. The bundled manifest is | ||
| 33 | configuration and is validated by its schema when imported. | ||
| 34 | |||
| 35 | Add tests only when implementation behavior changes: | ||
| 36 | |||
| 37 | - Fetching, caching, fallback, or schema-version handling changes in the manifest service. | ||
| 38 | - Provider-neutral profile resolution gains new semantics. | ||
| 39 | - A provider adapter gains a new compatibility or dispatch mapping type. | ||
| 40 | |||
| 41 | Resolver tests must use synthetic providers and model names so normal JSON edits never create test | ||
| 42 | churn. |
docs/internals/overview.md+65-177| ... | @@ -1,179 +1,67 @@ | ... | @@ -1,179 +1,67 @@ |
| 1 | # Architecture | 1 | # Architecture |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | T3 Code keeps execution in the environment that owns the workspace. Web, desktop, and mobile |
| 4 | 4 | clients control it over authenticated RPC. A remote client must never substitute its own filesystem, | |
| 5 | T3 Code is a server runtime that owns agent sessions, workspaces, and version control, plus clients | 5 | provider credentials, or machine state for the environment's. The desktop app bundles a server, |
| 6 | (web, desktop, mobile) that talk to it over one authenticated Effect RPC WebSocket. The server is the | 6 | but its renderer follows the same boundary. |
| 7 | execution boundary: every provider process, terminal, git operation, and filesystem read happens | 7 | |
| 8 | there, never in the client. | 8 | ## Ownership boundaries |
| 9 | 9 | ||
| 10 | ``` | 10 | Provider processes, terminals, Git, and project files belong to the server. Shared connection and |
| 11 | ┌────────────────────────────────────────────────┐ | 11 | domain state belongs in `packages/client-runtime`; clients supply platform services and UI. |
| 12 | │ Clients: apps/web, apps/desktop, apps/mobile │ | 12 | Keeping that logic shared prevents reconnect and multi-environment behavior from diverging between |
| 13 | │ shared runtime: packages/client-runtime │ | 13 | web and mobile. See [connection runtime](./connection-runtime.md) and |
| 14 | │ connection supervisor, RPC session, Atom state│ | 14 | [remote environments](./remote.md). |
| 15 | └──────────────────┬─────────────────────────────┘ | 15 | |
| 16 | │ Effect RPC over WebSocket (/ws) | 16 | The [RPC contract](../../packages/contracts/src/rpc.ts) is the boundary between independently |
| 17 | │ contract: packages/contracts | 17 | versioned clients and servers. Subscriptions send the state a client needs, so a client viewing one |
| 18 | ┌──────────────────â–¼─────────────────────────────┐ | 18 | thread does not pay for every thread's history. Authentication of a socket does not authorize every |
| 19 | │ apps/server │ | 19 | method on it. See [environment auth](./environment-auth.md). |
| 20 | │ orchestration engine (event-sourced) │ | 20 | |
| 21 | │ provider driver registry (6 built-in drivers) │ | 21 | Provider-specific behavior belongs behind an adapter. Orchestration works with normalized commands |
| 22 | │ checkpointing, VCS, terminals, filesystem │ | 22 | and events, so adding a provider should not require branches throughout the domain or clients. |
| 23 | └──────────────────┬─────────────────────────────┘ | 23 | See [provider constraints](./providers.md). |
| 24 | │ per-driver transport | 24 | |
| 25 | ┌──────────────────â–¼─────────────────────────────┐ | 25 | ## Durable intent and side effects |
| 26 | │ Agent CLIs: Codex, Claude, Cursor, Grok, │ | 26 | |
| 27 | │ OpenCode, Antigravity │ | 27 | The event log is the source of truth for orchestration state. The |
| 28 | └────────────────────────────────────────────────┘ | 28 | [engine](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts) serializes commands; |
| 29 | ``` | 29 | the [decider](../../apps/server/src/orchestration/decider.ts) produces events without performing |
| 30 | 30 | provider or filesystem work. Events, persisted projections, and the accepted command receipt commit | |
| 31 | ## The RPC boundary | 31 | in one database transaction. The in-memory state changes and subscribers receive events after that |
| 32 | 32 | commit. This keeps command retries idempotent and prevents a persisted projection from getting | |
| 33 | The client/server contract is an Effect RPC group, not a hand-rolled push protocol. [`rpc.ts`][rpc] | 33 | ahead of the event log. |
| 34 | declares `WS_METHODS` and assembles `WsRpcGroup`; each member is either unary or a server stream | 34 | |
| 35 | (`stream: true`). Streaming members such as `orchestration.subscribeShell`, | 35 | Reactors perform side effects after intent has been recorded, then feed results back through |
| 36 | `orchestration.subscribeThread`, `subscribeServerConfig`, and `terminal.attach` replace what used to | 36 | commands. A command acknowledgement therefore means the intent committed, not that the provider, |
| 37 | be a broadcast push bus: a client subscribes to what it needs and the server pushes only on that | 37 | checkpoint, or other follow-up work finished. Keep external I/O out of the decider and the database |
| 38 | subscription. | 38 | transaction. |
| 39 | 39 | ||
| 40 | [`ws.ts`][ws] serves the group. `websocketRpcRouteLayer` mounts `GET /ws`, authenticates the upgrade | 40 | Persisted events must remain decodable on replay. Changing a schema affects old environments at |
| 41 | through `EnvironmentAuth.authenticateWebSocketUpgrade`, then hands the socket to | 41 | startup as well as live RPC traffic. Compatibility work must account for stored history, not just |
| 42 | `RpcServer.toHttpEffectWebsocket`. Authorization is per method: `RPC_REQUIRED_SCOPE` maps each method | 42 | what the newest client sends. |
| 43 | to a scope, and `authorizeEffect`/`authorizeStream` enforce it. Holding a valid socket is not | 43 | |
| 44 | authorization to call everything on it. See [environment-auth.md](./environment-auth.md). | 44 | ## Turn completion and checkpoints |
| 45 | 45 | ||
| 46 | On the client, [`session.ts`][session] opens the socket and builds the typed client. | 46 | A turn ending and its follow-up work settling are separate milestones. The |
| 47 | `RpcSessionFactory` is the service; a session exposes `client`, `initialConfig`, `ready`, `probe`, | 47 | [projector](../../apps/server/src/orchestration/projector.ts) settles the turn from its session |
| 48 | and `closed`. It performs one attempt and does not retry. Retry, backoff, and offline policy belong | 48 | status. A late checkpoint or diff must not extend the recorded turn duration or keep the client |
| 49 | to the connection supervisor. | 49 | showing provider work as active. |
| 50 | 50 | ||
| 51 | ## Shared client runtime | 51 | [Checkpoints](../../apps/server/src/checkpointing/CheckpointStore.ts) use hidden Git refs to |
| 52 | 52 | capture workspace state without adding commits to the user's branch. A revert must coordinate | |
| 53 | `packages/client-runtime` holds every non-visual client concern: connection lifecycle, | 53 | workspace state with the provider conversation. A provider that cannot roll back its conversation |
| 54 | authentication, RPC, cached environment data, and domain state as Atom factories. Web and mobile | 54 | must reject that operation before changing the filesystem. |
| 55 | compose it the same way (`apps/web/src/connection/runtime.ts` and | 55 | |
| 56 | `apps/mobile/src/connection/runtime.ts` mirror each other, differing only in platform-specific | 56 | ## Waiting for asynchronous work |
| 57 | background-activity layers) and differ beyond that only in the platform layer they supply and the | 57 | |
| 58 | UI they build on top. React components never construct transports, retry loops, | 58 | Tests use [drainable workers](../../packages/shared/src/DrainableWorker.ts) to wait until both the |
| 59 | or RPC clients. See [connection-runtime.md](./connection-runtime.md). | 59 | queue and its current item have finished. An empty queue alone does not prove the worker is idle. |
| 60 | 60 | ||
| 61 | ## Orchestration is event-sourced | 61 | Runtime receipts mark specific test milestones. Their |
| 62 | 62 | [production layer](../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts) is a no-op; | |
| 63 | The server does not mutate app state directly. Clients dispatch typed commands; the engine turns them | 63 | production behavior must use persisted state and events. These test signals are separate from the |
| 64 | into persisted events; projections derive the read model. | 64 | durable command receipts that make dispatch idempotent. |
| 65 | 65 | ||
| 66 | [`OrchestrationEngine.ts`][engine] serializes this. `dispatch` offers a `CommandEnvelope` onto | 66 | See the [glossary](./glossary.md) for shared terms and the |
| 67 | `commandQueue` and awaits its result; a single worker fiber takes envelopes one at a time, so command | 67 | [development runbook](../operations/development.md) for setup and checks. |
| 68 | processing is totally ordered. For each envelope `processEnvelope`: | ||
| 69 | |||
| 70 | 1. checks the durable command receipt, making retries idempotent; | ||
| 71 | 2. runs `decideOrchestrationCommand` ([`decider.ts`][decider]) to produce events from command plus | ||
| 72 | current state, pure and side-effect free; | ||
| 73 | 3. inside one SQL transaction, appends events to the event store, applies them to the in-memory read | ||
| 74 | model via [`projector.ts`][projector], projects them into persisted tables, and writes the | ||
| 75 | accepted receipt; | ||
| 76 | 4. after commit, swaps in the new read model, cleans up attachments, and publishes committed events | ||
| 77 | to subscribers. Attachment cleanup failures are logged and do not reject committed commands. | ||
| 78 | |||
| 79 | Because persistence and projection share a transaction, the read model cannot durably disagree with | ||
| 80 | the event log. On dispatch failure the engine rereads persisted events past the starting sequence and | ||
| 81 | reconciles. | ||
| 82 | |||
| 83 | Command and event names live in [`orchestration.ts`][contracts]. Some commands are client | ||
| 84 | dispatchable (`thread.create`, `thread.turn.start`, `thread.approval.respond`); others are internal | ||
| 85 | and produced only by server-side reactors (`thread.message.assistant.delta`, | ||
| 86 | `thread.turn.diff.complete`). | ||
| 87 | |||
| 88 | A turn is complete when its session leaves `running` status, projected by | ||
| 89 | `settledTurnStateForSessionStatus` in [`projector.ts`][projector]. Checkpoint work settling later | ||
| 90 | does not define turn end. | ||
| 91 | |||
| 92 | Thread settlement is server-owned. Each server's own settings control PR and inactivity | ||
| 93 | settlement. Those keys are user preferences, so clients write them to every shared-settings sync | ||
| 94 | target (`SHARED_SERVER_SETTING_KEYS` in `packages/client-runtime/src/state/sharedSettings.ts`) and | ||
| 95 | warn when another target drifts. A target must have an active connection and advertise the | ||
| 96 | `threadAutoSettlement` capability, which signals that the server can hold every shared key. | ||
| 97 | [`ThreadSettlementReactor`][settlement] checks threads at startup, when those settings change, and | ||
| 98 | once per minute, including when no client is connected. It dispatches the guarded internal | ||
| 99 | `thread.auto-settle` command, which uses the existing settlement event lifecycle. Automatic | ||
| 100 | settlement excludes live background work and requires a comparable PR timestamp for immediate PR | ||
| 101 | settlement. The command carries the latest activity timestamp and rejects any later event for its | ||
| 102 | thread after the reactor's snapshot. The sweep looks a branch up from the thread's worktree when it | ||
| 103 | still exists, so it shares the per-cwd PR cache the sidebar polls instead of spending a second host | ||
| 104 | request. Clients render the persisted settlement state and do not derive settlement from PR or | ||
| 105 | inactivity state. A committed `thread.settled` event also lets `ProviderCommandReactor` stop an idle | ||
| 106 | provider session. | ||
| 107 | |||
| 108 | At turn completion, `CheckpointReactor` refreshes PR discovery when the checkout matches the | ||
| 109 | thread's non-default branch. `VcsStatusBroadcaster` requires loaded remote status and permission | ||
| 110 | from background policy. `GitManager` retries only a successful "no PR" cache entry for the current | ||
| 111 | branch, preserving known PRs and failure backoff without fetching remotes. Remote status reads | ||
| 112 | that write the broadcaster cache share a lock per cwd, including the initial status load. | ||
| 113 | |||
| 114 | ## Drainable workers | ||
| 115 | |||
| 116 | Follow-up work runs asynchronously in queue-backed workers built on [`DrainableWorker`][worker]: | ||
| 117 | [`ProviderRuntimeIngestion`][ingest] normalizes provider runtime streams into orchestration commands, | ||
| 118 | [`ProviderCommandReactor`][cmd] dispatches provider calls in response to intent events, | ||
| 119 | [`CheckpointReactor`][checkpoint] captures and reverts workspace checkpoints, and | ||
| 120 | [`ThreadSettlementReactor`][settlement] evaluates server-owned automatic settlement rules. | ||
| 121 | |||
| 122 | `DrainableWorker` pairs a transactional queue with a transactional count of outstanding items. | ||
| 123 | `enqueue` atomically offers and increments; processing always decrements. `drain` retries until the | ||
| 124 | count reaches zero, so a test can await "queue empty and current item finished" instead of sleeping. | ||
| 125 | Each of these four services exposes `drain` for exactly this. | ||
| 126 | |||
| 127 | Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in | ||
| 128 | [`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not | ||
| 129 | build production behavior on receipts. | ||
| 130 | |||
| 131 | ## Provider drivers | ||
| 132 | |||
| 133 | Six drivers ship built in, registered in [`builtInDrivers.ts`][drivers] as `BUILT_IN_DRIVERS`: | ||
| 134 | Codex, Claude, Cursor, Grok, OpenCode, and Antigravity. A driver declares its kind and config schema and creates a | ||
| 135 | scoped adapter; `ProviderInstanceRegistry` owns live instances and `ProviderAdapterRegistry` resolves | ||
| 136 | an instance to its adapter, so `ProviderService` routes session and turn operations without knowing | ||
| 137 | which agent is behind them. See [providers.md](./providers.md). | ||
| 138 | |||
| 139 | ## Checkpointing | ||
| 140 | |||
| 141 | Each turn is bracketed by workspace checkpoints so diffs and reverts are exact. `CheckpointStore` | ||
| 142 | captures state as hidden Git refs through the VCS driver's checkpoint operations; | ||
| 143 | `CheckpointDiffQuery` answers turn and full-thread diff requests; `CheckpointReactor` coordinates | ||
| 144 | baseline capture, completed-turn capture, diff projection, and reverting both the workspace and the | ||
| 145 | provider conversation. The storage contract is `VcsCheckpointOps` in | ||
| 146 | [`VcsDriver.ts`](../../apps/server/src/vcs/VcsDriver.ts), implemented for Git in the same directory. | ||
| 147 | |||
| 148 | ## Startup | ||
| 149 | |||
| 150 | [`serverRuntimeStartup.ts`][startup] runs a fixed lifecycle: start keybindings, settings, and | ||
| 151 | reactors; publish welcome; signal command readiness (logged as `Accepting commands`); wait for the | ||
| 152 | HTTP listener via `markHttpListening`; publish ready; fork the heartbeat; then either print headless | ||
| 153 | output or open the browser. Command readiness precedes the listener, so a socket that opens can | ||
| 154 | already dispatch. | ||
| 155 | |||
| 156 | ## Related | ||
| 157 | |||
| 158 | - [Workspace layout](./workspace-layout.md), [Glossary](./glossary.md) | ||
| 159 | - [Mobile navigation headers](./mobile-navigation.md) | ||
| 160 | - [Remote environments](./remote.md), [Server updates](./server-updates.md) | ||
| 161 | - [Resource telemetry](./resource-telemetry.md) | ||
| 162 | - [Product analytics](./product-analytics.md) | ||
| 163 | - [Scripts](./scripts.md), [CI gates](./ci.md) | ||
| 164 | |||
| 165 | [rpc]: ../../packages/contracts/src/rpc.ts | ||
| 166 | [contracts]: ../../packages/contracts/src/orchestration.ts | ||
| 167 | [ws]: ../../apps/server/src/ws.ts | ||
| 168 | [session]: ../../packages/client-runtime/src/rpc/session.ts | ||
| 169 | [startup]: ../../apps/server/src/serverRuntimeStartup.ts | ||
| 170 | [engine]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts | ||
| 171 | [decider]: ../../apps/server/src/orchestration/decider.ts | ||
| 172 | [projector]: ../../apps/server/src/orchestration/projector.ts | ||
| 173 | [worker]: ../../packages/shared/src/DrainableWorker.ts | ||
| 174 | [ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | ||
| 175 | [cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | ||
| 176 | [checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts | ||
| 177 | [settlement]: ../../apps/server/src/orchestration/ThreadSettlementReactor.ts | ||
| 178 | [receipts]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts | ||
| 179 | [drivers]: ../../apps/server/src/provider/builtInDrivers.ts |
docs/internals/product-analytics.md+47-175| ... | @@ -1,177 +1,49 @@ | ... | @@ -1,177 +1,49 @@ |
| 1 | # Product analytics | 1 | # Product analytics |
| 2 | 2 | ||
| 3 | T3 Code sends anonymous product events from the server to PostHog. The server | 3 | The server owns PostHog delivery, opt-out, and identity for every connected client. |
| 4 | uses the first available hashed Codex account ID, hashed Claude user ID, or | 4 | [Identity selection](../../apps/server/src/telemetry/Identify.ts) hashes an available |
| 5 | installation-scoped anonymous ID as the distinct ID. It also keeps the | 5 | provider account ID, falling back to an installation-scoped ID. This identity can |
| 6 | telemetry opt-out, event buffer, and batch delivery. Clients do not load the | 6 | span several clients; it does not identify a browser session. Clients do not load |
| 7 | PostHog browser SDK. | 7 | the PostHog browser SDK. Client-use events require an authenticated connection, so |
| 8 | 8 | visiting the hosted app without connecting does not count as product use. | |
| 9 | ## Client events | 9 | |
| 10 | 10 | ## Attribution boundaries | |
| 11 | These events use the metadata from the WebSocket connection that caused them. | 11 | |
| 12 | The metadata is not a person property or server-global current-client value. | 12 | Client dimensions belong to the event's WebSocket connection. A server-global |
| 13 | Two clients connected to one server can report different values at the same | 13 | "current client" would misattribute simultaneous web, desktop, and mobile use. |
| 14 | time. | 14 | Provider execution has its own events because a turn can outlive the requesting |
| 15 | 15 | connection. | |
| 16 | | Event | Description | | 16 | |
| 17 | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | 17 | Keep client and server dimensions separate. A desktop host can serve a phone or a |
| 18 | | `client.connected` | The server accepted an authenticated WebSocket connection. Reconnects count again. Use this event for connection diagnostics, not active-use counts. | | 18 | remote browser, and a direct connection can cross a network. Older clients omit |
| 19 | | `client.thread.started` | The server accepted a command that created a thread. | | 19 | metadata. Missing client values must stay unknown rather than being backfilled |
| 20 | | `client.turn.requested` | The server accepted a turn request. This is the standard active-use event. | | 20 | from server properties. The legacy `clientType` property describes how the server |
| 21 | 21 | runs; use `surface` for the connected client. | |
| 22 | `provider.turn.sent` stays a provider execution event. It does not receive | 22 | |
| 23 | client metadata because a provider turn can continue after the requesting | 23 | ## Interpreting events |
| 24 | client disconnects. | 24 | |
| 25 | 25 | Use `client.turn.requested` for active-use reports. `client.connected` counts | |
| 26 | ## Provider turn events | 26 | reconnects, so network behavior can inflate it. One identity can appear in several |
| 27 | 27 | client groups during a period; adding those groups double-counts users. | |
| 28 | `provider.turn.completed` records one anonymous event when a provider emits a | 28 | |
| 29 | canonical completed or aborted turn. The event reports normalized main-agent | 29 | Provider send and completion counts need not match. Providers can emit synthetic |
| 30 | token counts when the provider supplies enough data. | 30 | turns without a send request. Collection is best effort, with no scan or backfill |
| 31 | 31 | of provider history. | |
| 32 | | Property | Values and meaning | | 32 | |
| 33 | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | 33 | Token totals cover the main agent. Child agents and model rerouting prevent a turn |
| 34 | | `provider` | Provider driver, such as `codex`, `claudeAgent`, or `opencode`. Use this as the harness breakdown. | | 34 | from representing one provider/model combination's full cost. For provider |
| 35 | | `model` | Selected or provider-reported model when known. This is metadata, not proof that one model produced every token. | | 35 | comparisons, require complete usage, no observed subagents, and no mixed models; |
| 36 | | `effort` | Selected or provider-reported reasoning effort when known. | | 36 | compare matching model, effort, interaction mode, and terminal status. Aggregate |
| 37 | | `interactionMode` | Turn interaction mode, `default` or `plan`, when known. | | 37 | output/input ratios should divide the summed totals. Averaging per-turn ratios |
| 38 | | `runtimeMode` | Session permission mode when known. | | 38 | lets small-input turns dominate. |
| 39 | | `mixedModels` | `true` when T3 Code observed `model.rerouted` during the turn. This does not split token counts by model. | | 39 | |
| 40 | | `durationMs` | Time from the observed turn start or accepted send request to the terminal event. Omitted when start metadata is not available. | | 40 | Unknown counts stay absent. Partial usage contains valid observed counts but |
| 41 | | `terminalStatus` | `completed`, `failed`, `interrupted`, or `cancelled`. | | 41 | cannot establish a whole-turn total. Keep these distinctions when changing token |
| 42 | | `usageStatus` | `complete`, `partial`, or `unavailable`. | | 42 | normalization or building reports. |
| 43 | | `usageScope` | `main_agent`. Child-agent token use is not included. | | 43 | |
| 44 | | `hasSubagents` | Whether the adapter observed child agents during the turn. Omitted when the adapter does not provide a normalized usage report. | | 44 | ## Collection boundary |
| 45 | | `inputTokens` | Total main-agent input tokens. This includes uncached input, cache reads, and cache creation or writes. | | 45 | |
| 46 | | `cachedInputTokens` | Cache-read tokens. This is a subset of `inputTokens`. | | 46 | Keep analytics payloads to product metadata and normalized measurements. Do not |
| 47 | | `cacheCreationTokens` | Cache creation or write tokens. This is a subset of `inputTokens`. | | 47 | send prompts, authentication material, raw provider payloads, user-assigned device |
| 48 | | `outputTokens` | Total main-agent output tokens, including reasoning tokens. | | 48 | names, or conversation identifiers. Client metadata is best effort; invalid values |
| 49 | | `reasoningTokens` | Reasoning tokens when the provider reports them separately. This is a subset of `outputTokens`. | | 49 | must not reject a connection. PostHog person profiles remain disabled. |
| 50 | |||
| 51 | `complete` means the provider supplied full input and output totals for the | ||
| 52 | turn. `partial` means each included count is valid, but T3 Code did not observe | ||
| 53 | the full turn. This can happen after an interruption, provider failure, or | ||
| 54 | reconnect. `unavailable` means the provider did not supply trustworthy counts. | ||
| 55 | Numeric properties are omitted when their values are unknown. They are not set | ||
| 56 | to zero. | ||
| 57 | |||
| 58 | Codex uses deltas from its cumulative token totals, and falls back to the | ||
| 59 | newest response's `last` usage when no prior total exists or the total was | ||
| 60 | reset. Claude uses the final result's per-turn `usage` value and does not use | ||
| 61 | cumulative `modelUsage`. | ||
| 62 | OpenCode sums unique main-session step totals. Cursor, Grok, and Antigravity | ||
| 63 | usage stays `unavailable` until their ACP token fields and scope are verified. | ||
| 64 | |||
| 65 | The completed event count does not have to match `provider.turn.sent`. For | ||
| 66 | example, Claude can emit synthetic turns without a matching send request. | ||
| 67 | Duplicate terminal events for the same provider instance, thread, and turn | ||
| 68 | produce one analytics event. | ||
| 69 | |||
| 70 | Collection is best effort and starts with the first server release that | ||
| 71 | contains this event. T3 Code does not scan provider history or backfill older | ||
| 72 | turns. | ||
| 73 | |||
| 74 | ## Recommended properties | ||
| 75 | |||
| 76 | Client properties appear on the three client events when the connected client | ||
| 77 | reports them. Older clients can omit every client property. | ||
| 78 | |||
| 79 | | Property | Values and meaning | | ||
| 80 | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| 81 | | `surface` | Product client: `web`, `desktop`, or `mobile`. | | ||
| 82 | | `webDeployment` | Web delivery: `hosted` for the hosted app or `server` for web files served by a T3 server. Web only. This does not describe connection distance. | | ||
| 83 | | `clientOs` | `macOS`, `Windows`, `Linux`, `iOS`, `Android`, `ChromeOS`, `other`, or `unknown`. | | ||
| 84 | | `clientDeviceType` | `desktop`, `phone`, `tablet`, or `unknown`. This is separate from `surface`. | | ||
| 85 | | `clientBrowser` | Normalized browser family. Web only. Browser detection is best effort. | | ||
| 86 | | `clientAppVersion` | Version of the connected client. | | ||
| 87 | | `clientOsMajorVersion` | Client OS major version when the native client reports it. Initially mobile only. | | ||
| 88 | | `clientDeviceModel` | Hardware model when the native client reports it. Initially mobile only. This is not a user-assigned device name. | | ||
| 89 | | `connectionMethod` | `direct`, `ssh`, `relay`, or `unknown`. `direct` means that the client connected to the server endpoint without an SSH or relay connection. It does not mean both processes run on one machine. | | ||
| 90 | |||
| 91 | Server properties appear on all events, including server boot and background | ||
| 92 | events. | ||
| 93 | |||
| 94 | | Property | Values and meaning | | ||
| 95 | | ------------------ | -------------------------------------------------------------- | | ||
| 96 | | `serverOs` | Server process OS, normalized to the same names as `clientOs`. | | ||
| 97 | | `serverArch` | Server process architecture. | | ||
| 98 | | `serverWslDistro` | WSL distribution from `WSL_DISTRO_NAME`, when present. | | ||
| 99 | | `serverAppVersion` | T3 server version. | | ||
| 100 | | `serverMode` | Server runtime mode: `desktop` or `web`. | | ||
| 101 | |||
| 102 | ## Legacy properties | ||
| 103 | |||
| 104 | Existing property meanings do not change: | ||
| 105 | |||
| 106 | - `clientType` describes how the server runs. It is `desktop-app` for a desktop | ||
| 107 | server and `cli-web-client` for a CLI web server. It does not describe the | ||
| 108 | connected client. Use `surface` and `webDeployment` for new reports. | ||
| 109 | - `platform`, `arch`, `wsl`, and `t3CodeVersion` describe the server. Use the | ||
| 110 | new `server*` names for new reports. | ||
| 111 | - `appVersion` describes the connected client. Use `clientAppVersion` for new | ||
| 112 | reports. | ||
| 113 | - Mobile connection events keep `os`, `osMajorVersion`, and `deviceModel`. | ||
| 114 | Use the new `client*` names for new reports. | ||
| 115 | |||
| 116 | ## PostHog dashboard | ||
| 117 | |||
| 118 | Create one saved dashboard named `Client and platform usage`. Set | ||
| 119 | `client.turn.requested` as the event for active-use reports. A user can appear | ||
| 120 | in several client groups during one period, so do not add breakdown values to | ||
| 121 | calculate a total. | ||
| 122 | |||
| 123 | Save these insights: | ||
| 124 | |||
| 125 | | Insight | Configuration | | ||
| 126 | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| 127 | | Active users, daily | Trends, `client.turn.requested`, unique users, daily interval. | | ||
| 128 | | Active users, weekly | Trends, `client.turn.requested`, unique users, weekly interval. | | ||
| 129 | | Active users, monthly | Trends, `client.turn.requested`, unique users, monthly interval. | | ||
| 130 | | Client usage | Trends, `client.turn.requested`, unique users. Save four filtered series: `surface = desktop`, `surface = mobile`, `surface = web` and `webDeployment = hosted`, and `surface = web` and `webDeployment = server`. Name them Desktop, Native mobile, Hosted web, and Server-served web. | | ||
| 131 | | Client OS, active users | Trends, `client.turn.requested`, unique users, breakdown by `clientOs`. | | ||
| 132 | | Client OS, turns | Trends, `client.turn.requested`, total events, breakdown by `clientOs`. | | ||
| 133 | | Client versus server OS | Table, `client.turn.requested`, breakdown by `clientOs` and `serverOs`. | | ||
| 134 | | Connection method, active users | Trends, `client.turn.requested`, unique users, breakdown by `connectionMethod`. | | ||
| 135 | | Connection method, turns | Trends, `client.turn.requested`, total events, breakdown by `connectionMethod`. | | ||
| 136 | | Mobile devices | Table, `client.turn.requested`, filter `surface = mobile`, breakdown by `clientOs`, `clientOsMajorVersion`, and `clientDeviceType`. | | ||
| 137 | | Client version adoption | Trends, `client.turn.requested`, unique users, breakdown by `clientAppVersion`. | | ||
| 138 | | Server version adoption | Trends, `client.turn.requested`, unique users, breakdown by `serverAppVersion`. | | ||
| 139 | | Missing metadata | Table or SQL insight that shows the percentage of `client.turn.requested` events where each of `surface`, `clientOs`, `clientDeviceType`, `clientAppVersion`, and `connectionMethod` is absent. Track `webDeployment` and `clientBrowser` only within `surface = web`. | | ||
| 140 | |||
| 141 | In PostHog Data management, use the event and property descriptions from this | ||
| 142 | document. Mark the recommended properties as verified. Keep `clientType` | ||
| 143 | visible with its legacy description so old reports remain understandable. | ||
| 144 | |||
| 145 | Create a second saved dashboard named `Provider token efficiency`. Start each | ||
| 146 | token insight with these filters: | ||
| 147 | |||
| 148 | - `usageStatus = complete` | ||
| 149 | - `hasSubagents = false` | ||
| 150 | - `mixedModels = false` | ||
| 151 | |||
| 152 | Break down input tokens, output tokens, and their ratio by `provider`. Compare | ||
| 153 | the same `model`, `effort`, `interactionMode`, and `terminalStatus`. Without | ||
| 154 | these filters, model choice, reasoning level, plan mode, failures, and child | ||
| 155 | agents can look like harness differences. Keep separate views for median turn | ||
| 156 | usage and aggregate `sum(outputTokens) / sum(inputTokens)`. Do not average each | ||
| 157 | turn's ratio because small-input turns can dominate that result. | ||
| 158 | |||
| 159 | ## Collection and release boundary | ||
| 160 | |||
| 161 | Client values are best effort. Invalid values are ignored and never reject a | ||
| 162 | connection. Browser clients use user-agent data for broad OS, browser, phone, | ||
| 163 | and tablet groups. They do not infer CPU architecture or an exact OS version | ||
| 164 | from `navigator.platform`. | ||
| 165 | |||
| 166 | This change does not collect URLs, authentication tokens, prompts, IP | ||
| 167 | addresses, or user-assigned device names. The token event does not include | ||
| 168 | thread IDs, turn IDs, provider instance IDs, event IDs, request IDs, raw | ||
| 169 | provider payloads, or child-agent output. It does not scan provider history on | ||
| 170 | disk or start external work. It measures authenticated product use. It does | ||
| 171 | not measure a person who visits the hosted app without connecting to a server. | ||
| 172 | |||
| 173 | The new fields start with the first client and server release that contains | ||
| 174 | this metadata path. Historical events cannot reliably identify the client OS, | ||
| 175 | hosted web use, device type, or connection method when the old client did not | ||
| 176 | send those fields. Reports must treat missing values as pre-release or older | ||
| 177 | client data instead of backfilling them from server fields. |
docs/internals/providers.md+88-395| ... | @@ -1,395 +1,88 @@ | ... | @@ -1,395 +1,88 @@ |
| 1 | # Provider architecture | 1 | # Provider constraints |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | Orchestration records intent and state without knowing which provider runs a thread. Provider |
| 4 | 4 | protocols, account ownership, permissions, and capabilities belong at the | |
| 5 | A provider is the agent runtime that does the actual work. T3 Code supports several, and the | 5 | [adapter boundary](../../apps/server/src/provider/Services/ProviderAdapter.ts). Normalize there |
| 6 | orchestration layer does not know which one is behind a thread. | 6 | instead of spreading provider checks through reactors and clients. |
| 7 | 7 | ||
| 8 | ## Built-in drivers | 8 | A driver kind identifies an integration; an instance identifies one configuration and account |
| 9 | 9 | lifecycle. Route work by instance, so two accounts using the same driver do not share mutable | |
| 10 | [`builtInDrivers.ts`][drivers] exports `BUILT_IN_DRIVERS` with six entries: | 10 | session or catalog state. |
| 11 | 11 | ||
| 12 | | Driver kind | Driver source | | 12 | ## Process and account isolation |
| 13 | | ------------- | --------------------------------------------- | | 13 | |
| 14 | | `codex` | [`Drivers/CodexDriver.ts`][codex] | | 14 | T3-managed OpenCode chat uses one server per thread. Its MCP registrations are directory-scoped, while |
| 15 | | `claudeAgent` | [`Drivers/ClaudeDriver.ts`][claude] | | 15 | T3's MCP connection is thread-scoped. Sharing a chat server between threads in one directory would |
| 16 | | `cursor` | [`Drivers/CursorDriver.ts`][cursor] | | 16 | let them replace each other's connection. Catalog and text-generation work can share the |
| 17 | | `grok` | [`Drivers/GrokDriver.ts`][grok] | | 17 | [instance-owned helper](../../apps/server/src/provider/OpenCodeServerOwner.ts), which closes |
| 18 | | `opencode` | [`Drivers/OpenCodeDriver.ts`][opencode] | | 18 | after an idle period. External OpenCode servers remain externally owned and can require an |
| 19 | | `antigravity` | [`Drivers/AntigravityDriver.ts`][antigravity] | | 19 | external restart to pick up configuration changes. |
| 20 | 20 | ||
| 21 | Each driver declares its `driverKind`, a `configSchema`, and a `create` function that builds an | 21 | OpenCode also stores persistent approval grants per directory. Automatic full-access replies use |
| 22 | adapter in a child scope. Adapter implementations live beside them in | 22 | `once` so they cannot widen a supervised thread's permissions on a shared external server. |
| 23 | `apps/server/src/provider/Layers/` (`CodexAdapter.ts`, `ClaudeAdapter.ts`, and so on) and conform to | 23 | See the [adapter](../../apps/server/src/provider/Layers/OpenCodeAdapter.ts). |
| 24 | [`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's | 24 | |
| 25 | transport, config, and event shapes are mapped. | 25 | Antigravity separates account profiles per instance while sharing installed executables across the |
| 26 | 26 | environment. It forces file-based credential storage because the native macOS keychain entry would | |
| 27 | ## Runtime context | 27 | otherwise be shared across instances. The launch environment removes ambient Google credentials, |
| 28 | 28 | so an instance cannot silently use another account or billing project. | |
| 29 | Every adapter uses `apps/server/src/provider/RuntimeInstructions.ts` to identify T3 Code and | 29 | See [profile isolation](../../apps/server/src/provider/antigravityAuthSupport.ts). |
| 30 | the harness, and describe Markdown image/video embeds. Codex includes it in developer | 30 | |
| 31 | instructions; Claude appends it to its system preset; OpenCode sends it in each prompt's | 31 | The [Antigravity installer](../../apps/server/src/provider/AntigravityInstallation.ts) outlives |
| 32 | `system` field. Cursor, Grok, and Antigravity append a separate text block to ACP prompts, | 32 | client connections and provider-instance rebuilds. Releases are immutable, with an atomic pointer |
| 33 | which have no system-message field. This does not change the stored user message. | 33 | selecting the version for new processes. Running processes hold leases on their version. Updates |
| 34 | 34 | and removal must respect those leases instead of replacing executables under a running agent. | |
| 35 | Per-turn context includes the current model when known. Codex includes reasoning effort; | 35 | |
| 36 | Grok includes it when explicitly selected for the turn. Claude's session-level context omits model and effort because they can | 36 | ## Setup must not happen as a health-check side effect |
| 37 | change during a session. OpenCode variants are not assumed to be reasoning-effort levels. | 37 | |
| 38 | 38 | Opening a provider session can start MCP servers, run hooks, or launch a login browser. | |
| 39 | ## Codex async questions | 39 | [Grok probes](../../apps/server/src/provider/Layers/GrokProvider.ts) avoid authentication and |
| 40 | 40 | session creation for this reason. Antigravity likewise reserves authenticated catalog sessions for | |
| 41 | Codex 0.153 exposes `request_user_input_async` through `item/started` and `item/completed` | 41 | explicit setup or model refresh; background checks use initialization only. |
| 42 | notifications. The item has `type: "agentMessage"`, `delivery: "async"`, and a `questions` array. | 42 | |
| 43 | Each question has a `title` and an optional `options` array of strings. The tool returns `{"accepted":true}` | 43 | [Antigravity sign-in](../../apps/server/src/provider/AntigravityAuth.ts) belongs to the initiating |
| 44 | without waiting. This is separate from the `item/tool/requestUserInput` server request. | 44 | T3 auth session. The client carries the return URL back to the environment because the provider's |
| 45 | See the [Codex tool handler](https://github.com/openai/codex/blob/d979df154cf60e13eafb5453e75b6d84f21c67bf/codex-rs/core/src/tools/handlers/request_user_input_async.rs). | 45 | loopback listener may be on another machine. Forward only the callback for the owned pending flow; |
| 46 | 46 | a successful callback HTTP request is not proof that provider authentication finished. The native | |
| 47 | The Codex adapter maps completed question items to `user-input.requested` with | 47 | process owns token exchange and storage. |
| 48 | `responseMode: "message"` and stable request and event IDs. Questions use the existing web, | 48 | |
| 49 | desktop, and mobile panels. They stay pending while the turn runs and after it finishes. | 49 | Antigravity sign-out closes admission to new processes and stops existing processes before clearing account |
| 50 | 50 | metadata. Otherwise a helper or resumed session could retain the old account. Cached model lists | |
| 51 | The engine reads the request's latest stored activity before deciding a reply. This works after | 51 | do not establish current access, and an authoritative empty catalog must clear the old list. |
| 52 | startup, when the command snapshot has no activities, and after a resolution leaves the recent | 52 | |
| 53 | activity window. The query returns one activity, not the full thread history. | 53 | Antigravity text-generation helpers deny tool requests, but native hooks and MCP configuration can |
| 54 | 54 | run before the prompt. They reject profiles with such configuration before launch. Prompt | |
| 55 | For these requests, the decider saves the resolution and a user message in one transaction. | 55 | instructions and tool denial do not create a native sandbox. |
| 56 | The standard turn path delivers the message, including session resume and active-turn input. | 56 | See [helper constraints](../../apps/server/src/textGeneration/AntigravityTextGeneration.ts). |
| 57 | It does not send a JSON-RPC response to Codex. Other providers and blocking Codex questions | 57 | |
| 58 | keep their existing response paths. | 58 | ## Protocol traps |
| 59 | 59 | ||
| 60 | ## Registry and routing | 60 | Codex async questions arrive as notifications and are answered with a new user message. There is |
| 61 | 61 | no pending RPC response to send. Blocking questions still use the request/response path. The | |
| 62 | Two registries separate configuration from live processes: | 62 | [adapter](../../apps/server/src/provider/Layers/CodexAdapter.ts) distinguishes them; the |
| 63 | 63 | [decider](../../apps/server/src/orchestration/decider.ts) records an async answer and its user | |
| 64 | - [`ProviderInstanceRegistry`][instances] keys configured instances by `ProviderInstanceId`. Creating | 64 | message together. |
| 65 | one looks up the driver by `driverKind`, decodes `entry.config` with that driver's schema, opens a | 65 | |
| 66 | child scope, and calls `driver.create`. | 66 | An async question can outlive the turn or a server restart. The engine reads that request's |
| 67 | - [`ProviderAdapterRegistry`][registry] resolves an instance ID to its live adapter via | 67 | durable activity before resolving it because the in-memory command snapshot omits old activities. |
| 68 | `getByInstance`. | 68 | Do not infer that a request has disappeared merely because it is outside the recent window. |
| 69 | 69 | ||
| 70 | [`ProviderService`][service] sits on top. It combines the adapter registry with the provider session | 70 | Capabilities must describe what the provider can actually do. Antigravity can capture workspace |
| 71 | directory to route session and turn operations for a thread, so callers name a thread, not an agent. | 71 | checkpoints but cannot roll back its conversation. The [checkpoint boundary](./overview.md#turn-completion-and-checkpoints) |
| 72 | 72 | therefore rejects revert before touching files. Native permission and question option IDs must | |
| 73 | `ProviderService.sendTurn` expands [assistant citations](./assistant-citations.md) into quoted | 73 | also survive normalization; a display label is not necessarily a valid reply. |
| 74 | reference data before dispatching to any adapter. Bound user comments remain distinct from the quoted | 74 | |
| 75 | assistant text. Persisted messages keep their serialized links. | 75 | ## Attachments and stored history |
| 76 | 76 | ||
| 77 | Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No | 77 | Attachments live outside the project workspace. [ProviderService](../../apps/server/src/provider/Layers/ProviderService.ts) |
| 78 | orchestration, contract, or client change is required for the common case. | 78 | puts their environment-local paths in turn input and lets adapters choose native input formats. |
| 79 | 79 | A path in the prompt does not grant filesystem access. Keep provider sandbox and approval rules | |
| 80 | ### Grok health check | 80 | in force; copying uploads into the project to bypass them changes that boundary. |
| 81 | 81 | ||
| 82 | `checkGrokProviderStatus` never opens an ACP session. It runs `grok --version`, then `grok models` | 82 | File attachments introduced a replay compatibility limit. Image-only clients cannot decode |
| 83 | for login state and model slugs, then a single ACP `initialize` and reads models from | 83 | file-bearing messages, and an image-only server can fail the entire environment's startup when |
| 84 | `_meta.modelState`. `authenticate` and `session/new` are skipped on purpose: `authenticate` can open | 84 | replaying one such event. Rollouts and downgrades must account for persisted history as well as |
| 85 | a browser login and `session/new` boots every configured MCP server, both of which made background | 85 | current client support. |
| 86 | probes hang or surprise the user. A failed `initialize` degrades to `warning` with the CLI's model | 86 | |
| 87 | list instead of persisting `error` over a working install. The built-in `grok-build` slug is the | 87 | Model classification has its own [manifest constraints](./model-manifest.md). Assistant-reference |
| 88 | CLI's product name, not an ACP model id. `applyGrokAcpModelSelection` treats it as "keep the | 88 | handling is documented under [citations](./assistant-citations.md). |
| 89 | session's current model" and never sends it in `session/set_model`. | ||
| 90 | |||
| 91 | ## Antigravity ownership and protocol | ||
| 92 | |||
| 93 | [`AntigravityDriver`][antigravity] uses Google's official ACP executable. The instance config | ||
| 94 | selects the ACP auth method: `oauth-personal` (default), `oauth-business`, `gemini-api-key`, or | ||
| 95 | `agent-platform`. The two OAuth methods share the loopback sign-in flow below. The API key | ||
| 96 | methods pass the configured key to the agent as `GEMINI_API_KEY` or `GOOGLE_API_KEY` and never | ||
| 97 | open a browser. A GCP project and location are written to the profile's `settings.json` on each | ||
| 98 | launch. The driver never reuses CLI credentials or ambient `GOOGLE_*` variables and never falls | ||
| 99 | back to another method. Antigravity is disabled by default and supports multiple provider | ||
| 100 | instances. The open driver and instance identifiers require no database migration. | ||
| 101 | |||
| 102 | ### Runtime installation | ||
| 103 | |||
| 104 | [`AntigravityInstallation`][antigravity-installation] belongs to the environment, outside | ||
| 105 | WebSocket and provider-instance scopes. Instances share an explicit download operation and the | ||
| 106 | completed runtime. Client disconnects and instance rebuilds do not cancel installation. | ||
| 107 | |||
| 108 | The fixed [release table][antigravity-release] contains official Google URLs, SHA-256 hashes, | ||
| 109 | archive sizes, and the exact executable pair for each published host. Downloads stream to disk. | ||
| 110 | Lazy `yauzl` entry streams extract only that pair, with member names, types, duplicates, and | ||
| 111 | sizes checked. Validation runs ACP `initialize` in a temporary profile without authentication | ||
| 112 | or a session. Progress updates are bounded, not sent for every network chunk. | ||
| 113 | |||
| 114 | Complete releases live in immutable version directories under the T3 home | ||
| 115 | `tools/antigravity-acp/<platform>-<arch>/versions`. An atomic `active.json` change selects the | ||
| 116 | release for new processes. Each process holds a version lease until it exits. Updates do not | ||
| 117 | replace running executables. Removal refuses active leases or explicit binary paths that still | ||
| 118 | reference the managed files. Failure or cancellation removes owned partial files, not the | ||
| 119 | previous release or account data. | ||
| 120 | |||
| 121 | Resolution order is explicit `binaryPath`, active managed release, then the instance's `PATH`. | ||
| 122 | An invalid explicit path fails without fallback. Manual installations are never changed by the | ||
| 123 | installer. Every launch pins `ANTIGRAVITY_HARNESS_PATH` to the selected executable's sibling. | ||
| 124 | |||
| 125 | ### Google profiles and sign-in | ||
| 126 | |||
| 127 | Each instance owns a stable profile at | ||
| 128 | `<stateDir>/providers/antigravity/<sha256(instanceId)>`. | ||
| 129 | [`antigravityAuthSupport.ts`][antigravity-auth-support] sets `GEMINI_HOME` to this directory and | ||
| 130 | `AGY_ACP_FORCE_FILE_STORAGE=1` after merging instance environment variables. File storage avoids | ||
| 131 | the official macOS keychain entry being shared across instances. Profile directories use mode | ||
| 132 | `0700` on POSIX. This is file storage, not an encrypted keychain. Windows uses the host profile's | ||
| 133 | filesystem permissions. | ||
| 134 | |||
| 135 | The launch environment removes API-key and cloud-billing variables, disables inherited | ||
| 136 | environment extension, sets `PYTHONUNBUFFERED=1`, and controls `BROWSER`. A tested Node or | ||
| 137 | Electron-as-Node helper prevents the official agent from opening a browser on the environment. | ||
| 138 | The same launch factory serves setup, health checks, chat, and text generation. | ||
| 139 | |||
| 140 | The official agent prints a non-JSON OAuth line on stderr in version 1.1.1. Earlier versions | ||
| 141 | print it on stdout. T3 accepts the exact native prefix on either stream and its browser-helper | ||
| 142 | marker on stderr. Fragmented lines are joined and bounded. Other malformed protocol output | ||
| 143 | remains fatal. Authorization URLs are validated before use. Other stderr is discarded because | ||
| 144 | it can contain OAuth data. Normal work rejects an interactive login request with a | ||
| 145 | sign-in-required error instead of waiting for consent. A rejected stderr callback fails pending | ||
| 146 | ACP requests and closes the owned process. | ||
| 147 | |||
| 148 | [`AntigravityAuth`][antigravity-auth] owns each sign-in process and deadline in the instance | ||
| 149 | scope. Only the initiating T3 auth session receives its URL and flow ID or can complete or | ||
| 150 | cancel it. Other clients receive busy state without those values. Subscriptions follow | ||
| 151 | controller replacement when settings rebuild an instance. | ||
| 152 | |||
| 153 | For remote completion, the client sends the full return URL through the typed setup RPC. | ||
| 154 | The server validates the pending loopback origin, port, root path, and single matching state | ||
| 155 | before forwarding once to the owned listener. It does not probe the listener or follow | ||
| 156 | redirects. Google's process owns PKCE, token exchange, refresh, and storage. Callback HTTP | ||
| 157 | success is not authentication success. The controller waits for authenticated session setup | ||
| 158 | and catalog discovery. Cancellation closes the process instead of sending a synthetic denial. | ||
| 159 | |||
| 160 | Auth RPCs `provider.auth.start`, `complete`, `cancel`, `logout`, and `subscribe` require | ||
| 161 | `orchestration:operate`. Install `start`, `cancel`, and `remove` use that scope too. | ||
| 162 | `provider.install.subscribe` and public provider snapshots require `orchestration:read`. | ||
| 163 | [`providerSetup.ts`][provider-setup] defines the operation IDs, states, and safe errors. | ||
| 164 | |||
| 165 | Sign-out closes process admission for the instance, stops provider bindings through | ||
| 166 | [`ProviderAuthService`][provider-auth-service], then stops owned startup and helper processes. | ||
| 167 | A fresh official process calls `initialize` and native `logout` without authenticating. | ||
| 168 | Only then does the provider clear auth, models, commands, skills, and workspace metadata. | ||
| 169 | Thread history and native session files remain. Settings sign-out and a text-only `/logout` | ||
| 170 | use this same path. The command is handled before model prompting or title generation. | ||
| 171 | Disabling an instance closes its processes but keeps credentials. Account replacement is | ||
| 172 | explicit sign-out followed by sign-in. | ||
| 173 | |||
| 174 | ### Sessions, models, and client capabilities | ||
| 175 | |||
| 176 | [`AntigravityAdapter`][antigravity-adapter] owns one ACP process per active thread. It uses | ||
| 177 | native `session/resume` without transcript replay and reapplies the persisted model and | ||
| 178 | permission mode after new or resumed setup. An unavailable explicit model fails instead of | ||
| 179 | accepting the native default. Steering cancels the previous prompt, waits for its result and | ||
| 180 | event drain, then sends the replacement. Native background commands use T3's existing | ||
| 181 | background-task state. | ||
| 182 | |||
| 183 | The permission mapping is `approval-required` and `auto` to `default`, `auto-accept-edits` to | ||
| 184 | `auto_edit`, and `full-access` to `yolo`. Native requests still need replies in `yolo`. | ||
| 185 | `interaction_` requests are user questions, not approvals. T3 keeps opaque option IDs in | ||
| 186 | `UserInputQuestion.options[].value` and sets `allowCustomAnswer=false`. Both clients preserve | ||
| 187 | these values. Ordinary approval replies use only offered option IDs, including `allow_always` | ||
| 188 | only when present. Existing providers keep their prior behavior when the optional fields are | ||
| 189 | absent. | ||
| 190 | |||
| 191 | `showInteractionModeToggle=false` keeps native `/plan` separate from T3 Plan mode. | ||
| 192 | `supportsConversationRollback=false` hides unsupported client actions and makes checkpoint | ||
| 193 | revert fail before filesystem changes. Checkpoint capture and diffs remain supported. | ||
| 194 | |||
| 195 | Automatic status refreshes, reconnects, and workspace checks do not open catalog sessions. | ||
| 196 | Health probes use `initialize` only. Disabled instances do not run background probes. | ||
| 197 | An explicit `serverRefreshProviders` request with `refreshModels: true` calls the driver's | ||
| 198 | optional `refreshModels` operation. Antigravity opens a short-lived catalog session under the | ||
| 199 | instance's process admission guard, uses saved credentials, publishes models and commands, | ||
| 200 | then closes the process. An interactive login request fails with sign-in required. Web's | ||
| 201 | **Refresh provider status** and mobile's **Refresh models** actions request this operation. | ||
| 202 | Account access starts unknown and becomes authenticated after successful session setup, | ||
| 203 | including an explicit model refresh. | ||
| 204 | The [provider snapshot][antigravity-provider] takes models and commands from setup and native | ||
| 205 | updates. It preserves returned Gemini model IDs, labels, order, and thinking-level choices. | ||
| 206 | ACP `config_option_update` notifications and `session/set_config_option` responses replace | ||
| 207 | the instance's model catalog. Child session notifications do not change the root catalog. | ||
| 208 | The registry treats a successful empty catalog as authoritative and clears cached metadata | ||
| 209 | after sign-out. It must not retain a previous account's models. Cached models do not prove | ||
| 210 | current access. The auth response does not supply an email, plan tier, or reliable quota. | ||
| 211 | |||
| 212 | Some upstream failures arrive as assistant text followed by `end_turn`. Preserve that message | ||
| 213 | without treating model-written text as a structured error or successful task completion. | ||
| 214 | |||
| 215 | ### Text generation | ||
| 216 | |||
| 217 | [`AntigravityTextGeneration`][antigravity-text] implements titles, branch names, commit text, | ||
| 218 | and PR text through the same instance and Google sign-in. Each helper uses a temporary empty | ||
| 219 | workspace, no injected MCP servers, native `default` mode, and explicit denial of tools and | ||
| 220 | questions. Output is bounded, parsed against the existing schemas, and sanitized. Cancellation, | ||
| 221 | timeout, and sign-out close the process. Cleanup removes only that helper's verified temporary | ||
| 222 | native session files. | ||
| 223 | |||
| 224 | The official agent has no verified hard no-tools setting. Global hooks and MCP configuration | ||
| 225 | can run before a prompt. Helpers check the profile's `config/hooks.json` and | ||
| 226 | `config/mcp_config.json` before launch and reject nonempty, malformed, or oversized | ||
| 227 | configuration. `supportsTextGeneration=false` keeps such an instance out of system-model | ||
| 228 | pickers. Empty managed profiles are supported. Do not describe prompt-time denial as a native | ||
| 229 | sandbox. | ||
| 230 | |||
| 231 | ## OpenCode server ownership and catalog | ||
| 232 | |||
| 233 | Native OpenCode event logs skip text deltas and running tool snapshots. Pending, completed, | ||
| 234 | and failed tool states remain, including final output and errors. This filter does not change | ||
| 235 | events sent to clients or orchestration storage. | ||
| 236 | |||
| 237 | Each OpenCode provider instance owns one lazy local server for catalog discovery and | ||
| 238 | text-generation helpers through [`OpenCodeServerOwner.ts`][opencode-server-owner]. Concurrent | ||
| 239 | borrowers share startup. The server closes 30 seconds after the last borrower releases it, or | ||
| 240 | when the provider instance closes. A failed or exited process can be started again on the next | ||
| 241 | use. An externally configured OpenCode server remains externally owned. | ||
| 242 | |||
| 243 | The local server and its SDK clients use one resolved password. An explicit provider password | ||
| 244 | overrides `OPENCODE_SERVER_PASSWORD` in the spawned environment. Without an explicit password, | ||
| 245 | the client uses the password from the environment that the process inherits. External servers use | ||
| 246 | only their explicit provider password and never inherit the host's local password. | ||
| 247 | |||
| 248 | Every server connection must pass the authenticated `/global/health` check before inventory or | ||
| 249 | session operations start. The response must contain a valid version at or above 1.14.19. Local | ||
| 250 | owners cache this result for the lifetime of the spawned process. External actions check once when | ||
| 251 | they create their server connection, not for each model or SDK request. | ||
| 252 | |||
| 253 | Chat adapters keep their own server per thread. They register a thread-specific `t3-code` MCP | ||
| 254 | connection, while OpenCode stores MCP connections by directory. Sharing these chat servers | ||
| 255 | without changing MCP routing would let two threads in one directory replace each other's | ||
| 256 | connection. | ||
| 257 | |||
| 258 | Chat adapters send the runtime mode as a session ruleset, but upstream OpenCode evaluates | ||
| 259 | doom-loop and subagent asks against the agent ruleset only. In full access the adapter answers | ||
| 260 | those asks itself so the user never sees an approval they already granted. It replies `once` | ||
| 261 | rather than `always` because OpenCode stores `always` grants per directory, and on a shared | ||
| 262 | external server that would widen what a supervised thread in the same directory may do. | ||
| 263 | |||
| 264 | OpenCode loads its catalog through the HTTP API when an enabled provider instance starts. The | ||
| 265 | provider registry keeps the snapshot in memory and persists it in the existing per-instance cache. | ||
| 266 | Each `subscribeServerConfig` connection refreshes all providers, so a client reconnect reloads the | ||
| 267 | OpenCode catalog from the current helper. The `serverRefreshProviders` request also refreshes it. | ||
| 268 | Periodic OpenCode probes remain disabled. OpenCode reads credentials for each inventory request, | ||
| 269 | but its native configuration files can remain cached for the lifetime of the helper process. The | ||
| 270 | helper closes 30 seconds after its last inventory or text-generation borrower releases it. A | ||
| 271 | refresh after that idle period starts a new helper and reads file changes. Repeated refreshes and | ||
| 272 | active text-generation work can extend process reuse. Changes to the provider configuration or | ||
| 273 | environment replace the instance and start a new discovery. Changes to unrelated settings only | ||
| 274 | update snapshot enrichment. Other providers retain their existing refresh policy. | ||
| 275 | |||
| 276 | T3 Code does not own an external OpenCode process. Native configuration changes there can require | ||
| 277 | an external reload or restart before T3 Code's next refresh sees them. | ||
| 278 | |||
| 279 | The shared server's idle shutdown does not clear the catalog. Failed discovery keeps the last | ||
| 280 | known models, slash commands, and skills through the registry's existing merge rules. A successful | ||
| 281 | empty inventory is authoritative. Existing threads keep their explicit model identifier and | ||
| 282 | options when catalog metadata is missing; the catalog is not permission to choose a different | ||
| 283 | model for a thread. | ||
| 284 | |||
| 285 | ## Model manifest | ||
| 286 | |||
| 287 | The model picker's legacy section is driven by `apps/server/src/provider/model-manifest.json`, which | ||
| 288 | lists the current (non-legacy) model slugs per driver kind. The `ModelManifest` service | ||
| 289 | (`apps/server/src/provider/ModelManifest.ts`) refreshes that data from the same file on `main` via | ||
| 290 | raw.githubusercontent.com, so moving a model in or out of the legacy section is a commit, not a | ||
| 291 | release. Preference order is remote fetch, then the on-disk copy of the last successful fetch (in | ||
| 292 | the state directory), then the bundled copy. Fetches are TTL-gated, run concurrently with provider | ||
| 293 | probes, respect the `enableProviderUpdateChecks` setting, and never fail a provider check. The | ||
| 294 | Codex and Claude drivers apply the classification to every snapshot with `applyModelManifest`; | ||
| 295 | driver kinds absent from the manifest have no legacy concept. | ||
| 296 | |||
| 297 | ## Attachment access | ||
| 298 | |||
| 299 | The server stores uploaded attachments in its attachment directory, outside the project workspace. | ||
| 300 | `ProviderService` adds the absolute path of each attachment to the turn text, then passes every | ||
| 301 | attachment to the provider adapter. Each adapter decides what its provider ingests natively: | ||
| 302 | |||
| 303 | - Codex, Claude, Cursor, and Grok send images as native image inputs and skip generic files. For | ||
| 304 | these providers, generic files reach the agent only as file paths in the turn text. | ||
| 305 | - Antigravity sends BMP, JPEG, PNG, and WebP images and common audio formats as native blocks, | ||
| 306 | text files as embedded resources, and PDFs as resource links. Other files are rejected with an | ||
| 307 | error instead of being dropped. The session advertises the ACP client file system capability, | ||
| 308 | so workspace reads and writes come back through `fs/read_text_file` and `fs/write_text_file` | ||
| 309 | and are confined to the workspace and the attachments directory. | ||
| 310 | - OpenCode sends PNG/JPEG/GIF/WebP images, text files, and PDFs up to 20 MB as native file parts | ||
| 311 | with their real mime type. Everything else (ZIP and other binaries, image formats model APIs | ||
| 312 | reject, oversized files) falls back to the file path in the turn text, like the other providers. | ||
| 313 | - Antigravity sends BMP/JPEG/PNG/WebP images as native image blocks, UTF-8 text as embedded | ||
| 314 | resources, and PDFs as local resource links. Text is limited to 1 MiB per file, images to | ||
| 315 | 10 MiB each, and all attachments to 50 MiB per turn. Unsupported formats or oversized inputs | ||
| 316 | fail explicitly. Native path permissions still apply to PDFs. | ||
| 317 | |||
| 318 | Claude receives the attachment directory as an allowed additional directory. Codex keeps its | ||
| 319 | configured sandbox policy, so access depends on that policy and the selected runtime mode. OpenCode | ||
| 320 | allows all paths in full-access mode and requests approval for directories outside the workspace in | ||
| 321 | restricted modes. Cursor and Grok use their own provider permission rules. | ||
| 322 | |||
| 323 | The server does not copy attachments into a project or bypass provider approval rules. If an agent | ||
| 324 | cannot read an attachment, the user must approve the access or select a runtime mode that permits it. | ||
| 325 | |||
| 326 | Updated attachment schemas tolerate unknown attachment members, but old image-only clients still | ||
| 327 | cannot decode messages that contain file attachments. Client file-picking rollouts must account for | ||
| 328 | this limit. | ||
| 329 | |||
| 330 | Do not run an old image-only server against state that contains file attachments. Replay decodes | ||
| 331 | each persisted event before projection. A file-bearing event can make `ProjectionPipeline` bootstrap | ||
| 332 | and `OrchestrationEngine` startup fail for the entire environment, not only the affected thread. | ||
| 333 | |||
| 334 | ## How provider work is requested | ||
| 335 | |||
| 336 | Clients never call a provider directly. They dispatch orchestration commands over the RPC method | ||
| 337 | `orchestration.dispatchCommand`, defined with the rest of the orchestration surface in | ||
| 338 | [`orchestration.ts`][contracts]. The client-dispatchable provider-facing commands are | ||
| 339 | `thread.turn.start`, `thread.turn.interrupt`, `thread.approval.respond`, | ||
| 340 | `thread.user-input.respond`, `thread.checkpoint.revert`, and `thread.session.stop`, plus the mode | ||
| 341 | setters `thread.runtime-mode.set` and `thread.interaction-mode.set`. | ||
| 342 | |||
| 343 | The engine persists an event for the command, and a server-side reactor performs the provider call. | ||
| 344 | Provider output comes back as internal commands such as `thread.message.assistant.delta` and | ||
| 345 | `thread.session.set`, which clients observe through `orchestration.subscribeThread`. See | ||
| 346 | [overview.md](./overview.md) for the command/event loop. | ||
| 347 | |||
| 348 | ## Server-side workers | ||
| 349 | |||
| 350 | Provider work flows through three queue-backed workers. All three are built with | ||
| 351 | `makeDrainableWorker` from [`DrainableWorker.ts`][worker] and expose `drain` for deterministic test | ||
| 352 | synchronization. | ||
| 353 | |||
| 354 | 1. [`ProviderRuntimeIngestion`][ingest] consumes provider runtime streams and emits orchestration | ||
| 355 | commands. | ||
| 356 | 2. [`ProviderCommandReactor`][cmd] reacts to orchestration intent events and dispatches provider | ||
| 357 | calls. | ||
| 358 | 3. [`CheckpointReactor`][checkpoint] captures workspace checkpoints on turn start and completion, and | ||
| 359 | performs reverts. | ||
| 360 | |||
| 361 | ### Buffered assistant delivery | ||
| 362 | |||
| 363 | A thread in `buffered` assistant delivery mode accumulates assistant text instead of streaming each | ||
| 364 | delta. The buffer is not held until turn completion. In [`ProviderRuntimeIngestion`][ingest], | ||
| 365 | `MAX_BUFFERED_ASSISTANT_CHARS` is 24,000: the append that would exceed it invalidates the buffer and | ||
| 366 | spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries, | ||
| 367 | when a request opens (approval) or user input is requested, via | ||
| 368 | `flushBufferedAssistantMessagesForTurn`. | ||
| 369 | |||
| 370 | [drivers]: ../../apps/server/src/provider/builtInDrivers.ts | ||
| 371 | [codex]: ../../apps/server/src/provider/Drivers/CodexDriver.ts | ||
| 372 | [claude]: ../../apps/server/src/provider/Drivers/ClaudeDriver.ts | ||
| 373 | [cursor]: ../../apps/server/src/provider/Drivers/CursorDriver.ts | ||
| 374 | [grok]: ../../apps/server/src/provider/Drivers/GrokDriver.ts | ||
| 375 | [opencode]: ../../apps/server/src/provider/Drivers/OpenCodeDriver.ts | ||
| 376 | [antigravity]: ../../apps/server/src/provider/Drivers/AntigravityDriver.ts | ||
| 377 | [antigravity-adapter]: ../../apps/server/src/provider/Layers/AntigravityAdapter.ts | ||
| 378 | [antigravity-provider]: ../../apps/server/src/provider/Layers/AntigravityProvider.ts | ||
| 379 | [antigravity-installation]: ../../apps/server/src/provider/AntigravityInstallation.ts | ||
| 380 | [antigravity-release]: ../../apps/server/src/provider/antigravityRelease.ts | ||
| 381 | [antigravity-auth]: ../../apps/server/src/provider/AntigravityAuth.ts | ||
| 382 | [antigravity-auth-support]: ../../apps/server/src/provider/antigravityAuthSupport.ts | ||
| 383 | [antigravity-text]: ../../apps/server/src/textGeneration/AntigravityTextGeneration.ts | ||
| 384 | [provider-auth-service]: ../../apps/server/src/provider/Layers/ProviderAuthService.ts | ||
| 385 | [provider-setup]: ../../packages/contracts/src/providerSetup.ts | ||
| 386 | [opencode-server-owner]: ../../apps/server/src/provider/OpenCodeServerOwner.ts | ||
| 387 | [adapter]: ../../apps/server/src/provider/Services/ProviderAdapter.ts | ||
| 388 | [instances]: ../../apps/server/src/provider/Services/ProviderInstanceRegistry.ts | ||
| 389 | [registry]: ../../apps/server/src/provider/Services/ProviderAdapterRegistry.ts | ||
| 390 | [service]: ../../apps/server/src/provider/Layers/ProviderService.ts | ||
| 391 | [contracts]: ../../packages/contracts/src/orchestration.ts | ||
| 392 | [worker]: ../../packages/shared/src/DrainableWorker.ts | ||
| 393 | [ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | ||
| 394 | [cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts | ||
| 395 | [checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts |
docs/internals/remote.md+59-256| ... | @@ -1,256 +1,59 @@ | ... | @@ -1,256 +1,59 @@ |
| 1 | # Remote Architecture | 1 | # Remote architecture |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | Each connection joins a client to one environment over HTTP and WebSocket. The |
| 4 | 4 | environment owns providers, execution, files, and durable state. Direct access, | |
| 5 | Remote environments are shipped, not planned. Direct, bearer-paired, relay-tunneled, Tailscale, and | 5 | Tailscale, SSH, and T3 Connect change how the client reaches that server; they do |
| 6 | desktop-managed SSH access all exist today. This document describes the model they share and where | 6 | not introduce another execution model. See |
| 7 | each piece lives. For the user-facing setup guide see | 7 | [remote access](../user/remote-access.md) for setup. |
| 8 | [remote access](../user/remote-access.md). | 8 | |
| 9 | 9 | ## Identity is independent of the route | |
| 10 | ## The model | 10 | |
| 11 | 11 | An environment keeps its ID across server restarts and endpoint changes. Saved | |
| 12 | T3 has one runtime boundary: a client talks to a T3 server over HTTP and WebSocket, and the server | 12 | connections are local to a client profile; the server's identity and state are |
| 13 | owns orchestration, providers, terminals, git, and filesystem operations. Remoteness is expressed at | 13 | not. A repository identity can correlate clones across environments, but never |
| 14 | the connection layer, never by splitting the runtime. | 14 | routes work between them. A project and its threads belong to one environment. |
| 15 | 15 | ||
| 16 | ```text | 16 | [Environment ID initialization](../../apps/server/src/environment/ServerEnvironment.ts) |
| 17 | ┌──────────────────────────────────────────────┐ | 17 | must publish a complete ID atomically. Repair of an empty ID file retains a |
| 18 | │ Client (desktop / mobile / web) │ | 18 | recovery file so concurrent or delayed initializers choose the same winner. |
| 19 | │ known environments, connection supervisor │ | 19 | Removing that recovery state as ordinary temporary-file cleanup can change the |
| 20 | └───────────────┬──────────────────────────────┘ | 20 | identity underneath an already-running server. |
| 21 | │ resolves one access endpoint | 21 | |
| 22 | ┌───────────────â–¼──────────────────────────────┐ | 22 | Advertised endpoints are reachability hints. Only the connecting device can |
| 23 | │ Access method │ | 23 | prove that a route works. In particular, a host's loopback address refers to a |
| 24 | │ direct ws/wss, relay tunnel, │ | 24 | different machine when another device opens it. Endpoint selection must not |
| 25 | │ Tailscale serve, desktop-managed ssh │ | 25 | silently fall back to loopback when a shareable endpoint is unavailable. |
| 26 | └───────────────┬──────────────────────────────┘ | 26 | |
| 27 | │ connects to one T3 server | 27 | ## Hosted web is a client |
| 28 | ┌───────────────â–¼──────────────────────────────┐ | 28 | |
| 29 | │ Execution environment = one T3 server │ | 29 | The hosted web app stores its connection catalog in the browser and connects |
| 30 | │ identity, providers, projects/threads, │ | 30 | directly to each environment. It does not proxy traffic or hold server-side |
| 31 | │ terminals, git, filesystem │ | 31 | pairing state. Hosting the UI over HTTPS therefore cannot make a plain HTTP LAN |
| 32 | └──────────────────────────────────────────────┘ | 32 | backend accessible from that browser context. |
| 33 | ``` | 33 | |
| 34 | 34 | A [hosted pairing URL](../../apps/web/src/hostedPairing.ts) identifies the backend | |
| 35 | ### ExecutionEnvironment | 35 | in its query and carries the pairing secret in its fragment. Fragments stay out |
| 36 | 36 | of requests to the hosted origin. The browser exchanges the secret with the | |
| 37 | One running T3 server instance. It owns provider availability and auth, model availability, projects | 37 | environment and strips it from its history. Moving the token into a query |
| 38 | and threads, terminal processes, filesystem access, git operations, and server settings. | 38 | parameter would disclose it to the wrong origin. |
| 39 | 39 | ||
| 40 | It is identified by a stable `environmentId`, persisted by the server at `<stateDir>/environment-id` | 40 | ## Access and process ownership are different |
| 41 | and generated on first start (`apps/server/src/environment/ServerEnvironment.ts`). Desktop, mobile, | 41 | |
| 42 | and web all reason about the same concept. | 42 | Tailscale supplies an endpoint for ordinary pairing, so it needs no separate |
| 43 | 43 | environment type. Authentication remains the environment's responsibility for | |
| 44 | Initialization publishes a complete ID atomically. Empty or whitespace-only ID files are repaired | 44 | every route. See [environment authentication](./environment-auth.md) and the |
| 45 | using a retained `<stateDir>/environment-id.recovery` file so concurrent and delayed repairs choose | 45 | [T3 Connect trust boundary](./t3-connect.md). |
| 46 | the same ID. Existing nonempty ID files remain authoritative. | 46 | |
| 47 | 47 | SSH can launch a server as well as forward a port. Desktop main owns that | |
| 48 | ### Known environments and connection targets | 48 | lifecycle because it can spawn SSH and handle authentication prompts. The |
| 49 | 49 | renderer uses the forwarded endpoint through the shared connection runtime. | |
| 50 | A saved client-side entry for an environment the client knows how to reach. It is not | 50 | [SSH cleanup](../../packages/ssh/src/tunnel.ts) stops a remote server only if the |
| 51 | server-authored; it is local to a device or client profile. In the hosted web app these entries are | 51 | launcher owns it; a server it discovered already running must survive a client |
| 52 | browser-local. A hosted pairing URL can create one, but it does not give the hosted app a server-side | 52 | disconnect. Reconnection restores the forward before opening the application |
| 53 | control plane or a copy of session state. | 53 | transport. |
| 54 | 54 | ||
| 55 | [`connection/model.ts`][model] defines four target tags, which are the real access taxonomy: | 55 | Remote servers can outlive several client releases. Clients must use advertised |
| 56 | 56 | capabilities and handle their absence, rather than assume their own version | |
| 57 | | Target | Used for | | 57 | describes the server. Process replacement belongs to the launcher's |
| 58 | | ------------------------- | ------------------------------------------------------------------------ | | 58 | [update protocol](./server-updates.md); the connection runtime handles the |
| 59 | | `PrimaryConnectionTarget` | The platform-managed local server (desktop backend, CLI-served web app). | | 59 | resulting disconnect. |
| 60 | | `BearerConnectionTarget` | Any manually paired endpoint reached over direct HTTP/WebSocket. | | ||
| 61 | | `RelayConnectionTarget` | Managed T3 Connect relay tunnels. | | ||
| 62 | | `SshConnectionTarget` | Desktop-managed SSH environments. | | ||
| 63 | |||
| 64 | Bearer, relay, and SSH are persisted; primary is platform-managed. Note that Tailscale is not a | ||
| 65 | separate target kind. A Tailscale URL is paired through the ordinary bearer path in | ||
| 66 | [`onboarding.ts`][onboarding] (`preparePairingRegistration`), which accepts either a pairing URL or a | ||
| 67 | host plus pairing code. Tailscale is an endpoint provider and transport, not a distinct runtime | ||
| 68 | concept. | ||
| 69 | |||
| 70 | ### AdvertisedEndpoint | ||
| 71 | |||
| 72 | A server- or desktop-authored candidate endpoint for an environment: a concrete HTTP and WebSocket | ||
| 73 | base URL pair, a default/available/unavailable marker, reachability hints (loopback, LAN, private, | ||
| 74 | public, tunnel), and compatibility hints such as whether the hosted HTTPS app can use it. | ||
| 75 | |||
| 76 | Clients treat advertised endpoints as hints, not proof that a route works from the current device. | ||
| 77 | The connection attempt decides. | ||
| 78 | |||
| 79 | The UI shows one default endpoint in the network-access summary and keeps the rest behind an advanced | ||
| 80 | list. `selectPairingEndpoint` in | ||
| 81 | [`ConnectionsSettings.tsx`](../../apps/web/src/components/settings/ConnectionsSettings.tsx) excludes | ||
| 82 | unavailable endpoints and then picks, in order: | ||
| 83 | |||
| 84 | 1. the saved `defaultEndpointKey` override; | ||
| 85 | 2. the first endpoint marked `isDefault`; | ||
| 86 | 3. the first endpoint whose reachability is not `loopback`; | ||
| 87 | 4. the first endpoint compatible with the hosted HTTPS app; | ||
| 88 | 5. otherwise nothing. | ||
| 89 | |||
| 90 | There is no unconditional loopback fallback. A loopback endpoint only wins through an explicit saved | ||
| 91 | override or `isDefault`. Persist the override by stable endpoint kind rather than raw URL where | ||
| 92 | possible, since LAN addresses change with networks; Tailscale endpoints use provider-specific stable | ||
| 93 | keys (`tailscale-ip:`, `tailscale-magicdns:`). | ||
| 94 | |||
| 95 | ### Endpoint providers | ||
| 96 | |||
| 97 | Endpoint providers contribute advertised endpoints without becoming part of the core environment | ||
| 98 | model: core owns environments, pairing, and connection lifecycle, and providers return normalized | ||
| 99 | `AdvertisedEndpoint` records. | ||
| 100 | |||
| 101 | Tailscale is the first provider, and T3 manages more than discovery. When `tailscaleServeEnabled` is | ||
| 102 | set, the server acquires a Tailscale serve mapping for its actual listening port at startup with | ||
| 103 | `ensureTailscaleServe` and releases it with `disableTailscaleServe` on scope close | ||
| 104 | (`apps/server/src/server.ts`, using [`@t3tools/tailscale`](../../packages/tailscale/src/tailscale.ts)). | ||
| 105 | Endpoint identifiers are synthesized in `apps/desktop/src/backend/tailscaleEndpointProvider.ts` with | ||
| 106 | `private-network` reachability. | ||
| 107 | |||
| 108 | ### Hosted pairing request | ||
| 109 | |||
| 110 | A hosted pairing request is a bootstrap URL for the static web app, not a transport: | ||
| 111 | |||
| 112 | ```text | ||
| 113 | https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE | ||
| 114 | ``` | ||
| 115 | |||
| 116 | The hosted app reads `host`, takes the token from the URL hash, exchanges it directly with that | ||
| 117 | backend, strips the token from browser history, and saves the environment record locally. Helpers | ||
| 118 | live in [`shared/remote.ts`](../../packages/shared/src/remote.ts) (`setPairingTokenOnUrl`, | ||
| 119 | `getPairingTokenFromUrl`, `stripPairingTokenFromUrl`) and `apps/web/src/hostedPairing.ts`. | ||
| 120 | |||
| 121 | Constraints: | ||
| 122 | |||
| 123 | - the hosted app does not proxy HTTP or WebSocket traffic; | ||
| 124 | - the backend must be directly reachable from the browser; | ||
| 125 | - HTTPS pages can only reach HTTPS/WSS backends; | ||
| 126 | - HTTP LAN endpoints keep using direct desktop or CLI pairing URLs; | ||
| 127 | - the token belongs in the hash so it is never sent to the hosted app origin. | ||
| 128 | |||
| 129 | ### RepositoryIdentity and Project | ||
| 130 | |||
| 131 | `RepositoryIdentity` is a best-effort logical repo grouping across environments, used for UI grouping | ||
| 132 | and correlation only, never for routing. `Project` remains environment-local: a local clone and a | ||
| 133 | remote clone are different projects that may share a `RepositoryIdentity`, and threads bind to one | ||
| 134 | project in one environment. | ||
| 135 | |||
| 136 | ## Access methods | ||
| 137 | |||
| 138 | Access answers one question: how does the client speak WebSocket to a T3 server? It does not answer | ||
| 139 | how the server got started or who manages the process. | ||
| 140 | |||
| 141 | ### Direct WebSocket access | ||
| 142 | |||
| 143 | `wss://t3.example.com` or `ws://10.0.0.15:3773`, paired as a bearer target. This is the base model. | ||
| 144 | It works for desktop, mobile, and web with no client-side process management. Browser security rules | ||
| 145 | are part of it: a hosted HTTPS client cannot connect to plain `ws://` or `http://` LAN backends. | ||
| 146 | |||
| 147 | ### Relay-tunneled access | ||
| 148 | |||
| 149 | Managed T3 Connect relay tunnels use `RelayConnectionTarget` and are the answer when the host is | ||
| 150 | behind NAT, inbound ports are unavailable, or mobile must reach a desktop-hosted environment. From | ||
| 151 | the client's perspective this is still an ordinary WebSocket connection; the route is mediated. The | ||
| 152 | relay Worker only brokers credentials and a managed endpoint; application traffic then flows over | ||
| 153 | the provisioned Cloudflare tunnel hostname for the life of the connection, not through the relay | ||
| 154 | Worker itself. See [t3-connect.md](./t3-connect.md). | ||
| 155 | |||
| 156 | ### Tailscale access | ||
| 157 | |||
| 158 | A T3-managed `tailscale serve` mapping exposes the server on the tailnet over HTTPS, and the | ||
| 159 | resulting private-network endpoints are advertised for pairing. Connection then follows the ordinary | ||
| 160 | bearer path. | ||
| 161 | |||
| 162 | ### Desktop-managed SSH access | ||
| 163 | |||
| 164 | SSH is an access and launch helper, not a separate environment type. `DesktopSshEnvironment` | ||
| 165 | ([apps/desktop/src/ssh/DesktopSshEnvironment.ts][sshenv]) exposes `discoverHosts`, | ||
| 166 | `ensureEnvironment`, and `disconnectEnvironment`. It discovers targets from SSH config and known | ||
| 167 | hosts, owns password/askpass prompts, and delegates lifecycle to `SshEnvironmentManager` in | ||
| 168 | [packages/ssh/src/tunnel.ts][sshtunnel], which resolves the target, launches or reuses the remote T3 | ||
| 169 | server, opens a local tunnel, checks HTTP readiness, optionally issues a remote pairing token, and | ||
| 170 | returns local HTTP/WS endpoints. Disconnect closes the tunnel and stops the remote server if the | ||
| 171 | launcher started it; a server that was already running (marked `external`) is left running. | ||
| 172 | |||
| 173 | The desktop main process owns this because it can spawn SSH, manage prompts, write launch scripts, | ||
| 174 | and clean up forwards. The renderer connects through the forwarded URL like any other environment and | ||
| 175 | needs no SSH-specific RPC path. | ||
| 176 | |||
| 177 | Failure handling is explicit: SSH auth failure surfaces before an environment is saved, remote launch | ||
| 178 | failure includes launcher output where available, forwarded-port failure leaves the environment | ||
| 179 | disconnected rather than falling back to an unrelated endpoint, and reconnect restores the SSH bridge | ||
| 180 | before reconnecting the WebSocket client. | ||
| 181 | |||
| 182 | ## Launch methods | ||
| 183 | |||
| 184 | Launch answers a different question: how does a T3 server come to exist on the target machine? Keep | ||
| 185 | it separate from access. | ||
| 186 | |||
| 187 | - **Pre-existing server.** The operator already runs T3 and the client connects directly or through a | ||
| 188 | tunnel. | ||
| 189 | - **Desktop-managed remote launch over SSH.** Desktop probes the machine, launches or reuses a remote | ||
| 190 | server, forwards a port, and the renderer connects normally. The saved environment records that it | ||
| 191 | came from SSH launch for reconnect and lifecycle UX only; that metadata never changes the protocol | ||
| 192 | or the identity model. | ||
| 193 | - **Client-managed local publish.** A local server is published through the relay with | ||
| 194 | `t3 connect link`, exposing a desktop-hosted environment to mobile without router or firewall | ||
| 195 | changes. | ||
| 196 | |||
| 197 | The same `ExecutionEnvironment` can be reached several of these ways. Only the launch and access | ||
| 198 | paths differ. | ||
| 199 | |||
| 200 | ## Security model | ||
| 201 | |||
| 202 | Some environments are reachable over untrusted networks, so remote-capable environments require | ||
| 203 | explicit authentication, tunnel exposure never relies on obscurity, and saved endpoints carry enough | ||
| 204 | auth metadata to reconnect safely. | ||
| 205 | |||
| 206 | WebSocket authentication is a dedicated short-lived ticket, not a token in a query string. The client | ||
| 207 | presents its long-lived bearer or DPoP credential in HTTP headers to | ||
| 208 | `POST /api/auth/websocket-ticket` ([authorization/remote.ts][authremote]), and appends only the | ||
| 209 | returned ticket as `wsTicket` on the socket URL. The server issues it through | ||
| 210 | `EnvironmentAuth.issueWebSocketTicket`; tickets are tagged `kind: "websocket"` and default to a | ||
| 211 | five-minute TTL (`DEFAULT_WEBSOCKET_TOKEN_TTL` in `apps/server/src/auth/SessionStore.ts`). The | ||
| 212 | handshake verifies the ticket, and each RPC method still enforces its own scope. See | ||
| 213 | [environment-auth.md](./environment-auth.md). | ||
| 214 | |||
| 215 | Hosted pairing is a client-side convenience only. The hosted app must not receive pairing tokens | ||
| 216 | through query parameters, must not store pairing state server-side, and must not imply that an HTTP | ||
| 217 | backend is reachable from an HTTPS browser context. | ||
| 218 | |||
| 219 | ## Version coordination | ||
| 220 | |||
| 221 | Remote environments stay online while clients move to newer releases. The environment descriptor | ||
| 222 | carries the running server version and may advertise a safe replacement path, so the UI can show the | ||
| 223 | right action without making the transport responsible for process management. The connection | ||
| 224 | supervisor owns the resulting disconnect and reconnect like any other involuntary close. See | ||
| 225 | [server-updates.md](./server-updates.md). | ||
| 226 | |||
| 227 | ## Machine identity | ||
| 228 | |||
| 229 | Every environment is drawn with one of a fixed set of machine glyphs (`EnvironmentMachineKind` in | ||
| 230 | contracts: server, cloud, desktop, laptop, mac-mini, mac-studio). Two inputs feed it, and the | ||
| 231 | precedence lives in one helper, `resolveEnvironmentMachineKind`, so web and mobile cannot drift: | ||
| 232 | |||
| 233 | 1. `settings.environmentIcon`, a nullable server setting the user picks in Settings → Connections. | ||
| 234 | It is server state rather than client state so every device connecting to that server agrees. | ||
| 235 | 2. `environment.platform.machine`, detected once at startup by [ServerEnvironmentMachine.ts][machine] | ||
| 236 | (IOKit product name / `hw.model` on macOS, `/sys/class/dmi/id` on Linux with virtualization | ||
| 237 | markers mapped to "cloud"). Absent when there is no signal, so older servers, Windows hosts, and | ||
| 238 | containers decode without it. | ||
| 239 | 3. A generic server otherwise. | ||
| 240 | |||
| 241 | Clients treat the field like any other capability: absent means "use the fallback", never "wait". | ||
| 242 | |||
| 243 | ## Future work | ||
| 244 | |||
| 245 | These remain unbuilt and are listed to keep the model honest: | ||
| 246 | |||
| 247 | - third-party tunnel products as additional endpoint providers; | ||
| 248 | - a relay-hosted OAuth callback broker (see [t3-connect.md](./t3-connect.md)); | ||
| 249 | - richer multi-environment UI beyond the current connections list. | ||
| 250 | |||
| 251 | [model]: ../../packages/client-runtime/src/connection/model.ts | ||
| 252 | [onboarding]: ../../packages/client-runtime/src/connection/onboarding.ts | ||
| 253 | [authremote]: ../../packages/client-runtime/src/authorization/remote.ts | ||
| 254 | [sshenv]: ../../apps/desktop/src/ssh/DesktopSshEnvironment.ts | ||
| 255 | [sshtunnel]: ../../packages/ssh/src/tunnel.ts | ||
| 256 | [machine]: ../../apps/server/src/environment/ServerEnvironmentMachine.ts |
docs/internals/resource-telemetry.md+50-394| ... | @@ -1,394 +1,50 @@ | ... | @@ -1,394 +1,50 @@ |
| 1 | # Resource telemetry architecture | 1 | # Resource telemetry |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | Process counters come from a [standalone Rust monitor](../../native/resource-monitor/src/main.rs) |
| 4 | 4 | using `sysinfo`. Electron main supplies host power and Electron process metrics. | |
| 5 | Status: implemented | 5 | Keeping native collection outside Node isolates collector crashes and avoids a |
| 6 | 6 | Node/Electron addon ABI matrix. Desktop and CLI servers use the same child-process | |
| 7 | ## Purpose | 7 | protocol. A missing or failed collector leaves the server running. |
| 8 | 8 | ||
| 9 | Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset` | 9 | ## Collection cost |
| 10 | subprocess probes with two persistent, direct data sources: | 10 | |
| 11 | 11 | The native child owns sampling and bounded in-memory history. The server requests | |
| 12 | 1. a standalone Rust resource-monitor executable that reads process counters | 12 | continuous snapshots only while diagnostics has live subscribers and fetches |
| 13 | through operating-system APIs via `sysinfo`; | 13 | history on demand. Consuming host power for background scheduling must not retain |
| 14 | 2. Electron main-process APIs for Electron process metrics and host power state. | 14 | live diagnostics. There is no telemetry database or recurring shell-probe fallback. |
| 15 | 15 | ||
| 16 | The native monitor owns bounded in-memory history. The server only merges and | 16 | History has independent bounds for age, snapshot count, process rows, and retained |
| 17 | summarizes that history when diagnostics requests it. Telemetry history is not | 17 | bytes. A count limit alone cannot bound memory when command lines vary in size. |
| 18 | persisted to disk or continuously copied into Node. | 18 | Large process trees therefore shorten the available history window. Linux task |
| 19 | 19 | enumeration is disabled because walking every `/proc/<pid>/task/<tid>` directory | |
| 20 | ## Why a standalone executable | 20 | makes sampling itself expensive. |
| 21 | 21 | ||
| 22 | The monitor is intentionally not a Node native addon. | 22 | Electron power updates travel over private inherited pipes, independent of the |
| 23 | 23 | renderer connection. Power events and slow heartbeats continue with diagnostics | |
| 24 | - No N-API, `ffi-rs`, or dynamic-library ABI is loaded into the server process. | 24 | closed; `app.getAppMetrics()` runs only on live demand. The receiver's stale deadline |
| 25 | - A monitor crash cannot corrupt the Node runtime. | 25 | must exceed the slowest configured heartbeat plus scheduling grace, or intentional |
| 26 | - The server can supervise, restart, version-check, and measure the monitor as a | 26 | idle polling makes background policy oscillate between constrained and |
| 27 | normal child process. | 27 | unconstrained states. Headless servers leave unavailable power data unknown. |
| 28 | - The same protocol works for the desktop app and the published CLI. | 28 | |
| 29 | - Packaging is a single platform executable instead of an addon toolchain plus | 29 | ## Measurement traps |
| 30 | Node/Electron ABI matrix. | 30 | |
| 31 | 31 | - Process identity includes start time because operating systems reuse PIDs. | |
| 32 | The cost is one persistent child process and NDJSON serialization. That is a | 32 | Electron and native start times have different precision, so merging allows a |
| 33 | better failure boundary than repeatedly spawning shell utilities or loading | 33 | small tolerance. Process signaling rechecks the native identity with a fresh |
| 34 | native code into Node. | 34 | sample. |
| 35 | 35 | - Snapshot sequence numbers belong to a monitor generation. Comparing them across | |
| 36 | ## Runtime topology | 36 | restarts would discard the new monitor's samples until its sequence caught up. |
| 37 | 37 | - Sampling can miss a process that starts and exits between samples. Cumulative | |
| 38 | ### Desktop | 38 | counters still yield deltas for processes observed across samples. |
| 39 | 39 | - Windows process I/O includes more than disk traffic. Unix counters report storage | |
| 40 | ```text | 40 | I/O, which can differ from logical application reads and writes because of OS |
| 41 | Electron main | 41 | caching. Keep instrumented logical I/O separate from these counters. |
| 42 | ├─ powerMonitor | 42 | - Group totals accumulate observed deltas since telemetry started. Per-process |
| 43 | ├─ app.getAppMetrics() while diagnostics is open | 43 | cumulative counters cover the operating system's lifetime for that process. |
| 44 | ├─ inherited fd 4, telemetry NDJSON ─────────────┐ | 44 | - Historical replay uses native samples without current Electron CPU or memory |
| 45 | └─ inherited fd 5, demand-control NDJSON â—€──────┤ | 45 | metrics. Merging the latest Electron values would overwrite the past. |
| 46 | â–¼ | 46 | |
| 47 | Node server ── stdin/stdout NDJSON ── Rust resource monitor | 47 | A WSL backend needs a Linux monitor even though Electron runs on Windows. Windows |
| 48 | │ | 48 | desktop packages currently supply only the Windows executable, so native process |
| 49 | ├─ ResourceTelemetry Effect service | 49 | telemetry for the WSL backend is unavailable. The inherited Electron power feed |
| 50 | ├─ background power policy projection | 50 | still works. |
| 51 | └─ WebSocket RPC/subscription ── diagnostics UI | ||
| 52 | ``` | ||
| 53 | |||
| 54 | ### Web, headless, and remote server | ||
| 55 | |||
| 56 | Electron telemetry is unavailable. The native monitor still runs beside the | ||
| 57 | server and tracks the server process tree. Power fields degrade to `unknown` | ||
| 58 | instead of invoking platform shell commands. | ||
| 59 | |||
| 60 | ### WSL backend limitation | ||
| 61 | |||
| 62 | Windows desktop packages currently ship the Windows resource-monitor executable. | ||
| 63 | That executable cannot run inside the Linux WSL backend, so a WSL-only backend | ||
| 64 | does not receive `resourceMonitorPath` and reports native process telemetry as | ||
| 65 | unavailable. Electron host-power telemetry remains available over the inherited | ||
| 66 | desktop pipe. Supporting native WSL process telemetry requires publishing a | ||
| 67 | Linux sidecar for each supported architecture in the Windows artifact and | ||
| 68 | converting its packaged path into the selected distro; the configuration | ||
| 69 | deliberately does not pass the Windows `.exe` into WSL as a false fallback. | ||
| 70 | |||
| 71 | ## Native monitor | ||
| 72 | |||
| 73 | The executable lives in `native/resource-monitor`. | ||
| 74 | |||
| 75 | It receives schema-compatible commands on stdin and emits one JSON object per | ||
| 76 | line on stdout: | ||
| 77 | |||
| 78 | - `configure` | ||
| 79 | - `setExternalProcesses` | ||
| 80 | - `setSampleInterval` | ||
| 81 | - `setStreaming` | ||
| 82 | - `sampleNow` | ||
| 83 | - `readHistory` | ||
| 84 | - `shutdown` | ||
| 85 | - `hello` | ||
| 86 | - `snapshot` | ||
| 87 | - `historyChunk` | ||
| 88 | - `error` | ||
| 89 | |||
| 90 | The protocol version is defined by | ||
| 91 | `RESOURCE_MONITOR_PROTOCOL_VERSION` in | ||
| 92 | `packages/contracts/src/resourceTelemetry.ts`. | ||
| 93 | |||
| 94 | ### Collection | ||
| 95 | |||
| 96 | The monitor keeps one `sysinfo::System` instance and refreshes it at the | ||
| 97 | power-adaptive interval selected by the server. It collects: | ||
| 98 | |||
| 99 | - PID and parent PID; | ||
| 100 | - process start time and run time; | ||
| 101 | - process name and command line; | ||
| 102 | - current and cumulative CPU usage; | ||
| 103 | - resident and virtual memory; | ||
| 104 | - cumulative process I/O counters. | ||
| 105 | |||
| 106 | On Linux, task/thread enumeration is disabled. Command lines are refreshed with | ||
| 107 | each sample so process replacements remain visible. Disabling task enumeration | ||
| 108 | avoids walking every `/proc/<pid>/task/<tid>` directory on each refresh. | ||
| 109 | |||
| 110 | ### Process-tree selection | ||
| 111 | |||
| 112 | Each sample scans the accessible process table, builds the PID/PPID graph, and | ||
| 113 | retains: | ||
| 114 | |||
| 115 | - the server process; | ||
| 116 | - every descendant of the server, including provider-spawned grandchildren such | ||
| 117 | as shells, `node`, `tsgo`, language servers, and other tools; | ||
| 118 | - Electron processes supplied as explicit external roots; | ||
| 119 | - descendants of those Electron roots; | ||
| 120 | - the resource monitor itself, because it is a server child. | ||
| 121 | |||
| 122 | Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native | ||
| 123 | start times are matched with a two-second tolerance because native start times | ||
| 124 | can have coarser platform resolution. | ||
| 125 | |||
| 126 | The process list is emitted in depth-first tree order so renderer collapse and | ||
| 127 | expansion preserves complete subtrees. | ||
| 128 | |||
| 129 | ### Native history and streaming | ||
| 130 | |||
| 131 | Every native sample is appended to a one-hour in-memory ring bounded to 3,600 | ||
| 132 | snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes. | ||
| 133 | History stays in the sidecar until a `readHistory` request and is returned in | ||
| 134 | bounded chunks. The first bound reached wins, so high process counts or large | ||
| 135 | process names and command lines shorten the effective history window. | ||
| 136 | |||
| 137 | Periodic snapshot streaming is disabled by default. The server enables it only | ||
| 138 | while at least one diagnostics subscription is retained. `sampleNow` remains | ||
| 139 | available for explicit refreshes and identity validation. | ||
| 140 | |||
| 141 | The server adjusts native sampling without restarting the sidecar: | ||
| 142 | |||
| 143 | - suspended, locked, low-power, or serious/critical thermal state: 15 seconds; | ||
| 144 | - battery: 5 seconds; | ||
| 145 | - normal AC: 5 seconds in the background and 1 second while live diagnostics is | ||
| 146 | open; | ||
| 147 | - unknown or stale power: 5 seconds in the background and 1 second while live | ||
| 148 | diagnostics is open. | ||
| 149 | |||
| 150 | ### Sampling limits | ||
| 151 | |||
| 152 | This is counter sampling, not syscall tracing. | ||
| 153 | |||
| 154 | - A process that starts and exits entirely between samples may not be observed. | ||
| 155 | - Cumulative CPU and I/O counters still provide accurate deltas for processes | ||
| 156 | that survive across samples. | ||
| 157 | - Exact file paths, individual write syscalls, ETW events, eBPF events, and | ||
| 158 | Endpoint Security events are outside this implementation. | ||
| 159 | |||
| 160 | Those deeper tracing systems can be added later as opt-in diagnostic modes | ||
| 161 | without changing the public `ResourceTelemetry` model. | ||
| 162 | |||
| 163 | ## I/O semantics | ||
| 164 | |||
| 165 | The monitor preserves platform semantics instead of presenting all counters as | ||
| 166 | equivalent: | ||
| 167 | |||
| 168 | - Unix-like platforms report storage I/O counters exposed by `sysinfo`. | ||
| 169 | - Windows reports all process I/O bytes, not only disk bytes. | ||
| 170 | - Operating-system caches can prevent logical application reads or writes from | ||
| 171 | appearing as physical storage bytes. | ||
| 172 | |||
| 173 | The UI therefore labels these values as I/O reads and writes and exposes the | ||
| 174 | per-process `ioSemantics` value. | ||
| 175 | |||
| 176 | Group totals are observed deltas since telemetry startup. Per-process total | ||
| 177 | columns are the operating system's cumulative counters for that process. | ||
| 178 | |||
| 179 | ## Electron telemetry | ||
| 180 | |||
| 181 | Electron main owns `DesktopTelemetryPublisher`. | ||
| 182 | |||
| 183 | Power events trigger an immediate snapshot. While diagnostics is closed, the | ||
| 184 | server sends the configured active and idle host-power intervals to Electron | ||
| 185 | (30 seconds and 2 minutes in the balanced profile). During those heartbeats | ||
| 186 | Electron reads: | ||
| 187 | |||
| 188 | - `powerMonitor.isOnBatteryPower()`; | ||
| 189 | - `powerMonitor.getSystemIdleTime()`; | ||
| 190 | - `powerMonitor.getSystemIdleState()`; | ||
| 191 | - `powerMonitor.getCurrentThermalState()`. | ||
| 192 | |||
| 193 | `app.getAppMetrics()` is only called while diagnostics demand is active. Its | ||
| 194 | live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while | ||
| 195 | locked, suspended, or thermally constrained. | ||
| 196 | |||
| 197 | It also listens for: | ||
| 198 | |||
| 199 | - lock and unlock; | ||
| 200 | - suspend and resume; | ||
| 201 | - AC and battery transitions; | ||
| 202 | - thermal-state changes; | ||
| 203 | - CPU speed-limit changes. | ||
| 204 | |||
| 205 | Suspension stays latched across event-driven snapshots. An explicit resume | ||
| 206 | clears it immediately; a periodic heartbeat also clears a stale latch so a | ||
| 207 | missed resume event cannot leave telemetry permanently constrained. | ||
| 208 | |||
| 209 | Electron does not expose a cross-platform low-power-mode getter, so that field | ||
| 210 | remains `unknown`. | ||
| 211 | |||
| 212 | The desktop backend is spawned with: | ||
| 213 | |||
| 214 | - fd 3 for the existing bootstrap payload; | ||
| 215 | - fd 4 for Electron-to-server telemetry NDJSON; | ||
| 216 | - fd 5 for server-to-Electron diagnostics-demand NDJSON. | ||
| 217 | |||
| 218 | These are private Electron-main/server pipes. They do not use the renderer | ||
| 219 | WebSocket and are recreated for every backend restart. | ||
| 220 | |||
| 221 | ## Server Effect services | ||
| 222 | |||
| 223 | The implementation is under `apps/server/src/resourceTelemetry`. | ||
| 224 | |||
| 225 | ### `ResourceMonitorBinary` | ||
| 226 | |||
| 227 | Resolves an executable from: | ||
| 228 | |||
| 229 | 1. `T3CODE_RESOURCE_MONITOR_PATH`; | ||
| 230 | 2. desktop bootstrap configuration; | ||
| 231 | 3. bundled CLI resources; | ||
| 232 | 4. local Cargo build outputs. | ||
| 233 | |||
| 234 | Unsupported platforms, missing binaries, and non-executable binaries use | ||
| 235 | schema-backed tagged errors with descriptive messages. | ||
| 236 | |||
| 237 | ### `NativeTelemetryClient` | ||
| 238 | |||
| 239 | Owns the resource-monitor process and protocol. | ||
| 240 | |||
| 241 | - validates the hello/version handshake; | ||
| 242 | - sends configuration and external process roots; | ||
| 243 | - adapts the native interval from host power state; | ||
| 244 | - enables streaming only for scoped live subscribers; | ||
| 245 | - reads chunked native history on demand; | ||
| 246 | - exposes `sampleNow`; | ||
| 247 | - serializes commands; | ||
| 248 | - supervises process exit and protocol failure; | ||
| 249 | - restarts with bounded exponential backoff; | ||
| 250 | - opens a circuit after repeated failures; | ||
| 251 | - supports explicit retry; | ||
| 252 | - publishes health changes immediately. | ||
| 253 | |||
| 254 | Snapshot sequence numbers are scoped to a monitor generation. Server ingestion | ||
| 255 | uses the monitor restart count as the generation key, so sequence reset after a | ||
| 256 | restart cannot freeze telemetry. | ||
| 257 | |||
| 258 | ### `DesktopTelemetryReceiver` | ||
| 259 | |||
| 260 | Reads fd 4, decodes schema-validated messages, stores the latest Electron | ||
| 261 | snapshot, and publishes desktop health. It writes diagnostics demand to fd 5 | ||
| 262 | and gives the first sample a 90-second startup deadline. Once samples arrive, | ||
| 263 | the stale deadline stays beyond the slower configured host-power interval with | ||
| 264 | 30 seconds of scheduling grace, so intentional 2–10 minute idle polling does | ||
| 265 | not oscillate the policy between constrained and unconstrained states. Decode | ||
| 266 | errors, protocol mismatch, control-write failure, stream failure, stale input, | ||
| 267 | and normal stream closure are represented explicitly. | ||
| 268 | |||
| 269 | ### `ResourceTelemetry` | ||
| 270 | |||
| 271 | Merges native and Electron data and owns public telemetry semantics. | ||
| 272 | |||
| 273 | - calculates CPU and I/O rates from cumulative native counters; | ||
| 274 | - preserves the last native rates during desktop-only updates; | ||
| 275 | - classifies backend, Electron, and monitor processes; | ||
| 276 | - computes process depth and child relationships; | ||
| 277 | - tracks starts, exits, CPU time, and observed I/O; | ||
| 278 | - projects power data; | ||
| 279 | - acquires native streaming and Electron process metrics only for scoped live | ||
| 280 | subscribers; | ||
| 281 | - queries and replays native history only when requested; | ||
| 282 | - validates `(pid, startTimeMs)` before process signaling; | ||
| 283 | - updates history health even when no further native sample arrives. | ||
| 284 | |||
| 285 | Electron and monitor processes are visible but are not valid targets for the | ||
| 286 | existing process-signal RPC. | ||
| 287 | |||
| 288 | ### History projection | ||
| 289 | |||
| 290 | `ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native | ||
| 291 | snapshots to derive rates, lifecycle counters, buckets, and process summaries. | ||
| 292 | Current Electron process metrics are intentionally excluded from historical | ||
| 293 | replay so they cannot overwrite older native CPU or memory samples. | ||
| 294 | |||
| 295 | ### `ResourceAttribution` | ||
| 296 | |||
| 297 | Tracks known logical application I/O separately from OS counters. Current | ||
| 298 | integration points record successful writes for: | ||
| 299 | |||
| 300 | - provider native and canonical event logs; | ||
| 301 | - the local server trace sink. | ||
| 302 | |||
| 303 | Entries contain component, operation, logical bytes, count, and elapsed time. | ||
| 304 | Future persistence paths should call `ResourceAttribution.record` rather than | ||
| 305 | adding diagnostics-specific counters. | ||
| 306 | |||
| 307 | ## Background policy integration | ||
| 308 | |||
| 309 | `HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host | ||
| 310 | power does not retain live resource diagnostics or invoke shell probes. | ||
| 311 | |||
| 312 | The monitor updates its latest timestamp on every Electron sample but only | ||
| 313 | publishes semantic state changes. Increasing idle seconds alone does not cause a | ||
| 314 | background-policy broadcast every second. | ||
| 315 | |||
| 316 | ## Public API and UI | ||
| 317 | |||
| 318 | The WebSocket RPC surface provides: | ||
| 319 | |||
| 320 | - current snapshot; | ||
| 321 | - bounded history; | ||
| 322 | - explicit monitor retry; | ||
| 323 | - a live snapshot subscription. | ||
| 324 | |||
| 325 | The diagnostics page displays: | ||
| 326 | |||
| 327 | - aggregate CPU, memory, I/O, and process counts; | ||
| 328 | - backend, Electron, and monitor overhead groups; | ||
| 329 | - power and thermal state; | ||
| 330 | - collector health and restart information; | ||
| 331 | - CPU and I/O history; | ||
| 332 | - a collapsible live process tree; | ||
| 333 | - safe process signaling for backend descendants; | ||
| 334 | - instrumented logical application I/O. | ||
| 335 | |||
| 336 | Legacy process diagnostics RPCs are projected from the same service so they no | ||
| 337 | longer start recurring process-table commands. | ||
| 338 | |||
| 339 | ## Packaging | ||
| 340 | |||
| 341 | Desktop artifact builds compile the Rust target, stage it as | ||
| 342 | `resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the | ||
| 343 | backend bootstrap. | ||
| 344 | |||
| 345 | CLI release jobs upload each active platform monitor artifact and copy it into: | ||
| 346 | |||
| 347 | ```text | ||
| 348 | apps/server/dist/resource-monitor/<platform>-<arch>/ | ||
| 349 | ``` | ||
| 350 | |||
| 351 | The published server package already includes `dist`, so those executables ship | ||
| 352 | with the CLI. Missing platform artifacts degrade native telemetry to | ||
| 353 | `unavailable`; the server continues running. | ||
| 354 | |||
| 355 | ## Resource and failure behavior | ||
| 356 | |||
| 357 | Steady state uses: | ||
| 358 | |||
| 359 | - one native process; | ||
| 360 | - power-adaptive native counter sampling with no periodic Node snapshot stream; | ||
| 361 | - event-driven Electron power updates plus profile-driven heartbeats (30–60 | ||
| 362 | seconds while active and 2–10 minutes while idle); | ||
| 363 | - no `app.getAppMetrics()` calls while diagnostics is closed; | ||
| 364 | - no telemetry database; | ||
| 365 | - no recurring shell probes; | ||
| 366 | - bounded PubSub queues and native ring history. | ||
| 367 | |||
| 368 | The diagnostics page exposes the monitor's own process resource usage and | ||
| 369 | collection duration so the observer's cost is measurable. | ||
| 370 | |||
| 371 | Failures are isolated: | ||
| 372 | |||
| 373 | - native failure does not stop the server; | ||
| 374 | - Electron telemetry loss does not stop native telemetry; | ||
| 375 | - schema/version errors are visible in health; | ||
| 376 | - repeated native failures stop automatic restart churn until explicit retry; | ||
| 377 | - server and desktop shutdown close their respective streams and child process | ||
| 378 | scopes. | ||
| 379 | |||
| 380 | ## Future integration points | ||
| 381 | |||
| 382 | High-value follow-up work can use the existing service boundaries: | ||
| 383 | |||
| 384 | - opt-in file-path attribution through platform-specific tracing; | ||
| 385 | - process lifecycle events to reduce the chance of missing very short-lived | ||
| 386 | children; | ||
| 387 | - additional `ResourceAttribution` instrumentation for databases, checkpoints, | ||
| 388 | caches, and file synchronization; | ||
| 389 | - exported diagnostic bundles; | ||
| 390 | - adaptive sample intervals based on diagnostics visibility and active work. | ||
| 391 | |||
| 392 | These additions should preserve the current rules: direct platform APIs, | ||
| 393 | schema-validated boundaries, explicit metric semantics, bounded retention, and | ||
| 394 | no mandatory telemetry persistence. |
docs/internals/scripts.md deleted-215| ... | @@ -1,215 +0,0 @@ | ||
| 1 | # Scripts | ||
| 2 | |||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | ||
| 4 | |||
| 5 | ## First checkout | ||
| 6 | |||
| 7 | T3 Code uses [Vite+](https://viteplus.dev/guide/). Install the global `vp` command, install | ||
| 8 | dependencies, then start the dev stack: | ||
| 9 | |||
| 10 | ```bash | ||
| 11 | curl -fsSL https://vite.plus | bash # Windows: irm https://vite.plus/ps1 | iex | ||
| 12 | vp i | ||
| 13 | vp run dev | ||
| 14 | ``` | ||
| 15 | |||
| 16 | Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to | ||
| 17 | Node otherwise, and nothing in contributor setup needs it. | ||
| 18 | |||
| 19 | `vp run dev` prints a one-time pairing URL. Open it so the first browser navigation is | ||
| 20 | authenticated. | ||
| 21 | |||
| 22 | ## Dev | ||
| 23 | |||
| 24 | - `vp run dev`: Starts contracts, server, and web in watch mode. | ||
| 25 | - `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The | ||
| 26 | startup pairing URL is built against the shared origin, and the mapping is removed on exit. | ||
| 27 | Shared runs default to Vite's bundled dev mode (`T3CODE_BUNDLED_DEV=1`): a remote browser pays a | ||
| 28 | network round trip per import level in unbundled dev, which turns a cold module graph into | ||
| 29 | minutes of waterfall. Set `T3CODE_BUNDLED_DEV=0` to opt a shared run back out. | ||
| 30 | The web entry loads the app with a dynamic import so React refresh starts before shared UI | ||
| 31 | chunks run. Keep app imports out of that entry. A static import can work on the first load | ||
| 32 | and then fail on reload after Vite splits code for lazy routes. | ||
| 33 | Bundled dev uses Tailwind's watched files to rebuild CSS. Its Vite-only hot-update hook is | ||
| 34 | disabled in this mode because Rolldown does not supply the Vite server or module graph. | ||
| 35 | - `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes | ||
| 36 | `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has | ||
| 37 | no effect; use `--browser`. | ||
| 38 | - `vp run dev:server`: Starts just the server. It runs on Node (`node --watch src/bin.ts`), so | ||
| 39 | without Bun present it selects `NodePtyAdapter` and `NodeHttpServer`. | ||
| 40 | - `vp run dev:web`: Starts just the Vite dev server for the web app. | ||
| 41 | - `vp run dev:desktop`: Starts the Electron shell against the dev server. | ||
| 42 | - `vp run dev:marketing`: Starts the Astro marketing site. | ||
| 43 | - Pass dev-runner flags directly after the root task name, for example: | ||
| 44 | `vp run dev --home-dir /tmp/t3code-dev` | ||
| 45 | |||
| 46 | ### Dev state directories | ||
| 47 | |||
| 48 | - Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even | ||
| 49 | when `T3CODE_HOME` is set, storing state in `<worktree>/.t3/userdata`. Pass `--home-dir <path>` to | ||
| 50 | choose another isolated directory explicitly. Submodules are not worktrees and keep the normal | ||
| 51 | precedence. | ||
| 52 | - From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state | ||
| 53 | separate from `~/.t3/userdata`. An explicit `--home-dir <path>` stores state under | ||
| 54 | `<path>/userdata`; the base directory remains available for caches, worktrees, and other shared | ||
| 55 | data. | ||
| 56 | |||
| 57 | ## Build, check, test | ||
| 58 | |||
| 59 | - `vp run build`: Fans out over `apps/*`, `packages/*`, `oxlint-plugin-t3code`, and `scripts`. | ||
| 60 | Workspaces that define a build task run one: desktop, marketing, server (which depends on web), and | ||
| 61 | web. Shared packages are consumed and bundled transitively rather than built separately. | ||
| 62 | - `vp run build:desktop`: Builds the desktop pipeline (desktop plus server). | ||
| 63 | - `vp run start`: Runs the production server (serves the built web app as static files). | ||
| 64 | - `vp check`: Vite+ format, lint, and type checks. This repo sets `typeCheck: false` in its lint | ||
| 65 | options, so workspace type checking runs separately. | ||
| 66 | - `vp run typecheck`: Strict TypeScript checks for all packages. | ||
| 67 | - `vp run test`: Runs workspace tests. | ||
| 68 | - `vp run lint:mobile`: Mobile native static analysis (`scripts/mobile-native-static-check.ts`). | ||
| 69 | - `node apps/server/scripts/t3-sqlite-state.ts <query|exec> --base-dir <path> ...`: Inspects or seeds | ||
| 70 | an isolated T3 SQLite database; writes create a private backup first. | ||
| 71 | |||
| 72 | ## Desktop artifacts | ||
| 73 | |||
| 74 | - `vp run dist:desktop:artifact --platform <mac|linux|win> --target <target> --arch <arch>`: Builds a desktop artifact for a specific platform/target/arch. | ||
| 75 | - `vp run dist:desktop:dmg`: Builds a shareable macOS `.dmg` into `./release`. Architecture defaults | ||
| 76 | to the host, so this produces an arm64 DMG on Apple Silicon. Use `dist:desktop:dmg:arm64` or | ||
| 77 | `dist:desktop:dmg:x64`, or pass `--arch <arm64|x64|universal>`, to force one. | ||
| 78 | - `vp run dist:desktop:linux`: Builds a Linux AppImage into `./release`. | ||
| 79 | - `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64` | ||
| 80 | variants exist. | ||
| 81 | |||
| 82 | ### Linux AppImage prerequisites | ||
| 83 | |||
| 84 | Linux AppImage packaging compiles the Rust resource monitor and the libsecret browser import | ||
| 85 | helper. Install a Rust toolchain, the standard C/C++ build tools, libsecret development headers, | ||
| 86 | pkg-config, and ImageMagick before running `vp run dist:desktop:linux`. | ||
| 87 | |||
| 88 | Ubuntu and Debian: | ||
| 89 | |||
| 90 | ```bash | ||
| 91 | sudo apt-get update | ||
| 92 | sudo apt-get install cargo rustc build-essential libsecret-1-dev pkg-config imagemagick | ||
| 93 | ``` | ||
| 94 | |||
| 95 | Fedora: | ||
| 96 | |||
| 97 | ```bash | ||
| 98 | sudo dnf install rust cargo gcc gcc-c++ make libsecret-devel pkgconf-pkg-config ImageMagick | ||
| 99 | ``` | ||
| 100 | |||
| 101 | Arch Linux: | ||
| 102 | |||
| 103 | ```bash | ||
| 104 | sudo pacman -S rust base-devel libsecret pkgconf imagemagick | ||
| 105 | ``` | ||
| 106 | |||
| 107 | Linux desktop development also needs the C compiler, pkg-config, and libsecret headers. Its build | ||
| 108 | and launch commands compile `native/browser-secret/main.c` into the gitignored native build | ||
| 109 | directory. Releases place the executable in `resources/browser-secret`, outside `app.asar`. | ||
| 110 | The helper performs a read-only Chromium schema lookup through libsecret and writes the exact | ||
| 111 | secret bytes to its stdout pipe. Exit codes are 2 for a missing key, 3 for denied/locked access, | ||
| 112 | and 4 for other keyring failures; the desktop importer preserves these distinctions. | ||
| 113 | |||
| 114 | The artifact script checks these capabilities before starting the web and desktop builds. If | ||
| 115 | anything is unavailable, it reports every failed check together and prints the Ubuntu/Debian | ||
| 116 | installation command. The check compiles and links tiny temporary programs, so it also catches | ||
| 117 | installed runtime X11 libraries that are missing their development headers or linker symlinks. | ||
| 118 | |||
| 119 | ### macOS DMG prerequisites | ||
| 120 | |||
| 121 | Install the Xcode Command Line Tools and Rust before building a DMG: | ||
| 122 | |||
| 123 | ```bash | ||
| 124 | xcode-select --install | ||
| 125 | ``` | ||
| 126 | |||
| 127 | Install Rust from [rustup.rs](https://rustup.rs). The artifact script checks Cargo, Clang, Make, | ||
| 128 | `sips`, and `iconutil`. Universal builds additionally require `lipo`. It also verifies that Rust | ||
| 129 | has every requested target; add missing targets with: | ||
| 130 | |||
| 131 | ```bash | ||
| 132 | rustup target add aarch64-apple-darwin x86_64-apple-darwin | ||
| 133 | ``` | ||
| 134 | |||
| 135 | Unsigned local builds need no Apple credentials. Builds using `--signed` additionally require the | ||
| 136 | certificate, provisioning profile, team ID, and notarization configuration described below. | ||
| 137 | |||
| 138 | ### Windows installer prerequisites | ||
| 139 | |||
| 140 | Install Rust from [rustup.rs](https://rustup.rs), Python 3, and Visual Studio Build Tools. In the | ||
| 141 | Visual Studio Installer, select **Desktop development with C++** and include: | ||
| 142 | |||
| 143 | - MSVC x64/x86 build tools | ||
| 144 | - A Windows 10 or Windows 11 SDK | ||
| 145 | - MSVC Spectre-mitigated libraries | ||
| 146 | |||
| 147 | ARM64 installers require the corresponding MSVC ARM64 build tools and Spectre-mitigated libraries | ||
| 148 | instead of the x64/x86 components. | ||
| 149 | |||
| 150 | Add the Rust target matching the installer architecture: | ||
| 151 | |||
| 152 | ```powershell | ||
| 153 | rustup target add x86_64-pc-windows-msvc | ||
| 154 | # For an arm64 installer: | ||
| 155 | rustup target add aarch64-pc-windows-msvc | ||
| 156 | ``` | ||
| 157 | |||
| 158 | Windows supplies `tar.exe`; it is checked when `--wsl-prebuild` makes the artifact include the WSL | ||
| 159 | runtime. NSIS is downloaded by electron-builder and does not need a separate installation. | ||
| 160 | When `T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR=true` points the build at an existing resource monitor, | ||
| 161 | the artifact script skips the Rust and Visual Studio checks because it does not compile the monitor. | ||
| 162 | Unsigned local builds need no Azure credentials. Builds using `--signed` additionally require the | ||
| 163 | Azure Trusted Signing configuration described below. | ||
| 164 | |||
| 165 | ### Desktop `.dmg` packaging notes | ||
| 166 | |||
| 167 | - Default build is unsigned/not notarized for local sharing. | ||
| 168 | - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. | ||
| 169 | - The DMG chrome follows the release channel: neutral for Latest and the Nightly sky artwork for | ||
| 170 | Nightly. Blueprint artwork remains exclusive to Dev builds. Packaging rasterizes the selected | ||
| 171 | SVG into standard and Retina PNGs inside the disposable staging directory. | ||
| 172 | - The Finder window is 540×412 while its background is 540×380; the extra 32px accounts for the | ||
| 173 | title bar included in Finder's window bounds. | ||
| 174 | - Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a | ||
| 175 | `127.0.0.1` document URL, and not an explicit `index.html` path). | ||
| 176 | - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an | ||
| 177 | auth token for WebSocket/API traffic. | ||
| 178 | - Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first | ||
| 179 | launch. | ||
| 180 | - To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg --keep-stage` | ||
| 181 | - To allow code-signing/notarization when configured in CI/secrets, add: `--signed`. | ||
| 182 | - Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and | ||
| 183 | `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from | ||
| 184 | `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it. | ||
| 185 | - Windows `--signed` uses Azure Trusted Signing and expects: | ||
| 186 | `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`, | ||
| 187 | `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`. | ||
| 188 | - Azure authentication env vars are also required (for example service principal with secret): | ||
| 189 | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. | ||
| 190 | |||
| 191 | ## Browser development | ||
| 192 | |||
| 193 | `dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend | ||
| 194 | from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the | ||
| 195 | server, allowing the same bundle to work from localhost or a tailnet hostname. | ||
| 196 | |||
| 197 | ## Running multiple dev instances | ||
| 198 | |||
| 199 | Worktrees derive a preferred port offset from their path. | ||
| 200 | |||
| 201 | - Default ports: server `13773`, web `5733` | ||
| 202 | - Shifted ports: `base + offset` | ||
| 203 | - Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop` | ||
| 204 | |||
| 205 | Offset resolution, in order: | ||
| 206 | |||
| 207 | 1. `T3CODE_PORT_OFFSET`, which must be a non-negative integer. Negative values are rejected. | ||
| 208 | 2. `T3CODE_DEV_INSTANCE`. An all-digit value is used directly as the offset; any other non-empty | ||
| 209 | value is hashed into one. | ||
| 210 | 3. The worktree path hash. | ||
| 211 | |||
| 212 | Collision scanning depends on the mode. `dev:web` scans only the web port and shifts only the web | ||
| 213 | offset. `dev:server` scans only the server port. `dev` and `dev:desktop` scan both and shift them | ||
| 214 | together as one shared offset. Explicit server or dev-URL overrides remove the corresponding port | ||
| 215 | from the availability check. Treat the `[dev-runner]` output as authoritative. | ||
docs/internals/server-updates.md+57-107| ... | @@ -1,107 +1,57 @@ | ... | @@ -1,107 +1,57 @@ |
| 1 | # Server Update Architecture | 1 | # Server updates |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | A [stable launcher](../../apps/server/src/serviceLauncher.ts) owns the runtime |
| 4 | 4 | selected by systemd or launchd. It is the only runtime writer of durable service | |
| 5 | Remote server updates use one stable launcher selected by the platform service manager (systemd on | 5 | state. Server children request updates over inherited IPC; they never rewrite |
| 6 | Linux, launchd on macOS). Foreground CLI processes do not self-update, and a running server never | 6 | their service definition or select their own replacement. Local service commands |
| 7 | edits its service definition or durable service state. | 7 | may replace the launcher and state while the service is stopped. Foreground CLI |
| 8 | 8 | processes do not self-update. | |
| 9 | ## Ownership | 9 | |
| 10 | 10 | Exact-version installs keep restarts independent of npm cache eviction or a moving | |
| 11 | The service files under `<baseDir>/runtime` are: | 11 | release tag. Installation and preflight happen in staging before publishing an |
| 12 | 12 | immutable runtime. Preflight checks the launcher protocol because a target that | |
| 13 | - `service-launcher.mjs`, the stable process selected by the service manager; | 13 | needs new rollback guarantees cannot safely run under an older launcher. Upgrading |
| 14 | - `service-state.json`, the launcher's durable selection state; | 14 | that launcher requires a local service update. |
| 15 | - `versions/<version>`, immutable exact-version npm installs. | 15 | |
| 16 | 16 | ## Commit boundary | |
| 17 | The launcher is the only runtime writer of `service-state.json`. `t3 service install` and | 17 | |
| 18 | `t3 service update` may replace the launcher and state while the unit is stopped. Server children | 18 | The launcher durably records the pending update before acknowledging it, then |
| 19 | only communicate with the launcher over their inherited IPC channel. | 19 | stops the old child and starts the target as a trial. Service-state writes use |
| 20 | 20 | same-directory replacement with file and directory fsync. Invalid state stops | |
| 21 | The state contains one active version and, at most, one update record: | 21 | startup rather than guessing which runtime to boot. |
| 22 | 22 | ||
| 23 | - `pending A → B` selects B as a retryable trial; | 23 | The trial must finish migrations, acquire dependencies, bind HTTP, and park every |
| 24 | - `committed A → B` selects B for ordinary restarts; | 24 | long-running root at the activation gate before reporting `prepared`. The launcher |
| 25 | - `rolled-back A → B` or `failed A → B` selects A; | 25 | then commits the target version durably and replies `committed`. Only then may the |
| 26 | - invalid state fails closed so the service manager cannot guess at a runtime. | 26 | child release its gates, accept commands, and publish ready. Keep fallible startup |
| 27 | 27 | acquisitions before this boundary. A listener alone does not prove the runtime is | |
| 28 | Every write uses same-directory replacement plus file and directory fsync. | 28 | ready to commit. |
| 29 | 29 | ||
| 30 | ## Remote Update | 30 | A failed or timed-out trial returns to the old version. After commit, the target |
| 31 | 31 | is authoritative and the service manager's ordinary restart policy applies. | |
| 32 | 1. The active server installs `t3@<target>` into a unique staging directory. | 32 | |
| 33 | 2. The target runs `__service-preflight` and verifies that the stable launcher supports its update | 33 | ## Database rollback |
| 34 | protocol. | 34 | |
| 35 | 3. The staging directory is renamed to its immutable version path only after preflight succeeds. | 35 | After the old child exits, the launcher snapshots SQLite's main file, WAL, and |
| 36 | 4. The active child sends `request-update`. The launcher validates the child and target, writes | 36 | shared-memory file. This makes trial migrations reversible without down |
| 37 | pending state, generates the update ID, then replies `update-accepted`. | 37 | migrations. The snapshot is made once per update and survives launcher restarts; |
| 38 | 5. After a short response-flush grace period, the launcher stops the active child. | 38 | replacing it during a retry could capture changes from the failed trial. |
| 39 | 6. With SQLite quiescent, the launcher snapshots the database, WAL, and shared-memory files. | 39 | |
| 40 | 7. The launcher starts the target as a trial and gives it the pending update over IPC. | 40 | Rollback stops the trial before restoring. A durable restore marker makes an |
| 41 | 8. The trial runs migrations, acquires dependencies, binds HTTP, starts every long-running root | 41 | interrupted restore finish before either version boots. Keep the snapshot until |
| 42 | fiber, and verifies that each root is parked at the activation gate. | 42 | commit, or until both restoration and the terminal rollback state are durable. |
| 43 | 9. The trial sends `prepared`. The launcher durably commits B, deletes the snapshot, then replies | 43 | Attachments and other files outside SQLite are outside this rollback boundary. |
| 44 | `committed`. | 44 | |
| 45 | 10. The child opens the existing activation gate, accepts commands, and publishes lifecycle ready | 45 | ## Client acknowledgement |
| 46 | with the terminal update outcome. | 46 | |
| 47 | 47 | An accepted update is still pending. Clients correlate the launcher's update ID | |
| 48 | Post-commit startup does not call service `start`, `initialize`, `connect`, `load`, or `acquire` | 48 | with the ready event after reconnecting, then check the outcome and target version. |
| 49 | operations. It only opens prepared gates and publishes prepared lifecycle state. | 49 | A reconnect alone cannot distinguish successful replacement from rollback. Older |
| 50 | 50 | servers without an update ID retain version-only correlation. | |
| 51 | The launcher serializes child exits, IPC messages, and timers. A trial must report prepared within | 51 | |
| 52 | 120 seconds. If the trial exits or times out before prepared, the launcher stops it, restores the | 52 | Desktop updates have a separate two-phase handoff because installing the app stops |
| 53 | snapshot, records rollback, and starts A. A durable restore marker makes an interrupted restore | 53 | its bundled backend. Preparation returns a token while the connection is alive; |
| 54 | resume before either version can boot. After commit, B is active and the service manager's normal | 54 | the client commits that token only after receiving it. Otherwise backend shutdown |
| 55 | restart policy applies. | 55 | could lose the only successful RPC result. The client must then observe the |
| 56 | 56 | prepared version after reconnecting. If installation fails, desktop restarts the | |
| 57 | ## Database Rollback | 57 | stopped backends and replays the failure for the same token. |
| 58 | |||
| 59 | The launcher snapshots `state.sqlite`, `state.sqlite-wal`, and `state.sqlite-shm` after the old | ||
| 60 | server stops and before the trial starts. This makes trial migrations and writes reversible without | ||
| 61 | requiring down migrations. The snapshot is retained across launcher restarts and is removed only | ||
| 62 | after commit or after both restore and the terminal rollback state are durable. | ||
| 63 | |||
| 64 | The protocol version is part of the safety boundary. A target that requires database snapshots is | ||
| 65 | blocked when the installed launcher is too old. Upgrade the launcher once with: | ||
| 66 | |||
| 67 | ```sh | ||
| 68 | npx t3@<version> service update | ||
| 69 | ``` | ||
| 70 | |||
| 71 | The local command stops the unit, selects the new launcher and exact runtime, then restarts the | ||
| 72 | service. Later releases, including releases with migrations, can use the remote trial path. | ||
| 73 | |||
| 74 | Snapshots briefly require enough free disk for another copy of the SQLite files. Attachments and | ||
| 75 | other files under the state directory are outside this rollback boundary. | ||
| 76 | |||
| 77 | ## Client Correlation | ||
| 78 | |||
| 79 | The update acknowledgement includes the launcher-generated update ID. After reconnecting, clients | ||
| 80 | wait for a lifecycle ready event carrying that same ID. `committed` completes the operation only | ||
| 81 | when the ready server is the target version. `rolled-back` and `failed` end it immediately with the | ||
| 82 | recorded reason. Older servers without an ID retain version-only reconnect behavior. | ||
| 83 | |||
| 84 | ## Capability and Compatibility | ||
| 85 | |||
| 86 | The existing additive RPC and lifecycle schemas remain compatible with older clients. New servers | ||
| 87 | advertise remote self-update only when they have valid launcher context and a live IPC channel. | ||
| 88 | Desktop-managed servers advertise `desktopAppUpdate` when the desktop telemetry control fd is | ||
| 89 | attached. The progress RPC asks the desktop app to check and download, then returns a preparation | ||
| 90 | token while the backend is still connected. Only after the client receives that result does it send | ||
| 91 | `server.commitDesktopUpdate`. A successful commit closes the connection and must reconnect at the | ||
| 92 | prepared version. The desktop app and bundled server versions stay equal because | ||
| 93 | `scripts/update-release-package-versions.ts` bumps them together. If install fails, the desktop keeps its windows, restarts stopped backends, and | ||
| 94 | replays the failure for the same token. This two-phase handoff prevents backend shutdown from | ||
| 95 | dropping the only successful RPC result. Desktop servers without the capability direct the user to | ||
| 96 | update the desktop app locally. Other process shapes provide a manual command; the old detached | ||
| 97 | foreground respawn path no longer exists. | ||
| 98 | |||
| 99 | ## Source Map | ||
| 100 | |||
| 101 | - Launcher and state machine: `apps/server/src/serviceLauncher.ts` | ||
| 102 | - IPC and durable state types: `apps/server/src/cloud/serviceProtocol.ts` | ||
| 103 | - Child IPC adapter: `apps/server/src/cloud/serviceLauncherClient.ts` | ||
| 104 | - Staging and preflight: `apps/server/src/cloud/pinnedRuntime.ts` and `servicePreflight.ts` | ||
| 105 | - Service installation: `apps/server/src/cloud/bootService.ts` | ||
| 106 | - Activation boundary: `apps/server/src/serverRuntimeStartup.ts` and `serverActivation.ts` | ||
| 107 | - Client outcome correlation: `packages/client-runtime/src/state/server.ts` |
docs/internals/t3-code-connect-auth-flow.html deleted-1160| ... | @@ -1,1160 +0,0 @@ | ||
| 1 | <!doctype html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8" /> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 6 | <title>T3 Connect Architecture</title> | ||
| 7 | <style> | ||
| 8 | :root { | ||
| 9 | color-scheme: light; | ||
| 10 | --ink: #171717; | ||
| 11 | --muted: #62646b; | ||
| 12 | --hairline: #d8d8dd; | ||
| 13 | --panel: #ffffff; | ||
| 14 | --paper: #f5f5f2; | ||
| 15 | --blue: #1457d9; | ||
| 16 | --green: #1d7d4d; | ||
| 17 | --amber: #a65f00; | ||
| 18 | --red: #b8242c; | ||
| 19 | --violet: #6150b8; | ||
| 20 | --shadow: 0 18px 55px rgba(25, 25, 28, 0.08); | ||
| 21 | } | ||
| 22 | |||
| 23 | * { | ||
| 24 | box-sizing: border-box; | ||
| 25 | } | ||
| 26 | |||
| 27 | body { | ||
| 28 | margin: 0; | ||
| 29 | background: | ||
| 30 | linear-gradient(90deg, rgba(0, 0, 0, 0.035) 1px, transparent 1px), | ||
| 31 | linear-gradient(rgba(0, 0, 0, 0.035) 1px, transparent 1px), var(--paper); | ||
| 32 | background-size: 34px 34px; | ||
| 33 | color: var(--ink); | ||
| 34 | font-family: | ||
| 35 | ui-sans-serif, | ||
| 36 | -apple-system, | ||
| 37 | BlinkMacSystemFont, | ||
| 38 | "Segoe UI", | ||
| 39 | sans-serif; | ||
| 40 | line-height: 1.48; | ||
| 41 | } | ||
| 42 | |||
| 43 | .shell { | ||
| 44 | width: min(1540px, calc(100vw - 40px)); | ||
| 45 | margin: 0 auto; | ||
| 46 | padding: 38px 0 72px; | ||
| 47 | } | ||
| 48 | |||
| 49 | header { | ||
| 50 | border-bottom: 2px solid var(--ink); | ||
| 51 | padding-bottom: 24px; | ||
| 52 | margin-bottom: 28px; | ||
| 53 | } | ||
| 54 | |||
| 55 | .eyebrow { | ||
| 56 | color: var(--blue); | ||
| 57 | font-size: 12px; | ||
| 58 | font-weight: 800; | ||
| 59 | letter-spacing: 0.08em; | ||
| 60 | text-transform: uppercase; | ||
| 61 | } | ||
| 62 | |||
| 63 | h1 { | ||
| 64 | max-width: 980px; | ||
| 65 | margin: 8px 0 12px; | ||
| 66 | font-size: clamp(34px, 5vw, 66px); | ||
| 67 | line-height: 0.98; | ||
| 68 | } | ||
| 69 | |||
| 70 | h2 { | ||
| 71 | margin: 0 0 14px; | ||
| 72 | font-size: 26px; | ||
| 73 | line-height: 1.08; | ||
| 74 | } | ||
| 75 | |||
| 76 | h3 { | ||
| 77 | margin: 22px 0 8px; | ||
| 78 | font-size: 17px; | ||
| 79 | } | ||
| 80 | |||
| 81 | p { | ||
| 82 | margin: 0 0 12px; | ||
| 83 | } | ||
| 84 | |||
| 85 | code, | ||
| 86 | .mono { | ||
| 87 | font-family: | ||
| 88 | ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | ||
| 89 | } | ||
| 90 | |||
| 91 | code { | ||
| 92 | border-radius: 4px; | ||
| 93 | background: #f0f0ed; | ||
| 94 | padding: 1px 4px; | ||
| 95 | font-size: 0.92em; | ||
| 96 | } | ||
| 97 | |||
| 98 | .lede { | ||
| 99 | max-width: 1000px; | ||
| 100 | margin: 0; | ||
| 101 | color: var(--muted); | ||
| 102 | font-size: 18px; | ||
| 103 | } | ||
| 104 | |||
| 105 | .grid { | ||
| 106 | display: grid; | ||
| 107 | grid-template-columns: repeat(12, 1fr); | ||
| 108 | gap: 18px; | ||
| 109 | } | ||
| 110 | |||
| 111 | .panel { | ||
| 112 | grid-column: span 12; | ||
| 113 | border: 1px solid var(--hairline); | ||
| 114 | border-radius: 10px; | ||
| 115 | background: var(--panel); | ||
| 116 | box-shadow: var(--shadow); | ||
| 117 | padding: 22px; | ||
| 118 | } | ||
| 119 | |||
| 120 | .half { | ||
| 121 | grid-column: span 6; | ||
| 122 | } | ||
| 123 | |||
| 124 | .callout { | ||
| 125 | border-left: 5px solid var(--blue); | ||
| 126 | background: #eef4ff; | ||
| 127 | padding: 14px 16px; | ||
| 128 | margin-top: 14px; | ||
| 129 | } | ||
| 130 | |||
| 131 | .callout.warning { | ||
| 132 | border-left-color: var(--amber); | ||
| 133 | background: #fff6e6; | ||
| 134 | } | ||
| 135 | |||
| 136 | .badge-row { | ||
| 137 | display: flex; | ||
| 138 | flex-wrap: wrap; | ||
| 139 | gap: 8px; | ||
| 140 | margin-top: 14px; | ||
| 141 | } | ||
| 142 | |||
| 143 | .badge { | ||
| 144 | border: 1px solid var(--hairline); | ||
| 145 | border-radius: 999px; | ||
| 146 | background: #fff; | ||
| 147 | padding: 5px 9px; | ||
| 148 | font-size: 12px; | ||
| 149 | font-weight: 700; | ||
| 150 | } | ||
| 151 | |||
| 152 | .badge.user { | ||
| 153 | border-color: rgba(20, 87, 217, 0.3); | ||
| 154 | color: var(--blue); | ||
| 155 | } | ||
| 156 | |||
| 157 | .badge.env { | ||
| 158 | border-color: rgba(29, 125, 77, 0.3); | ||
| 159 | color: var(--green); | ||
| 160 | } | ||
| 161 | |||
| 162 | .badge.local { | ||
| 163 | border-color: rgba(166, 95, 0, 0.35); | ||
| 164 | color: var(--amber); | ||
| 165 | } | ||
| 166 | |||
| 167 | .diagram { | ||
| 168 | overflow-x: auto; | ||
| 169 | margin: 12px 0 0; | ||
| 170 | border: 1px solid var(--hairline); | ||
| 171 | border-radius: 8px; | ||
| 172 | background: #fbfbf8; | ||
| 173 | color: var(--ink); | ||
| 174 | padding: 18px; | ||
| 175 | } | ||
| 176 | |||
| 177 | .diagram svg { | ||
| 178 | display: block; | ||
| 179 | width: auto !important; | ||
| 180 | min-width: 100%; | ||
| 181 | max-width: none !important; | ||
| 182 | height: auto; | ||
| 183 | margin: 0 auto; | ||
| 184 | } | ||
| 185 | |||
| 186 | table { | ||
| 187 | width: 100%; | ||
| 188 | border-collapse: collapse; | ||
| 189 | font-size: 14px; | ||
| 190 | } | ||
| 191 | |||
| 192 | th, | ||
| 193 | td { | ||
| 194 | border-bottom: 1px solid var(--hairline); | ||
| 195 | padding: 11px 9px; | ||
| 196 | text-align: left; | ||
| 197 | vertical-align: top; | ||
| 198 | } | ||
| 199 | |||
| 200 | th { | ||
| 201 | color: var(--muted); | ||
| 202 | font-size: 12px; | ||
| 203 | letter-spacing: 0.05em; | ||
| 204 | text-transform: uppercase; | ||
| 205 | } | ||
| 206 | |||
| 207 | ul { | ||
| 208 | margin: 8px 0 0; | ||
| 209 | padding-left: 20px; | ||
| 210 | } | ||
| 211 | |||
| 212 | li + li { | ||
| 213 | margin-top: 6px; | ||
| 214 | } | ||
| 215 | |||
| 216 | .flow { | ||
| 217 | display: grid; | ||
| 218 | gap: 10px; | ||
| 219 | margin-top: 12px; | ||
| 220 | } | ||
| 221 | |||
| 222 | .step { | ||
| 223 | border: 1px solid var(--hairline); | ||
| 224 | border-left: 5px solid var(--green); | ||
| 225 | border-radius: 8px; | ||
| 226 | background: #fff; | ||
| 227 | padding: 12px 14px; | ||
| 228 | } | ||
| 229 | |||
| 230 | .step strong { | ||
| 231 | display: block; | ||
| 232 | margin-bottom: 3px; | ||
| 233 | } | ||
| 234 | |||
| 235 | .source-list a { | ||
| 236 | color: var(--blue); | ||
| 237 | font-weight: 700; | ||
| 238 | } | ||
| 239 | |||
| 240 | @media (max-width: 860px) { | ||
| 241 | .half { | ||
| 242 | grid-column: span 12; | ||
| 243 | } | ||
| 244 | |||
| 245 | .shell { | ||
| 246 | width: min(100vw - 24px, 1540px); | ||
| 247 | padding-top: 24px; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | </style> | ||
| 251 | </head> | ||
| 252 | <body> | ||
| 253 | <main class="shell"> | ||
| 254 | <header> | ||
| 255 | <div class="eyebrow">Implemented architecture</div> | ||
| 256 | <h1>T3 Connect Control Plane and Managed Endpoint Flow</h1> | ||
| 257 | <p class="lede"> | ||
| 258 | T3 Connect links a locally authorized T3 environment to a signed-in cloud user, provisions | ||
| 259 | a managed HTTPS/WSS endpoint for that environment, and brokers proof-bound remote | ||
| 260 | connections. The local environment remains the authority that issues environment access | ||
| 261 | credentials. The hosted relay stores links, reconciles managed endpoint resources, | ||
| 262 | validates signed proofs, and delivers agent-activity notifications. | ||
| 263 | </p> | ||
| 264 | <div class="badge-row"> | ||
| 265 | <span class="badge user">Client -> Relay: Clerk bearer or relay DPoP</span> | ||
| 266 | <span class="badge env">Relay -> Environment: relay-signed health and mint proofs</span> | ||
| 267 | <span class="badge env" | ||
| 268 | >Environment -> Relay: environment bearer plus signed activity proof</span | ||
| 269 | > | ||
| 270 | <span class="badge local">Client -> Environment: environment access token plus DPoP</span> | ||
| 271 | </div> | ||
| 272 | </header> | ||
| 273 | |||
| 274 | <section class="grid" aria-label="Architecture summary"> | ||
| 275 | <article class="panel half"> | ||
| 276 | <h2>Security Invariant</h2> | ||
| 277 | <p> | ||
| 278 | A cloud identity is not an environment login. Remote access requires all of the | ||
| 279 | following: | ||
| 280 | </p> | ||
| 281 | <ul> | ||
| 282 | <li>a Clerk-authenticated T3 Connect user;</li> | ||
| 283 | <li>an active relay link for that user and environment;</li> | ||
| 284 | <li>a DPoP proof key held by the remote client;</li> | ||
| 285 | <li>a relay-signed mint request accepted by the linked local environment; and</li> | ||
| 286 | <li>an environment-issued credential bound to the remote client's DPoP thumbprint.</li> | ||
| 287 | </ul> | ||
| 288 | <div class="callout"> | ||
| 289 | The relay cannot mint an environment access token by itself. It can only ask the local | ||
| 290 | environment to mint a short-lived bootstrap credential for an authorized cloud user. | ||
| 291 | </div> | ||
| 292 | <p>During an honest connection flow, the relay cannot act as the remote client:</p> | ||
| 293 | <ul> | ||
| 294 | <li> | ||
| 295 | Normal environment API traffic goes directly from the remote client through the | ||
| 296 | managed endpoint. The relay does not receive the resulting environment access token. | ||
| 297 | </li> | ||
| 298 | <li> | ||
| 299 | The one-time bootstrap credential and resulting environment access token are bound to | ||
| 300 | the remote client's DPoP public-key thumbprint. Redeeming or using them requires | ||
| 301 | request proofs signed by the corresponding private key, which stays on the client. | ||
| 302 | </li> | ||
| 303 | <li> | ||
| 304 | Relay-signed health and mint proofs are short-lived, audience-bound, scoped, and | ||
| 305 | replay-guarded. They are accepted only by the narrow cloud endpoints, not as normal | ||
| 306 | environment API credentials. | ||
| 307 | </li> | ||
| 308 | <li> | ||
| 309 | Environment health and mint responses are signed by the linked environment and bound | ||
| 310 | to the request nonce, so a different process behind the tunnel cannot impersonate the | ||
| 311 | linked environment. | ||
| 312 | </li> | ||
| 313 | </ul> | ||
| 314 | <div class="callout warning"> | ||
| 315 | The hosted relay remains a trusted mint broker: it holds the cloud-mint signing key. | ||
| 316 | DPoP prevents credential reuse by the relay or tunnel transport during an honest flow, | ||
| 317 | but it does not make compromise of the relay signing authority harmless. | ||
| 318 | </div> | ||
| 319 | </article> | ||
| 320 | |||
| 321 | <article class="panel half"> | ||
| 322 | <h2>Product Boundaries</h2> | ||
| 323 | <p> | ||
| 324 | The generic local connector is called the <strong>relay client</strong>. The current | ||
| 325 | implementation layer uses <code>cloudflared</code>, but that is an internal transport | ||
| 326 | detail rather than the product contract. | ||
| 327 | </p> | ||
| 328 | <ul> | ||
| 329 | <li> | ||
| 330 | The relay Worker owns cloud links, managed endpoint allocations, and notifications. | ||
| 331 | </li> | ||
| 332 | <li> | ||
| 333 | The environment server owns local sessions, environment keys, and access tokens. | ||
| 334 | </li> | ||
| 335 | <li>The relay client exposes only the environment's loopback HTTP server.</li> | ||
| 336 | <li> | ||
| 337 | Web and mobile clients connect directly to the managed environment endpoint after | ||
| 338 | bootstrap. | ||
| 339 | </li> | ||
| 340 | </ul> | ||
| 341 | </article> | ||
| 342 | </section> | ||
| 343 | |||
| 344 | <section class="panel" aria-label="Topology"> | ||
| 345 | <h2>Current Topology</h2> | ||
| 346 | <pre class="diagram mermaid"> | ||
| 347 | flowchart LR | ||
| 348 | subgraph Clients["User-facing clients"] | ||
| 349 | Web["Desktop / hosted web UI"] | ||
| 350 | Mobile["Mobile app"] | ||
| 351 | CLI["Headless CLI"] | ||
| 352 | end | ||
| 353 | |||
| 354 | Clerk["Clerk<br/>user identity and OAuth"] | ||
| 355 | |||
| 356 | subgraph Relay["T3 Code Relay on Cloudflare"] | ||
| 357 | Worker["Relay Worker<br/>HTTP API"] | ||
| 358 | Queue["APNs delivery queue<br/>with dead-letter queue"] | ||
| 359 | Hyperdrive["Hyperdrive"] | ||
| 360 | CF["Cloudflare tunnel + DNS APIs"] | ||
| 361 | end | ||
| 362 | |||
| 363 | DB["PlanetScale Postgres<br/>prod database + stage branches"] | ||
| 364 | APNs["Apple Push Notification service"] | ||
| 365 | Traces["Axiom OTLP traces"] | ||
| 366 | |||
| 367 | subgraph Local["Linked T3 environment"] | ||
| 368 | Env["Environment server<br/>local auth authority"] | ||
| 369 | RelayClient["Relay client<br/>cloudflared implementation"] | ||
| 370 | Secrets["Server secret store<br/>keys + relay config"] | ||
| 371 | end | ||
| 372 | |||
| 373 | Endpoint["Managed HTTPS/WSS endpoint"] | ||
| 374 | |||
| 375 | Web -->|"Clerk bearer / relay DPoP"| Worker | ||
| 376 | Mobile -->|"Clerk bearer / relay DPoP"| Worker | ||
| 377 | CLI -->|"Clerk OAuth bearer"| Worker | ||
| 378 | Web --> Clerk | ||
| 379 | Mobile --> Clerk | ||
| 380 | CLI -->|"PKCE authorization code flow"| Clerk | ||
| 381 | Worker --> Hyperdrive --> DB | ||
| 382 | Worker --> Traces | ||
| 383 | Worker --> CF | ||
| 384 | Worker --> Queue --> APNs | ||
| 385 | Worker --> Endpoint | ||
| 386 | Endpoint --> RelayClient -->|"http://127.0.0.1:port"| Env | ||
| 387 | Env --> Secrets | ||
| 388 | Web -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 389 | Mobile -->|"environment token + DPoP<br/>HTTPS / WSS"| Endpoint | ||
| 390 | Env -->|"environment bearer + signed activity proof"| Worker | ||
| 391 | </pre> | ||
| 392 | </section> | ||
| 393 | |||
| 394 | <section class="panel" aria-label="Authentication matrix"> | ||
| 395 | <h2>Authentication and Transport Matrix</h2> | ||
| 396 | <table> | ||
| 397 | <thead> | ||
| 398 | <tr> | ||
| 399 | <th>Boundary</th> | ||
| 400 | <th>Transport</th> | ||
| 401 | <th>Authentication</th> | ||
| 402 | <th>Purpose</th> | ||
| 403 | </tr> | ||
| 404 | </thead> | ||
| 405 | <tbody> | ||
| 406 | <tr> | ||
| 407 | <td>Web or mobile -> relay link management</td> | ||
| 408 | <td>HTTPS</td> | ||
| 409 | <td>Clerk session-template bearer token</td> | ||
| 410 | <td>Create a link challenge, submit an environment proof, list links, or unlink.</td> | ||
| 411 | </tr> | ||
| 412 | <tr> | ||
| 413 | <td>CLI -> relay link management</td> | ||
| 414 | <td>HTTPS</td> | ||
| 415 | <td>Clerk OAuth access token obtained with PKCE</td> | ||
| 416 | <td>Reconcile the desired headless cloud link when the server starts.</td> | ||
| 417 | </tr> | ||
| 418 | <tr> | ||
| 419 | <td>Web or mobile -> relay protected client endpoints</td> | ||
| 420 | <td>HTTPS</td> | ||
| 421 | <td>Relay-issued DPoP token plus per-request DPoP proof</td> | ||
| 422 | <td> | ||
| 423 | Check environment status, request a remote connection, and register mobile devices. | ||
| 424 | </td> | ||
| 425 | </tr> | ||
| 426 | <tr> | ||
| 427 | <td>Relay -> managed environment endpoint</td> | ||
| 428 | <td>HTTPS over the managed tunnel</td> | ||
| 429 | <td>Short-lived relay-signed JWT request proof</td> | ||
| 430 | <td> | ||
| 431 | Request a signed health response or ask the local environment to mint a credential. | ||
| 432 | </td> | ||
| 433 | </tr> | ||
| 434 | <tr> | ||
| 435 | <td>Environment -> relay activity publication</td> | ||
| 436 | <td>HTTPS</td> | ||
| 437 | <td>Relay-issued environment bearer credential plus environment-signed JWT proof</td> | ||
| 438 | <td> | ||
| 439 | Publish redacted agent-activity state for push notifications and Live Activities. | ||
| 440 | </td> | ||
| 441 | </tr> | ||
| 442 | <tr> | ||
| 443 | <td>Remote client -> environment</td> | ||
| 444 | <td>HTTPS and WSS over the managed tunnel</td> | ||
| 445 | <td>Environment-issued access token bound to the client DPoP key</td> | ||
| 446 | <td>Use the normal environment HTTP APIs and request a one-time WebSocket ticket.</td> | ||
| 447 | </tr> | ||
| 448 | </tbody> | ||
| 449 | </table> | ||
| 450 | </section> | ||
| 451 | |||
| 452 | <section class="panel" aria-label="Credential ownership"> | ||
| 453 | <h2>Credential Ownership</h2> | ||
| 454 | <table> | ||
| 455 | <thead> | ||
| 456 | <tr> | ||
| 457 | <th>Credential</th> | ||
| 458 | <th>Created by</th> | ||
| 459 | <th>Stored by</th> | ||
| 460 | <th>Use</th> | ||
| 461 | </tr> | ||
| 462 | </thead> | ||
| 463 | <tbody> | ||
| 464 | <tr> | ||
| 465 | <td>Clerk session-template JWT</td> | ||
| 466 | <td>Clerk</td> | ||
| 467 | <td>Web or mobile client</td> | ||
| 468 | <td> | ||
| 469 | Authenticate cloud-user relay requests and bootstrap relay DPoP token exchange. | ||
| 470 | </td> | ||
| 471 | </tr> | ||
| 472 | <tr> | ||
| 473 | <td>CLI OAuth access and refresh token</td> | ||
| 474 | <td>Clerk OAuth</td> | ||
| 475 | <td>Local server secret store</td> | ||
| 476 | <td>Authorize headless CLI link reconciliation.</td> | ||
| 477 | </tr> | ||
| 478 | <tr> | ||
| 479 | <td>Environment Ed25519 key pair</td> | ||
| 480 | <td>Local environment server</td> | ||
| 481 | <td>Local server secret store</td> | ||
| 482 | <td> | ||
| 483 | Sign link proofs, health responses, mint responses, and activity publication proofs. | ||
| 484 | </td> | ||
| 485 | </tr> | ||
| 486 | <tr> | ||
| 487 | <td>Relay cloud-mint key pair</td> | ||
| 488 | <td>Relay deployment</td> | ||
| 489 | <td>Private key in relay Worker config; public key installed into the environment</td> | ||
| 490 | <td>Sign relay-to-environment health and mint requests.</td> | ||
| 491 | </tr> | ||
| 492 | <tr> | ||
| 493 | <td>Relay environment bearer credential</td> | ||
| 494 | <td>Relay</td> | ||
| 495 | <td>Hashed in relay Postgres; plaintext in local server secret store</td> | ||
| 496 | <td>Authenticate environment-to-relay agent-activity publication.</td> | ||
| 497 | </tr> | ||
| 498 | <tr> | ||
| 499 | <td>Relay DPoP access token</td> | ||
| 500 | <td>Relay</td> | ||
| 501 | <td>Remote client memory cache</td> | ||
| 502 | <td> | ||
| 503 | Authorize status, connect, and mobile registration endpoints with proof of | ||
| 504 | possession. | ||
| 505 | </td> | ||
| 506 | </tr> | ||
| 507 | <tr> | ||
| 508 | <td>Environment bootstrap credential</td> | ||
| 509 | <td>Local environment server</td> | ||
| 510 | <td>Passed relay -> remote client</td> | ||
| 511 | <td>One-time exchange at the environment <code>/oauth/token</code> endpoint.</td> | ||
| 512 | </tr> | ||
| 513 | <tr> | ||
| 514 | <td>Environment access token</td> | ||
| 515 | <td>Local environment server</td> | ||
| 516 | <td>Remote client</td> | ||
| 517 | <td>Authorize environment HTTP operations and WebSocket ticket issuance.</td> | ||
| 518 | </tr> | ||
| 519 | <tr> | ||
| 520 | <td>WebSocket ticket</td> | ||
| 521 | <td>Local environment server</td> | ||
| 522 | <td>Remote client until consumed</td> | ||
| 523 | <td>Short-lived one-time credential for the WSS upgrade.</td> | ||
| 524 | </tr> | ||
| 525 | <tr> | ||
| 526 | <td>Relay client connector token</td> | ||
| 527 | <td>Cloudflare tunnel API</td> | ||
| 528 | <td>Local server secret store</td> | ||
| 529 | <td> | ||
| 530 | Start the local relay client. It is passed through <code>TUNNEL_TOKEN</code>, not a | ||
| 531 | shell argument. | ||
| 532 | </td> | ||
| 533 | </tr> | ||
| 534 | </tbody> | ||
| 535 | </table> | ||
| 536 | </section> | ||
| 537 | |||
| 538 | <section class="grid" aria-label="Core flows"> | ||
| 539 | <article class="panel"> | ||
| 540 | <h2>Flow 1: Link From the Desktop Web UI</h2> | ||
| 541 | <pre class="diagram mermaid"> | ||
| 542 | sequenceDiagram | ||
| 543 | participant U as Signed-in user | ||
| 544 | participant C as Desktop web UI | ||
| 545 | participant E as Local environment | ||
| 546 | participant R as Relay Worker | ||
| 547 | participant CF as Cloudflare APIs | ||
| 548 | participant RC as Local relay client | ||
| 549 | |||
| 550 | U->>C: Enable T3 Connect for a locally paired environment | ||
| 551 | C->>E: Check relay client availability | ||
| 552 | alt relay client is missing | ||
| 553 | C->>U: Prompt before download and install | ||
| 554 | U->>C: Confirm | ||
| 555 | C->>E: Stream relay-client install RPC | ||
| 556 | E-->>C: Progress stages | ||
| 557 | end | ||
| 558 | C->>R: POST /v1/client/environment-link-challenges (Clerk bearer) | ||
| 559 | R-->>C: Short-lived challenge | ||
| 560 | C->>E: POST /api/connect/link-proof (local relay:write bearer) | ||
| 561 | E->>E: Authenticate, reject forwarded authority, get or create key pair, validate loopback origin | ||
| 562 | E-->>C: Environment-signed link proof | ||
| 563 | C->>R: POST /v1/client/environment-links (Clerk bearer + proof) | ||
| 564 | R->>R: Verify challenge, proof, capabilities, nonce, and loopback origin | ||
| 565 | R->>CF: Reconcile tunnel, ingress, and CNAME | ||
| 566 | R->>R: Upsert user/environment link and issue environment bearer | ||
| 567 | R-->>C: Managed endpoint, relay config, and connector token | ||
| 568 | C->>E: POST /api/connect/relay-config (local relay:write bearer) | ||
| 569 | E->>RC: Start relay client with TUNNEL_TOKEN | ||
| 570 | </pre> | ||
| 571 | <div class="callout"> | ||
| 572 | The local link-proof endpoint requires <code>relay:write</code>, rejects forwarded | ||
| 573 | authority headers, and accepts only an exact loopback origin. Environment keypair | ||
| 574 | persistence is atomic across concurrent link attempts. | ||
| 575 | </div> | ||
| 576 | <p> | ||
| 577 | A locally paired mobile client uses the same relay challenge, local proof, relay link, | ||
| 578 | and local relay-config endpoints. The desktop web UI is the surface that checks relay | ||
| 579 | client availability and offers the managed install dialog. | ||
| 580 | </p> | ||
| 581 | </article> | ||
| 582 | |||
| 583 | <article class="panel"> | ||
| 584 | <h2>Flow 2: Headless CLI Link</h2> | ||
| 585 | <pre class="diagram mermaid"> | ||
| 586 | sequenceDiagram | ||
| 587 | participant U as Operator | ||
| 588 | participant CLI as t3 connect CLI | ||
| 589 | participant Clerk as Clerk OAuth | ||
| 590 | participant S as Environment server on next start | ||
| 591 | participant R as Relay Worker | ||
| 592 | participant RC as Local relay client | ||
| 593 | |||
| 594 | U->>CLI: t3 connect link | ||
| 595 | CLI->>CLI: Resolve relay client | ||
| 596 | alt relay client is missing | ||
| 597 | CLI->>U: Confirm managed relay-client install | ||
| 598 | U->>CLI: Confirm | ||
| 599 | CLI->>CLI: Install with terminal progress updates | ||
| 600 | end | ||
| 601 | CLI->>Clerk: Browser PKCE authorization-code flow | ||
| 602 | Clerk-->>CLI: OAuth access and refresh tokens | ||
| 603 | CLI->>CLI: Persist desired cloud-link state | ||
| 604 | U->>S: Start T3 environment server | ||
| 605 | S->>R: Create challenge and submit local signed proof | ||
| 606 | R-->>S: Managed endpoint config and connector token | ||
| 607 | S->>RC: Start relay client | ||
| 608 | </pre> | ||
| 609 | <p> | ||
| 610 | <code>t3 connect login</code> stores authorization without enabling exposure. | ||
| 611 | <code>t3 connect unlink</code> disables exposure while retaining authorization. | ||
| 612 | <code>t3 connect logout</code> also removes the stored CLI authorization. | ||
| 613 | </p> | ||
| 614 | </article> | ||
| 615 | |||
| 616 | <article class="panel"> | ||
| 617 | <h2>Flow 3: Remote Connect Bootstrap</h2> | ||
| 618 | <pre class="diagram mermaid"> | ||
| 619 | sequenceDiagram | ||
| 620 | participant C as Web or mobile client | ||
| 621 | participant Clerk as Clerk | ||
| 622 | participant R as Relay Worker | ||
| 623 | participant E as Managed environment endpoint | ||
| 624 | |||
| 625 | C->>Clerk: Get Clerk session-template JWT | ||
| 626 | C->>R: POST /v1/client/dpop-token (Clerk JWT + DPoP proof) | ||
| 627 | R-->>C: Relay DPoP access token | ||
| 628 | C->>R: POST /v1/environments/:id/connect (relay DPoP) | ||
| 629 | R->>E: POST /api/t3-connect/mint-credential (relay-signed proof) | ||
| 630 | E->>E: Verify relay signer, linked user, scope, lifetime, cnf, and replay guards | ||
| 631 | E-->>R: Bootstrap credential + environment-signed proof | ||
| 632 | R->>R: Verify environment signature, nonce, endpoint, and DPoP binding | ||
| 633 | R-->>C: Managed endpoint + bootstrap credential | ||
| 634 | C->>E: POST /oauth/token (bootstrap credential + DPoP proof) | ||
| 635 | E-->>C: Environment DPoP-bound access token | ||
| 636 | C->>E: POST /api/auth/websocket-ticket | ||
| 637 | E-->>C: One-time WebSocket ticket | ||
| 638 | C->>E: WSS /ws with ticket | ||
| 639 | </pre> | ||
| 640 | <div class="callout"> | ||
| 641 | Relay-to-environment control requests disable redirects, use only a reconciled managed | ||
| 642 | endpoint allocation, and require a signed environment response before the relay returns | ||
| 643 | data to the client. | ||
| 644 | </div> | ||
| 645 | </article> | ||
| 646 | |||
| 647 | <article class="panel"> | ||
| 648 | <h2>Flow 4: Agent Activity Notifications</h2> | ||
| 649 | <pre class="diagram mermaid"> | ||
| 650 | sequenceDiagram | ||
| 651 | participant E as Local environment | ||
| 652 | participant R as Relay Worker | ||
| 653 | participant DB as Relay Postgres | ||
| 654 | participant Q as APNs queue | ||
| 655 | participant APNs as Apple Push Notification service | ||
| 656 | participant M as Mobile app | ||
| 657 | |||
| 658 | E->>E: Project publishable thread state and redact failure detail | ||
| 659 | E->>R: POST /v1/environments/:environmentId/threads/:threadId/agent-activity | ||
| 660 | Note over E,R: Environment bearer credential + environment-signed per-state proof | ||
| 661 | R->>R: Verify credential, signature, scope, expiry, and nonce | ||
| 662 | R->>DB: Store current activity row | ||
| 663 | R->>Q: Enqueue push or Live Activity delivery jobs | ||
| 664 | Q->>APNs: Deliver signed APNs request | ||
| 665 | APNs-->>M: Push notification or Live Activity update | ||
| 666 | </pre> | ||
| 667 | <p> | ||
| 668 | The local server publishes a deliberately narrow projection. Failed runs use a fixed | ||
| 669 | redacted summary, and detail strings are capped before publication. | ||
| 670 | </p> | ||
| 671 | </article> | ||
| 672 | </section> | ||
| 673 | |||
| 674 | <section class="grid" aria-label="Managed endpoint lifecycle"> | ||
| 675 | <article class="panel half"> | ||
| 676 | <h2>Managed Endpoint Reconciliation</h2> | ||
| 677 | <p> | ||
| 678 | Managed endpoint resources are keyed by <code>(userId, environmentId)</code>. The relay | ||
| 679 | reserves a stable hostname and tunnel name before provisioning, then checkpoints the | ||
| 680 | tunnel ID, DNS record ID, and ready state in Postgres. | ||
| 681 | </p> | ||
| 682 | <div class="flow"> | ||
| 683 | <div class="step"> | ||
| 684 | <strong>1. Reserve deterministic allocation</strong> | ||
| 685 | Hostname and tunnel name derive from relay stage, cloud user, and environment ID. | ||
| 686 | </div> | ||
| 687 | <div class="step"> | ||
| 688 | <strong>2. Reuse or create tunnel</strong> | ||
| 689 | Existing named tunnels are reused. Tunnel ingress is rewritten to the validated | ||
| 690 | loopback origin. | ||
| 691 | </div> | ||
| 692 | <div class="step"> | ||
| 693 | <strong>3. Reconcile DNS</strong> | ||
| 694 | Existing CNAMEs are updated and duplicate records are removed. Create conflicts are | ||
| 695 | recovered by listing and reconciling the winning record. | ||
| 696 | </div> | ||
| 697 | <div class="step"> | ||
| 698 | <strong>4. Mark ready</strong> | ||
| 699 | Connect and status requests use only ready allocations whose hostname still matches | ||
| 700 | the relay-managed namespace. | ||
| 701 | </div> | ||
| 702 | </div> | ||
| 703 | </article> | ||
| 704 | |||
| 705 | <article class="panel half"> | ||
| 706 | <h2>Unlink Cleanup</h2> | ||
| 707 | <p> | ||
| 708 | Relay unlink removes the user's managed endpoint allocation before revoking the | ||
| 709 | user/environment link: | ||
| 710 | </p> | ||
| 711 | <ul> | ||
| 712 | <li>delete the managed DNS record if present;</li> | ||
| 713 | <li>delete the Cloudflare tunnel if present;</li> | ||
| 714 | <li>remove the allocation row;</li> | ||
| 715 | <li>revoke the user's environment link; and</li> | ||
| 716 | <li> | ||
| 717 | revoke the environment publication credential only after the final active link for | ||
| 718 | that environment key disappears. | ||
| 719 | </li> | ||
| 720 | </ul> | ||
| 721 | <p> | ||
| 722 | Local unlink stops the relay client and removes the locally persisted relay URL, issuer, | ||
| 723 | linked user, environment publication credential, cloud-mint public key, connector | ||
| 724 | config, and agent-activity preference. | ||
| 725 | </p> | ||
| 726 | </article> | ||
| 727 | </section> | ||
| 728 | |||
| 729 | <section class="panel" aria-label="Cloudflare tunnel operational profile"> | ||
| 730 | <h2>Cloudflare Tunnel Operational Profile</h2> | ||
| 731 | <p> | ||
| 732 | Managed tunnels are private application backends, not general-purpose public hosting. The | ||
| 733 | relay provisions one named Cloudflare tunnel and one DNS record per active | ||
| 734 | <code>(userId, environmentId)</code> allocation. | ||
| 735 | </p> | ||
| 736 | <ul> | ||
| 737 | <li> | ||
| 738 | <strong>Lifecycle:</strong> the tunnel is created when an environment is linked, reused | ||
| 739 | across retry-safe reconciliation, kept for the lifetime of that link, and deleted on | ||
| 740 | unlink. If teardown fails, the allocation checkpoint remains so cleanup can be retried. | ||
| 741 | </li> | ||
| 742 | <li> | ||
| 743 | <strong>Origin:</strong> tunnel ingress is restricted to the linked environment server's | ||
| 744 | validated loopback HTTP origin. Arbitrary upstream hosts and raw TCP origins are | ||
| 745 | rejected. | ||
| 746 | </li> | ||
| 747 | <li> | ||
| 748 | <strong>Protocols:</strong> managed endpoints carry HTTPS request/response traffic and | ||
| 749 | WSS connections only. Remote clients use the normal environment HTTP APIs and | ||
| 750 | authenticated WebSocket RPC transport through the tunnel. | ||
| 751 | </li> | ||
| 752 | <li> | ||
| 753 | <strong>Relay-generated traffic:</strong> the hosted relay sends sparse HTTPS health | ||
| 754 | checks and credential-mint requests during status checks and remote connection | ||
| 755 | bootstrap. It does not proxy steady-state remote sessions. | ||
| 756 | </li> | ||
| 757 | <li> | ||
| 758 | <strong>Traffic shape:</strong> expected load is interactive and user-driven. Active | ||
| 759 | remote sessions may keep WebSockets open and exchange terminal, agent, and UI updates; | ||
| 760 | inactive linked environments should carry little or no tunnel traffic. | ||
| 761 | </li> | ||
| 762 | <li> | ||
| 763 | <strong>Capacity planning:</strong> the project is pre-launch, so there is not yet a | ||
| 764 | production bandwidth baseline. Tunnel count grows with active linked user/environment | ||
| 765 | allocations, while bandwidth and concurrent WebSockets grow with active remote use. | ||
| 766 | </li> | ||
| 767 | </ul> | ||
| 768 | </section> | ||
| 769 | |||
| 770 | <section class="panel" aria-label="SSRF and tunnel hardening"> | ||
| 771 | <h2>SSRF and Tunnel Hardening</h2> | ||
| 772 | <table> | ||
| 773 | <thead> | ||
| 774 | <tr> | ||
| 775 | <th>Risk</th> | ||
| 776 | <th>Implemented control</th> | ||
| 777 | </tr> | ||
| 778 | </thead> | ||
| 779 | <tbody> | ||
| 780 | <tr> | ||
| 781 | <td>A client asks the relay to tunnel an arbitrary host.</td> | ||
| 782 | <td> | ||
| 783 | The environment and relay both accept managed tunnel origins only for loopback hosts | ||
| 784 | and valid ports. | ||
| 785 | </td> | ||
| 786 | </tr> | ||
| 787 | <tr> | ||
| 788 | <td>A forwarded request tricks local origin validation.</td> | ||
| 789 | <td> | ||
| 790 | The local link-proof handler requires an exact loopback request URL and rejects | ||
| 791 | forwarded host or protocol headers. | ||
| 792 | </td> | ||
| 793 | </tr> | ||
| 794 | <tr> | ||
| 795 | <td>A stored endpoint is replaced with an arbitrary external URL.</td> | ||
| 796 | <td> | ||
| 797 | Connect and status resolve a ready allocation from Postgres and require a hostname | ||
| 798 | under the configured managed endpoint zone. | ||
| 799 | </td> | ||
| 800 | </tr> | ||
| 801 | <tr> | ||
| 802 | <td>A managed endpoint redirects relay egress elsewhere.</td> | ||
| 803 | <td> | ||
| 804 | Relay-to-environment status and mint requests set redirect handling to | ||
| 805 | <code>manual</code>. | ||
| 806 | </td> | ||
| 807 | </tr> | ||
| 808 | <tr> | ||
| 809 | <td>A process behind the tunnel returns attacker-controlled data.</td> | ||
| 810 | <td> | ||
| 811 | The relay verifies environment-signed health and mint responses against the linked | ||
| 812 | environment public key, expected nonce, and request binding. | ||
| 813 | </td> | ||
| 814 | </tr> | ||
| 815 | <tr> | ||
| 816 | <td>A stolen remote access token is replayed.</td> | ||
| 817 | <td> | ||
| 818 | Relay and environment access tokens are DPoP-bound; per-request proofs and replay | ||
| 819 | guards are required. | ||
| 820 | </td> | ||
| 821 | </tr> | ||
| 822 | <tr> | ||
| 823 | <td>The relay client token leaks through process listings or shell parsing.</td> | ||
| 824 | <td> | ||
| 825 | The relay client is spawned without a shell and receives its connector token in | ||
| 826 | <code>TUNNEL_TOKEN</code>. | ||
| 827 | </td> | ||
| 828 | </tr> | ||
| 829 | </tbody> | ||
| 830 | </table> | ||
| 831 | <div class="callout warning"> | ||
| 832 | Managed tunnel hostnames live under the configured tunnel DNS zone. Serving them from a | ||
| 833 | dedicated registrable domain with a Public Suffix List entry remains an operational | ||
| 834 | isolation requirement before broad untrusted use. | ||
| 835 | </div> | ||
| 836 | </section> | ||
| 837 | |||
| 838 | <section class="panel" aria-label="Relay HTTP API"> | ||
| 839 | <h2>Relay HTTP API</h2> | ||
| 840 | <p> | ||
| 841 | The relay publishes an OpenAPI document at <code>/openapi.json</code>, interactive API | ||
| 842 | docs at <code>/docs</code>, and redirects <code>/</code> to the docs. | ||
| 843 | </p> | ||
| 844 | <table> | ||
| 845 | <thead> | ||
| 846 | <tr> | ||
| 847 | <th>Endpoint</th> | ||
| 848 | <th>Authentication</th> | ||
| 849 | <th>Purpose</th> | ||
| 850 | </tr> | ||
| 851 | </thead> | ||
| 852 | <tbody> | ||
| 853 | <tr> | ||
| 854 | <td><code>GET /health</code></td> | ||
| 855 | <td>None</td> | ||
| 856 | <td>Relay health check.</td> | ||
| 857 | </tr> | ||
| 858 | <tr> | ||
| 859 | <td><code>GET /.well-known/oauth-authorization-server</code></td> | ||
| 860 | <td>None</td> | ||
| 861 | <td>Relay OAuth token-exchange discovery metadata.</td> | ||
| 862 | </tr> | ||
| 863 | <tr> | ||
| 864 | <td><code>GET /.well-known/oauth-protected-resource</code></td> | ||
| 865 | <td>None</td> | ||
| 866 | <td>Relay DPoP protected-resource discovery metadata.</td> | ||
| 867 | </tr> | ||
| 868 | <tr> | ||
| 869 | <td><code>GET /v1/environments</code></td> | ||
| 870 | <td>Clerk bearer</td> | ||
| 871 | <td>List environments linked to the signed-in user.</td> | ||
| 872 | </tr> | ||
| 873 | <tr> | ||
| 874 | <td><code>POST /v1/client/environment-link-challenges</code></td> | ||
| 875 | <td>Clerk bearer</td> | ||
| 876 | <td>Create a short-lived user-bound environment link challenge.</td> | ||
| 877 | </tr> | ||
| 878 | <tr> | ||
| 879 | <td><code>POST /v1/client/environment-links</code></td> | ||
| 880 | <td>Clerk bearer</td> | ||
| 881 | <td> | ||
| 882 | Verify a local environment proof, reconcile a managed endpoint, and upsert the user | ||
| 883 | link. | ||
| 884 | </td> | ||
| 885 | </tr> | ||
| 886 | <tr> | ||
| 887 | <td><code>DELETE /v1/client/environment-links/:environmentId</code></td> | ||
| 888 | <td>Clerk bearer</td> | ||
| 889 | <td>Remove managed endpoint resources and revoke the user's link.</td> | ||
| 890 | </tr> | ||
| 891 | <tr> | ||
| 892 | <td><code>POST /v1/client/dpop-token</code></td> | ||
| 893 | <td>Clerk token in token-exchange payload plus DPoP proof</td> | ||
| 894 | <td>Issue a relay DPoP access token for the requested supported scopes.</td> | ||
| 895 | </tr> | ||
| 896 | <tr> | ||
| 897 | <td><code>POST /v1/environments/:environmentId/status</code></td> | ||
| 898 | <td>Relay DPoP</td> | ||
| 899 | <td>Request and validate a signed environment health response.</td> | ||
| 900 | </tr> | ||
| 901 | <tr> | ||
| 902 | <td><code>POST /v1/environments/:environmentId/connect</code></td> | ||
| 903 | <td>Relay DPoP</td> | ||
| 904 | <td>Request and validate an environment bootstrap credential.</td> | ||
| 905 | </tr> | ||
| 906 | <tr> | ||
| 907 | <td><code>POST /v1/mobile/devices</code></td> | ||
| 908 | <td>Relay DPoP</td> | ||
| 909 | <td>Register or update a mobile device for notifications.</td> | ||
| 910 | </tr> | ||
| 911 | <tr> | ||
| 912 | <td><code>POST /v1/mobile/live-activities</code></td> | ||
| 913 | <td>Relay DPoP</td> | ||
| 914 | <td>Register a Live Activity push token.</td> | ||
| 915 | </tr> | ||
| 916 | <tr> | ||
| 917 | <td><code>DELETE /v1/mobile/devices/:deviceId</code></td> | ||
| 918 | <td>Relay DPoP</td> | ||
| 919 | <td>Unregister a mobile device.</td> | ||
| 920 | </tr> | ||
| 921 | <tr> | ||
| 922 | <td> | ||
| 923 | <code>POST /v1/environments/:environmentId/threads/:threadId/agent-activity</code> | ||
| 924 | </td> | ||
| 925 | <td>Environment bearer plus signed publish proof</td> | ||
| 926 | <td>Publish redacted current agent activity for notification delivery.</td> | ||
| 927 | </tr> | ||
| 928 | </tbody> | ||
| 929 | </table> | ||
| 930 | </section> | ||
| 931 | |||
| 932 | <section class="panel" aria-label="Environment HTTP API"> | ||
| 933 | <h2>Environment Cloud HTTP API</h2> | ||
| 934 | <table> | ||
| 935 | <thead> | ||
| 936 | <tr> | ||
| 937 | <th>Endpoint</th> | ||
| 938 | <th>Authentication</th> | ||
| 939 | <th>Purpose</th> | ||
| 940 | </tr> | ||
| 941 | </thead> | ||
| 942 | <tbody> | ||
| 943 | <tr> | ||
| 944 | <td><code>POST /api/connect/link-proof</code></td> | ||
| 945 | <td>Local environment token with <code>relay:write</code></td> | ||
| 946 | <td>Validate local origin and sign an environment link proof.</td> | ||
| 947 | </tr> | ||
| 948 | <tr> | ||
| 949 | <td><code>POST /api/connect/relay-config</code></td> | ||
| 950 | <td>Local environment token with <code>relay:write</code></td> | ||
| 951 | <td>Persist linked relay configuration and start the relay client.</td> | ||
| 952 | </tr> | ||
| 953 | <tr> | ||
| 954 | <td><code>GET /api/connect/link-state</code></td> | ||
| 955 | <td>Local environment token with <code>relay:read</code></td> | ||
| 956 | <td>Read the environment's current T3 Connect link state.</td> | ||
| 957 | </tr> | ||
| 958 | <tr> | ||
| 959 | <td><code>POST /api/connect/preferences</code></td> | ||
| 960 | <td>Local environment token with <code>relay:write</code></td> | ||
| 961 | <td>Enable or disable agent-activity publication.</td> | ||
| 962 | </tr> | ||
| 963 | <tr> | ||
| 964 | <td><code>POST /api/connect/unlink</code></td> | ||
| 965 | <td>Local environment token with <code>relay:write</code></td> | ||
| 966 | <td>Stop the relay client and clear local T3 Connect configuration.</td> | ||
| 967 | </tr> | ||
| 968 | <tr> | ||
| 969 | <td><code>POST /api/t3-connect/health</code></td> | ||
| 970 | <td>Relay-signed proof</td> | ||
| 971 | <td>Return a signed environment health response.</td> | ||
| 972 | </tr> | ||
| 973 | <tr> | ||
| 974 | <td><code>POST /api/t3-connect/mint-credential</code></td> | ||
| 975 | <td>Relay-signed proof</td> | ||
| 976 | <td>Mint a DPoP-bound one-time bootstrap credential for a remote client.</td> | ||
| 977 | </tr> | ||
| 978 | <tr> | ||
| 979 | <td><code>POST /oauth/token</code></td> | ||
| 980 | <td>Bootstrap credential plus DPoP proof</td> | ||
| 981 | <td>Exchange a bootstrap credential for an environment access token.</td> | ||
| 982 | </tr> | ||
| 983 | <tr> | ||
| 984 | <td><code>POST /api/auth/websocket-ticket</code></td> | ||
| 985 | <td>Environment access token</td> | ||
| 986 | <td>Issue a one-time ticket for the WSS upgrade.</td> | ||
| 987 | </tr> | ||
| 988 | </tbody> | ||
| 989 | </table> | ||
| 990 | </section> | ||
| 991 | |||
| 992 | <section class="grid" aria-label="Runtime and infrastructure"> | ||
| 993 | <article class="panel half"> | ||
| 994 | <h2>Relay Client Installation</h2> | ||
| 995 | <p> | ||
| 996 | Desktop web UI availability checks and installs are local WebSocket RPC methods: | ||
| 997 | <code>cloud.getRelayClientStatus</code> and streaming | ||
| 998 | <code>cloud.installRelayClient</code>. The install stream emits the current stage: | ||
| 999 | checking, waiting for lock, downloading, verifying, installing, validating, and | ||
| 1000 | activating. | ||
| 1001 | </p> | ||
| 1002 | <p> | ||
| 1003 | The web UI asks for confirmation before installation and displays a custom progress | ||
| 1004 | dialog. The CLI invokes the same local relay-client service directly and reports the | ||
| 1005 | same progress stages in the terminal. | ||
| 1006 | </p> | ||
| 1007 | </article> | ||
| 1008 | |||
| 1009 | <article class="panel half"> | ||
| 1010 | <h2>Relay Infrastructure</h2> | ||
| 1011 | <ul> | ||
| 1012 | <li>Cloudflare Worker for the hosted relay HTTP API.</li> | ||
| 1013 | <li>PlanetScale Postgres database named <code>t3coderelay</code>.</li> | ||
| 1014 | <li> | ||
| 1015 | Production uses the shared database; non-production stages use database branches. | ||
| 1016 | </li> | ||
| 1017 | <li> | ||
| 1018 | Cloudflare Hyperdrive with caching disabled and a constrained origin connection limit. | ||
| 1019 | </li> | ||
| 1020 | <li>Cloudflare tunnel and DNS bindings for managed endpoint reconciliation.</li> | ||
| 1021 | <li>Cloudflare APNs delivery queue plus dead-letter queue.</li> | ||
| 1022 | <li>A five-minute cron job that prunes expired DPoP replay rows.</li> | ||
| 1023 | <li>Axiom OTLP trace dataset, scoped ingest token, and recent-spans view per stage.</li> | ||
| 1024 | </ul> | ||
| 1025 | </article> | ||
| 1026 | </section> | ||
| 1027 | |||
| 1028 | <section class="panel" aria-label="Deployment configuration"> | ||
| 1029 | <h2>Deployment Configuration</h2> | ||
| 1030 | <table> | ||
| 1031 | <thead> | ||
| 1032 | <tr> | ||
| 1033 | <th>Boundary</th> | ||
| 1034 | <th>Configuration</th> | ||
| 1035 | </tr> | ||
| 1036 | </thead> | ||
| 1037 | <tbody> | ||
| 1038 | <tr> | ||
| 1039 | <td>Relay infrastructure</td> | ||
| 1040 | <td> | ||
| 1041 | <code>RELAY_API_ZONE_NAME</code>, <code>RELAY_TUNNEL_ZONE_NAME</code>, optional | ||
| 1042 | <code>RELAY_DOMAIN</code>, <code>CLERK_PUBLISHABLE_KEY</code>, | ||
| 1043 | <code>CLERK_SECRET_KEY</code>, <code>CLERK_JWT_AUDIENCE</code>, and APNs | ||
| 1044 | credentials. | ||
| 1045 | </td> | ||
| 1046 | </tr> | ||
| 1047 | <tr> | ||
| 1048 | <td>Source-built desktop/server cloud features</td> | ||
| 1049 | <td> | ||
| 1050 | Optional <code>T3CODE_RELAY_URL</code>, <code>T3CODE_CLERK_PUBLISHABLE_KEY</code>, | ||
| 1051 | and <code>T3CODE_CLERK_CLI_OAUTH_CLIENT_ID</code>. Release builds inject public | ||
| 1052 | values at build time. | ||
| 1053 | </td> | ||
| 1054 | </tr> | ||
| 1055 | <tr> | ||
| 1056 | <td>Web and mobile cloud clients</td> | ||
| 1057 | <td> | ||
| 1058 | Public Clerk publishable key, Clerk JWT template name, and relay URL. Relay URLs | ||
| 1059 | must normalize to an absolute HTTPS origin without credentials, query, fragment, or | ||
| 1060 | non-root path. | ||
| 1061 | </td> | ||
| 1062 | </tr> | ||
| 1063 | </tbody> | ||
| 1064 | </table> | ||
| 1065 | </section> | ||
| 1066 | |||
| 1067 | <section class="panel" aria-label="Implementation checklist"> | ||
| 1068 | <h2>Implementation Checklist</h2> | ||
| 1069 | <ul> | ||
| 1070 | <li> | ||
| 1071 | Keep the relay cloud-mint private key hosted-only; install only its public key into | ||
| 1072 | environments. | ||
| 1073 | </li> | ||
| 1074 | <li> | ||
| 1075 | Keep environment signing private keys local and persist keypair creation atomically. | ||
| 1076 | </li> | ||
| 1077 | <li> | ||
| 1078 | Require a local <code>relay:write</code> session before linking or changing relay | ||
| 1079 | config. | ||
| 1080 | </li> | ||
| 1081 | <li>Validate relay URLs as secure absolute HTTPS origins before sending credentials.</li> | ||
| 1082 | <li>Require loopback-only tunnel origins on both sides of the link-proof boundary.</li> | ||
| 1083 | <li> | ||
| 1084 | Resolve managed endpoint egress from ready allocation rows, not client-supplied URLs. | ||
| 1085 | </li> | ||
| 1086 | <li>Disable redirects on relay-to-environment requests.</li> | ||
| 1087 | <li>Validate environment-signed response proofs before consuming tunneled responses.</li> | ||
| 1088 | <li>Keep tunnel and DNS provisioning retry-safe and deprovision them on unlink.</li> | ||
| 1089 | <li> | ||
| 1090 | Revoke shared environment publication credentials only after the final active link | ||
| 1091 | disappears. | ||
| 1092 | </li> | ||
| 1093 | <li>Redact and cap local agent failure details before publishing them externally.</li> | ||
| 1094 | </ul> | ||
| 1095 | </section> | ||
| 1096 | |||
| 1097 | <section class="panel" aria-label="Standards references"> | ||
| 1098 | <h2>Standards References</h2> | ||
| 1099 | <ul class="source-list"> | ||
| 1100 | <li> | ||
| 1101 | <a href="https://www.rfc-editor.org/rfc/rfc8252">RFC 8252</a>: browser-based OAuth for | ||
| 1102 | native apps, used by the CLI PKCE authorization flow. | ||
| 1103 | </li> | ||
| 1104 | <li> | ||
| 1105 | <a href="https://www.rfc-editor.org/rfc/rfc8693">RFC 8693</a>: OAuth token exchange, | ||
| 1106 | used for Clerk-token to relay-DPoP-token exchange. | ||
| 1107 | </li> | ||
| 1108 | <li> | ||
| 1109 | <a href="https://www.rfc-editor.org/rfc/rfc9449">RFC 9449</a>: DPoP proof of possession, | ||
| 1110 | used for relay and environment access tokens. | ||
| 1111 | </li> | ||
| 1112 | </ul> | ||
| 1113 | </section> | ||
| 1114 | </main> | ||
| 1115 | <script type="module"> | ||
| 1116 | import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"; | ||
| 1117 | |||
| 1118 | mermaid.initialize({ | ||
| 1119 | startOnLoad: false, | ||
| 1120 | securityLevel: "strict", | ||
| 1121 | theme: "base", | ||
| 1122 | themeVariables: { | ||
| 1123 | fontFamily: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | ||
| 1124 | primaryColor: "#eef4ff", | ||
| 1125 | primaryBorderColor: "#1457d9", | ||
| 1126 | primaryTextColor: "#171717", | ||
| 1127 | lineColor: "#62646b", | ||
| 1128 | signalColor: "#171717", | ||
| 1129 | signalTextColor: "#171717", | ||
| 1130 | noteBkgColor: "#fff6e6", | ||
| 1131 | noteTextColor: "#171717", | ||
| 1132 | actorBkg: "#fbfbf8", | ||
| 1133 | actorBorder: "#62646b", | ||
| 1134 | actorTextColor: "#171717", | ||
| 1135 | }, | ||
| 1136 | flowchart: { | ||
| 1137 | htmlLabels: true, | ||
| 1138 | curve: "basis", | ||
| 1139 | padding: 18, | ||
| 1140 | }, | ||
| 1141 | sequence: { | ||
| 1142 | showSequenceNumbers: true, | ||
| 1143 | mirrorActors: false, | ||
| 1144 | rightAngles: false, | ||
| 1145 | }, | ||
| 1146 | }); | ||
| 1147 | |||
| 1148 | await mermaid.run({ querySelector: ".diagram.mermaid" }); | ||
| 1149 | |||
| 1150 | for (const svg of document.querySelectorAll(".diagram.mermaid svg")) { | ||
| 1151 | const viewBox = svg.getAttribute("viewBox"); | ||
| 1152 | const width = viewBox?.split(/\s+/).map(Number)[2]; | ||
| 1153 | if (Number.isFinite(width) && width > 0) { | ||
| 1154 | svg.style.setProperty("width", `${Math.ceil(width)}px`, "important"); | ||
| 1155 | svg.style.setProperty("max-width", "none", "important"); | ||
| 1156 | } | ||
| 1157 | } | ||
| 1158 | </script> | ||
| 1159 | </body> | ||
| 1160 | </html> | ||
docs/internals/t3-connect.md+79-271| ... | @@ -1,273 +1,81 @@ | ... | @@ -1,273 +1,81 @@ |
| 1 | # T3 Connect | 1 | # T3 Connect |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | 3 | T3 Connect uses Clerk for cloud identity. The relay manages environment links, |
| 4 | 4 | credentials for reaching environments, and managed tunnel allocations. After | |
| 5 | T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay verifies | 5 | bootstrap, clients send application traffic through the environment's tunnel |
| 6 | two kinds of bearer credential: template JWTs generated from the `t3-relay` template with the shared | 6 | hostname; the relay Worker does not proxy their HTTP or WebSocket sessions. |
| 7 | `t3-code-relay` audience, and Clerk OAuth tokens issued to the CLI. `verifyRelayClientBearerToken` in | 7 | |
| 8 | `infra/relay/src/http/Api.ts` tries the template/session path first and falls back to OAuth | 8 | Clerk, deployment, and native authentication setup live in the |
| 9 | verification (`acceptsToken: "oauth_token"`), so the CLI's OAuth credential works without a JWT | 9 | [Connect setup runbook](../operations/connect-setup.md). |
| 10 | template. | 10 | |
| 11 | 11 | ## The relay is a trusted broker | |
| 12 | For the wider system diagram, see | 12 | |
| 13 | [t3-code-connect-auth-flow.html](./t3-code-connect-auth-flow.html). | 13 | An authenticated cloud user still needs an active environment link. The relay |
| 14 | 14 | asks that environment to mint a one-time bootstrap credential bound to the | |
| 15 | ## Application Keys | 15 | client's DPoP key. The client exchanges it directly with the environment for an |
| 16 | 16 | [environment session](./environment-auth.md). The relay never receives that | |
| 17 | T3 Connect is disabled in a fresh clone. To enable it for source builds against the production | 17 | session token, and possessing the bootstrap credential alone does not permit |
| 18 | deployment, copy the repository-root example file: | 18 | redeeming it without the client's private key. |
| 19 | 19 | ||
| 20 | ```sh | 20 | Both sides authenticate this exchange. The environment accepts only bounded, |
| 21 | cp .env.example .env | 21 | replay-guarded relay proofs for its own identity, linked user, and requested |
| 22 | ``` | 22 | operation. Signed environment responses bind the result to the request nonce; |
| 23 | 23 | mint responses also bind the credential to the client proof key. The relay | |
| 24 | `.env.example` carries the production public identifiers (the same values baked into official | 24 | verifies those bindings before returning a credential. This prevents a different |
| 25 | release builds). To target a different Clerk application or relay, set the values yourself in a | 25 | process behind the tunnel from impersonating the linked environment. The checks |
| 26 | repository-root `.env` or `.env.local` file: | 26 | meet in the |
| 27 | 27 | [environment cloud handlers](../../apps/server/src/cloud/http.ts) and | |
| 28 | ```dotenv | 28 | [relay connector](../../infra/relay/src/environments/EnvironmentConnector.ts). |
| 29 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | 29 | |
| 30 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | 30 | The relay holds the signing authority for mint requests. DPoP protects an honest |
| 31 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | 31 | exchange from credential reuse; it does not make a compromised relay signing |
| 32 | T3CODE_RELAY_URL=https://relay.example.com | 32 | key harmless. Keep that trust assumption explicit when changing the protocol. |
| 33 | ``` | 33 | |
| 34 | 34 | Managed tunnels expose only a validated loopback HTTP origin. Link proof checks | |
| 35 | The shared client loader projects these canonical values into framework-specific `VITE_*` and | 35 | reject forwarded authority headers, and the relay resolves endpoints from its |
| 36 | `EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new | 36 | own managed allocations rather than a caller-supplied URL. Health and mint |
| 37 | client configuration should use the canonical names. | 37 | requests must not follow redirects. These restrictions keep endpoint discovery |
| 38 | 38 | from turning into arbitrary relay egress or exposing another service on the | |
| 39 | Configuration precedence is: | 39 | environment host. |
| 40 | 40 | ||
| 41 | 1. Process or CI environment variables. | 41 | ## A link outlives a connector process |
| 42 | 2. Repository-root `.env.local`. | 42 | |
| 43 | 3. Repository-root `.env`. | 43 | CLI authorization, desired exposure, and a running connector have different |
| 44 | 44 | lifetimes. Linking can record intent while the server is stopped. Startup | |
| 45 | The Clerk publishable key, JWT template name, CLI OAuth client ID, and relay URL are public | 45 | reconciles that intent. CLI logout removes the stored cloud credential and |
| 46 | identifiers, not secrets. | 46 | disables exposure without uninstalling the environment's background service. |
| 47 | Web, desktop, mobile, and bundled server builds statically inject the values they consume during | 47 | |
| 48 | their build step. A built artifact does not need an environment file at runtime. CI release builds | 48 | Managed allocations belong to a user/environment pair. Provisioning checkpoints |
| 49 | should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, | 49 | external tunnel and DNS resources so retries can reconcile partial work. A |
| 50 | `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and | 50 | normal shutdown of a CLI-managed link releases its tunnel to avoid paying for |
| 51 | production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS | 51 | an idle resource, retaining the hostname reservation for the next startup. |
| 52 | environment. | 52 | It also retains the allocation record so the environment remains "offline" |
| 53 | 53 | rather than becoming "not authorized". | |
| 54 | When any client-facing public value is absent, cloud UI is omitted. The `t3 connect` command group is | 54 | |
| 55 | always registered: when the CLI public values are absent, `makeCli` in `apps/server/src/bin.ts` | 55 | Two cases must retain the tunnel across shutdown. A link installed through a |
| 56 | registers a hidden fallback `connect` command that reports the missing configuration instead of | 56 | client has no startup provisioning path and depends on its stored connector |
| 57 | silently vanishing from help. The bundled server still accepts runtime overrides for self-hosted or | 57 | token. An update handoff immediately starts a replacement server, and replacing |
| 58 | operator-managed deployments. | 58 | the tunnel would add routing propagation delay to every update. These exceptions |
| 59 | 59 | belong to [shutdown handling](../../apps/server/src/cloud/http.ts). | |
| 60 | For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay | 60 | |
| 61 | deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, | 61 | Release and unlink claim the allocation generation before deleting external |
| 62 | `CLERK_PUBLISHABLE_KEY`, and `CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in | 62 | resources. A delayed cleanup must not delete a tunnel reused by a concurrent |
| 63 | deployment defaults. | 63 | restart or relink. Unlink commits authorization revocation before external |
| 64 | `vp run --filter t3code-relay deploy` invokes Alchemy from the relay directory, so Alchemy loads | 64 | teardown, because a database failure must leave the active link usable. Failed |
| 65 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` | 65 | teardown retains enough state to retry. See the |
| 66 | with the deployed HTTPS relay URL. The relay still requires | 66 | [managed endpoint lifecycle](../../infra/relay/src/environments/ManagedEndpointProvider.ts). |
| 67 | `CLERK_SECRET_KEY` as an Alchemy secret. Never put `CLERK_SECRET_KEY` in a client application | 67 | |
| 68 | environment or commit it to the repository. | 68 | ## OAuth traps |
| 69 | 69 | ||
| 70 | The `prod` Alchemy stage owns the retained PlanetScale database. Non-production stages reference | 70 | Interactive clients and the headless CLI use the same Clerk application but |
| 71 | that database and provision isolated PlanetScale branches, so deploy `prod` before creating a | 71 | different credentials. The relay accepts both session-template JWTs and CLI |
| 72 | personal developer stage. | 72 | OAuth tokens; requiring a JWT template for the CLI would reject valid logins. |
| 73 | 73 | The CLI is a public OAuth client using PKCE and stores no client secret. | |
| 74 | ## Headless CLI OAuth Application | 74 | |
| 75 | 75 | CLI authorization starts on the hosted `/connect` page so sign-in completes | |
| 76 | The `t3 connect` commands authorize a headless environment with a separate Clerk OAuth application. | 76 | before entering Clerk's authorize endpoint. Sending a signed-out browser |
| 77 | This uses an OAuth public client with PKCE, so the CLI stores no client secret. | 77 | straight to that endpoint loses the authorize parameters during the sign-in |
| 78 | 78 | redirect. The [shared flow](../../packages/shared/src/connectAuth.ts) preserves | |
| 79 | In **Clerk Dashboard > OAuth applications**: | 79 | PKCE and state for both loopback and pasted-code callbacks. SSH and headless |
| 80 | 80 | sessions use the pasted-code flow because the browser cannot ordinarily reach a | |
| 81 | 1. Create an OAuth application for the T3 CLI. | 81 | listener on the remote machine. |
| 82 | 2. Enable the **Public** option so authorization-code exchange uses PKCE. | ||
| 83 | 3. Add **both** allowed redirect URIs: | ||
| 84 | - `http://127.0.0.1:34338/callback` for the loopback listener; | ||
| 85 | - `https://app.t3.codes/connect/callback` for the hosted out-of-band flow. This is | ||
| 86 | `connectCallbackUrl(DEFAULT_HOSTED_APP_URL)` from `packages/shared/src/connectAuth.ts`, so a | ||
| 87 | custom `T3CODE_HOSTED_APP_URL` means `$T3CODE_HOSTED_APP_URL/connect/callback` instead. | ||
| 88 | Omitting it breaks headless and SSH authorization. | ||
| 89 | 4. Enable the `openid`, `profile`, and `email` scopes. | ||
| 90 | 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build | ||
| 91 | environment to the generated public client ID. | ||
| 92 | |||
| 93 | Both CLI flows start at the hosted `/connect` page (`buildConnectAuthorizeRequestUrl` in | ||
| 94 | `packages/shared/src/connectAuth.ts`), which waits for a Clerk session and then forwards the request | ||
| 95 | to Clerk's `/oauth/authorize`. The CLI never opens `/oauth/authorize` directly: a signed-out browser | ||
| 96 | sent there goes through Clerk's sign-in redirect, which drops the authorize query parameters and | ||
| 97 | fails the flow with `unsupported_response_type` or an empty `state` (#5051). The loopback flow marks | ||
| 98 | the request with a `port` fragment parameter so the hosted page asks Clerk to redirect the | ||
| 99 | authorization code straight to `http://127.0.0.1:<port>/callback`; the out-of-band flow omits it and | ||
| 100 | uses the hosted `/connect/callback` page instead. The CLI derives Clerk's frontend API URL from the | ||
| 101 | publishable key and calls only the `/oauth/token` endpoint directly. The relay is not involved in | ||
| 102 | the OAuth handshake; it only validates the issued Clerk bearer token when the CLI manages an | ||
| 103 | environment link. | ||
| 104 | |||
| 105 | The connect command group is: | ||
| 106 | |||
| 107 | ```sh | ||
| 108 | t3 connect # default: onboarding | ||
| 109 | t3 connect login | ||
| 110 | t3 connect link # --publish-only | ||
| 111 | t3 connect status # --json | ||
| 112 | t3 connect publish # --disable | ||
| 113 | t3 connect unlink | ||
| 114 | t3 connect logout | ||
| 115 | ``` | ||
| 116 | |||
| 117 | `t3 serve` is a separate top-level command, not a connect subcommand. | ||
| 118 | |||
| 119 | `t3 connect login` opens the Clerk authorization flow and stores the CLI credential without enabling | ||
| 120 | cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed, | ||
| 121 | authorizes when needed, and records durable intent to expose the environment. It works without a | ||
| 122 | running T3 server. The next `t3 serve` or `t3 start` reconciles the relay link and launches the | ||
| 123 | managed tunnel. `t3 connect unlink` records disabled intent immediately, stops a reachable running | ||
| 124 | connector, and attempts to revoke the relay-side environment record. It retains the stored CLI | ||
| 125 | authorization so `t3 connect link` can re-enable exposure without another browser flow. `t3 connect | ||
| 126 | logout` performs the same cleanup and removes the stored CLI authorization. | ||
| 127 | |||
| 128 | The background service has an independent lifecycle. Connect setup may offer to install it, but | ||
| 129 | logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`. | ||
| 130 | |||
| 131 | ### Headless and SSH authorization | ||
| 132 | |||
| 133 | The loopback OAuth callback listener binds to port `34338`. That path only works when a browser on | ||
| 134 | the same machine can reach it, so `authorizeCli` in `apps/server/src/cli/connect.ts` automatically | ||
| 135 | selects the out-of-band flow when `--headless` is passed or when it detects SSH through | ||
| 136 | `SSH_CONNECTION` or `SSH_TTY`. The out-of-band flow prints the hosted `/connect` authorization URL | ||
| 137 | and accepts a pasted authorization code, so no port is involved. | ||
| 138 | |||
| 139 | Port forwarding is therefore optional, not required. Forward the port only if you specifically want | ||
| 140 | the loopback flow over SSH: | ||
| 141 | |||
| 142 | ```sh | ||
| 143 | ssh -L 34338:127.0.0.1:34338 <host> | ||
| 144 | ``` | ||
| 145 | |||
| 146 | ## JWT Template | ||
| 147 | |||
| 148 | In **Clerk Dashboard > JWT templates**, create a template with: | ||
| 149 | |||
| 150 | | Setting | Value | | ||
| 151 | | ------- | ---------------------------- | | ||
| 152 | | Name | `t3-relay` | | ||
| 153 | | Claims | `{ "aud": "t3-code-relay" }` | | ||
| 154 | |||
| 155 | Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set | ||
| 156 | `CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and | ||
| 157 | `CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value | ||
| 158 | is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still | ||
| 159 | selects the concrete relay deployment, but changing that URL does not require a JWT template change. | ||
| 160 | |||
| 161 | ## Desktop OAuth Redirect Allowlist | ||
| 162 | |||
| 163 | The desktop app opens OAuth in the system browser and returns to the app with a custom URL scheme. | ||
| 164 | In **Clerk Dashboard > Native applications**, enable the Native API and add these entries under the | ||
| 165 | mobile SSO redirect allowlist: | ||
| 166 | |||
| 167 | ```text | ||
| 168 | t3code-dev://app/ | ||
| 169 | t3code://app/ | ||
| 170 | ``` | ||
| 171 | |||
| 172 | Local desktop development uses `t3code-dev://app`, while packaged builds use `t3code://app`. Add the | ||
| 173 | matching origin to each Clerk instance's Backend API `allowed_origins` array as well. The development | ||
| 174 | Clerk instance should only need `t3code-dev://app`; the production Clerk instance should only need | ||
| 175 | `t3code://app`. `@clerk/electron` owns the native request adapter, encrypted Clerk token persistence, | ||
| 176 | external-browser OAuth transport, and callback delivery for initial sign-in and linked-account flows. | ||
| 177 | |||
| 178 | There is currently no Dashboard UI for `allowed_origins`. Preserve any existing entries and update | ||
| 179 | the instance through the Backend API: | ||
| 180 | |||
| 181 | ```sh | ||
| 182 | curl -X PATCH https://api.clerk.com/v1/instance \ | ||
| 183 | -H "Content-Type: application/json" \ | ||
| 184 | -H "Authorization: Bearer $CLERK_SECRET_KEY" \ | ||
| 185 | -d '{"allowed_origins":["t3code://app"]}' | ||
| 186 | ``` | ||
| 187 | |||
| 188 | Never put `CLERK_SECRET_KEY` in the desktop app, a client-facing environment file, or a build | ||
| 189 | artifact. | ||
| 190 | |||
| 191 | ## Desktop Passkeys | ||
| 192 | |||
| 193 | The production macOS bundle ID is `com.t3tools.t3code`. To enable native passkeys: | ||
| 194 | |||
| 195 | 1. Create an explicit macOS App ID for `com.t3tools.t3code` in the Apple Developer portal and enable | ||
| 196 | **Associated Domains**. | ||
| 197 | 2. Create a compatible macOS provisioning profile for that App ID and the certificate used to sign | ||
| 198 | the distributed app. | ||
| 199 | 3. In Clerk's Native API settings, add an iOS app with the same Apple Team ID and bundle ID. This is | ||
| 200 | also the configuration point for Electron/macOS passkeys. | ||
| 201 | 4. Confirm Clerk serves `https://<frontend-api>/.well-known/apple-app-site-association` and that | ||
| 202 | `webcredentials.apps` contains `<TEAM_ID>.com.t3tools.t3code`. | ||
| 203 | 5. Set the local or CI signing configuration described below. | ||
| 204 | |||
| 205 | For a local signed build, add these values to `.env.local` or export them before invoking the | ||
| 206 | desktop artifact command: | ||
| 207 | |||
| 208 | ```dotenv | ||
| 209 | T3CODE_APPLE_TEAM_ID=ABC1234567 | ||
| 210 | T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile | ||
| 211 | # Optional: comma-separated override when Clerk's RP ID differs from the Frontend API hostname. | ||
| 212 | T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com | ||
| 213 | ``` | ||
| 214 | |||
| 215 | When `T3CODE_CLERK_PASSKEY_RP_DOMAINS` is absent, the build derives the RP domain from | ||
| 216 | `T3CODE_CLERK_PUBLISHABLE_KEY`. Signed macOS builds fail early if the Team ID, provisioning profile, | ||
| 217 | or RP-domain configuration is missing. The generated main-app entitlements include every configured | ||
| 218 | `webcredentials:<domain>` entry; helper apps keep Electron's minimal default entitlements. | ||
| 219 | |||
| 220 | The normal `dev:desktop` launcher is unsigned and cannot complete macOS passkey ceremonies. For | ||
| 221 | renderer HMR, build and install a signed app first, run the renderer dev server, then launch the | ||
| 222 | installed app executable with `VITE_DEV_SERVER_URL` and `T3CODE_PORT` set. Rebuild the signed app | ||
| 223 | after native dependency, main-process, preload, entitlement, provisioning, or signing changes; | ||
| 224 | renderer-only changes can reuse the installed app. | ||
| 225 | |||
| 226 | For the default development ports, run `pnpm dev:web` in one terminal and launch the installed | ||
| 227 | binary from another: | ||
| 228 | |||
| 229 | ```sh | ||
| 230 | VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \ | ||
| 231 | T3CODE_PORT=13773 \ | ||
| 232 | "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)" | ||
| 233 | ``` | ||
| 234 | |||
| 235 | After changing Associated Domains, bump the build version before rebuilding; macOS may otherwise | ||
| 236 | reuse stale Shared Web Credentials metadata for the same app/version pair. | ||
| 237 | |||
| 238 | Verify the installed bundle before testing: | ||
| 239 | |||
| 240 | ```sh | ||
| 241 | codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app" | ||
| 242 | codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app" | ||
| 243 | ``` | ||
| 244 | |||
| 245 | The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth | ||
| 246 | flow uses a custom redirect URI, add that exact URI to the same allowlist. | ||
| 247 | |||
| 248 | ## Sign-in Surfaces | ||
| 249 | |||
| 250 | Signed-in users manage T3 Connect under **Connections**. The settings sidebar also has dedicated | ||
| 251 | controls, rendered by `SettingsSidebarNav.tsx`: `T3ConnectSidebarSignIn` in the footer shows a | ||
| 252 | **Sign in to T3 Connect** button while signed out, and `T3ConnectSidebarAvatar` shows a Clerk | ||
| 253 | `UserButton` account control while signed in. Both are gated on cloud public configuration. | ||
| 254 | Desktop renders the same web bundle, so it has them too. The waitlist enrollment flow from the | ||
| 255 | private beta was removed when Connect went GA; sign-up is open unless a Clerk restriction below is | ||
| 256 | enabled. | ||
| 257 | |||
| 258 | ## Restricting Sign-ups: Known-User Allowlist | ||
| 259 | |||
| 260 | For a closed deployment where all permitted users are known in advance, restrict sign-up to | ||
| 261 | permitted email addresses or domains: | ||
| 262 | |||
| 263 | 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email | ||
| 264 | domain. | ||
| 265 | 2. Enable the allowlist and save. | ||
| 266 | 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or | ||
| 267 | manually created. | ||
| 268 | |||
| 269 | Do not enable an empty allowlist: it blocks all new sign-ups. | ||
| 270 | |||
| 271 | Clerk allowlists control who can sign up. They do not revoke an existing user's active cloud | ||
| 272 | access. To remove an already-created user's access, ban that user in Clerk so their active | ||
| 273 | sessions are ended and future sign-ins are rejected. |
docs/internals/terminal-runtime.md+42-50| ... | @@ -1,52 +1,44 @@ | ... | @@ -1,52 +1,44 @@ |
| 1 | # Terminal runtime | 1 | # Terminal runtime |
| 2 | 2 | ||
| 3 | The environment server owns terminal processes, retained output history, and | 3 | The environment server owns PTYs, session lifetime, and retained output. Every |
| 4 | session lifecycle. Web, desktop, and mobile clients attach to the same | 4 | client, including the desktop renderer, attaches through the environment connection. |
| 5 | server-owned session over the environment RPC connection. The desktop renderer | 5 | This lets clients reconnect or share a running session. Renderer choices stay local |
| 6 | does not own a separate PTY. Clients can reconnect to a running PTY or share | 6 | to each client and do not change terminal contracts. |
| 7 | it with another client. | 7 | |
| 8 | 8 | ## Output and retention | |
| 9 | ## Output path | 9 | |
| 10 | 10 | [Terminal history](../../apps/server/src/terminal/Manager.ts) is incremental. | |
| 11 | PTY output follows this path: | 11 | PTY callbacks append new chunks; live events carry only those chunks. Materializing |
| 12 | 12 | or copying full scrollback on every callback makes output cost grow with retained | |
| 13 | ```text | 13 | history, so snapshots and coalesced persistence are the materialization boundaries. |
| 14 | PTY callback | 14 | Persistence queues the mutable history buffer and reads its latest value when the |
| 15 | -> ordered process-event drain | 15 | write runs. Clear, restart, and close must drain writes before completing their |
| 16 | -> bounded retained-history append | 16 | lifecycle boundary. |
| 17 | -> live terminal output event | 17 | |
| 18 | -> coalesced history persistence | 18 | Server history is capped at 5,000 lines and 8 MiB of UTF-8 text per terminal, so a |
| 19 | ``` | 19 | long unterminated line cannot bypass retention. Eviction removes the oldest output |
| 20 | 20 | without splitting Unicode code points; live output is not truncated. Release | |
| 21 | Live output events contain only the new PTY data. Full retained history is | 21 | discarded chunk references immediately, even if array compaction happens later. |
| 22 | materialized only when the server returns a snapshot or when the coalescing | 22 | Client buffers have a separate 512 KiB cap. Measure throughput with full scrollback |
| 23 | persistence worker writes the latest state. | 23 | when changing this path. |
| 24 | 24 | ||
| 25 | Retained history is limited to 5,000 lines and 8 MiB of UTF-8 text per terminal. | 25 | Restoration must read only the bounded tail of current or legacy history files, |
| 26 | The server discards the oldest output when either limit is reached. A byte | 26 | skip any incomplete UTF-8 prefix, and apply the line limit. Close the read handle |
| 27 | cutoff can shorten the oldest retained line, but does not split a Unicode code | 27 | before rewriting the capped file. Reading whole old logs would defeat the memory |
| 28 | point. Live output is not truncated. | 28 | bound during startup. |
| 29 | 29 | ||
| 30 | History uses small chunks with byte and newline counts. Appending output scans | 30 | ## Renderer ownership |
| 31 | the new text and any removed chunk prefix, not the full retained history. | 31 | |
| 32 | Empty lines, incomplete final lines, and trailing newlines remain unchanged | 32 | Android and web use the same `libghostty-vt` C ABI for terminal behavior. Platform |
| 33 | within the limits. Split surrogate pairs are joined before byte eviction. | 33 | adapters own drawing and input integration, and React stays out of terminal frames. |
| 34 | 34 | The web adapter shares one WebAssembly instance per browser tab while each terminal | |
| 35 | Discard each chunk's string reference as soon as it leaves retained history. | 35 | owns and frees its own handles. The canonical upstream pin is |
| 36 | Array compaction can run later. Shared web and mobile client state retains at | 36 | [`native/libghostty-vt/VERSION`](../../native/libghostty-vt/VERSION); both native and |
| 37 | most 512 KiB, so each client can display less scrollback than the server keeps. | 37 | web artifacts must be rebuilt when it changes. Web embeds the revision in its build |
| 38 | 38 | info so the ABI check can detect drift without a second pin. | |
| 39 | Measure sustained-output changes against a full retained history so terminal | 39 | |
| 40 | throughput does not regress unnoticed. | 40 | Restoring scrollback must not send terminal replies to the current shell. Historical |
| 41 | 41 | device queries can otherwise provoke fresh replies that appear as junk at the | |
| 42 | ## Persistence | 42 | prompt. The server strips query/response traffic from retained history, and the |
| 43 | 43 | [web renderer](../../apps/web/src/terminal/ghostty/core.ts) detaches its PTY writer | |
| 44 | History persistence is keyed by terminal session and coalesces pending writes. | 44 | during replay. Preserve both protections when changing retention or renderer code. |
| 45 | The worker reads the newest bounded-history state after its debounce instead | ||
| 46 | of receiving a newly materialized full string for every PTY callback. Clear, | ||
| 47 | restart, close, and final flush operations still force the latest state to | ||
| 48 | disk before their lifecycle boundary completes. | ||
| 49 | |||
| 50 | Restoration reads at most the last 8 MiB from current and legacy history files. | ||
| 51 | It skips an incomplete UTF-8 code point at the start and applies the line limit | ||
| 52 | before rewriting oversized files. File handles close before that rewrite. |
docs/internals/voice-input.md+18-100| ... | @@ -1,102 +1,20 @@ | ... | @@ -1,102 +1,20 @@ |
| 1 | # Voice input | 1 | # Voice input |
| 2 | 2 | ||
| 3 | > For maintainers. Using T3 Code? See [voice input on iPhone](../user/composer.md#voice-input-on-iphone). | 3 | Transcription edits a composer draft. It does not submit an agent turn. Audio is |
| 4 | 4 | temporary client input, and only normal message submission sends the resulting | |
| 5 | Voice input produces editable composer text. The current implementation records on the client and | 5 | text. The current implementation transcribes locally on supported iOS devices; |
| 6 | transcribes locally with Apple's `SpeechAnalyzer` and `SpeechTranscriber` on supported iOS 26+ | 6 | environment-backed transcription is not implemented. |
| 7 | devices. Environment-provided transcription and transcription on web and desktop are not implemented. | 7 | |
| 8 | 8 | The [shared controller](../../packages/client-runtime/src/voice-input/controller.ts) | |
| 9 | ## Current boundaries | 9 | owns the operation while the client supplies capture and transcription. Preparation |
| 10 | 10 | binds the transcriber and resolved locale for the whole recording. Draft ownership, | |
| 11 | The shared [`VoiceInputController`][controller] in `packages/client-runtime` owns preparation, | 11 | text, and revision are captured before recording and checked before insertion, so |
| 12 | recording, transcription, cancellation, temporary-file cleanup, and insertion into the captured | 12 | a late transcript cannot overwrite a draft that was edited or replaced. |
| 13 | draft selection. Applications import it through the [voice-input entry point][voice-input] as | 13 | |
| 14 | `@t3tools/client-runtime/voice-input`. Its dependencies separate capture from transcription; the | 14 | Cancellation invalidates a result immediately, but resources stay owned until the |
| 15 | controller imports neither React Native nor an Apple transcription API. | 15 | underlying work settles. Apple's native transcription call cannot be interrupted |
| 16 | 16 | once started. Releasing the session or deleting its recording when the abort signal | |
| 17 | The shared [transcription contract][transcription] defines `VoiceTranscriber`, | 17 | fires would race that work. The [transcription contract](../../packages/client-runtime/src/voice-input/transcription.ts) |
| 18 | `PreparedVoiceTranscription`, and transcription errors. The controller calls `getTranscriber()` once | 18 | therefore requires implementations to settle only after their work has stopped; |
| 19 | at the start of an operation, before asking for microphone permission. Preparation returns a resolved | 19 | the [Apple binding](../../apps/mobile/src/native/voiceTranscription.ios.ts) checks |
| 20 | locale and a bound `transcribe` function. The controller retains that result for the recording, so a | 20 | cancellation between native calls and discards late results. |
| 21 | selection change cannot prepare with one implementation and transcribe with another. | ||
| 22 | |||
| 23 | [`useVoiceInputController`][hook] supplies Expo audio capture, microphone permissions, audio-session | ||
| 24 | management, waveform samples, and app and navigation lifecycle handling. It normalizes Expo's | ||
| 25 | `mediaServicesDidReset` into a generic recorder error. [`voiceTranscription.ios.ts`][ios] adapts | ||
| 26 | `@react-native-ai/apple` through `getLocalVoiceTranscriber()`, capturing the requested device locale | ||
| 27 | and binding the prepared transcriber to Apple's resolved locale. The other-platform binding returns | ||
| 28 | no local transcriber. That result describes the local implementation, not whether a client could use | ||
| 29 | an environment's transcription service. | ||
| 30 | |||
| 31 | Mobile's [`voiceInputPresentation.ts`][presentation] maps shared state to toolbar labels and actions. | ||
| 32 | Waveform and toolbar rendering stay in mobile. The composer edits draft text without selecting a | ||
| 33 | speech vendor. | ||
| 34 | Recording captures the draft owner, revision, text, and selection. A late transcript cannot overwrite | ||
| 35 | a different or edited draft. Only normal message submission sends the resulting text to an agent. | ||
| 36 | |||
| 37 | Each operation passes one `AbortSignal` through preparation and transcription. Cancellation | ||
| 38 | invalidates the operation and aborts that signal immediately. Implementations settle their promises | ||
| 39 | only after their underlying work stops. The Apple binding checks cancellation between asynchronous | ||
| 40 | steps but cannot interrupt an in-flight native request. The controller retains its session until | ||
| 41 | that work settles, ignores its result, and cleans up the recording. | ||
| 42 | |||
| 43 | ## Ownership decisions | ||
| 44 | |||
| 45 | The extension boundary distinguishes transcription on the client device from transcription through | ||
| 46 | the composer's environment. These constraints apply when adding selectable transcription services: | ||
| 47 | |||
| 48 | - Local means the client device, regardless of which machine hosts the environment. A device's lack | ||
| 49 | of local recognition does not prevent it from recording audio for an environment service. | ||
| 50 | - Remote service configuration and API keys belong to the environment. The environment calls the | ||
| 51 | external service. Clients receive service identifiers, labels, and availability information, never | ||
| 52 | credential values. Transcription services are independent of coding-agent `providerInstances`; | ||
| 53 | selecting OpenAI for transcription does not select Codex for the thread. | ||
| 54 | - The client owns its transcription preference, scoped by stable `environmentId`. Its choices are | ||
| 55 | supported local recognition and the services exposed by the composer's environment. A service ID | ||
| 56 | is meaningful only within that environment. Different clients can make different choices. | ||
| 57 | - Resolve and capture the environment, selected service, and locale when an operation starts. | ||
| 58 | Preparation and transcription use the same selection; preference changes affect the next | ||
| 59 | recording. Capture environment identity explicitly rather than recovering it from a draft key. | ||
| 60 | Keep the existing draft-owner and revision checks before inserting text. | ||
| 61 | - If the selected option is unavailable, report that state and let the user choose another option. | ||
| 62 | A local failure must not silently upload audio, and a disconnected environment must not redirect | ||
| 63 | a recording to another environment or service. | ||
| 64 | - Transcription audio is temporary input, separate from durable chat attachments and messages. | ||
| 65 | Remote adapters need cancellation of upload and transcription where supported, cleanup after | ||
| 66 | success, failure, or cancellation, and the same protection against late results as local transcription. | ||
| 67 | |||
| 68 | ## Existing integration points | ||
| 69 | |||
| 70 | [`ServerSettingsService`][settings] and [`ServerSecretStore`][secrets] provide environment-owned | ||
| 71 | configuration and secret persistence. Existing settings redaction handles coding-provider environment | ||
| 72 | variables only. Any transcription credential fields need their own explicit separation and redaction | ||
| 73 | before settings responses or subscriptions reach client caches. | ||
| 74 | |||
| 75 | [`ExecutionEnvironmentCapabilities`][capabilities] handles version skew. Remote transcription must be | ||
| 76 | opt-in: a missing transcription capability means unsupported. The authenticated server-config | ||
| 77 | subscription and [shared environment state][server-state] already distribute configuration per | ||
| 78 | environment. A transcription service catalog belongs behind that capability and authenticated | ||
| 79 | boundary. Older servers expose no remote transcription choices. | ||
| 80 | |||
| 81 | The [attachment upload contracts][uploads] and [shared upload operations][attachment-state] provide a | ||
| 82 | pattern for authorized binary uploads through an environment, including remote connections. Their | ||
| 83 | existing chat-attachment retention is not a transcription cleanup policy. | ||
| 84 | |||
| 85 | Future service selection and environment requests belong alongside the controller in | ||
| 86 | `packages/client-runtime`, with wire contracts in `packages/contracts`. Capture and native local | ||
| 87 | recognition remain client-specific. An environment-backed transcriber implements the same shared | ||
| 88 | contract, with its environment and service bound when selected. The controller does not own service | ||
| 89 | credentials, provider SDKs, or transport selection. | ||
| 90 | |||
| 91 | [controller]: ../../packages/client-runtime/src/voice-input/controller.ts | ||
| 92 | [voice-input]: ../../packages/client-runtime/src/voice-input/index.ts | ||
| 93 | [transcription]: ../../packages/client-runtime/src/voice-input/transcription.ts | ||
| 94 | [hook]: ../../apps/mobile/src/features/voice-input/useVoiceInputController.ts | ||
| 95 | [presentation]: ../../apps/mobile/src/features/voice-input/voiceInputPresentation.ts | ||
| 96 | [ios]: ../../apps/mobile/src/native/voiceTranscription.ios.ts | ||
| 97 | [settings]: ../../apps/server/src/serverSettings.ts | ||
| 98 | [secrets]: ../../apps/server/src/auth/ServerSecretStore.ts | ||
| 99 | [capabilities]: ../../packages/contracts/src/environment.ts | ||
| 100 | [server-state]: ../../packages/client-runtime/src/state/server.ts | ||
| 101 | [uploads]: ../../packages/contracts/src/assets.ts | ||
| 102 | [attachment-state]: ../../packages/client-runtime/src/state/attachments.ts |
docs/internals/work-artifacts.md deleted-25| ... | @@ -1,25 +0,0 @@ | ||
| 1 | # Engineering work artifacts | ||
| 2 | |||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | ||
| 4 | |||
| 5 | Keep planned work out of the source tree. Code search should return the product as it exists, not a mix of current behavior and abandoned intentions. | ||
| 6 | |||
| 7 | ## Current facts belong in the docs | ||
| 8 | |||
| 9 | Put durable architecture, constraints, and operational knowledge in `docs/internals/` or `docs/operations/`. Write these documents in the present tense and update them with the code they describe. | ||
| 10 | |||
| 11 | When the reason for a decision will matter after implementation, record it in the relevant internal document. Use a separate decision record under `docs/internals/` only when the rationale does not fit cleanly beside the current architecture. | ||
| 12 | |||
| 13 | ## Planned work belongs in GitHub | ||
| 14 | |||
| 15 | Track active maintainer work in its GitHub issue or project item. The tracking item should state the outcome, constraints, and acceptance criteria, then link the pull requests that implement it. Split large efforts into one durable specification and small work items that can each close independently. | ||
| 16 | |||
| 17 | Close completed items. Update or delete invalidated work before starting the next implementation session. External proposals follow [CONTRIBUTING.md](../../CONTRIBUTING.md) and belong in Ideas discussions rather than issues. | ||
| 18 | |||
| 19 | ## Temporary work stays temporary | ||
| 20 | |||
| 21 | Keep agent scratch files, exploratory research, transcripts, and session handoff notes outside the worktree. They are inputs to the work, not project documentation. | ||
| 22 | |||
| 23 | `.plans/` is gitignored as a safety net for legacy tools. Its presence does not make it an accepted project artifact. Pull requests must not add implementation plans or temporary research under another name. | ||
| 24 | |||
| 25 | A pull request records what changed and why. If a fact must survive after the pull request merges, update the relevant documentation. Otherwise, the tracking item and pull request are the record. | ||
docs/internals/workspace-layout.md deleted-63| ... | @@ -1,63 +0,0 @@ | ||
| 1 | # Workspace layout | ||
| 2 | |||
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). | ||
| 4 | |||
| 5 | A pnpm workspace driven by [vite-plus](https://vite.plus) (`vp`). See [scripts.md](./scripts.md) for | ||
| 6 | the task commands. | ||
| 7 | |||
| 8 | ## apps | ||
| 9 | |||
| 10 | - `apps/server` (`t3`): the execution runtime and the published CLI. Owns orchestration, provider | ||
| 11 | drivers, checkpointing, VCS, terminals, filesystem access, auth, and the HTTP + WebSocket surface. | ||
| 12 | Also serves the built web app. | ||
| 13 | - `apps/web` (`@t3tools/web`): React + Vite UI. Consumes the shared client runtime and adds routing, | ||
| 14 | components, and web-specific platform layers. | ||
| 15 | - `apps/desktop` (`@t3tools/desktop`): Electron shell. Supervises a desktop-scoped `t3` backend, | ||
| 16 | loads the web bundle over the `t3code://` protocol, and owns SSH-managed remote environments. | ||
| 17 | - `apps/mobile` (`@t3tools/mobile`): Expo/React Native client. Same client runtime composition as | ||
| 18 | web, different platform layer and UI. | ||
| 19 | - `apps/marketing` (`@t3tools/marketing`): Astro marketing site. | ||
| 20 | |||
| 21 | ## packages | ||
| 22 | |||
| 23 | - `packages/contracts` (`@t3tools/contracts`): shared Effect Schema definitions. RPC group, | ||
| 24 | orchestration commands/events/read model, auth scopes, environment descriptors, settings. | ||
| 25 | - `packages/shared` (`@t3tools/shared`): framework-agnostic utilities used by server and clients | ||
| 26 | (`DrainableWorker`, git and source-control helpers, relay auth and signing, DPoP, semver, logging, | ||
| 27 | observability, and more). | ||
| 28 | - `packages/client-runtime` (`@t3tools/client-runtime`): connection lifecycle, authorization, RPC | ||
| 29 | session, environment registry, and Atom-based domain state shared by web and mobile. See its | ||
| 30 | [README](../../packages/client-runtime/README.md). | ||
| 31 | - `packages/ssh` (`@t3tools/ssh`): SSH config parsing, auth prompts, command execution, and the | ||
| 32 | tunnel/environment manager behind desktop-managed SSH environments. | ||
| 33 | - `packages/tailscale` (`@t3tools/tailscale`): Tailscale CLI wrapper, including the | ||
| 34 | `ensureTailscaleServe` / `disableTailscaleServe` serve lifecycle the server drives. | ||
| 35 | - `packages/effect-acp` (`effect-acp`): Effect client and agent implementation of the Agent Client | ||
| 36 | Protocol, used by ACP-speaking provider drivers. | ||
| 37 | - `packages/effect-codex-app-server` (`effect-codex-app-server`): Effect client for the | ||
| 38 | `codex app-server` JSON-RPC protocol. | ||
| 39 | |||
| 40 | ## infra | ||
| 41 | |||
| 42 | - `infra/relay` (`t3code-relay`): the hosted T3 Connect relay, deployed with Alchemy. Handles | ||
| 43 | environment discovery, cloud-side records, and mobile notifications. It is not in the hot path; | ||
| 44 | after connect, client traffic goes directly to the environment. See | ||
| 45 | [t3-connect.md](./t3-connect.md). | ||
| 46 | |||
| 47 | ## Other top-level directories | ||
| 48 | |||
| 49 | - `scripts/`: workspace tooling run through `vp run`. Dev runner, desktop artifact builds, release | ||
| 50 | helpers, mobile static checks and showcase capture, update-manifest merging. | ||
| 51 | - `assets/`: brand and app icon sources per channel (`dev`, `nightly`, `prod`). | ||
| 52 | - `patches/`: pnpm patches for pinned upstream dependencies. | ||
| 53 | - `oxlint-plugin-t3code/`: repo-specific lint rules. | ||
| 54 | - `experiments/`: throwaway prototypes. Not part of the shipped build. | ||
| 55 | - `docs/`: this documentation tree. | ||
| 56 | |||
| 57 | ## Import conventions | ||
| 58 | |||
| 59 | `@t3tools/shared` and `@t3tools/client-runtime` use explicit subpath exports with no barrel index and | ||
| 60 | no root export. Import the narrow path (`@t3tools/shared/DrainableWorker`, | ||
| 61 | `@t3tools/client-runtime/state/threads`) rather than the package root. Files that are not exported | ||
| 62 | are implementation details. `@t3tools/contracts` does export a root alongside `./settings` and | ||
| 63 | `./relay`. | ||
docs/operations/connect-setup.md created+126| ... | @@ -0,0 +1,126 @@ | ||
| 1 | # T3 Connect setup | ||
| 2 | |||
| 3 | Deployment and client configuration for T3 Connect. The [architecture note](../internals/t3-connect.md) | ||
| 4 | explains the trust boundaries; the [relay README](../../infra/relay/README.md#deployment) owns relay | ||
| 5 | provisioning instructions. | ||
| 6 | |||
| 7 | ## Public application configuration | ||
| 8 | |||
| 9 | T3 Connect is disabled in a fresh clone. To build against the production deployment, copy the | ||
| 10 | repository-root example: | ||
| 11 | |||
| 12 | ```sh | ||
| 13 | cp .env.example .env | ||
| 14 | ``` | ||
| 15 | |||
| 16 | For another deployment, set these values in the repository-root `.env` or `.env.local`: | ||
| 17 | |||
| 18 | ```dotenv | ||
| 19 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> | ||
| 20 | T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | ||
| 21 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=<public OAuth application client ID> | ||
| 22 | T3CODE_RELAY_URL=https://relay.example.com | ||
| 23 | ``` | ||
| 24 | |||
| 25 | Process variables take precedence over `.env.local`, then `.env`. Use these canonical names; | ||
| 26 | the build loader supplies framework-specific aliases. These values are public identifiers. | ||
| 27 | `CLERK_SECRET_KEY` belongs only in the relay's secrets, never in client configuration. | ||
| 28 | |||
| 29 | Client and bundled-server builds embed the public values, so set them before building. | ||
| 30 | EAS preview and production environments need the publishable key, JWT template name, and relay URL. | ||
| 31 | Bundled servers also accept runtime overrides for operator-managed deployments. | ||
| 32 | |||
| 33 | Copy `infra/relay/.env.example` to `infra/relay/.env` for relay deployment settings. | ||
| 34 | Deploy `prod` before personal stages because it owns the retained database that their branches | ||
| 35 | depend on. The deploy wrapper writes the resulting relay URL back to the root `.env`. | ||
| 36 | |||
| 37 | ## CLI OAuth application | ||
| 38 | |||
| 39 | In Clerk's OAuth applications settings: | ||
| 40 | |||
| 41 | 1. Create a public OAuth application for the T3 CLI, using authorization-code exchange with PKCE. | ||
| 42 | 2. Allow both redirect URIs: `http://127.0.0.1:34338/callback` and | ||
| 43 | `https://app.t3.codes/connect/callback`. A custom `T3CODE_HOSTED_APP_URL` needs its own | ||
| 44 | `/connect/callback` URL. Headless and SSH authorization depend on the hosted redirect. | ||
| 45 | 3. Enable the `openid`, `profile`, and `email` scopes. | ||
| 46 | 4. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` to the generated public client ID in local and release | ||
| 47 | build environments. | ||
| 48 | |||
| 49 | ## JWT template | ||
| 50 | |||
| 51 | Create a Clerk JWT template named `t3-relay` with claims: | ||
| 52 | |||
| 53 | ```json | ||
| 54 | { "aud": "t3-code-relay" } | ||
| 55 | ``` | ||
| 56 | |||
| 57 | Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` for clients and | ||
| 58 | `CLERK_JWT_AUDIENCE=t3-code-relay` for the relay. The production relay deployment environment | ||
| 59 | also defines `CLERK_JWT_TEMPLATE`. The audience stays the same across relay stages; the relay | ||
| 60 | URL selects the deployment. | ||
| 61 | |||
| 62 | ## Desktop OAuth redirects | ||
| 63 | |||
| 64 | Enable Clerk's Native API and add the desktop redirects to its SSO redirect allowlist: | ||
| 65 | |||
| 66 | ```text | ||
| 67 | t3code-dev://app/ | ||
| 68 | t3code://app/ | ||
| 69 | ``` | ||
| 70 | |||
| 71 | Add the corresponding origin to the Clerk instance's Backend API `allowed_origins` array. | ||
| 72 | Development uses `t3code-dev://app`; production uses `t3code://app`. Update the array with | ||
| 73 | `PATCH https://api.clerk.com/v1/instance` using the Clerk secret key, preserving existing entries. | ||
| 74 | The Clerk Electron integration handles token | ||
| 75 | persistence and system-browser callback delivery. | ||
| 76 | |||
| 77 | ## Desktop passkeys | ||
| 78 | |||
| 79 | For a production macOS app with bundle ID `com.t3tools.t3code`: | ||
| 80 | |||
| 81 | 1. Create an explicit macOS App ID in the Apple Developer portal with **Associated Domains**. | ||
| 82 | 2. Create a provisioning profile for that App ID and the distribution signing certificate. | ||
| 83 | 3. In Clerk's Native API settings, add an iOS app with the same Apple Team ID and bundle ID. | ||
| 84 | This setting also configures Electron/macOS passkeys. | ||
| 85 | 4. Check `https://<frontend-api>/.well-known/apple-app-site-association`. Its | ||
| 86 | `webcredentials.apps` must include `<TEAM_ID>.com.t3tools.t3code`. | ||
| 87 | 5. Configure signing as described in the [release runbook](./release.md#2-apple-signing--notarization-setup-macos). | ||
| 88 | |||
| 89 | Local signed builds additionally use: | ||
| 90 | |||
| 91 | ```dotenv | ||
| 92 | T3CODE_APPLE_TEAM_ID=ABC1234567 | ||
| 93 | T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile | ||
| 94 | # Override only when the RP domain differs from the Clerk Frontend API hostname. | ||
| 95 | T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com | ||
| 96 | ``` | ||
| 97 | |||
| 98 | Without the override, the build derives the RP domain from the Clerk publishable key. | ||
| 99 | After changing Associated Domains, bump the build version before rebuilding. macOS can otherwise | ||
| 100 | reuse stale Shared Web Credentials metadata for the same app/version pair. | ||
| 101 | |||
| 102 | The ordinary `dev:desktop` launcher is unsigned and cannot exercise macOS passkeys. For renderer | ||
| 103 | HMR, install a signed build, start `vp run dev:web`, and launch the installed executable with the | ||
| 104 | actual web and server ports. For example, with the default ports: | ||
| 105 | |||
| 106 | ```sh | ||
| 107 | VITE_DEV_SERVER_URL=http://127.0.0.1:5733 \ | ||
| 108 | T3CODE_PORT=13773 \ | ||
| 109 | "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)" | ||
| 110 | ``` | ||
| 111 | |||
| 112 | Rebuild the signed app after native dependency, main-process, preload, entitlement, provisioning, | ||
| 113 | or signing changes. Renderer edits can reuse it. Verify the installed bundle before testing: | ||
| 114 | |||
| 115 | ```sh | ||
| 116 | codesign --verify --deep --strict "/Applications/T3 Code (Alpha).app" | ||
| 117 | codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app" | ||
| 118 | ``` | ||
| 119 | |||
| 120 | ## Restricting sign-ups | ||
| 121 | |||
| 122 | Use Clerk's allowlist for permitted email addresses or domains, or Restricted mode for invitation-only | ||
| 123 | sign-up. An enabled empty allowlist blocks all new sign-ups. | ||
| 124 | |||
| 125 | Sign-up restrictions do not revoke an existing account's access. Ban the account in Clerk when | ||
| 126 | its active sessions and future sign-ins must be disabled. | ||
docs/operations/development.md created+140| ... | @@ -0,0 +1,140 @@ | ||
| 1 | # Development | ||
| 2 | |||
| 3 | ## First checkout | ||
| 4 | |||
| 5 | Install `vp` using the [root README](../../README.md#install-vp). The checkout requires Node 24; | ||
| 6 | Bun is optional. From the repository root: | ||
| 7 | |||
| 8 | ```sh | ||
| 9 | vp i | ||
| 10 | vp run dev | ||
| 11 | ``` | ||
| 12 | |||
| 13 | Open the one-time pairing URL printed by the dev runner. The bare origin does not authenticate | ||
| 14 | a new browser. | ||
| 15 | |||
| 16 | ## Choosing a dev process | ||
| 17 | |||
| 18 | Use `vp run dev` for server and web, or `vp run dev:desktop` for the Electron client. | ||
| 19 | `dev:server` and `dev:web` start those processes separately. | ||
| 20 | See the [mobile README](../../apps/mobile/README.md) for native builds and Metro. | ||
| 21 | |||
| 22 | Flags go directly after the task name, for example `vp run dev --home-dir /tmp/t3code-dev`. | ||
| 23 | Add `--browser` to open a browser automatically. | ||
| 24 | |||
| 25 | ### State and ports | ||
| 26 | |||
| 27 | Linked worktrees default to their own `.t3/userdata`, even when `T3CODE_HOME` is set. | ||
| 28 | The main checkout defaults to `~/.t3/dev/userdata`. An explicit `--home-dir` wins in both cases. | ||
| 29 | Never run a development server against the live `~/.t3/userdata`. | ||
| 30 | See [test data](../../AGENTS.md#test-data) for copying a consistent database snapshot. | ||
| 31 | |||
| 32 | Read ports from the `[dev-runner]` output. Worktrees derive stable preferences from their paths, | ||
| 33 | but occupied ports can shift them. `T3CODE_PORT_OFFSET` or `T3CODE_DEV_INSTANCE` can select a | ||
| 34 | different preference when needed. | ||
| 35 | |||
| 36 | ### Sharing and remote debugging | ||
| 37 | |||
| 38 | `vp run dev --share` publishes the web port over the machine's tailnet and prints a pairing URL | ||
| 39 | for that origin. Give the tester the complete URL, including its token. The dev runner removes | ||
| 40 | its mapping on exit. | ||
| 41 | |||
| 42 | Leave `VITE_HTTP_URL` and `VITE_WS_URL` unset. Vite proxies the backend through the browser's | ||
| 43 | origin so the same build works over localhost and remote connections. | ||
| 44 | |||
| 45 | Shared runs enable bundled dev to avoid a network round trip for each import level. | ||
| 46 | `T3CODE_BUNDLED_DEV=0` opts out when debugging bundler differences. Two reload traps matter | ||
| 47 | when changing this setup: | ||
| 48 | |||
| 49 | - The web entry must dynamically import the app so React refresh initializes before application | ||
| 50 | chunks. Static imports can work on first load and fail after a route split. | ||
| 51 | - Bundled dev rebuilds Tailwind through watched files. Its ordinary Vite hot-update hook expects | ||
| 52 | a server/module graph that Rolldown does not provide. | ||
| 53 | |||
| 54 | The workarounds live in the [web entry](../../apps/web/src/bootstrap.ts) and | ||
| 55 | [Tailwind plugin](../../apps/web/vite/tailwind.ts). | ||
| 56 | |||
| 57 | ## Checks | ||
| 58 | |||
| 59 | Run checks for the files and packages you changed: | ||
| 60 | |||
| 61 | ```sh | ||
| 62 | vp test run <files> | ||
| 63 | vp lint <files> | ||
| 64 | vp run --filter <package> typecheck | ||
| 65 | ``` | ||
| 66 | |||
| 67 | Use `vp run lint:mobile` for native mobile changes. CI owns the full suite; see | ||
| 68 | [ci.yml](../../.github/workflows/ci.yml) for its current jobs. | ||
| 69 | The [manual Windows lane](../../.github/workflows/windows-tests.yml) is available for focused | ||
| 70 | Windows investigation while that suite is not a required gate. | ||
| 71 | |||
| 72 | ## Desktop artifacts | ||
| 73 | |||
| 74 | Local artifact builds are unsigned by default and write to `release/`: | ||
| 75 | |||
| 76 | ```sh | ||
| 77 | vp run dist:desktop:dmg | ||
| 78 | vp run dist:desktop:linux | ||
| 79 | vp run dist:desktop:win | ||
| 80 | ``` | ||
| 81 | |||
| 82 | DMGs default to the host architecture. Use `--arch` to choose another target and `--keep-stage` | ||
| 83 | to retain packaging files for inspection. Run `vp run dist:desktop:artifact --help` for other | ||
| 84 | options. | ||
| 85 | |||
| 86 | ### Linux AppImage prerequisites | ||
| 87 | |||
| 88 | Build on Linux because the browser-secret helper links against the host's libsecret. Install | ||
| 89 | Rust, C/C++ build tools, libsecret development headers, pkg-config, and ImageMagick. | ||
| 90 | |||
| 91 | Ubuntu and Debian: | ||
| 92 | |||
| 93 | ```sh | ||
| 94 | sudo apt-get update | ||
| 95 | sudo apt-get install cargo rustc build-essential libsecret-1-dev pkg-config imagemagick | ||
| 96 | ``` | ||
| 97 | |||
| 98 | Fedora: | ||
| 99 | |||
| 100 | ```sh | ||
| 101 | sudo dnf install rust cargo gcc gcc-c++ make libsecret-devel pkgconf-pkg-config ImageMagick | ||
| 102 | ``` | ||
| 103 | |||
| 104 | Arch Linux: | ||
| 105 | |||
| 106 | ```sh | ||
| 107 | sudo pacman -S rust base-devel libsecret pkgconf imagemagick | ||
| 108 | ``` | ||
| 109 | |||
| 110 | The C toolchain, pkg-config, and libsecret headers are also needed for Linux desktop development. | ||
| 111 | |||
| 112 | ### macOS DMG prerequisites | ||
| 113 | |||
| 114 | Install the Xcode Command Line Tools with `xcode-select --install` and install Rust. | ||
| 115 | For a cross-architecture or universal build, add the requested Rust targets: | ||
| 116 | |||
| 117 | ```sh | ||
| 118 | rustup target add aarch64-apple-darwin x86_64-apple-darwin | ||
| 119 | ``` | ||
| 120 | |||
| 121 | ### Windows installer prerequisites | ||
| 122 | |||
| 123 | Install Rust, Python 3, and Visual Studio Build Tools with **Desktop development with C++**. | ||
| 124 | Include the Windows SDK and the MSVC build tools and Spectre-mitigated libraries for the target | ||
| 125 | architecture. Add its Rust target: | ||
| 126 | |||
| 127 | ```powershell | ||
| 128 | rustup target add x86_64-pc-windows-msvc | ||
| 129 | # For an ARM64 installer: | ||
| 130 | rustup target add aarch64-pc-windows-msvc | ||
| 131 | ``` | ||
| 132 | |||
| 133 | NSIS is downloaded by electron-builder. WSL support additionally needs a Linux node-pty prebuild; | ||
| 134 | see the [release runbook](./release.md#windows-payload-topology-and-update-validation). | ||
| 135 | |||
| 136 | ### Signing and passkeys | ||
| 137 | |||
| 138 | Add `--signed` after configuring the platform credentials in the | ||
| 139 | [release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the | ||
| 140 | [Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR. | ||
docs/operations/release.md+1-1| ... | @@ -329,7 +329,7 @@ Checklist: | ... | @@ -329,7 +329,7 @@ Checklist: |
| 329 | - `APPLE_API_KEY`: contents of the downloaded `.p8` | 329 | - `APPLE_API_KEY`: contents of the downloaded `.p8` |
| 330 | - `APPLE_API_KEY_ID`: Key ID | 330 | - `APPLE_API_KEY_ID`: Key ID |
| 331 | - `APPLE_API_ISSUER`: Issuer ID | 331 | - `APPLE_API_ISSUER`: Issuer ID |
| 332 | 10. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../internals/t3-connect.md#desktop-passkeys). | 332 | 10. Complete the Clerk Native API and AASA setup in [T3 Connect setup](./connect-setup.md#desktop-passkeys). |
| 333 | 11. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected | 333 | 11. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected |
| 334 | `com.apple.developer.associated-domains` entitlement. | 334 | `com.apple.developer.associated-domains` entitlement. |
| 335 | 335 |
infra/relay/README.md+3-5| ... | @@ -9,7 +9,7 @@ optional mobile notifications and Live Activities. | ... | @@ -9,7 +9,7 @@ optional mobile notifications and Live Activities. |
| 9 | 9 | ||
| 10 | The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects, | 10 | The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects, |
| 11 | regular API and WebSocket traffic goes directly between that client and the selected environment. | 11 | regular API and WebSocket traffic goes directly between that client and the selected environment. |
| 12 | See the [T3 Connect architecture overview](../../docs/internals/t3-code-connect-auth-flow.html) for the larger system | 12 | See the [T3 Connect architecture note](../../docs/internals/t3-connect.md) for the larger system |
| 13 | design. | 13 | design. |
| 14 | 14 | ||
| 15 | ## Responsibilities | 15 | ## Responsibilities |
| ... | @@ -159,8 +159,6 @@ and hosted web builds. | ... | @@ -159,8 +159,6 @@ and hosted web builds. |
| 159 | 159 | ||
| 160 | See: | 160 | See: |
| 161 | 161 | ||
| 162 | - [T3 Connect Clerk Setup](../../docs/internals/t3-connect.md) for Clerk keys, JWT templates, and sign-up restrictions | 162 | - [T3 Connect setup](../../docs/operations/connect-setup.md) for Clerk keys, JWT templates, and sign-up restrictions. |
| 163 | setup. | ||
| 164 | - [Relay Observability](../../docs/operations/relay-observability.md) for deployment tracing and diagnostics. | 163 | - [Relay Observability](../../docs/operations/relay-observability.md) for deployment tracing and diagnostics. |
| 165 | - [T3 Connect Architecture Overview](../../docs/internals/t3-code-connect-auth-flow.html) for the full link, | 164 | - [T3 Connect architecture](../../docs/internals/t3-connect.md) for environment linking and trust boundaries. |
| 166 | connect, endpoint, and notification flows. |
scripts/build-desktop-artifact.ts+2-2| ... | @@ -348,7 +348,7 @@ export class LinuxDesktopBuildPrerequisitesMissingError extends Schema.TaggedErr | ... | @@ -348,7 +348,7 @@ export class LinuxDesktopBuildPrerequisitesMissingError extends Schema.TaggedErr |
| 348 | ? ["Add the requested Rust target with:", ` rustup target add ${this.rustTarget}`] | 348 | ? ["Add the requested Rust target with:", ` rustup target add ${this.rustTarget}`] |
| 349 | : []), | 349 | : []), |
| 350 | "", | 350 | "", |
| 351 | "For other distributions, see docs/internals/scripts.md#linux-appimage-prerequisites.", | 351 | "For other distributions, see docs/operations/development.md#linux-appimage-prerequisites.", |
| 352 | "Then rerun `vp run dist:desktop:linux`.", | 352 | "Then rerun `vp run dist:desktop:linux`.", |
| 353 | ].join("\n"); | 353 | ].join("\n"); |
| 354 | } | 354 | } |
| ... | @@ -420,7 +420,7 @@ export class WindowsDesktopBuildPrerequisitesMissingError extends Schema.TaggedE | ... | @@ -420,7 +420,7 @@ export class WindowsDesktopBuildPrerequisitesMissingError extends Schema.TaggedE |
| 420 | "Install Rust from https://rustup.rs and add the requested target:", | 420 | "Install Rust from https://rustup.rs and add the requested target:", |
| 421 | ` rustup target add ${this.rustTarget}`, | 421 | ` rustup target add ${this.rustTarget}`, |
| 422 | "Install Python 3 and the Visual Studio Build Tools components listed in", | 422 | "Install Python 3 and the Visual Studio Build Tools components listed in", |
| 423 | "docs/internals/scripts.md#windows-installer-prerequisites.", | 423 | "docs/operations/development.md#windows-installer-prerequisites.", |
| 424 | "", | 424 | "", |
| 425 | "Then rerun the desktop artifact command.", | 425 | "Then rerun the desktop artifact command.", |
| 426 | ].join("\n"); | 426 | ].join("\n"); |