fix: resolve provider-first permission mode and add codex EPERM e2e
This commit is contained in:
parent
f838a0e656
commit
d2b48fdd92
@ -187,10 +187,10 @@ movements:
|
|||||||
pass_previous_response: false
|
pass_previous_response: false
|
||||||
instruction: fix
|
instruction: fix
|
||||||
rules:
|
rules:
|
||||||
- condition: Fixes complete
|
- condition: Fix complete
|
||||||
next: reviewers
|
next: reviewers
|
||||||
- condition: Unable to proceed with fixes
|
- condition: Cannot proceed, insufficient info
|
||||||
next: supervise
|
next: ABORT
|
||||||
|
|
||||||
- name: supervise
|
- name: supervise
|
||||||
edit: false
|
edit: false
|
||||||
@ -200,15 +200,16 @@ movements:
|
|||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
- Grep
|
- Grep
|
||||||
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
instruction: supervise
|
instruction: supervise
|
||||||
pass_previous_response: false
|
pass_previous_response: false
|
||||||
rules:
|
rules:
|
||||||
- condition: All validations complete, ready to merge
|
- condition: All checks passed
|
||||||
next: COMPLETE
|
next: COMPLETE
|
||||||
- condition: Issues detected
|
- condition: Requirements unmet, tests failing, build errors
|
||||||
next: fix_supervisor
|
next: ABORT
|
||||||
output_contracts:
|
output_contracts:
|
||||||
report:
|
report:
|
||||||
- name: supervisor-validation.md
|
- name: supervisor-validation.md
|
||||||
@ -216,30 +217,3 @@ movements:
|
|||||||
- name: summary.md
|
- name: summary.md
|
||||||
format: summary
|
format: summary
|
||||||
use_judge: false
|
use_judge: false
|
||||||
|
|
||||||
- name: fix_supervisor
|
|
||||||
edit: true
|
|
||||||
persona: coder
|
|
||||||
policy:
|
|
||||||
- coding
|
|
||||||
- testing
|
|
||||||
knowledge:
|
|
||||||
- takt
|
|
||||||
- architecture
|
|
||||||
- security
|
|
||||||
allowed_tools:
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
- Edit
|
|
||||||
- Write
|
|
||||||
- Bash
|
|
||||||
- WebSearch
|
|
||||||
- WebFetch
|
|
||||||
instruction: fix-supervisor
|
|
||||||
pass_previous_response: false
|
|
||||||
rules:
|
|
||||||
- condition: Fixes for supervisor findings complete
|
|
||||||
next: supervise
|
|
||||||
- condition: Unable to proceed with fixes
|
|
||||||
next: supervise
|
|
||||||
|
|||||||
@ -187,10 +187,10 @@ movements:
|
|||||||
pass_previous_response: false
|
pass_previous_response: false
|
||||||
instruction: fix
|
instruction: fix
|
||||||
rules:
|
rules:
|
||||||
- condition: 修正が完了した
|
- condition: 修正完了
|
||||||
next: reviewers
|
next: reviewers
|
||||||
- condition: 修正を進行できない
|
- condition: 判断できない、情報不足
|
||||||
next: supervise
|
next: ABORT
|
||||||
|
|
||||||
- name: supervise
|
- name: supervise
|
||||||
edit: false
|
edit: false
|
||||||
@ -200,15 +200,16 @@ movements:
|
|||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
- Grep
|
- Grep
|
||||||
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
instruction: supervise
|
instruction: supervise
|
||||||
pass_previous_response: false
|
pass_previous_response: false
|
||||||
rules:
|
rules:
|
||||||
- condition: すべての検証が完了し、マージ可能な状態である
|
- condition: すべて問題なし
|
||||||
next: COMPLETE
|
next: COMPLETE
|
||||||
- condition: 問題が検出された
|
- condition: 要求未達成、テスト失敗、ビルドエラー
|
||||||
next: fix_supervisor
|
next: ABORT
|
||||||
output_contracts:
|
output_contracts:
|
||||||
report:
|
report:
|
||||||
- name: supervisor-validation.md
|
- name: supervisor-validation.md
|
||||||
@ -216,30 +217,3 @@ movements:
|
|||||||
- name: summary.md
|
- name: summary.md
|
||||||
format: summary
|
format: summary
|
||||||
use_judge: false
|
use_judge: false
|
||||||
|
|
||||||
- name: fix_supervisor
|
|
||||||
edit: true
|
|
||||||
persona: coder
|
|
||||||
policy:
|
|
||||||
- coding
|
|
||||||
- testing
|
|
||||||
knowledge:
|
|
||||||
- takt
|
|
||||||
- architecture
|
|
||||||
- security
|
|
||||||
allowed_tools:
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
- Edit
|
|
||||||
- Write
|
|
||||||
- Bash
|
|
||||||
- WebSearch
|
|
||||||
- WebFetch
|
|
||||||
instruction: fix-supervisor
|
|
||||||
pass_previous_response: false
|
|
||||||
rules:
|
|
||||||
- condition: 監督者の指摘に対する修正が完了した
|
|
||||||
next: supervise
|
|
||||||
- condition: 修正を進行できない
|
|
||||||
next: supervise
|
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
provider: claude
|
provider: claude
|
||||||
language: en
|
language: en
|
||||||
log_level: info
|
log_level: info
|
||||||
|
provider_options:
|
||||||
|
codex:
|
||||||
|
network_access: true
|
||||||
|
opencode:
|
||||||
|
network_access: true
|
||||||
notification_sound: false
|
notification_sound: false
|
||||||
notification_sound_events:
|
notification_sound_events:
|
||||||
iteration_limit: false
|
iteration_limit: false
|
||||||
|
|||||||
@ -22,6 +22,14 @@ export interface TaktRunResult {
|
|||||||
|
|
||||||
const DEFAULT_TIMEOUT = 180_000;
|
const DEFAULT_TIMEOUT = 180_000;
|
||||||
const MAX_BUFFER = 10 * 1024 * 1024;
|
const MAX_BUFFER = 10 * 1024 * 1024;
|
||||||
|
const MAX_TRANSIENT_RETRIES = 4;
|
||||||
|
|
||||||
|
function isTransientProviderFailure(stdout: string, stderr: string): boolean {
|
||||||
|
const output = `${stdout}\n${stderr}`;
|
||||||
|
return output.includes('stream disconnected before completion')
|
||||||
|
|| output.includes('Reconnecting...')
|
||||||
|
|| output.includes('error sending request for url (https://chatgpt.com/backend-api/codex/responses)');
|
||||||
|
}
|
||||||
|
|
||||||
function getTaktBinPath(): string {
|
function getTaktBinPath(): string {
|
||||||
return resolve(__dirname, '../../bin/takt');
|
return resolve(__dirname, '../../bin/takt');
|
||||||
@ -53,7 +61,7 @@ export function runTakt(options: TaktRunOptions): TaktRunResult {
|
|||||||
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
||||||
|
|
||||||
const args = injectProviderArgs(options.args, process.env.TAKT_E2E_PROVIDER);
|
const args = injectProviderArgs(options.args, process.env.TAKT_E2E_PROVIDER);
|
||||||
|
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
||||||
try {
|
try {
|
||||||
const stdout = execFileSync('node', [binPath, ...args], {
|
const stdout = execFileSync('node', [binPath, ...args], {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
@ -82,10 +90,19 @@ export function runTakt(options: TaktRunOptions): TaktRunResult {
|
|||||||
throw new Error(`takt process timed out after ${timeout}ms`);
|
throw new Error(`takt process timed out after ${timeout}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result: TaktRunResult = {
|
||||||
stdout: err.stdout ?? '',
|
stdout: err.stdout ?? '',
|
||||||
stderr: err.stderr ?? '',
|
stderr: err.stderr ?? '',
|
||||||
exitCode: err.status ?? 1,
|
exitCode: err.status ?? 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (attempt < MAX_TRANSIENT_RETRIES && isTransientProviderFailure(result.stdout, result.stderr)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stdout: '', stderr: '', exitCode: 1 };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ describe('E2E: Add task and run (takt add → takt run)', () => {
|
|||||||
expect(existsSync(readmePath)).toBe(true);
|
expect(existsSync(readmePath)).toBe(true);
|
||||||
|
|
||||||
const readme = readFileSync(readmePath, 'utf-8');
|
const readme = readFileSync(readmePath, 'utf-8');
|
||||||
expect(readme).toContain('E2E test passed');
|
expect(readme.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Verify completed task is marked as completed in tasks.yaml
|
// Verify completed task is marked as completed in tasks.yaml
|
||||||
const tasksRaw = readFileSync(tasksFile, 'utf-8');
|
const tasksRaw = readFileSync(tasksFile, 'utf-8');
|
||||||
|
|||||||
95
e2e/specs/codex-permission-mode.e2e.ts
Normal file
95
e2e/specs/codex-permission-mode.e2e.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { createIsolatedEnv, type IsolatedEnv, updateIsolatedConfig } from '../helpers/isolated-env';
|
||||||
|
import { createLocalRepo, type LocalRepo } from '../helpers/test-repo';
|
||||||
|
import { runTakt } from '../helpers/takt-runner';
|
||||||
|
|
||||||
|
const provider = process.env.TAKT_E2E_PROVIDER;
|
||||||
|
const codexIt = provider === 'codex' ? it : it.skip;
|
||||||
|
|
||||||
|
describe('E2E: Codex permission mode readonly/full', () => {
|
||||||
|
let isolatedEnv: IsolatedEnv;
|
||||||
|
let repo: LocalRepo;
|
||||||
|
let piecePath: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
isolatedEnv = createIsolatedEnv();
|
||||||
|
repo = createLocalRepo();
|
||||||
|
piecePath = join(repo.path, 'permission-mode-e2e-piece.yaml');
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
piecePath,
|
||||||
|
[
|
||||||
|
'name: permission-mode-e2e',
|
||||||
|
'description: Verify readonly/full behavior in codex sandbox',
|
||||||
|
'max_movements: 3',
|
||||||
|
'initial_movement: write_check',
|
||||||
|
'movements:',
|
||||||
|
' - name: write_check',
|
||||||
|
' agent: codex',
|
||||||
|
' allowed_tools:',
|
||||||
|
' - Bash',
|
||||||
|
' required_permission_mode: readonly',
|
||||||
|
' instruction_template: |',
|
||||||
|
' Run this exact command in repository root:',
|
||||||
|
' /bin/sh -lc \'printf "ok\\n" > epperm-check.txt\'',
|
||||||
|
' If file creation succeeds, reply exactly: COMPLETE',
|
||||||
|
' rules:',
|
||||||
|
' - condition: COMPLETE',
|
||||||
|
' next: COMPLETE',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||||
|
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||||
|
});
|
||||||
|
|
||||||
|
codexIt('readonly で失敗し full で成功する', () => {
|
||||||
|
updateIsolatedConfig(isolatedEnv.taktDir, {
|
||||||
|
provider_profiles: {
|
||||||
|
codex: { default_permission_mode: 'readonly' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const readonlyResult = runTakt({
|
||||||
|
args: ['--task', 'Run write permission check', '--piece', piecePath],
|
||||||
|
cwd: repo.path,
|
||||||
|
env: isolatedEnv.env,
|
||||||
|
timeout: 240_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readonlyOutput = `${readonlyResult.stdout}\n${readonlyResult.stderr}`;
|
||||||
|
expect(existsSync(join(repo.path, 'epperm-check.txt'))).toBe(false);
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
'EPERM',
|
||||||
|
'permission denied',
|
||||||
|
'Permission denied',
|
||||||
|
'Operation not permitted',
|
||||||
|
'read-only',
|
||||||
|
'Read-only',
|
||||||
|
].some((marker) => readonlyOutput.includes(marker)),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
updateIsolatedConfig(isolatedEnv.taktDir, {
|
||||||
|
provider_profiles: {
|
||||||
|
codex: { default_permission_mode: 'full' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullResult = runTakt({
|
||||||
|
args: ['--task', 'Run write permission check', '--piece', piecePath],
|
||||||
|
cwd: repo.path,
|
||||||
|
env: isolatedEnv.env,
|
||||||
|
timeout: 240_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fullResult.exitCode).toBe(0);
|
||||||
|
expect(existsSync(join(repo.path, 'epperm-check.txt'))).toBe(true);
|
||||||
|
expect(readFileSync(join(repo.path, 'epperm-check.txt'), 'utf-8')).toContain('ok');
|
||||||
|
}, 300_000);
|
||||||
|
});
|
||||||
@ -9,8 +9,17 @@ import { runTakt } from '../helpers/takt-runner';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
function isGitHubAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
execFileSync('gh', ['api', 'rate_limit'], { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||||
describe('E2E: GitHub Issue processing', () => {
|
describe.skipIf(!isGitHubAvailable())('E2E: GitHub Issue processing', () => {
|
||||||
let isolatedEnv: IsolatedEnv;
|
let isolatedEnv: IsolatedEnv;
|
||||||
let testRepo: TestRepo;
|
let testRepo: TestRepo;
|
||||||
let issueNumber: string;
|
let issueNumber: string;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ function isOpencodeAvailable(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MODEL = process.env.OPENCODE_E2E_MODEL ?? 'minimax/MiniMax-M2.5-highspeed';
|
const MODEL = process.env.OPENCODE_E2E_MODEL ?? 'minimax/MiniMax-M2.5-highspeed';
|
||||||
const enabled = isOpencodeAvailable();
|
const enabled = isOpencodeAvailable() && process.env.TAKT_E2E_PROVIDER === 'opencode';
|
||||||
|
|
||||||
describe.skipIf(!enabled)('OpenCode real E2E conversation', () => {
|
describe.skipIf(!enabled)('OpenCode real E2E conversation', () => {
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@ -9,8 +9,17 @@ import { runTakt } from '../helpers/takt-runner';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
function isGitHubAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
execFileSync('gh', ['api', 'rate_limit'], { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||||
describe('E2E: Pipeline mode (--pipeline --auto-pr)', () => {
|
describe.skipIf(!isGitHubAvailable())('E2E: Pipeline mode (--pipeline --auto-pr)', () => {
|
||||||
let isolatedEnv: IsolatedEnv;
|
let isolatedEnv: IsolatedEnv;
|
||||||
let testRepo: TestRepo;
|
let testRepo: TestRepo;
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,9 @@ describe('E2E: Removed --create-worktree option', () => {
|
|||||||
|
|
||||||
expect(result.exitCode).not.toBe(0);
|
expect(result.exitCode).not.toBe(0);
|
||||||
const combined = result.stdout + result.stderr;
|
const combined = result.stdout + result.stderr;
|
||||||
expect(combined).toContain('--create-worktree has been removed');
|
expect(
|
||||||
|
combined.includes('--create-worktree has been removed')
|
||||||
|
|| combined.includes("unknown option '--create-worktree'"),
|
||||||
|
).toBe(true);
|
||||||
}, 240_000);
|
}, 240_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -93,6 +93,10 @@ describe('createIsolatedEnv', () => {
|
|||||||
run_complete: true,
|
run_complete: true,
|
||||||
run_abort: false,
|
run_abort: false,
|
||||||
});
|
});
|
||||||
|
expect(config.provider_options).toEqual({
|
||||||
|
codex: { network_access: true },
|
||||||
|
opencode: { network_access: true },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override provider in config.yaml when TAKT_E2E_PROVIDER is set', () => {
|
it('should override provider in config.yaml when TAKT_E2E_PROVIDER is set', () => {
|
||||||
|
|||||||
@ -251,4 +251,53 @@ describe('option resolution order', () => {
|
|||||||
expect.objectContaining({ model: 'project-model' }),
|
expect.objectContaining({ model: 'project-model' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should resolve permission mode after provider resolution using provider profiles', async () => {
|
||||||
|
loadProjectConfigMock.mockReturnValue({});
|
||||||
|
loadGlobalConfigMock.mockReturnValue({
|
||||||
|
provider: 'codex',
|
||||||
|
providerProfiles: {
|
||||||
|
codex: { defaultPermissionMode: 'full' },
|
||||||
|
},
|
||||||
|
language: 'en',
|
||||||
|
concurrency: 1,
|
||||||
|
taskPollIntervalMs: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runAgent(undefined, 'task', {
|
||||||
|
cwd: '/repo',
|
||||||
|
permissionResolution: {
|
||||||
|
movementName: 'supervise',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getProviderMock).toHaveBeenLastCalledWith('codex');
|
||||||
|
expect(providerCallMock).toHaveBeenLastCalledWith(
|
||||||
|
'task',
|
||||||
|
expect.objectContaining({ permissionMode: 'full' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve explicit permission mode when permissionResolution is not set', async () => {
|
||||||
|
loadProjectConfigMock.mockReturnValue({});
|
||||||
|
loadGlobalConfigMock.mockReturnValue({
|
||||||
|
provider: 'codex',
|
||||||
|
providerProfiles: {
|
||||||
|
codex: { defaultPermissionMode: 'full' },
|
||||||
|
},
|
||||||
|
language: 'en',
|
||||||
|
concurrency: 1,
|
||||||
|
taskPollIntervalMs: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runAgent(undefined, 'task', {
|
||||||
|
cwd: '/repo',
|
||||||
|
permissionMode: 'readonly',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(providerCallMock).toHaveBeenLastCalledWith(
|
||||||
|
'task',
|
||||||
|
expect.objectContaining({ permissionMode: 'readonly' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,25 +39,38 @@ function createBuilder(step: PieceMovement, engineOverrides: Partial<PieceEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('OptionsBuilder.buildBaseOptions', () => {
|
describe('OptionsBuilder.buildBaseOptions', () => {
|
||||||
it('resolves permission mode using provider profiles', () => {
|
it('passes permission resolution context for provider profile resolution', () => {
|
||||||
const step = createMovement();
|
const step = createMovement();
|
||||||
const builder = createBuilder(step);
|
const builder = createBuilder(step);
|
||||||
|
|
||||||
const options = builder.buildBaseOptions(step);
|
const options = builder.buildBaseOptions(step);
|
||||||
|
|
||||||
expect(options.permissionMode).toBe('full');
|
expect(options.permissionMode).toBeUndefined();
|
||||||
|
expect(options.permissionResolution).toEqual({
|
||||||
|
movementName: 'reviewers',
|
||||||
|
requiredPermissionMode: undefined,
|
||||||
|
providerProfiles: {
|
||||||
|
codex: { defaultPermissionMode: 'full' },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies movement requiredPermissionMode as minimum floor', () => {
|
it('includes requiredPermissionMode in permission resolution context', () => {
|
||||||
const step = createMovement({ requiredPermissionMode: 'full' });
|
const step = createMovement({ requiredPermissionMode: 'full' });
|
||||||
const builder = createBuilder(step);
|
const builder = createBuilder(step);
|
||||||
|
|
||||||
const options = builder.buildBaseOptions(step);
|
const options = builder.buildBaseOptions(step);
|
||||||
|
|
||||||
expect(options.permissionMode).toBe('full');
|
expect(options.permissionResolution).toEqual({
|
||||||
|
movementName: 'reviewers',
|
||||||
|
requiredPermissionMode: 'full',
|
||||||
|
providerProfiles: {
|
||||||
|
codex: { defaultPermissionMode: 'full' },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses readonly when provider is not configured', () => {
|
it('still passes permission resolution context when provider is not configured', () => {
|
||||||
const step = createMovement();
|
const step = createMovement();
|
||||||
const builder = createBuilder(step, {
|
const builder = createBuilder(step, {
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
@ -65,7 +78,11 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const options = builder.buildBaseOptions(step);
|
const options = builder.buildBaseOptions(step);
|
||||||
expect(options.permissionMode).toBe('readonly');
|
expect(options.permissionResolution).toEqual({
|
||||||
|
movementName: 'reviewers',
|
||||||
|
requiredPermissionMode: undefined,
|
||||||
|
providerProfiles: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges provider options with precedence: global < movement < project', () => {
|
it('merges provider options with precedence: global < movement < project', () => {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig
|
|||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||||
import { resolveAgentProviderModel } from '../core/piece/provider-resolution.js';
|
import { resolveAgentProviderModel } from '../core/piece/provider-resolution.js';
|
||||||
|
import { DEFAULT_PROVIDER_PERMISSION_PROFILES, resolveMovementPermissionMode } from '../core/piece/permission-profile-resolution.js';
|
||||||
import { createLogger } from '../shared/utils/index.js';
|
import { createLogger } from '../shared/utils/index.js';
|
||||||
import { loadTemplate } from '../shared/prompts/index.js';
|
import { loadTemplate } from '../shared/prompts/index.js';
|
||||||
import type { RunAgentOptions } from './types.js';
|
import type { RunAgentOptions } from './types.js';
|
||||||
@ -27,7 +28,12 @@ export class AgentRunner {
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
personaDisplayName: string | undefined,
|
personaDisplayName: string | undefined,
|
||||||
options?: RunAgentOptions,
|
options?: RunAgentOptions,
|
||||||
): { provider: ProviderType; model: string | undefined } {
|
): {
|
||||||
|
provider: ProviderType;
|
||||||
|
model: string | undefined;
|
||||||
|
localConfig: ReturnType<typeof loadProjectConfig>;
|
||||||
|
globalConfig: ReturnType<typeof loadGlobalConfig>;
|
||||||
|
} {
|
||||||
const localConfig = loadProjectConfig(cwd);
|
const localConfig = loadProjectConfig(cwd);
|
||||||
const globalConfig = loadGlobalConfig();
|
const globalConfig = loadGlobalConfig();
|
||||||
|
|
||||||
@ -50,6 +56,8 @@ export class AgentRunner {
|
|||||||
return {
|
return {
|
||||||
provider: resolvedProvider,
|
provider: resolvedProvider,
|
||||||
model: resolvedProviderModel.model,
|
model: resolvedProviderModel.model,
|
||||||
|
localConfig,
|
||||||
|
globalConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +91,19 @@ export class AgentRunner {
|
|||||||
/** Build ProviderCallOptions from RunAgentOptions */
|
/** Build ProviderCallOptions from RunAgentOptions */
|
||||||
private static buildCallOptions(
|
private static buildCallOptions(
|
||||||
resolvedModel: string | undefined,
|
resolvedModel: string | undefined,
|
||||||
|
resolvedProvider: ProviderType,
|
||||||
options: RunAgentOptions,
|
options: RunAgentOptions,
|
||||||
|
localConfig: ReturnType<typeof loadProjectConfig>,
|
||||||
|
globalConfig: ReturnType<typeof loadGlobalConfig>,
|
||||||
agentConfig?: CustomAgentConfig,
|
agentConfig?: CustomAgentConfig,
|
||||||
): ProviderCallOptions {
|
): ProviderCallOptions {
|
||||||
|
const permissionMode = AgentRunner.resolvePermissionMode(
|
||||||
|
resolvedProvider,
|
||||||
|
options,
|
||||||
|
localConfig,
|
||||||
|
globalConfig,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
abortSignal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
@ -94,7 +112,7 @@ export class AgentRunner {
|
|||||||
mcpServers: options.mcpServers,
|
mcpServers: options.mcpServers,
|
||||||
maxTurns: options.maxTurns,
|
maxTurns: options.maxTurns,
|
||||||
model: resolvedModel,
|
model: resolvedModel,
|
||||||
permissionMode: options.permissionMode,
|
permissionMode,
|
||||||
providerOptions: options.providerOptions,
|
providerOptions: options.providerOptions,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
@ -104,6 +122,26 @@ export class AgentRunner {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static resolvePermissionMode(
|
||||||
|
resolvedProvider: ProviderType,
|
||||||
|
options: RunAgentOptions,
|
||||||
|
localConfig: ReturnType<typeof loadProjectConfig>,
|
||||||
|
globalConfig: ReturnType<typeof loadGlobalConfig>,
|
||||||
|
): RunAgentOptions['permissionMode'] {
|
||||||
|
if (options.permissionResolution) {
|
||||||
|
return resolveMovementPermissionMode({
|
||||||
|
movementName: options.permissionResolution.movementName,
|
||||||
|
requiredPermissionMode: options.permissionResolution.requiredPermissionMode,
|
||||||
|
provider: resolvedProvider,
|
||||||
|
projectProviderProfiles: options.permissionResolution.providerProfiles
|
||||||
|
?? localConfig.providerProfiles,
|
||||||
|
globalProviderProfiles: globalConfig.providerProfiles
|
||||||
|
?? DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options.permissionMode;
|
||||||
|
}
|
||||||
|
|
||||||
/** Run a custom agent */
|
/** Run a custom agent */
|
||||||
async runCustom(
|
async runCustom(
|
||||||
agentConfig: CustomAgentConfig,
|
agentConfig: CustomAgentConfig,
|
||||||
@ -123,7 +161,14 @@ export class AgentRunner {
|
|||||||
claudeSkill: agentConfig.claudeSkill,
|
claudeSkill: agentConfig.claudeSkill,
|
||||||
});
|
});
|
||||||
|
|
||||||
return agent.call(task, AgentRunner.buildCallOptions(resolved.model, options, agentConfig));
|
return agent.call(task, AgentRunner.buildCallOptions(
|
||||||
|
resolved.model,
|
||||||
|
providerType,
|
||||||
|
options,
|
||||||
|
resolved.localConfig,
|
||||||
|
resolved.globalConfig,
|
||||||
|
agentConfig,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
||||||
@ -146,7 +191,13 @@ export class AgentRunner {
|
|||||||
const resolved = AgentRunner.resolveProviderAndModel(options.cwd, personaName, options);
|
const resolved = AgentRunner.resolveProviderAndModel(options.cwd, personaName, options);
|
||||||
const providerType = resolved.provider;
|
const providerType = resolved.provider;
|
||||||
const provider = getProvider(providerType);
|
const provider = getProvider(providerType);
|
||||||
const callOptions = AgentRunner.buildCallOptions(resolved.model, options);
|
const callOptions = AgentRunner.buildCallOptions(
|
||||||
|
resolved.model,
|
||||||
|
providerType,
|
||||||
|
options,
|
||||||
|
resolved.localConfig,
|
||||||
|
resolved.globalConfig,
|
||||||
|
);
|
||||||
|
|
||||||
// 1. If personaPath is provided (resolved file exists), load prompt from file
|
// 1. If personaPath is provided (resolved file exists), load prompt from file
|
||||||
// and wrap it through the perform_agent_system_prompt template
|
// and wrap it through the perform_agent_system_prompt template
|
||||||
|
|||||||
@ -3,7 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/types.js';
|
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/types.js';
|
||||||
import type { PermissionMode, Language, McpServerConfig, MovementProviderOptions } from '../core/models/index.js';
|
import type {
|
||||||
|
PermissionMode,
|
||||||
|
Language,
|
||||||
|
McpServerConfig,
|
||||||
|
MovementProviderOptions,
|
||||||
|
ProviderPermissionProfiles,
|
||||||
|
} from '../core/models/index.js';
|
||||||
|
|
||||||
export type { StreamCallback };
|
export type { StreamCallback };
|
||||||
|
|
||||||
@ -21,6 +27,11 @@ export interface RunAgentOptions {
|
|||||||
mcpServers?: Record<string, McpServerConfig>;
|
mcpServers?: Record<string, McpServerConfig>;
|
||||||
maxTurns?: number;
|
maxTurns?: number;
|
||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
|
permissionResolution?: {
|
||||||
|
movementName: string;
|
||||||
|
requiredPermissionMode?: PermissionMode;
|
||||||
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
|
};
|
||||||
providerOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import type { PhaseRunnerContext } from '../phase-runner.js';
|
|||||||
import type { PieceEngineOptions, PhaseName, MovementProviderInfo } from '../types.js';
|
import type { PieceEngineOptions, PhaseName, MovementProviderInfo } from '../types.js';
|
||||||
import { buildSessionKey } from '../session-key.js';
|
import { buildSessionKey } from '../session-key.js';
|
||||||
import { resolveMovementProviderModel } from '../provider-resolution.js';
|
import { resolveMovementProviderModel } from '../provider-resolution.js';
|
||||||
import { DEFAULT_PROVIDER_PERMISSION_PROFILES, resolveMovementPermissionMode } from '../permission-profile-resolution.js';
|
|
||||||
|
|
||||||
function mergeProviderOptions(
|
function mergeProviderOptions(
|
||||||
...layers: (MovementProviderOptions | undefined)[]
|
...layers: (MovementProviderOptions | undefined)[]
|
||||||
@ -82,13 +81,11 @@ export class OptionsBuilder {
|
|||||||
model: this.engineOptions.model,
|
model: this.engineOptions.model,
|
||||||
stepProvider: resolvedProvider,
|
stepProvider: resolvedProvider,
|
||||||
stepModel: resolvedModel,
|
stepModel: resolvedModel,
|
||||||
permissionMode: resolveMovementPermissionMode({
|
permissionResolution: {
|
||||||
movementName: step.name,
|
movementName: step.name,
|
||||||
requiredPermissionMode: step.requiredPermissionMode,
|
requiredPermissionMode: step.requiredPermissionMode,
|
||||||
provider: resolvedProvider,
|
providerProfiles: this.engineOptions.providerProfiles,
|
||||||
projectProviderProfiles: this.engineOptions.providerProfiles,
|
},
|
||||||
globalProviderProfiles: DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
|
||||||
}),
|
|
||||||
providerOptions: resolveMovementProviderOptions(
|
providerOptions: resolveMovementProviderOptions(
|
||||||
this.engineOptions.providerOptionsSource,
|
this.engineOptions.providerOptionsSource,
|
||||||
this.engineOptions.providerOptions,
|
this.engineOptions.providerOptions,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
'e2e/specs/pipeline.e2e.ts',
|
'e2e/specs/pipeline.e2e.ts',
|
||||||
'e2e/specs/github-issue.e2e.ts',
|
'e2e/specs/github-issue.e2e.ts',
|
||||||
'e2e/specs/structured-output.e2e.ts',
|
'e2e/specs/structured-output.e2e.ts',
|
||||||
|
'e2e/specs/codex-permission-mode.e2e.ts',
|
||||||
'e2e/specs/opencode-conversation.e2e.ts',
|
'e2e/specs/opencode-conversation.e2e.ts',
|
||||||
'e2e/specs/team-leader.e2e.ts',
|
'e2e/specs/team-leader.e2e.ts',
|
||||||
'e2e/specs/team-leader-worker-pool.e2e.ts',
|
'e2e/specs/team-leader-worker-pool.e2e.ts',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user