feat(auth): stdio oauth helper
This commit is contained in:
parent
5e1491d86c
commit
4d3552b99b
31
src/cli.ts
31
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<ReturnType<typeof createRuntim
|
||||
logInfo(`Cleared cached credentials for '${target}'.`);
|
||||
}
|
||||
|
||||
if (definition.command.kind === 'stdio' && definition.oauthCommand) {
|
||||
logInfo(`Starting auth helper for '${target}' (stdio). Leave this running until the browser flow completes.`);
|
||||
await runStdioAuth(definition);
|
||||
logInfo(`Auth helper for '${target}' finished. You can now call tools.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kick off the interactive OAuth flow without blocking list output. We retry once if the
|
||||
// server gets auto-promoted to OAuth mid-flight.
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
@ -514,6 +523,28 @@ async function invokeAuthCommand(runtimeOptions: Parameters<typeof createRuntime
|
||||
}
|
||||
}
|
||||
|
||||
async function runStdioAuth(definition: ServerDefinition): Promise<void> {
|
||||
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';
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user