diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index 3c8263f..f393063 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -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 diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index bd280a6..272ee21 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -502,7 +502,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index d036a9b..c117a4c 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -515,7 +515,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml index 0eabd9c..d016579 100644 --- a/resources/global/en/workflows/simple.yaml +++ b/resources/global/en/workflows/simple.yaml @@ -95,7 +95,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit rules: - condition: Implementation complete next: ai_review diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 4e36fce..aa94b6b 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -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 diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index 89ce72b..4424d64 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -510,7 +510,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index 2fc19f2..02b9c43 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -501,7 +501,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml index 62871d6..b33e68b 100644 --- a/resources/global/ja/workflows/simple.yaml +++ b/resources/global/ja/workflows/simple.yaml @@ -94,7 +94,7 @@ steps: - Bash - WebSearch - WebFetch - permission_mode: acceptEdits + permission_mode: edit instruction_template: | planステップで立てた計画に従って実装してください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index 6a29e06..707675f 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -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', () => { diff --git a/src/__tests__/permission-mode.test.ts b/src/__tests__/permission-mode.test.ts new file mode 100644 index 0000000..44440df --- /dev/null +++ b/src/__tests__/permission-mode.test.ts @@ -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'); + } + }); +}); diff --git a/src/agents/runner.ts b/src/agents/runner.ts index 5ff393c..b27199b 100644 --- a/src/agents/runner.ts +++ b/src/agents/runner.ts @@ -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)); } } diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index bd6960f..5e9ab2c 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -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). diff --git a/src/core/models/status.ts b/src/core/models/status.ts index 687ecc5..e43b101 100644 --- a/src/core/models/status.ts +++ b/src/core/models/status.ts @@ -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'; diff --git a/src/infra/claude/options-builder.ts b/src/infra/claude/options-builder.ts index 4c1346b..36adc79 100644 --- a/src/infra/claude/options-builder.ts +++ b/src/infra/claude/options-builder.ts @@ -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 = { + 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'; diff --git a/src/infra/claude/types.ts b/src/infra/claude/types.ts index a4e5d16..ab7ba23 100644 --- a/src/infra/claude/types.ts +++ b/src/infra/claude/types.ts @@ -152,8 +152,8 @@ export interface ClaudeSpawnOptions { onStream?: StreamCallback; /** Custom agents to register */ agents?: Record; - /** 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 */ diff --git a/src/infra/codex/client.ts b/src/infra/codex/client.ts index 983dd88..b6953f4 100644 --- a/src/infra/codex/client.ts +++ b/src/infra/codex/client.ts @@ -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 { 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) diff --git a/src/infra/codex/index.ts b/src/infra/codex/index.ts index 884056d..e09c657 100644 --- a/src/infra/codex/index.ts +++ b/src/infra/codex/index.ts @@ -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'; diff --git a/src/infra/codex/types.ts b/src/infra/codex/types.ts index 45a689c..6c6aa9a 100644 --- a/src/infra/codex/types.ts +++ b/src/infra/codex/types.ts @@ -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 = { + 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) */ diff --git a/src/infra/providers/codex.ts b/src/infra/providers/codex.ts index cb54b55..178db42 100644 --- a/src/infra/providers/codex.ts +++ b/src/infra/providers/codex.ts @@ -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(), };