Connect daemons to Hub through browser approval (#2208)

* feat(cli): connect daemons to Hub through browser approval

Let interactive setup complete through a short-lived browser authorization while preserving --token for automation and existing integrations.

* ci: build server before CLI tests

* fix(cli): keep Hub approval polling within expiry

Bound both response headers and body consumption to the original authorization lifetime so stalled responses remain retryable without extending approval. Run the CLI test helper through Node and the resolved tsx entry for cross-platform behavior.

* fix(cli): bound Hub registration startup

Fail a stalled authorization start within a fixed timeout without retrying the non-idempotent request. Declare the response validator as a direct CLI runtime dependency.

* fix(cli): restrict Hub activation URLs

* fix(cli): harden Hub approval transport
This commit is contained in:
Mohamed Boudra
2026-07-24 21:10:21 +08:00
committed by GitHub
parent b218267c3c
commit 967edab497
9 changed files with 787 additions and 26 deletions
+19 -13
View File
@@ -16,6 +16,7 @@ import { mkdtemp, rm, mkdir } from "fs/promises";
import { existsSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { fileURLToPath } from "url";
import { ChildProcess, spawn } from "child_process";
import { getAvailablePort } from "./network.ts";
@@ -43,6 +44,7 @@ const TEST_DAEMON_ENV_DEFAULTS: Record<string, string> = {
PASEO_VOICE_MODE_ENABLED: process.env.PASEO_VOICE_MODE_ENABLED ?? "0",
};
const TEST_DAEMON_HOST = "127.0.0.1";
const TSX_ENTRY = fileURLToPath(import.meta.resolve("tsx/cli"));
const DEFAULT_OUTPUT_CAPTURE_LIMIT = 256 * 1024;
const TEST_OUTPUT_CAPTURE_LIMIT = Number.parseInt(
@@ -233,19 +235,23 @@ export async function startTestDaemon(options?: {
const cliSrcPath = join(cliDir, "src", "index.ts");
// Start daemon process using tsx to run TypeScript directly
const daemonProcess = spawn("npx", ["tsx", cliSrcPath, "daemon", "start", "--foreground"], {
env: {
...process.env,
...TEST_DAEMON_ENV_DEFAULTS,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `${TEST_DAEMON_HOST}:${port}`,
// Force no TTY to prevent QR code output
CI: "true",
...options?.env,
const daemonProcess = spawn(
process.execPath,
[TSX_ENTRY, cliSrcPath, "daemon", "start", "--foreground"],
{
env: {
...process.env,
...TEST_DAEMON_ENV_DEFAULTS,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `${TEST_DAEMON_HOST}:${port}`,
// Force no TTY to prevent QR code output
CI: "true",
...options?.env,
},
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
},
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
);
const stdout = createOutputCapture();
const stderr = createOutputCapture();
@@ -345,7 +351,7 @@ export async function runPaseoCli(
const cliSrcPath = join(cliDir, "src", "index.ts");
return new Promise((resolve, reject) => {
const proc = spawn("npx", ["tsx", cliSrcPath, ...args], {
const proc = spawn(process.execPath, [TSX_ENTRY, cliSrcPath, ...args], {
env: {
...process.env,
...TEST_DAEMON_ENV_DEFAULTS,