Skip to content

Add headless t3 serve pairing output#1871

Merged
juliusmarminge merged 6 commits intomainfrom
t3code/serve-headless-cli
Apr 10, 2026
Merged

Add headless t3 serve pairing output#1871
juliusmarminge merged 6 commits intomainfrom
t3code/serve-headless-cli

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Apr 10, 2026

Summary

  • Adds a new t3 serve command for headless startup, separate from the default browser-opening flow.
  • Emits pairing details on startup: connection string, one-time token, pairing URL, and an inline terminal QR code.
  • Introduces shared startup-access helpers for host resolution, pairing URL generation, and QR rendering.
  • Moves QR code generation into @t3tools/shared and reuses it in the web UI.
  • Updates server config and tests to account for the new startup presentation mode.

Testing

  • bun fmt not run in this summary step.
  • bun lint not run in this summary step.
  • bun typecheck not run in this summary step.
  • bun run test not run in this summary step.

Note

Medium Risk
Adds new authenticated HTTP endpoints and a CLI path that can mutate orchestration state, plus runtime-state persistence used to auto-route to a live server. Main risk is auth/authorization correctness for owner-only routes and correctness of live/offline switching and state-file cleanup.

Overview
Adds a headless remote-start flow via t3 serve. The new command starts the server in startupPresentation: "headless" (forcing noBrowser and disabling auto-bootstrap) and prints pairing details to the terminal (connection string, one-time token, pairing URL, and a rendered QR code) using new startupAccess helpers.

Enables CLI project management and live/offline routing. Introduces t3 project add/remove/rename, plus new owner-authenticated orchestration HTTP endpoints (GET /api/orchestration/snapshot, POST /api/orchestration/dispatch) and a persisted runtime-state file (server-runtime.json) that lets the CLI detect and talk to a running server; otherwise it falls back to offline SQLite/orchestration layers.

Also consolidates host classification helpers into startupAccess, refactors session token decoding to use effectful schema decoders, and moves the QR code generator from apps/web into @t3tools/shared/qrCode for reuse (updating the web UI import and docs in REMOTE.md).

Reviewed by Cursor Bugbot for commit b4aa1ca. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add t3 serve headless mode with terminal pairing output and project CLI subcommands

  • Adds a t3 serve command that starts the server in headless mode: no browser is opened, noBrowser is forced true, and auto-bootstrap from cwd is disabled.
  • In headless mode, serverRuntimeStartup.ts prints a connection string, session token, pairing URL, and QR code to stdout instead of launching a browser.
  • On startup, the server writes a JSON runtime state file (pid, port, origin, startedAt) to serverRuntimeStatePath and removes it on shutdown via serverRuntimeState.ts.
  • Adds t3 project add/remove/rename subcommands that dispatch orchestration commands either to a running server over HTTP (auto-detected via the runtime state file) or offline against local SQLite persistence.
  • Exposes GET /api/orchestration/snapshot and POST /api/orchestration/dispatch HTTP routes, restricted to owner-role sessions.
  • Moves the QR code library from a local vendor file in apps/web to @t3tools/shared/qrCode so it can be shared with the server CLI.

Macroscope summarized b4aa1ca.

- add `t3 serve` headless startup flow with pairing URL, token, and QR output
- share QR code generation between web and server
- update docs and config tests for the new startup presentation
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2574cb7f-dc3d-47c2-8ff7-c9fd198ae44e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/serve-headless-cli

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Apr 10, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 10, 2026

Approvability

Verdict: Needs human review

This PR introduces a new headless server mode with pairing functionality, new project management CLI commands, and new orchestration HTTP endpoints. The changes touch authentication flows (issuing pairing credentials, session tokens) and add new runtime behavior. An unresolved review comment identifies improper error handling for auth failures on the new routes. The scope and auth-sensitivity warrant human review.

You can customize Macroscope's approvability policy. Learn more.

- Keep headless startup from auto-bootstrapping projects from CWD
- Update config tests to cover the headless override
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Redundant isWildcardHost definition across two files
    • Removed the duplicate isWildcardHost definition from ServerAuthPolicy.ts and replaced it with an import from the shared startupAccess.ts module.

Create PR

Or push these changes by commenting:

@cursor push a323ddb0a2
Preview (a323ddb0a2)
diff --git a/apps/server/src/auth/Layers/ServerAuthPolicy.ts b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
--- a/apps/server/src/auth/Layers/ServerAuthPolicy.ts
+++ b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
@@ -2,12 +2,10 @@
 import { Effect, Layer } from "effect";
 
 import { ServerConfig } from "../../config.ts";
+import { isWildcardHost } from "../../startupAccess.ts";
 import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
 import { SESSION_COOKIE_NAME } from "../utils.ts";
 
-const isWildcardHost = (host: string | undefined): boolean =>
-  host === "0.0.0.0" || host === "::" || host === "[::]";
-
 const isLoopbackHost = (host: string | undefined): boolean => {
   if (!host || host.length === 0) {
     return true;

You can send follow-ups to the cloud agent here.

- Route project add/rename/remove through live server state when available
- Add orchestration snapshot and dispatch HTTP endpoints plus runtime state persistence
- Cover offline and live CLI project flows with tests
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Headless output uses config port, not actual listening port
    • Changed issueHeadlessServeAccessInfo to read the actual listening port from HttpServer.HttpServer.address instead of serverConfig.port, matching the safer pattern already used by runtimeStateLayer.
  • ✅ Fixed: Offline dispatch creates redundant duplicate service layer instances
    • Replaced the redundant inner Effect.provide(offlineRuntimeLayer) by resolving OrchestrationEngineService once from the outer scope and calling orchestrationEngine.dispatch directly in the dispatch closure.

Create PR

Or push these changes by commenting:

@cursor push 2123f23c4a
Preview (2123f23c4a)
diff --git a/apps/server/src/cli.ts b/apps/server/src/cli.ts
--- a/apps/server/src/cli.ts
+++ b/apps/server/src/cli.ts
@@ -683,13 +683,6 @@
   return yield* projectionSnapshotQuery.getSnapshot();
 });
 
