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

471 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `/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:
```typescript
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**
```bash
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:
```typescript
| { type: 'login'; provider?: string }
```
to:
```typescript
| { type: 'login'; provider?: string; mode?: 'api_key' | 'oauth' | 'auto' }
```
**Step 4: Update parseCommand**
Replace the existing login block (lines ~177183):
```typescript
// 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:
```typescript
// 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:
```typescript
'/login': 'Authenticate with provider; use "mode api_key|oauth|auto" to switch auth mode',
```
**Step 6: Run tests to verify they pass**
```bash
pnpm test:run src/frontends/tui/commands.test.ts
```
Expected: all PASS.
**Step 7: Commit**
```bash
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`:
```typescript
import type { Config, ModelConfig, ModelProvider } from '../../config/index.js';
import { persistConfig } from '../../config/persistence.js';
```
Then in `MinimalTuiConfig` add:
```typescript
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:
```typescript
configPath,
currentConfig: config,
```
**Step 3: Typecheck**
```bash
pnpm typecheck
```
Expected: no errors.
**Step 4: Commit**
```bash
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`:
```typescript
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**
```bash
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):
```typescript
/** 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**
```bash
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:
```typescript
case 'login':
await this.handleLoginCommand(command.provider, command.mode);
break;
```
Update the method signature:
```typescript
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:
```typescript
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):
```typescript
// 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**
```bash
pnpm typecheck
```
Expected: no errors.
**Step 8: Run full test suite**
```bash
pnpm test:run
```
Expected: all pass.
**Step 9: Commit**
```bash
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', ...)`:
```typescript
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**
```bash
pnpm test:run src/frontends/tui/commands.test.ts
```
Expected: FAIL.
**Step 3: Implement completions**
In `getCommandCompletions`, add before the generic slash-command fallback:
```typescript
// 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**
```bash
pnpm test:run src/frontends/tui/commands.test.ts
```
Expected: all PASS.
**Step 5: Run full suite and typecheck**
```bash
pnpm test:run && pnpm typecheck
```
Expected: all pass, no type errors.
**Step 6: Commit**
```bash
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
```bash
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