From 4d3552b99b425bb2bb4ce99383b7380e2cfac4e5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Dec 2025 17:12:35 +0000 Subject: [PATCH] feat(auth): stdio oauth helper --- src/cli.ts | 31 +++++++++++++++++++++++++++++++ src/config-normalize.ts | 8 ++++++++ src/config-schema.ts | 13 +++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 63371a3..ea073ff 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,6 +21,8 @@ import { consumeOutputFormat } from './cli/output-format.js'; import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js'; import { boldText, dimText, extraDimText, supportsAnsiColor } from './cli/terminal.js'; import { resolveConfigPath } from './config.js'; +import type { ServerDefinition } from './config-schema.js'; +import { spawn } from 'node:child_process'; import { DaemonClient } from './daemon/client.js'; import { createKeepAliveRuntime } from './daemon/runtime-wrapper.js'; import { analyzeConnectionError } from './error-classifier.js'; @@ -476,6 +478,13 @@ export async function handleAuth(runtime: Awaited { + const authArgs = [...(definition.command.kind === 'stdio' ? definition.command.args ?? [] : [])]; + if (definition.oauthCommand) { + authArgs.push(...definition.oauthCommand.args); + } + return new Promise((resolve, reject) => { + const child = spawn(definition.command.kind === 'stdio' ? definition.command.command : '', authArgs, { + stdio: 'inherit', + cwd: definition.command.kind === 'stdio' ? definition.command.cwd : process.cwd(), + env: process.env, + }); + child.on('error', reject); + child.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Auth helper exited with code ${code ?? 'null'}`)); + } + }); + }); +} + function shouldRetryAuthError(error: unknown): boolean { return analyzeConnectionError(error).kind === 'auth'; } diff --git a/src/config-normalize.ts b/src/config-normalize.ts index 036da3d..fd2eed3 100644 --- a/src/config-normalize.ts +++ b/src/config-normalize.ts @@ -15,6 +15,8 @@ export function normalizeServerEntry( const tokenCacheDir = normalizePath(raw.tokenCacheDir ?? raw.token_cache_dir); const clientName = raw.clientName ?? raw.client_name; const oauthRedirectUrl = raw.oauthRedirectUrl ?? raw.oauth_redirect_url ?? undefined; + const oauthCommandRaw = raw.oauthCommand ?? raw.oauth_command; + const oauthCommand = oauthCommandRaw ? { args: [...oauthCommandRaw.args] } : undefined; const headers = buildHeaders(raw); const httpUrl = getUrl(raw); @@ -42,6 +44,11 @@ export function normalizeServerEntry( const lifecycle = resolveLifecycle(name, raw.lifecycle, command); const logging = normalizeLogging(raw.logging); + const defaultedOauthCommand = + !oauthCommand && name.toLowerCase() === 'gmail' && command.kind === 'stdio' + ? { args: ['auth', 'http://localhost:3000/oauth2callback'] } + : oauthCommand; + return { name, description, @@ -51,6 +58,7 @@ export function normalizeServerEntry( tokenCacheDir, clientName, oauthRedirectUrl, + oauthCommand: defaultedOauthCommand, source, sources, lifecycle, diff --git a/src/config-schema.ts b/src/config-schema.ts index 1768935..5a1624e 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -62,6 +62,16 @@ export const RawEntrySchema = z.object({ client_name: z.string().optional(), oauthRedirectUrl: z.string().optional(), oauth_redirect_url: z.string().optional(), + oauthCommand: z + .object({ + args: z.array(z.string()), + }) + .optional(), + oauth_command: z + .object({ + args: z.array(z.string()), + }) + .optional(), bearerToken: z.string().optional(), bearer_token: z.string().optional(), bearerTokenEnv: z.string().optional(), @@ -123,6 +133,9 @@ export interface ServerDefinition { readonly tokenCacheDir?: string; readonly clientName?: string; readonly oauthRedirectUrl?: string; + readonly oauthCommand?: { + readonly args: string[]; + }; readonly source?: ServerSource; readonly sources?: readonly ServerSource[]; readonly lifecycle?: ServerLifecycle;