Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/shared/agentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ export const AGENT_PROVIDER_PRESETS: AgentProviderPreset[] = [
// `--yolo` is deprecated upstream; approval-mode is the current spelling.
autoModeFlag: '--approval-mode=yolo',
autoFlag: '--approval-mode=yolo',
// Gemini rejects combining either legacy yolo alias with --approval-mode.
// Treat every spelling as an explicit stance so the request remains owner.
autoStanceTokens: ['--yolo', '-y'],
supportsModel: true,
modelFlag: '--model',
hiveAware: false,
Expand Down Expand Up @@ -688,15 +691,30 @@ export function argsWithAutoModeFlag(args: string[], autoMode: boolean, provider
return [...args, ...flag.trim().split(/\s+/)];
}

/** Return a CLI option's identity while preserving positional values verbatim. */
function cliOptionName(token: string): string {
const trimmed = token.trim();
if (!trimmed.startsWith('-')) return trimmed;
const equals = trimmed.indexOf('=');
return equals > 0 ? trimmed.slice(0, equals) : trimmed;
}

/** True when argv already states a permission posture for this provider: the
* auto flag's leading token, or any of the preset's `autoStanceTokens`. Token
* match, not substring — copilot's flag starts with `-s`. */
* auto flag's leading option, or any of the preset's `autoStanceTokens`.
* Compare normalized option identity (not prefix) and stop at `--`, after
* which option-looking tokens are positional literals. */
export function hasAutoModeStance(args: string[], provider: AgentProvider): boolean {
const preset = providerPreset(provider);
const flag = preset.autoModeFlag ?? '';
const lead = flag.trim().split(/\s+/)[0];
const stance = new Set([...(lead ? [lead] : []), ...(preset.autoStanceTokens ?? [])]);
return args.some((a) => stance.has(a));
const stance = new Set(
[...(lead ? [lead] : []), ...(preset.autoStanceTokens ?? [])].map(cliOptionName)
);
for (const arg of args) {
if (arg === '--') break;
if (stance.has(cliOptionName(arg))) return true;
}
return false;
}

/** Returns any env vars the provider needs for non-interactive / first-run suppression. */
Expand Down
38 changes: 38 additions & 0 deletions test/agent-provider.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ test('codex preset still resolves (no regression)', () => {
assert.strictEqual(ap.providerPreset('codex').defaultCommand, 'codex');
});

test('gemini approval-mode values are explicit auto-mode stances', () => {
for (const mode of ['default', 'auto_edit', 'yolo', 'plan', 'future_mode', '']) {
assert.strictEqual(
ap.hasAutoModeStance([`--approval-mode=${mode}`], 'gemini'),
true,
`--approval-mode=${mode}`
);
}
assert.strictEqual(ap.hasAutoModeStance(['--approval-mode', 'plan'], 'gemini'), true);
});

test('gemini legacy yolo aliases are explicit auto-mode stances', () => {
for (const alias of ['--yolo', '-y']) {
assert.strictEqual(ap.hasAutoModeStance([alias], 'gemini'), true, alias);
assert.deepStrictEqual(ap.argsWithAutoModeFlag([alias], true, 'gemini'), [alias], alias);
}
});

test('auto-mode stance matching respects option identity and the argv terminator', () => {
assert.strictEqual(ap.hasAutoModeStance(['--approval-mode-extra=plan'], 'gemini'), false);
assert.strictEqual(ap.hasAutoModeStance(['--', '--approval-mode=plan'], 'gemini'), false);
assert.strictEqual(ap.hasAutoModeStance(['--model', 'pro', '--', '--approval-mode=plan'], 'gemini'), false);
assert.strictEqual(ap.hasAutoModeStance(['--approval-mode=plan', '--', 'hello'], 'gemini'), true);

// Existing providers share the option-identity behavior without prefix matches.
assert.strictEqual(ap.hasAutoModeStance(['--permission-mode=plan'], 'claude'), true);
assert.strictEqual(ap.hasAutoModeStance(['--sandbox=read-only'], 'codex'), true);
assert.strictEqual(ap.hasAutoModeStance(['--summarize'], 'copilot'), false);
});

test('gemini global auto-mode only fills an absent approval stance', () => {
assert.deepStrictEqual(
ap.argsWithAutoModeFlag(['--approval-mode=plan'], true, 'gemini'),
['--approval-mode=plan']
);
assert.deepStrictEqual(ap.argsWithAutoModeFlag([], true, 'gemini'), ['--approval-mode=yolo']);
});

if (failures > 0) {
console.log(`\n${failures} test(s) failed`);
process.exit(1);
Expand Down
37 changes: 37 additions & 0 deletions test/worker-launch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,43 @@ test('an explicit --permission-mode in the request always wins over auto-mode',
assert.deepEqual(l.args, ['--permission-mode', 'plan']);
});

test('gemini explicit approval modes win over global auto-mode', () => {
for (const mode of ['default', 'auto_edit', 'yolo', 'plan', 'future_mode']) {
const l = launch({ requestCommand: `gemini --approval-mode=${mode}`, autoMode: true });
assert.equal(l.command, `gemini --approval-mode=${mode}`);
assert.deepEqual(l.args, [`--approval-mode=${mode}`]);
}

const spaced = launch({ requestCommand: 'gemini --approval-mode plan', autoMode: true });
assert.equal(spaced.command, 'gemini --approval-mode plan');
assert.deepEqual(spaced.args, ['--approval-mode', 'plan']);
});

test('gemini legacy yolo aliases win over global auto-mode', () => {
// Gemini rejects combining --yolo/-y with --approval-mode, so auto-mode must
// not append the modern spelling over an explicit legacy stance.
for (const stance of ['--yolo', '-y']) {
const l = launch({ requestCommand: `gemini ${stance}`, autoMode: true });
assert.equal(l.command, `gemini ${stance}`);
assert.deepEqual(l.args, [stance]);
}
});

test('gemini still inherits global auto-mode when no approval stance is present', () => {
assert.deepEqual(
launch({ requestCommand: 'gemini', autoMode: true }).args,
['--approval-mode=yolo']
);
assert.deepEqual(
launch({ requestCommand: 'gemini --model pro', autoMode: true }).args,
['--model', 'pro', '--approval-mode=yolo']
);
assert.deepEqual(
launch({ requestCommand: 'gemini --approval-mode-extra=plan', autoMode: true }).args,
['--approval-mode-extra=plan', '--approval-mode=yolo']
);
});

test('auto-mode OFF appends nothing', () => {
assert.deepEqual(launch({ requestCommand: 'claude', autoMode: false }).args, []);
assert.deepEqual(launch({ requestCommand: 'codex', autoMode: false }).args, []);
Expand Down
Loading