Files
flynn/docs/plans/2026-02-26-login-auth-mode.md
T
William Valentin d07e05d4cc fix(config): change no_tools_mode default to false for pi_embedded
The previous default of true was overly restrictive. false is the correct
default — tool-like prompts fall through to native handling only when
explicitly enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:52:43 -08:00

14 KiB
Raw Blame History

/login Auth Mode Switching Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Extend the /login <provider> mode <value> slash command to set auth_mode (api_key/oauth/auto) in config.yaml for all model tiers using a given provider, with a warning for providers that don't support auth_mode.

Architecture: Add a mode field to the login command type; extend parseCommand to recognise /login <provider> mode <value>; add a standalone setProviderAuthMode() helper in minimal.ts that updates all matching tiers via persistConfig; thread configPath + config through MinimalTuiConfig so the handler can persist; skip the auth_mode prompt for providers not in AUTH_MODE_PROVIDERS.

Tech Stack: TypeScript, Vitest, yaml (via persistConfig), existing src/config/persistence.ts, src/frontends/tui/commands.ts, src/frontends/tui/minimal.ts, src/cli/tui.ts.


Task 1: Extend the login command type and parser

Files:

  • Modify: src/frontends/tui/commands.ts:18 (Command union type)
  • Modify: src/frontends/tui/commands.ts:178-182 (parseCommand login branch)
  • Modify: src/frontends/tui/commands.ts:240 (help text)
  • Modify: src/frontends/tui/commands.ts:289 (SLASH_COMMANDS list — no change needed)
  • Modify: src/frontends/tui/commands.ts:321 (COMMAND_TOOLTIPS)
  • Test: src/frontends/tui/commands.test.ts

Step 1: Write the failing tests

Add to commands.test.ts inside the existing describe('parseCommand', ...) block:

it('parses /login with mode subcommand', () => {
  expect(parseCommand('/login anthropic mode oauth')).toEqual({
    type: 'login', provider: 'anthropic', mode: 'oauth',
  });
  expect(parseCommand('/login openai mode api_key')).toEqual({
    type: 'login', provider: 'openai', mode: 'api_key',
  });
  expect(parseCommand('/login anthropic mode auto')).toEqual({
    type: 'login', provider: 'anthropic', mode: 'auto',
  });
});

it('parses /login without mode unchanged', () => {
  expect(parseCommand('/login')).toEqual({ type: 'login' });
  expect(parseCommand('/login anthropic')).toEqual({ type: 'login', provider: 'anthropic' });
});

Step 2: Run test to verify it fails

pnpm test:run src/frontends/tui/commands.test.ts

Expected: FAIL — mode property not present on result.

Step 3: Update the Command union type

In commands.ts line 18, change:

| { type: 'login'; provider?: string }

to:

| { type: 'login'; provider?: string; mode?: 'api_key' | 'oauth' | 'auto' }

Step 4: Update parseCommand

Replace the existing login block (lines ~177183):

// Login
if (trimmed === '/login') {
  return { type: 'login' };
}
if (trimmed.startsWith('/login ')) {
  const provider = trimmed.slice('/login '.length).trim();
  return { type: 'login', provider: provider || undefined };
}

with:

// Login
if (trimmed === '/login') {
  return { type: 'login' };
}
if (trimmed.startsWith('/login ')) {
  const rest = trimmed.slice('/login '.length).trim();
  // /login <provider> mode <value>
  const modeMatch = rest.match(/^(\S+)\s+mode\s+(\S+)$/);
  if (modeMatch) {
    const modeValue = modeMatch[2].toLowerCase();
    if (modeValue === 'api_key' || modeValue === 'oauth' || modeValue === 'auto') {
      return { type: 'login', provider: modeMatch[1] || undefined, mode: modeValue };
    }
  }
  return { type: 'login', provider: rest || undefined };
}

Step 5: Update help text

In getHelpText(), update the /login line to:

  /login [provider]      Authenticate (github, openai, anthropic, zai)
  /login <p> mode <m>    Set auth mode for provider (api_key|oauth|auto)

Update COMMAND_TOOLTIPS['/login'] to:

'/login': 'Authenticate with provider; use "mode api_key|oauth|auto" to switch auth mode',

Step 6: Run tests to verify they pass

pnpm test:run src/frontends/tui/commands.test.ts

Expected: all PASS.

Step 7: Commit

git add src/frontends/tui/commands.ts src/frontends/tui/commands.test.ts
git commit -m "feat(tui): extend /login parser to accept mode subcommand"

Task 2: Thread configPath + config through MinimalTuiConfig

Files:

  • Modify: src/frontends/tui/minimal.ts:63-81 (MinimalTuiConfig interface)
  • Modify: src/cli/tui.ts:437-458 (MinimalTui constructor call)