-const dispatchOfflineCommand = Effect.fn("dispatchOfflineCommand")(function* (
-  command: ProjectCliDispatchCommand,
-) {
-  const orchestrationEngine = yield* OrchestrationEngineService;
-  yield* orchestrationEngine.dispatch(command);
-});
-
 const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(
   function* (authControlPlane: AuthControlPlaneShape, config: ServerConfigShape) {
     const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
@@ -758,11 +751,11 @@
     );
 
     return yield* Effect.gen(function* () {
+      const orchestrationEngine = yield* OrchestrationEngineService;
       const snapshot = yield* getOfflineSnapshot();
       const output = yield* run({
         snapshot,
-        dispatch: (command) =>
-          dispatchOfflineCommand(command).pipe(Effect.provide(offlineRuntimeLayer)),
+        dispatch: (command) => Effect.asVoid(orchestrationEngine.dispatch(command)),
         mode: "offline",
       });
       yield* Console.log(output);

diff --git a/apps/server/src/startupAccess.ts b/apps/server/src/startupAccess.ts
--- a/apps/server/src/startupAccess.ts
+++ b/apps/server/src/startupAccess.ts
@@ -2,6 +2,7 @@
 
 import { QrCode } from "@t3tools/shared/qrCode";
 import { Effect } from "effect";
+import { HttpServer } from "effect/unstable/http";
 
 import { ServerConfig } from "./config";
 import { ServerAuth } from "./auth/Services/ServerAuth";
@@ -106,7 +107,10 @@
 export const issueHeadlessServeAccessInfo = Effect.gen(function* () {
   const serverConfig = yield* ServerConfig;
   const serverAuth = yield* ServerAuth;
-  const connectionString = resolveHeadlessConnectionString(serverConfig.host, serverConfig.port);
+  const server = yield* HttpServer.HttpServer;
+  const address = server.address;
+  const port = typeof address !== "string" && "port" in address ? address.port : serverConfig.port;
+  const connectionString = resolveHeadlessConnectionString(serverConfig.host, port);
   const issued = yield* serverAuth.issuePairingCredential({ role: "owner" });
 
   return {

You can send follow-ups to the cloud agent here.

- Use `Schema.fromJsonString` for session, websocket, and runtime state parsing
- Route decode failures through existing credential and state handling
- Resolve the actual HTTP server port before printing startup access details
- Share startup host helpers and reuse the offline orchestration engine
- Simplify persisted runtime state decoding
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Auth failures on orchestration routes return 500
    • Added Effect.catchTag("AuthError", respondToAuthError) to both orchestration route handlers so that authentication failures from authenticateHttpRequest are caught and returned with the proper HTTP status (401/403) instead of bubbling up as unhandled 500 errors.

Create PR

Or push these changes by commenting:

@cursor push 9c6222d303
Preview (9c6222d303)
diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts
--- a/apps/server/src/orchestration/http.ts
+++ b/apps/server/src/orchestration/http.ts
@@ -7,6 +7,7 @@
 import { Effect } from "effect";
 import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
 
+import { respondToAuthError } from "../auth/http.ts";
 import { ServerAuth } from "../auth/Services/ServerAuth.ts";
 import { normalizeDispatchCommand } from "./Normalizer.ts";
 import { OrchestrationEngineService } from "./Services/OrchestrationEngine.ts";
@@ -58,6 +59,7 @@
       status: 200,
     });
   }).pipe(
+    Effect.catchTag("AuthError", respondToAuthError),
     Effect.catchTag("OrchestrationDispatchCommandError", respondToOrchestrationHttpError),
     Effect.catchTag("OrchestrationGetSnapshotError", respondToOrchestrationHttpError),
   ),
@@ -89,5 +91,8 @@
       ),
     );
     return HttpServerResponse.jsonUnsafe(result, { status: 200 });
-  }).pipe(Effect.catchTag("OrchestrationDispatchCommandError", respondToOrchestrationHttpError)),
+  }).pipe(
+    Effect.catchTag("AuthError", respondToAuthError),
+    Effect.catchTag("OrchestrationDispatchCommandError", respondToOrchestrationHttpError),
+  ),
 );

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 2a9720c. Configure here.

});
}
return session;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth failures on orchestration routes return 500

Medium Severity

authenticateOwnerSession calls serverAuth.authenticateHttpRequest(request), which likely fails with an auth-specific error type (e.g. missing/invalid token) that is neither OrchestrationDispatchCommandError nor OrchestrationGetSnapshotError. The catchTag handlers on both the snapshot and dispatch routes only catch those two error types, so actual authentication failures (invalid credentials, expired sessions) would propagate as unhandled errors, resulting in 500 responses instead of proper 401/403 responses.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2a9720c. Configure here.

- Simplify runtime state loading
- Treat decode failures as absent state
@juliusmarminge juliusmarminge merged commit cf9f236 into main Apr 10, 2026
12 checks passed
@juliusmarminge juliusmarminge deleted the t3code/serve-headless-cli branch April 10, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant