feat: プロバイダー非依存の permission_mode 値を導入し sandboxMode を設定可能にする (#87)
- permission_mode を readonly/edit/full に統一(プロバイダー非依存) - Claude SDK: readonly→default, edit→acceptEdits, full→bypassPermissions とマッピング - Codex SDK: readonly→read-only, edit→workspace-write, full→danger-full-access とマッピング - Legacy値(default/acceptEdits/bypassPermissions)のサポートを削除 - 全ビルトインワークフローを新しい permission_mode 値に更新 - AgentRunner の ProviderCallOptions 生成ロジックをリファクタリング(DRY化)
This commit is contained in:
parent
18894e2587
commit
7377c5f9d9
@ -99,7 +99,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: ai_review
|
||||
@ -220,7 +220,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI issues fixed
|
||||
next: ai_review
|
||||
@ -389,7 +389,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
|
||||
@ -502,7 +502,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
|
||||
@ -515,7 +515,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
|
||||
@ -95,7 +95,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: ai_review
|
||||
|
||||
@ -90,7 +90,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
next: ai_review
|
||||
@ -215,7 +215,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
pass_previous_response: true
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
@ -395,7 +395,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
|
||||
@ -510,7 +510,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
|
||||
@ -501,7 +501,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
|
||||
@ -94,7 +94,7 @@ steps:
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: acceptEdits
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
planステップで立てた計画に従って実装してください。
|
||||
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
||||
|
||||
@ -43,14 +43,17 @@ describe('StatusSchema', () => {
|
||||
|
||||
describe('PermissionModeSchema', () => {
|
||||
it('should accept valid permission modes', () => {
|
||||
expect(PermissionModeSchema.parse('default')).toBe('default');
|
||||
expect(PermissionModeSchema.parse('acceptEdits')).toBe('acceptEdits');
|
||||
expect(PermissionModeSchema.parse('bypassPermissions')).toBe('bypassPermissions');
|
||||
expect(PermissionModeSchema.parse('readonly')).toBe('readonly');
|
||||
expect(PermissionModeSchema.parse('edit')).toBe('edit');
|
||||
expect(PermissionModeSchema.parse('full')).toBe('full');
|
||||
});
|
||||
|
||||
it('should reject invalid permission modes', () => {
|
||||
expect(() => PermissionModeSchema.parse('readOnly')).toThrow();
|
||||
expect(() => PermissionModeSchema.parse('admin')).toThrow();
|
||||
expect(() => PermissionModeSchema.parse('default')).toThrow();
|
||||
expect(() => PermissionModeSchema.parse('acceptEdits')).toThrow();
|
||||
expect(() => PermissionModeSchema.parse('bypassPermissions')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@ -87,7 +90,7 @@ describe('WorkflowConfigRawSchema', () => {
|
||||
name: 'implement',
|
||||
agent: 'coder',
|
||||
allowed_tools: ['Read', 'Edit', 'Write', 'Bash'],
|
||||
permission_mode: 'acceptEdits',
|
||||
permission_mode: 'edit',
|
||||
instruction: '{task}',
|
||||
rules: [
|
||||
{ condition: 'Done', next: 'COMPLETE' },
|
||||
@ -97,7 +100,7 @@ describe('WorkflowConfigRawSchema', () => {
|
||||
};
|
||||
|
||||
const result = WorkflowConfigRawSchema.parse(config);
|
||||
expect(result.steps[0]?.permission_mode).toBe('acceptEdits');
|
||||
expect(result.steps[0]?.permission_mode).toBe('edit');
|
||||
});
|
||||
|
||||
it('should allow omitting permission_mode', () => {
|
||||
|
||||
54
src/__tests__/permission-mode.test.ts
Normal file
54
src/__tests__/permission-mode.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Tests for permission mode mapping functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SdkOptionsBuilder } from '../infra/claude/options-builder.js';
|
||||
import { mapToCodexSandboxMode } from '../infra/codex/types.js';
|
||||
import type { PermissionMode } from '../core/models/index.js';
|
||||
|
||||
describe('SdkOptionsBuilder.mapToSdkPermissionMode', () => {
|
||||
it('should map readonly to SDK default', () => {
|
||||
expect(SdkOptionsBuilder.mapToSdkPermissionMode('readonly')).toBe('default');
|
||||
});
|
||||
|
||||
it('should map edit to SDK acceptEdits', () => {
|
||||
expect(SdkOptionsBuilder.mapToSdkPermissionMode('edit')).toBe('acceptEdits');
|
||||
});
|
||||
|
||||
it('should map full to SDK bypassPermissions', () => {
|
||||
expect(SdkOptionsBuilder.mapToSdkPermissionMode('full')).toBe('bypassPermissions');
|
||||
});
|
||||
|
||||
it('should map all PermissionMode values exhaustively', () => {
|
||||
const modes: PermissionMode[] = ['readonly', 'edit', 'full'];
|
||||
for (const mode of modes) {
|
||||
const result = SdkOptionsBuilder.mapToSdkPermissionMode(mode);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapToCodexSandboxMode', () => {
|
||||
it('should map readonly to read-only', () => {
|
||||
expect(mapToCodexSandboxMode('readonly')).toBe('read-only');
|
||||
});
|
||||
|
||||
it('should map edit to workspace-write', () => {
|
||||
expect(mapToCodexSandboxMode('edit')).toBe('workspace-write');
|
||||
});
|
||||
|
||||
it('should map full to danger-full-access', () => {
|
||||
expect(mapToCodexSandboxMode('full')).toBe('danger-full-access');
|
||||
});
|
||||
|
||||
it('should map all PermissionMode values exhaustively', () => {
|
||||
const modes: PermissionMode[] = ['readonly', 'edit', 'full'];
|
||||
for (const mode of modes) {
|
||||
const result = mapToCodexSandboxMode(mode);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -154,6 +154,26 @@ export class AgentRunner {
|
||||
return provider.callCustom(agentConfig.name, task, systemPrompt, callOptions);
|
||||
}
|
||||
|
||||
/** Build ProviderCallOptions from RunAgentOptions with optional systemPrompt override */
|
||||
private static buildProviderCallOptions(
|
||||
options: RunAgentOptions,
|
||||
systemPrompt?: string,
|
||||
): ProviderCallOptions {
|
||||
return {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools,
|
||||
maxTurns: options.maxTurns,
|
||||
model: AgentRunner.resolveModel(options.cwd, options),
|
||||
systemPrompt,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
||||
async run(
|
||||
agentSpec: string | undefined,
|
||||
@ -174,25 +194,9 @@ export class AgentRunner {
|
||||
// 1. If agentPath is provided (resolved file exists), load prompt from file
|
||||
if (options.agentPath) {
|
||||
const systemPrompt = AgentRunner.loadAgentPromptFromPath(options.agentPath);
|
||||
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
|
||||
const callOptions: ProviderCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools,
|
||||
maxTurns: options.maxTurns,
|
||||
model: AgentRunner.resolveModel(options.cwd, options),
|
||||
systemPrompt,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
};
|
||||
|
||||
return provider.call(agentName, task, callOptions);
|
||||
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));
|
||||
}
|
||||
|
||||
// 2. If agentSpec is provided but no agentPath (file not found), try custom agent first,
|
||||
@ -207,42 +211,13 @@ export class AgentRunner {
|
||||
// Use agentSpec string as inline system prompt
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
|
||||
const callOptions: ProviderCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools,
|
||||
maxTurns: options.maxTurns,
|
||||
model: AgentRunner.resolveModel(options.cwd, options),
|
||||
systemPrompt: agentSpec,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
};
|
||||
|
||||
return provider.call(agentName, task, callOptions);
|
||||
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, agentSpec));
|
||||
}
|
||||
|
||||
// 3. No agent specified — run with instruction_template only (no system prompt)
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
|
||||
const callOptions: ProviderCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools,
|
||||
maxTurns: options.maxTurns,
|
||||
model: AgentRunner.resolveModel(options.cwd, options),
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
};
|
||||
|
||||
return provider.call(agentName, task, callOptions);
|
||||
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ export const StatusSchema = z.enum([
|
||||
]);
|
||||
|
||||
/** Permission mode schema for tool execution */
|
||||
export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions']);
|
||||
export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']);
|
||||
|
||||
/**
|
||||
* Report object schema (new structured format).
|
||||
|
||||
@ -25,5 +25,5 @@ export type RuleMatchMethod =
|
||||
| 'ai_judge'
|
||||
| 'ai_judge_fallback';
|
||||
|
||||
/** Permission mode for tool execution */
|
||||
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
|
||||
/** Permission mode for tool execution (provider-agnostic) */
|
||||
export type PermissionMode = 'readonly' | 'edit' | 'full';
|
||||
|
||||
@ -14,8 +14,9 @@ import type {
|
||||
HookInput,
|
||||
HookJSONOutput,
|
||||
PreToolUseHookInput,
|
||||
PermissionMode,
|
||||
PermissionMode as SdkPermissionMode,
|
||||
} from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PermissionMode } from '../../core/models/index.js';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
import type {
|
||||
PermissionHandler,
|
||||
@ -86,13 +87,23 @@ export class SdkOptionsBuilder {
|
||||
return sdkOptions;
|
||||
}
|
||||
|
||||
/** Map TAKT PermissionMode to Claude SDK PermissionMode */
|
||||
static mapToSdkPermissionMode(mode: PermissionMode): SdkPermissionMode {
|
||||
const mapping: Record<PermissionMode, SdkPermissionMode> = {
|
||||
readonly: 'default',
|
||||
edit: 'acceptEdits',
|
||||
full: 'bypassPermissions',
|
||||
};
|
||||
return mapping[mode];
|
||||
}
|
||||
|
||||
/** Resolve permission mode with priority: bypassPermissions > explicit > callback-based > default */
|
||||
private resolvePermissionMode(): PermissionMode {
|
||||
private resolvePermissionMode(): SdkPermissionMode {
|
||||
if (this.options.bypassPermissions) {
|
||||
return 'bypassPermissions';
|
||||
}
|
||||
if (this.options.permissionMode) {
|
||||
return this.options.permissionMode;
|
||||
return SdkOptionsBuilder.mapToSdkPermissionMode(this.options.permissionMode);
|
||||
}
|
||||
if (this.options.onPermissionRequest) {
|
||||
return 'default';
|
||||
|
||||
@ -152,8 +152,8 @@ export interface ClaudeSpawnOptions {
|
||||
onStream?: StreamCallback;
|
||||
/** Custom agents to register */
|
||||
agents?: Record<string, AgentDefinition>;
|
||||
/** Permission mode for tool execution (default: 'default' for interactive) */
|
||||
permissionMode?: SdkPermissionMode;
|
||||
/** Permission mode for tool execution (TAKT abstract value, mapped to SDK value in SdkOptionsBuilder) */
|
||||
permissionMode?: PermissionMode;
|
||||
/** Custom permission handler for interactive permission prompts */
|
||||
onPermissionRequest?: PermissionHandler;
|
||||
/** Custom handler for AskUserQuestion tool */
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
import { Codex } from '@openai/codex-sdk';
|
||||
import type { AgentResponse } from '../../core/models/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import type { CodexCallOptions } from './types.js';
|
||||
import { mapToCodexSandboxMode, type CodexCallOptions } from './types.js';
|
||||
import {
|
||||
type CodexEvent,
|
||||
type CodexItem,
|
||||
@ -39,9 +39,13 @@ export class CodexClient {
|
||||
options: CodexCallOptions,
|
||||
): Promise<AgentResponse> {
|
||||
const codex = new Codex(options.openaiApiKey ? { apiKey: options.openaiApiKey } : undefined);
|
||||
const sandboxMode = options.permissionMode
|
||||
? mapToCodexSandboxMode(options.permissionMode)
|
||||
: 'workspace-write';
|
||||
const threadOptions = {
|
||||
model: options.model,
|
||||
workingDirectory: options.cwd,
|
||||
sandboxMode,
|
||||
};
|
||||
const thread = options.sessionId
|
||||
? await codex.resumeThread(options.sessionId, threadOptions)
|
||||
|
||||
@ -3,4 +3,5 @@
|
||||
*/
|
||||
|
||||
export { CodexClient, callCodex, callCodexCustom } from './client.js';
|
||||
export type { CodexCallOptions } from './types.js';
|
||||
export { mapToCodexSandboxMode } from './types.js';
|
||||
export type { CodexCallOptions, CodexSandboxMode } from './types.js';
|
||||
|
||||
@ -3,6 +3,20 @@
|
||||
*/
|
||||
|
||||
import type { StreamCallback } from '../claude/index.js';
|
||||
import type { PermissionMode } from '../../core/models/index.js';
|
||||
|
||||
/** Codex sandbox mode values */
|
||||
export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
||||
|
||||
/** Map TAKT PermissionMode to Codex sandbox mode */
|
||||
export function mapToCodexSandboxMode(mode: PermissionMode): CodexSandboxMode {
|
||||
const mapping: Record<PermissionMode, CodexSandboxMode> = {
|
||||
readonly: 'read-only',
|
||||
edit: 'workspace-write',
|
||||
full: 'danger-full-access',
|
||||
};
|
||||
return mapping[mode];
|
||||
}
|
||||
|
||||
/** Options for calling Codex */
|
||||
export interface CodexCallOptions {
|
||||
@ -10,6 +24,8 @@ export interface CodexCallOptions {
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
/** Permission mode for sandbox configuration */
|
||||
permissionMode?: PermissionMode;
|
||||
/** Enable streaming mode with callback (best-effort) */
|
||||
onStream?: StreamCallback;
|
||||
/** OpenAI API key (bypasses CLI auth) */
|
||||
|
||||
@ -15,6 +15,7 @@ export class CodexProvider implements Provider {
|
||||
sessionId: options.sessionId,
|
||||
model: options.model,
|
||||
systemPrompt: options.systemPrompt,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||
};
|
||||
@ -27,6 +28,7 @@ export class CodexProvider implements Provider {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
model: options.model,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user