authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 12:45:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 12:45:04-07:00
log2e688a53cf8fc7a9caa9fd5070148bfcd3d5a741
tree775a2b1d93b443074961842f71905a14d0f29321
parent13427ecd8ba5bfa7892fc90d1e3f0ef59514870f
signature Signed by PGP key B5690EEEBB952194

docs: keep internal guides focused on architecture (#9755)


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