No new tests needed — this is plumbing only. Existing tests cover no regression.

Step 1: Add fields to MinimalTuiConfig

In minimal.ts, import Config and persistConfig at the top. The file already imports from ../../config/index.js — add Config to that import and add a new import for persistConfig:

import type { Config, ModelConfig, ModelProvider } from '../../config/index.js';
import { persistConfig } from '../../config/persistence.js';

Then in MinimalTuiConfig add:

configPath?: string;
currentConfig?: Config;

Step 2: Pass configPath and config when constructing MinimalTui in tui.ts

In tui.ts line ~437, add two properties to the constructor object:

configPath,
currentConfig: config,

Step 3: Typecheck

pnpm typecheck

Expected: no errors.

Step 4: Commit

git add src/frontends/tui/minimal.ts src/cli/tui.ts
git commit -m "feat(tui): thread configPath and currentConfig into MinimalTuiConfig"

Task 3: Implement setProviderAuthMode and wire into the login handler

Files:

  • Modify: src/frontends/tui/minimal.ts (new helper + updated handler)
  • Test: src/frontends/tui/minimal.test.ts

Step 1: Write the failing test

minimal.test.ts tests the TUI at a higher level. Add a focused unit test for the new helper by extracting it. For now, add to minimal.test.ts:

import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock persistConfig so we can assert it was called correctly
const { mockPersistConfig } = vi.hoisted(() => ({ mockPersistConfig: vi.fn() }));
vi.mock('../../config/persistence.js', () => ({ persistConfig: mockPersistConfig }));

describe('applyAuthModeToConfig', () => {
  it('sets auth_mode on all tiers whose provider matches', () => {
    const config = {
      models: {
        default: { provider: 'anthropic', model: 'claude-sonnet-4' },
        fast: { provider: 'openai', model: 'gpt-4o-mini' },
        complex: { provider: 'anthropic', model: 'claude-opus-4' },
      },
    } as unknown as import('../../config/schema.js').Config;

    const updated = applyAuthModeToConfig(config, 'anthropic', 'oauth');

    expect(updated.models.default.auth_mode).toBe('oauth');
    expect(updated.models.complex.auth_mode).toBe('oauth');
    // openai tier must be untouched
    expect((updated.models.fast as { auth_mode?: string }).auth_mode).toBeUndefined();
  });

  it('updates local_providers entries that match', () => {
    const config = {
      models: {
        default: { provider: 'openai', model: 'gpt-4o' },
        local_providers: {
          myAnthropic: { provider: 'anthropic', model: 'claude-haiku' },
        },
      },
    } as unknown as import('../../config/schema.js').Config;

    const updated = applyAuthModeToConfig(config, 'anthropic', 'api_key');

    expect(updated.models.local_providers!['myAnthropic'].auth_mode).toBe('api_key');
    expect((updated.models.default as { auth_mode?: string }).auth_mode).toBeUndefined();
  });
});

Note: applyAuthModeToConfig must be exported from minimal.ts for test access.

Step 2: Run test to verify it fails

pnpm test:run src/frontends/tui/minimal.test.ts

Expected: FAIL — applyAuthModeToConfig is not exported.

Step 3: Implement applyAuthModeToConfig and AUTH_MODE_PROVIDERS

Add near the top of minimal.ts (after imports):

/** Providers that honour auth_mode at runtime. All others get a warning. */
export const AUTH_MODE_PROVIDERS: ReadonlySet<string> = new Set(['anthropic', 'openai']);

/**
 * Return a new Config with auth_mode set on every model tier whose provider
 * matches targetProvider. Does not mutate the original.
 */
export function applyAuthModeToConfig(
  config: Config,
  targetProvider: string,
  mode: 'api_key' | 'oauth' | 'auto',
): Config {
  const applyToTier = (tier: ModelConfig): ModelConfig =>
    tier.provider === targetProvider ? { ...tier, auth_mode: mode } : tier;

  const updatedModels = { ...config.models };

  if (updatedModels.default) {
    updatedModels.default = applyToTier(updatedModels.default);
  }
  for (const key of ['fast', 'complex', 'local'] as const) {
    const tier = updatedModels[key];
    if (tier) {
      updatedModels[key] = applyToTier(tier);
    }
  }
  if (updatedModels.local_providers) {
    const updatedLocalProviders: Record<string, ModelConfig> = {};
    for (const [name, tier] of Object.entries(updatedModels.local_providers)) {
      updatedLocalProviders[name] = applyToTier(tier);
    }
    updatedModels.local_providers = updatedLocalProviders;
  }

  return { ...config, models: updatedModels };
}

Step 4: Run tests to verify they pass

pnpm test:run src/frontends/tui/minimal.test.ts

Expected: PASS.

Step 5: Wire into handleLoginCommand

Update the switch dispatch in minimal.ts line ~537:

case 'login':
  await this.handleLoginCommand(command.provider, command.mode);
  break;

Update the method signature:

private async handleLoginCommand(
  provider?: string,
  mode?: 'api_key' | 'oauth' | 'auto',
): Promise<void> {

At the very top of handleLoginCommand, before the existing target resolution, add the mode-switch fast path:

if (mode !== undefined) {
  const resolvedProvider = provider ?? 'anthropic';
  if (!AUTH_MODE_PROVIDERS.has(resolvedProvider)) {
    console.log(
      `${colors.gray}auth_mode has no effect for ${resolvedProvider}. ` +
      `It is only supported for: ${[...AUTH_MODE_PROVIDERS].join(', ')}.${colors.reset}\n`,
    );
    return;
  }
  if (!this.config.currentConfig || !this.config.configPath) {
    console.log(`${colors.gray}Config not available — cannot persist auth_mode.${colors.reset}\n`);
    return;
  }
  const updated = applyAuthModeToConfig(this.config.currentConfig, resolvedProvider, mode);
  persistConfig(this.config.configPath, updated);
  console.log(
    `${colors.gray}auth_mode for ${resolvedProvider} set to ${colors.reset}${mode}` +
    `${colors.gray}. Restart Flynn for the change to take effect.${colors.reset}\n`,
  );
  return;
}

Step 6: Add post-credential auth_mode prompt for supported providers

In the anthropic branch of handleLoginCommand, after the credential is stored and before return, add (for both the api_key and token paths):

// Offer to set auth_mode if config is available and provider supports it
if (this.config.currentConfig && this.config.configPath) {
  const modeInput = (await this.prompt(
    `${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
  )).trim().toLowerCase();
  if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
    const updated = applyAuthModeToConfig(this.config.currentConfig, 'anthropic', modeInput);
    persistConfig(this.config.configPath, updated);
    console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
  }
}

Apply the same block to the openai branch (using 'openai' as the provider string).

Do not add this block to github or zai branches (they're not in AUTH_MODE_PROVIDERS).

Step 7: Typecheck

pnpm typecheck

Expected: no errors.

Step 8: Run full test suite

pnpm test:run

Expected: all pass.

Step 9: Commit

git add src/frontends/tui/minimal.ts src/frontends/tui/minimal.test.ts
git commit -m "feat(tui): implement /login <provider> mode <value> auth mode switching"

Task 4: Completions for the mode subcommand

Files:

  • Modify: src/frontends/tui/commands.ts (getCommandCompletions + getCommandTooltip)
  • Test: src/frontends/tui/commands.test.ts

Step 1: Write the failing test

Add to commands.test.ts inside describe('getCommandCompletions', ...):

it('completes /login <provider> mode values', () => {
  const completions = getCommandCompletions('/login anthropic mode ');
  expect(completions).toContain('/login anthropic mode api_key');
  expect(completions).toContain('/login anthropic mode oauth');
  expect(completions).toContain('/login anthropic mode auto');
});

it('filters mode completions by partial input', () => {
  const completions = getCommandCompletions('/login anthropic mode o');
  expect(completions).toEqual(['/login anthropic mode oauth']);
});

Step 2: Run test to verify it fails

pnpm test:run src/frontends/tui/commands.test.ts

Expected: FAIL.

Step 3: Implement completions

In getCommandCompletions, add before the generic slash-command fallback:

// Complete /login <provider> mode <value>
if (trimmed.startsWith('/login ')) {
  const rest = trimmed.slice('/login '.length);
  const parts = rest.split(/\s+/);
  if (parts.length === 3 && parts[1] === 'mode') {
    const partial = parts[2].toLowerCase();
    const modes = ['api_key', 'oauth', 'auto'];
    return modes
      .filter(m => m.startsWith(partial))
      .map(m => `/login ${parts[0]} mode ${m}`);
  }
  if (parts.length === 2 && parts[1] === 'mod') {
    return [`/login ${parts[0]} mode`];
  }
}

Step 4: Run tests

pnpm test:run src/frontends/tui/commands.test.ts

Expected: all PASS.

Step 5: Run full suite and typecheck

pnpm test:run && pnpm typecheck

Expected: all pass, no type errors.

Step 6: Commit

git add src/frontends/tui/commands.ts src/frontends/tui/commands.test.ts
git commit -m "feat(tui): add tab completions for /login mode subcommand"

Verification

pnpm test:run          # full suite passes
pnpm typecheck         # no type errors
pnpm lint              # no lint errors

Manual smoke test:

  1. pnpm tui → type /login anthropic mode oauth → confirm config written + restart message
  2. pnpm tui → type /login zhipuai mode oauth → confirm warning printed, no config write
  3. pnpm tui → type /login anthropic → confirm auth_mode prompt appears after credential entry