Repository navigation
Conversation
Contributor
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a substantial external-client capability with pairing, bearer-token storage, remote WebSocket/RPC connectivity, reconnect supervision, and message-sending workflows. It also changes shared connection infrastructure and authentication-related exports, warranting human review despite the extensive tests. You can add or adjust custom eligibility rules. Learn more. |
Contributor
Author
|
Review requested
Logged so this PR shows when a maintainer was asked to review it. |
saphid
force-pushed
the
stack/01-external-client
branch
from
October 10, 2026 00:19
027363f to
9f934ce
Compare
Add the private @t3tools/client workspace package. A script pairs with a T3 Code environment using an ordinary pairing link, stores a revocable read+operate credential, and talks to one or more environments over the existing authenticated HTTP and WebSocket RPC: typed request/subscribe over the full WsRpcGroup, the shell snapshot, and sendMessage. Transport, retry, negotiation and operations are the ones web and mobile already use. client-runtime gains namespace exports for the connection driver, resolver, RPC session factory and remote authorization, plus an additive EnvironmentSupervisor `control` that returns once the run loop has taken a connect/disconnect/retry request. The client needs it so a request made right after reconnect, retry or disconnect reports that request's outcome instead of the state it replaced. Existing supervisor methods are unchanged. examples/list-threads-and-send.ts is the runnable consumer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Effect 4.0.2 moved the http and socket modules out of unstable, and layers are now named layerXyz. Pairing no longer sends client-requested scopes: the pairing link decides them, so pair() drops its scopes option. request() leaves out protected writes, matching client-runtime's request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saphid
force-pushed
the
stack/01-external-client
branch
from
October 10, 2026 07:38
9f934ce to
11e28be
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
There is no supported way for a script to talk to a T3 Code environment. Anyone automating T3 Code today (list threads across machines, send a message from a cron job or another tool) has to reverse-engineer the WebSocket RPC, re-implement pairing, ticketed sockets, protocol negotiation and reconnects, and usually ends up holding a broad token. Web and mobile already have all of that in
client-runtime, but nothing outside the apps can use it.Why this qualifies
This implements the direction of Ideas discussion #6977 (local app integration SDK). No maintainer has agreed to that direction yet. We are opening it so the shape can be judged as real code. It does not depend on any other PR.
Who uses this? Honestly: nothing in the apps. The package is
private: true, and its only in-repo consumer is the runnable example inpackages/client/examples/. The users are people scripting their own environments, which is exactly the question #6977 asks a maintainer to answer. If the answer is no, we close this.Fix
@t3tools/client(no server change, nothing new on the wire):pair()pairs with an ordinary pairing link through the same onboarding path the apps use, and asks fororchestration:read+orchestration:operateonly. The result is a normal revocable session that shows in Settings > Connections under its label. The token isRedactedeverywhere exceptencodeCredential.connect(credential)builds one realEnvironmentSupervisorper environment with the existing connection driver, resolver and RPC session. It exposesready, typedrequestfor unary RPCs andsubscribefor durable subscriptions (types come straight from@t3tools/contracts; the four finite stream-command RPCs are not exposed),shell,sendMessage, andretryNow/disconnect/reconnect.readywaits for a live session, not just aconnectedphase, so a request made while a dropped session is still cleaning up waits for the replacement. Blocked states (auth, scope, server too old) fail at once; transient ones retry with the shared backoff. Mutations are never replayed after a reconnect.layerNodeRuntime(global fetch, WebSocket, Web Crypto; Node 22+).examples/list-threads-and-send.ts:pair,list(several environments at once),send. Credential files are created exclusively with mode 0600.client-runtime(needed by the package, so it stays in this PR):ConnectionDriver,ConnectionResolver,RpcSessionFactoryandRemoteEnvironmentAuthorization;subscribeArchivedShellandsubscribeBackgroundPolicyare now classified as subscriptions in the shared RPC tag union (they arestream: truein the contract but were typed as unary); no app calls either through these helpers, and web/mobile typecheck unchanged;EnvironmentSupervisor.make(...).control(request)that returns once the run loop has taken a connect/disconnect/retry request, with the state it replaces. Without it, arequestmade right afterreconnectorretryNowcan run on the old session or report the old block. Existingconnect/disconnect/retryNow, the retry ladder and probes are unchanged; web and mobile do not call it.knip.jsonc/knip:check: the package is included in the export audit, with the example as its entry.Size: 22 files, +1613 / −30. Source is +577 (client +472, client-runtime +105), the example +106, tests +854; the rest is package config and the lockfile.
Evidence
Environment: macOS 26.5 arm64, Node 24. This PR's head is
027363fb3eon upstream mainde09c7566c.Remote proof over the tailnet, at the pre-rebase head (
41ad831057on maineac52f0087). An isolatedvp run dev --shareserver from this PR's checkout, on fresh local state, paired only through its own startup pairing link. Every request below went to thehttps://<tailnet-origin>share (wssfor the socket), not localhost. Output is sanitized: no tokens, links, hostnames or home paths.Before (main):
packages/clientand the example don't exist (git cat-file -e eac52f0087:packages/client/examples/list-threads-and-send.ts→fatal: … not in 'eac52f0087', rc 128; still absent onde09c7566c). A script would have to rebuild pairing, the ticketed socket, protocol negotiation and reconnect from the wire format itself.After:
Transport interruption: a long-lived client listed threads, then the server was restarted under it (its
node --watchentry was touched).sendMessagewas called right after the session dropped. It waited through backoff and ran once on the new session:The server's store had each sent message exactly once, under one command id (
message.updated+turn-item.updated). It had no event for the refused send, and a Claude reply followed each accepted message.Setup notes: the fresh fixture's project and thread were created through this client's own
request(projects.mutate,thread.create). The read-only credential came frompair({ scopes: ["orchestration:read"] })on a second one-time link minted by that server (t3 auth pairing create). The example has no scope flag, so a small driver did those two steps and the restart run, using only the public API.Re-run at this head (
027363fb3e) after the rebase, on localhost. The rebase mergedcontrolinto the supervisor's new multi-route loop (#15467), so the same sequence ran again against an isolatedvp run devserver from this checkout, on fresh local state:pair(file mode-rw-------),listwith an operate and a read-only credential (scopes as above, both list the thread),send(rc 0), the read-onlysendrefused with the sameEnvironmentAuthorizationError(rc 1), and the restart run: connected → backoff → connected in about 6 s, withsendMessageissued while the session was down, then running once on the new session (+7.45s sendMessage succeeded). The store again had onemessage.updatedper accepted message under its own command id, and nothing for the refused send.Earlier local proof on the pre-port branch (two isolated servers) also covered
listacross two different environments.Checks at this head (
027363fb3e), run 2026-10-05 (CI=true, all exit 0):cd packages/client && vp test run: 3 files, 24/24 pass. On main with onlysupervisor.tsreverted, the environment tests fail withsupervisor.control is not a function; the credential and credential-file tests cover code that is new in this PR. The lease-wait regression (Deferred-gated cleanup of a dropped session) fails on the previous head with the request failing at once instead of waiting.cd packages/client-runtime && vp test run src/connection/supervisor.test.ts src/rpc/client.test.ts: 2 files, 76/76 pass; the new control test fails on main (recorded during development). A compile-time check inclient.test.tsfails typecheck if anystream: trueRPC is missing from the stream tag unions (it fails on the previous union).vp run --filter <pkg> typecheckfor@t3tools/client,@t3tools/client-runtime,@t3tools/weband@t3tools/mobile(the supervisor's return type gained a member);vp lint --report-unused-disable-directives(0 diagnostics) andvp fmt --checkon the touched files;vp run knip:check; web build;vp run build:desktop;node scripts/release-smoke.ts. All pass.Surfaces
control.sendMessagegoes through the samestartThreadTurnpath as the apps for every provider.sendMessageis attributed aswebbecauseOrchestrationV2CreationSourcehas noapivalue; adding one needs a capability flag (left out).disconnect↔reconnect.unsupportedblock for now.Not verified
controlinto the supervisor's new route-switch loop..tssources, so it runs inside the monorepo on Node 22+ type stripping only.Claude Opus 5.5 (build) and GPT-6.1 Sol (review) via T3 Code
🤖 Generated with Claude Code