From f065ee510f07cb296abd6dbe9b91932c5d7a5a93 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:00:03 +0900 Subject: [PATCH] feat: resolve movement permissions via provider profiles with required floor --- builtins/en/config.yaml | 116 +++++++++--------- builtins/en/pieces/backend-cqrs.yaml | 2 +- builtins/en/pieces/backend.yaml | 2 +- builtins/en/pieces/coding.yaml | 4 +- builtins/en/pieces/default.yaml | 6 +- builtins/en/pieces/e2e-test.yaml | 6 +- builtins/en/pieces/expert-cqrs.yaml | 2 +- builtins/en/pieces/expert.yaml | 2 +- builtins/en/pieces/frontend.yaml | 2 +- builtins/en/pieces/minimal.yaml | 10 +- builtins/en/pieces/passthrough.yaml | 2 +- builtins/en/pieces/review-fix-minimal.yaml | 10 +- builtins/en/pieces/structural-reform.yaml | 4 +- builtins/en/pieces/unit-test.yaml | 6 +- builtins/ja/config.yaml | 116 +++++++++--------- builtins/ja/pieces/backend-cqrs.yaml | 2 +- builtins/ja/pieces/backend.yaml | 2 +- builtins/ja/pieces/coding.yaml | 4 +- builtins/ja/pieces/default.yaml | 6 +- builtins/ja/pieces/e2e-test.yaml | 6 +- builtins/ja/pieces/expert-cqrs.yaml | 2 +- builtins/ja/pieces/expert.yaml | 2 +- builtins/ja/pieces/frontend.yaml | 2 +- builtins/ja/pieces/minimal.yaml | 10 +- builtins/ja/pieces/passthrough.yaml | 2 +- builtins/ja/pieces/review-fix-minimal.yaml | 10 +- builtins/ja/pieces/structural-reform.yaml | 4 +- builtins/ja/pieces/unit-test.yaml | 6 +- builtins/skill/references/yaml-schema.md | 2 +- e2e/fixtures/pieces/mock-cycle-detect.yaml | 2 +- e2e/fixtures/pieces/mock-max-iter.yaml | 4 +- e2e/fixtures/pieces/mock-no-match.yaml | 2 +- e2e/fixtures/pieces/mock-single-step.yaml | 2 +- e2e/fixtures/pieces/mock-two-step.yaml | 4 +- e2e/fixtures/pieces/multi-step-parallel.yaml | 4 +- e2e/fixtures/pieces/report-judge.yaml | 2 +- e2e/fixtures/pieces/simple.yaml | 2 +- e2e/fixtures/pieces/structured-output.yaml | 2 +- e2e/specs/runtime-config-provider.e2e.ts | 2 +- .../global-provider-profiles.test.ts | 75 +++++++++++ src/__tests__/models.test.ts | 30 +++-- src/__tests__/options-builder.test.ts | 48 +++++++- .../permission-profile-resolution.test.ts | 61 +++++++++ .../project-provider-profiles.test.ts | 62 ++++++++++ src/core/models/global-config.ts | 5 + src/core/models/index.ts | 3 + src/core/models/piece-types.ts | 4 +- src/core/models/provider-profiles.ts | 19 +++ src/core/models/schemas.ts | 30 ++++- src/core/models/types.ts | 8 ++ src/core/piece/engine/OptionsBuilder.ts | 16 ++- src/core/piece/engine/TeamLeaderRunner.ts | 2 +- .../piece/permission-profile-resolution.ts | 88 +++++++++++++ src/core/piece/types.ts | 9 ++ src/features/tasks/execute/pieceExecution.ts | 4 + src/features/tasks/execute/taskExecution.ts | 7 +- src/features/tasks/execute/types.ts | 9 ++ src/infra/config/global/globalConfig.ts | 34 +++++ src/infra/config/loaders/pieceParser.ts | 2 +- src/infra/config/project/projectConfig.ts | 42 ++++++- src/infra/config/types.ts | 5 + 61 files changed, 726 insertions(+), 213 deletions(-) create mode 100644 src/__tests__/global-provider-profiles.test.ts create mode 100644 src/__tests__/permission-profile-resolution.test.ts create mode 100644 src/__tests__/project-provider-profiles.test.ts create mode 100644 src/core/models/provider-profiles.ts create mode 100644 src/core/piece/permission-profile-resolution.ts diff --git a/builtins/en/config.yaml b/builtins/en/config.yaml index c0a7b33..9358150 100644 --- a/builtins/en/config.yaml +++ b/builtins/en/config.yaml @@ -1,91 +1,92 @@ -# TAKT Global Configuration +# TAKT global configuration sample # Location: ~/.takt/config.yaml -# ── Basic ── - -# Language (en | ja) +# ---- Core ---- language: en - -# Default piece when no piece is specified default_piece: default - -# Log level (debug | info | warn | error) log_level: info -# ── Provider & Model ── - -# Provider runtime (claude | codex) +# ---- Provider ---- +# provider: claude | codex | opencode | mock provider: claude -# Default model (optional) -# Claude: opus, sonnet, haiku -# Codex: gpt-5.2-codex, gpt-5.1-codex, etc. +# Model (optional) +# Claude examples: opus, sonnet, haiku +# Codex examples: gpt-5.2-codex, gpt-5.1-codex +# OpenCode format: provider/model # model: sonnet -# Per-persona provider override (optional) -# Override provider for specific personas. Others use the global provider. +# Per-persona provider override # persona_providers: # coder: codex +# reviewer: claude -# ── API Keys ── -# Optional. Environment variables take priority: -# TAKT_ANTHROPIC_API_KEY, TAKT_OPENAI_API_KEY +# Provider-specific movement permission policy +# Priority: +# 1) project provider_profiles override +# 2) global provider_profiles override +# 3) project provider_profiles default +# 4) global provider_profiles default +# 5) movement.required_permission_mode (minimum floor) +# provider_profiles: +# codex: +# default_permission_mode: full +# movement_permission_overrides: +# ai_review: readonly +# claude: +# default_permission_mode: edit +# Provider-specific runtime options +# provider_options: +# codex: +# network_access: true +# claude: +# sandbox: +# allow_unsandboxed_commands: true + +# ---- API Keys ---- +# Environment variables take priority: +# TAKT_ANTHROPIC_API_KEY / TAKT_OPENAI_API_KEY / TAKT_OPENCODE_API_KEY # anthropic_api_key: "" # openai_api_key: "" +# opencode_api_key: "" -# ── Execution ── - -# Worktree (shared clone) directory (default: ../{clone-name} relative to project) -# worktree_dir: ~/takt-worktrees - -# Auto-create PR after worktree execution (default: prompt in interactive mode) -# auto_pr: false - -# Prevent macOS idle sleep during execution using caffeinate (default: false) -# prevent_sleep: false - -# Runtime environment defaults (applies to all pieces unless piece_config.runtime overrides) +# ---- Runtime ---- +# Global runtime preparation (piece_config.runtime overrides this) # runtime: # prepare: # - gradle # - node -# ── Parallel Execution (takt run) ── +# ---- Execution ---- +# worktree_dir: ~/takt-worktrees +# auto_pr: false +# prevent_sleep: false -# Number of tasks to run concurrently (1 = sequential, max: 10) +# ---- Run Loop ---- # concurrency: 1 - -# Polling interval in ms for picking up new tasks (100-5000, default: 500) # task_poll_interval_ms: 500 - -# ── Interactive Mode ── - -# Number of movement previews shown in interactive mode (0 to disable, max: 10) # interactive_preview_movements: 3 - -# Branch name generation strategy (romaji: fast default | ai: slow) # branch_name_strategy: romaji -# ── Output ── - -# Notification sounds (default: true) -# notification_sound: true - -# Minimal output for CI - suppress AI output (default: false) +# ---- Output ---- # minimal_output: false +# notification_sound: true +# notification_sound_events: +# iteration_limit: true +# piece_complete: true +# piece_abort: true +# run_complete: true +# run_abort: true +# observability: +# provider_events: true -# ── Builtin Pieces ── - -# Enable builtin pieces (default: true) +# ---- Builtins ---- # enable_builtin_pieces: true - -# Exclude specific builtins from loading # disabled_builtins: # - magi -# ── Pipeline Mode (--pipeline) ── - +# ---- Pipeline ---- # pipeline: # default_branch_prefix: "takt/" # commit_message_template: "feat: {title} (#{issue})" @@ -94,14 +95,11 @@ provider: claude # {issue_body} # Closes #{issue} -# ── Preferences ── - -# Custom paths for preference files +# ---- Preferences ---- # bookmarks_file: ~/.takt/preferences/bookmarks.yaml # piece_categories_file: ~/.takt/preferences/piece-categories.yaml -# ── Debug ── - +# ---- Debug ---- # debug: # enabled: false # log_file: ~/.takt/logs/debug.log diff --git a/builtins/en/pieces/backend-cqrs.yaml b/builtins/en/pieces/backend-cqrs.yaml index e5c8d54..7003e13 100644 --- a/builtins/en/pieces/backend-cqrs.yaml +++ b/builtins/en/pieces/backend-cqrs.yaml @@ -219,7 +219,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/backend.yaml b/builtins/en/pieces/backend.yaml index 8719355..9d60c30 100644 --- a/builtins/en/pieces/backend.yaml +++ b/builtins/en/pieces/backend.yaml @@ -216,7 +216,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/coding.yaml b/builtins/en/pieces/coding.yaml index 642614a..bb82eb5 100644 --- a/builtins/en/pieces/coding.yaml +++ b/builtins/en/pieces/coding.yaml @@ -51,7 +51,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Implementation complete next: reviewers @@ -132,7 +132,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/default.yaml b/builtins/en/pieces/default.yaml index 270880f..93ce4af 100644 --- a/builtins/en/pieces/default.yaml +++ b/builtins/en/pieces/default.yaml @@ -82,7 +82,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Implementation complete next: ai_review @@ -142,7 +142,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI issues fixed next: ai_review @@ -234,7 +234,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/e2e-test.yaml b/builtins/en/pieces/e2e-test.yaml index 3de5d75..1c92646 100644 --- a/builtins/en/pieces/e2e-test.yaml +++ b/builtins/en/pieces/e2e-test.yaml @@ -85,7 +85,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Test implementation complete next: ai_review @@ -145,7 +145,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI issues fixed next: ai_review @@ -212,7 +212,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: review_test diff --git a/builtins/en/pieces/expert-cqrs.yaml b/builtins/en/pieces/expert-cqrs.yaml index f5e1182..301179f 100644 --- a/builtins/en/pieces/expert-cqrs.yaml +++ b/builtins/en/pieces/expert-cqrs.yaml @@ -254,7 +254,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/expert.yaml b/builtins/en/pieces/expert.yaml index d04524a..40c72c4 100644 --- a/builtins/en/pieces/expert.yaml +++ b/builtins/en/pieces/expert.yaml @@ -251,7 +251,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/frontend.yaml b/builtins/en/pieces/frontend.yaml index 077c6c8..7a3daaf 100644 --- a/builtins/en/pieces/frontend.yaml +++ b/builtins/en/pieces/frontend.yaml @@ -235,7 +235,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/minimal.yaml b/builtins/en/pieces/minimal.yaml index 0512ef3..214247b 100644 --- a/builtins/en/pieces/minimal.yaml +++ b/builtins/en/pieces/minimal.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: Implementation complete @@ -106,7 +106,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI Reviewer's issues fixed - condition: No fix needed (verified target files/spec) @@ -126,7 +126,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Supervisor's issues fixed - condition: Cannot proceed, insufficient info @@ -151,7 +151,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI Reviewer's issues fixed next: reviewers @@ -175,7 +175,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Supervisor's issues fixed next: reviewers diff --git a/builtins/en/pieces/passthrough.yaml b/builtins/en/pieces/passthrough.yaml index 2d730b7..a798695 100644 --- a/builtins/en/pieces/passthrough.yaml +++ b/builtins/en/pieces/passthrough.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Task complete next: COMPLETE diff --git a/builtins/en/pieces/review-fix-minimal.yaml b/builtins/en/pieces/review-fix-minimal.yaml index 5c3a936..e46f5c3 100644 --- a/builtins/en/pieces/review-fix-minimal.yaml +++ b/builtins/en/pieces/review-fix-minimal.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: Implementation complete @@ -106,7 +106,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI Reviewer's issues fixed - condition: No fix needed (verified target files/spec) @@ -126,7 +126,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Supervisor's issues fixed - condition: Cannot proceed, insufficient info @@ -151,7 +151,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI Reviewer's issues fixed next: reviewers @@ -175,7 +175,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Supervisor's issues fixed next: reviewers diff --git a/builtins/en/pieces/structural-reform.yaml b/builtins/en/pieces/structural-reform.yaml index 11d57ba..c0dacc9 100644 --- a/builtins/en/pieces/structural-reform.yaml +++ b/builtins/en/pieces/structural-reform.yaml @@ -226,7 +226,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: Implementation complete @@ -309,7 +309,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: reviewers diff --git a/builtins/en/pieces/unit-test.yaml b/builtins/en/pieces/unit-test.yaml index b1037ee..239937a 100644 --- a/builtins/en/pieces/unit-test.yaml +++ b/builtins/en/pieces/unit-test.yaml @@ -85,7 +85,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Test implementation complete next: ai_review @@ -145,7 +145,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI issues fixed next: ai_review @@ -212,7 +212,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: Fix complete next: review_test diff --git a/builtins/ja/config.yaml b/builtins/ja/config.yaml index b864599..7c86fec 100644 --- a/builtins/ja/config.yaml +++ b/builtins/ja/config.yaml @@ -1,91 +1,92 @@ -# TAKT グローバル設定 +# TAKT グローバル設定サンプル # 配置場所: ~/.takt/config.yaml -# ── 基本設定 ── - -# 言語 (en | ja) +# ---- 基本 ---- language: ja - -# デフォルトピース(指定なし時に使用) default_piece: default - -# ログレベル (debug | info | warn | error) log_level: info -# ── プロバイダー & モデル ── - -# プロバイダー (claude | codex) +# ---- プロバイダー ---- +# provider: claude | codex | opencode | mock provider: claude -# デフォルトモデル(オプション) -# Claude: opus, sonnet, haiku -# Codex: gpt-5.2-codex, gpt-5.1-codex など +# モデル(任意) +# Claude 例: opus, sonnet, haiku +# Codex 例: gpt-5.2-codex, gpt-5.1-codex +# OpenCode 形式: provider/model # model: sonnet -# ペルソナ単位のプロバイダー上書き(オプション) -# 特定ペルソナだけプロバイダーを変更。未指定のペルソナはグローバル設定を使用。 +# ペルソナ別プロバイダー上書き # persona_providers: # coder: codex +# reviewer: claude -# ── APIキー ── -# オプション。環境変数が優先: -# TAKT_ANTHROPIC_API_KEY, TAKT_OPENAI_API_KEY +# プロバイダー別 movement 権限ポリシー +# 優先順: +# 1) project provider_profiles override +# 2) global provider_profiles override +# 3) project provider_profiles default +# 4) global provider_profiles default +# 5) movement.required_permission_mode(下限補正) +# provider_profiles: +# codex: +# default_permission_mode: full +# movement_permission_overrides: +# ai_review: readonly +# claude: +# default_permission_mode: edit +# プロバイダー別ランタイムオプション +# provider_options: +# codex: +# network_access: true +# claude: +# sandbox: +# allow_unsandboxed_commands: true + +# ---- API キー ---- +# 環境変数が優先: +# TAKT_ANTHROPIC_API_KEY / TAKT_OPENAI_API_KEY / TAKT_OPENCODE_API_KEY # anthropic_api_key: "" # openai_api_key: "" +# opencode_api_key: "" -# ── 実行設定 ── - -# ワークツリー(shared clone)ディレクトリ(デフォルト: プロジェクトの ../{clone-name}) -# worktree_dir: ~/takt-worktrees - -# ワークツリー実行後に自動PR作成(デフォルト: 対話モードで確認) -# auto_pr: false - -# macOS のアイドルスリープを防止(デフォルト: false) -# prevent_sleep: false - -# 実行時ランタイム環境のデフォルト(piece_config.runtime があればそちらを優先) +# ---- ランタイム ---- +# グローバルなランタイム準備(piece_config.runtime があればそちらを優先) # runtime: # prepare: # - gradle # - node -# ── 並列実行 (takt run) ── +# ---- 実行 ---- +# worktree_dir: ~/takt-worktrees +# auto_pr: false +# prevent_sleep: false -# タスクの同時実行数(1 = 逐次実行、最大: 10) +# ---- Run Loop ---- # concurrency: 1 - -# 新規タスクのポーリング間隔 ms(100-5000、デフォルト: 500) # task_poll_interval_ms: 500 - -# ── 対話モード ── - -# ムーブメントプレビューの表示数(0 で無効、最大: 10) # interactive_preview_movements: 3 - -# ブランチ名の生成方式(romaji: 高速デフォルト | ai: 低速) # branch_name_strategy: romaji -# ── 出力 ── - -# 通知音(デフォルト: true) -# notification_sound: true - -# CI 向け最小出力 - AI 出力を抑制(デフォルト: false) +# ---- 出力 ---- # minimal_output: false +# notification_sound: true +# notification_sound_events: +# iteration_limit: true +# piece_complete: true +# piece_abort: true +# run_complete: true +# run_abort: true +# observability: +# provider_events: true -# ── ビルトインピース ── - -# ビルトインピースの有効化(デフォルト: true) +# ---- Builtins ---- # enable_builtin_pieces: true - -# 特定のビルトインを除外 # disabled_builtins: # - magi -# ── パイプラインモード (--pipeline) ── - +# ---- Pipeline ---- # pipeline: # default_branch_prefix: "takt/" # commit_message_template: "feat: {title} (#{issue})" @@ -94,14 +95,11 @@ provider: claude # {issue_body} # Closes #{issue} -# ── プリファレンス ── - -# プリファレンスファイルのカスタムパス +# ---- Preferences ---- # bookmarks_file: ~/.takt/preferences/bookmarks.yaml # piece_categories_file: ~/.takt/preferences/piece-categories.yaml -# ── デバッグ ── - +# ---- Debug ---- # debug: # enabled: false # log_file: ~/.takt/logs/debug.log diff --git a/builtins/ja/pieces/backend-cqrs.yaml b/builtins/ja/pieces/backend-cqrs.yaml index bf6b7ea..50f3922 100644 --- a/builtins/ja/pieces/backend-cqrs.yaml +++ b/builtins/ja/pieces/backend-cqrs.yaml @@ -219,7 +219,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/builtins/ja/pieces/backend.yaml b/builtins/ja/pieces/backend.yaml index fa7a395..c477b1c 100644 --- a/builtins/ja/pieces/backend.yaml +++ b/builtins/ja/pieces/backend.yaml @@ -216,7 +216,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/builtins/ja/pieces/coding.yaml b/builtins/ja/pieces/coding.yaml index 4a6ead8..8eb67e4 100644 --- a/builtins/ja/pieces/coding.yaml +++ b/builtins/ja/pieces/coding.yaml @@ -51,7 +51,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 実装完了 next: reviewers @@ -132,7 +132,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正完了 next: reviewers diff --git a/builtins/ja/pieces/default.yaml b/builtins/ja/pieces/default.yaml index d281dc3..dadee60 100644 --- a/builtins/ja/pieces/default.yaml +++ b/builtins/ja/pieces/default.yaml @@ -82,7 +82,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 実装完了 next: ai_review @@ -142,7 +142,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 next: ai_review @@ -234,7 +234,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正完了 next: reviewers diff --git a/builtins/ja/pieces/e2e-test.yaml b/builtins/ja/pieces/e2e-test.yaml index 11a8673..20521c5 100644 --- a/builtins/ja/pieces/e2e-test.yaml +++ b/builtins/ja/pieces/e2e-test.yaml @@ -85,7 +85,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: テスト実装完了 next: ai_review @@ -145,7 +145,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 next: ai_review @@ -212,7 +212,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正完了 next: review_test diff --git a/builtins/ja/pieces/expert-cqrs.yaml b/builtins/ja/pieces/expert-cqrs.yaml index 6851cf0..ebec536 100644 --- a/builtins/ja/pieces/expert-cqrs.yaml +++ b/builtins/ja/pieces/expert-cqrs.yaml @@ -254,7 +254,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/builtins/ja/pieces/expert.yaml b/builtins/ja/pieces/expert.yaml index 162b288..e6384fd 100644 --- a/builtins/ja/pieces/expert.yaml +++ b/builtins/ja/pieces/expert.yaml @@ -251,7 +251,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/builtins/ja/pieces/frontend.yaml b/builtins/ja/pieces/frontend.yaml index 19aa1f1..4487814 100644 --- a/builtins/ja/pieces/frontend.yaml +++ b/builtins/ja/pieces/frontend.yaml @@ -235,7 +235,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正が完了した next: reviewers diff --git a/builtins/ja/pieces/minimal.yaml b/builtins/ja/pieces/minimal.yaml index ea90200..eed090f 100644 --- a/builtins/ja/pieces/minimal.yaml +++ b/builtins/ja/pieces/minimal.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: 実装が完了した @@ -106,7 +106,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 - condition: 修正不要(指摘対象ファイル/仕様の確認済み) @@ -126,7 +126,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 監督者の指摘に対する修正が完了した - condition: 修正を進行できない @@ -151,7 +151,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 next: reviewers @@ -175,7 +175,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 監督者の指摘に対する修正が完了した next: reviewers diff --git a/builtins/ja/pieces/passthrough.yaml b/builtins/ja/pieces/passthrough.yaml index b4d11ea..51bc4ff 100644 --- a/builtins/ja/pieces/passthrough.yaml +++ b/builtins/ja/pieces/passthrough.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: タスク完了 next: COMPLETE diff --git a/builtins/ja/pieces/review-fix-minimal.yaml b/builtins/ja/pieces/review-fix-minimal.yaml index fa12b33..4e8e301 100644 --- a/builtins/ja/pieces/review-fix-minimal.yaml +++ b/builtins/ja/pieces/review-fix-minimal.yaml @@ -25,7 +25,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: 実装が完了した @@ -106,7 +106,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 - condition: 修正不要(指摘対象ファイル/仕様の確認済み) @@ -126,7 +126,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 監督者の指摘に対する修正が完了した - condition: 修正を進行できない @@ -151,7 +151,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 next: reviewers @@ -175,7 +175,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 監督者の指摘に対する修正が完了した next: reviewers diff --git a/builtins/ja/pieces/structural-reform.yaml b/builtins/ja/pieces/structural-reform.yaml index 7903163..6c56aaf 100644 --- a/builtins/ja/pieces/structural-reform.yaml +++ b/builtins/ja/pieces/structural-reform.yaml @@ -226,7 +226,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit instruction: implement rules: - condition: 実装完了 @@ -309,7 +309,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正完了 next: reviewers diff --git a/builtins/ja/pieces/unit-test.yaml b/builtins/ja/pieces/unit-test.yaml index 89f408c..e8b0710 100644 --- a/builtins/ja/pieces/unit-test.yaml +++ b/builtins/ja/pieces/unit-test.yaml @@ -85,7 +85,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: テスト実装完了 next: ai_review @@ -145,7 +145,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: AI問題の修正完了 next: ai_review @@ -212,7 +212,7 @@ movements: - Bash - WebSearch - WebFetch - permission_mode: edit + required_permission_mode: edit rules: - condition: 修正完了 next: review_test diff --git a/builtins/skill/references/yaml-schema.md b/builtins/skill/references/yaml-schema.md index 54e00d2..d9cadd1 100644 --- a/builtins/skill/references/yaml-schema.md +++ b/builtins/skill/references/yaml-schema.md @@ -51,7 +51,7 @@ movement 内では**キー名**で参照する(パスを直接書かない) instruction: implement # 指示テンプレートキー(instructions マップを参照、任意) knowledge: architecture # ナレッジキー(knowledge マップを参照、任意) edit: true # ファイル編集可否(必須) - permission_mode: edit # 権限モード: edit / readonly / full(任意) + required_permission_mode: edit # 必要最小権限: edit / readonly / full(任意) session: refresh # セッション管理(任意) pass_previous_response: true # 前の出力を渡すか(デフォルト: true) allowed_tools: [...] # 許可ツール一覧(任意、参考情報) diff --git a/e2e/fixtures/pieces/mock-cycle-detect.yaml b/e2e/fixtures/pieces/mock-cycle-detect.yaml index addd846..fce5766 100644 --- a/e2e/fixtures/pieces/mock-cycle-detect.yaml +++ b/e2e/fixtures/pieces/mock-cycle-detect.yaml @@ -36,7 +36,7 @@ movements: - name: fix persona: ../agents/test-coder.md edit: true - permission_mode: edit + required_permission_mode: edit instruction_template: | Fix the issues found in review. rules: diff --git a/e2e/fixtures/pieces/mock-max-iter.yaml b/e2e/fixtures/pieces/mock-max-iter.yaml index 632bbf9..f97b7c2 100644 --- a/e2e/fixtures/pieces/mock-max-iter.yaml +++ b/e2e/fixtures/pieces/mock-max-iter.yaml @@ -16,7 +16,7 @@ movements: - name: step-a edit: true persona: ../agents/test-coder.md - permission_mode: edit + required_permission_mode: edit instruction_template: | {task} rules: @@ -26,7 +26,7 @@ movements: - name: step-b edit: true persona: ../agents/test-coder.md - permission_mode: edit + required_permission_mode: edit instruction_template: | Continue the task. rules: diff --git a/e2e/fixtures/pieces/mock-no-match.yaml b/e2e/fixtures/pieces/mock-no-match.yaml index 142ee36..2334772 100644 --- a/e2e/fixtures/pieces/mock-no-match.yaml +++ b/e2e/fixtures/pieces/mock-no-match.yaml @@ -14,7 +14,7 @@ movements: - name: execute edit: true persona: ../agents/test-coder.md - permission_mode: edit + required_permission_mode: edit instruction_template: | {task} rules: diff --git a/e2e/fixtures/pieces/mock-single-step.yaml b/e2e/fixtures/pieces/mock-single-step.yaml index b2703ee..228bada 100644 --- a/e2e/fixtures/pieces/mock-single-step.yaml +++ b/e2e/fixtures/pieces/mock-single-step.yaml @@ -18,7 +18,7 @@ movements: - Read - Write - Edit - permission_mode: edit + required_permission_mode: edit instruction_template: | {task} rules: diff --git a/e2e/fixtures/pieces/mock-two-step.yaml b/e2e/fixtures/pieces/mock-two-step.yaml index 73e565e..8cdcd68 100644 --- a/e2e/fixtures/pieces/mock-two-step.yaml +++ b/e2e/fixtures/pieces/mock-two-step.yaml @@ -16,7 +16,7 @@ movements: - name: step-1 edit: true persona: ../agents/test-coder.md - permission_mode: edit + required_permission_mode: edit instruction_template: | {task} rules: @@ -26,7 +26,7 @@ movements: - name: step-2 edit: true persona: ../agents/test-coder.md - permission_mode: edit + required_permission_mode: edit instruction_template: | Continue the task. rules: diff --git a/e2e/fixtures/pieces/multi-step-parallel.yaml b/e2e/fixtures/pieces/multi-step-parallel.yaml index 236b354..e4238fc 100644 --- a/e2e/fixtures/pieces/multi-step-parallel.yaml +++ b/e2e/fixtures/pieces/multi-step-parallel.yaml @@ -16,7 +16,7 @@ movements: - name: plan persona: ../agents/test-coder.md edit: true - permission_mode: edit + required_permission_mode: edit instruction_template: | Create a plan for the task. rules: @@ -48,7 +48,7 @@ movements: - name: fix persona: ../agents/test-coder.md edit: true - permission_mode: edit + required_permission_mode: edit instruction_template: | Fix the issues found in review. rules: diff --git a/e2e/fixtures/pieces/report-judge.yaml b/e2e/fixtures/pieces/report-judge.yaml index b0a18b8..97ab923 100644 --- a/e2e/fixtures/pieces/report-judge.yaml +++ b/e2e/fixtures/pieces/report-judge.yaml @@ -18,7 +18,7 @@ movements: - Read - Write - Edit - permission_mode: edit + required_permission_mode: edit output_contracts: report: - Report: report.md diff --git a/e2e/fixtures/pieces/simple.yaml b/e2e/fixtures/pieces/simple.yaml index 1afd38f..4b9a14c 100644 --- a/e2e/fixtures/pieces/simple.yaml +++ b/e2e/fixtures/pieces/simple.yaml @@ -18,7 +18,7 @@ movements: - Read - Write - Edit - permission_mode: edit + required_permission_mode: edit instruction_template: | {task} rules: diff --git a/e2e/fixtures/pieces/structured-output.yaml b/e2e/fixtures/pieces/structured-output.yaml index d4df485..0bff372 100644 --- a/e2e/fixtures/pieces/structured-output.yaml +++ b/e2e/fixtures/pieces/structured-output.yaml @@ -14,7 +14,7 @@ movements: - name: execute edit: false persona: ../agents/test-coder.md - permission_mode: readonly + required_permission_mode: readonly instruction_template: | Reply with exactly: "Task completed successfully." Do not do anything else. diff --git a/e2e/specs/runtime-config-provider.e2e.ts b/e2e/specs/runtime-config-provider.e2e.ts index 63b68f6..4c8a29b 100644 --- a/e2e/specs/runtime-config-provider.e2e.ts +++ b/e2e/specs/runtime-config-provider.e2e.ts @@ -81,7 +81,7 @@ describe('E2E: runtime.prepare with provider', () => { ' allowed_tools:', ' - Read', ' - Bash', - ' permission_mode: edit', + ' required_permission_mode: edit', ' instruction_template: |', ' {task}', ' rules:', diff --git a/src/__tests__/global-provider-profiles.test.ts b/src/__tests__/global-provider-profiles.test.ts new file mode 100644 index 0000000..2b878e4 --- /dev/null +++ b/src/__tests__/global-provider-profiles.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { vi } from 'vitest'; + +const testHomeDir = join(tmpdir(), `takt-gpp-test-${Date.now()}`); + +vi.mock('node:os', async () => { + const actual = await vi.importActual('node:os'); + return { + ...actual, + homedir: () => testHomeDir, + }; +}); + +const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js'); +const { getGlobalConfigPath } = await import('../infra/config/paths.js'); + +describe('global provider_profiles', () => { + beforeEach(() => { + invalidateGlobalConfigCache(); + mkdirSync(testHomeDir, { recursive: true }); + }); + + afterEach(() => { + if (existsSync(testHomeDir)) { + rmSync(testHomeDir, { recursive: true }); + } + }); + + it('loads provider_profiles from yaml', () => { + const taktDir = join(testHomeDir, '.takt'); + mkdirSync(taktDir, { recursive: true }); + writeFileSync( + getGlobalConfigPath(), + [ + 'language: en', + 'provider_profiles:', + ' codex:', + ' default_permission_mode: full', + ' movement_permission_overrides:', + ' ai_fix: edit', + ].join('\n'), + 'utf-8', + ); + + const config = loadGlobalConfig(); + + expect(config.providerProfiles?.codex?.defaultPermissionMode).toBe('full'); + expect(config.providerProfiles?.codex?.movementPermissionOverrides?.ai_fix).toBe('edit'); + }); + + it('saves provider_profiles to yaml', () => { + const taktDir = join(testHomeDir, '.takt'); + mkdirSync(taktDir, { recursive: true }); + writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); + + const config = loadGlobalConfig(); + config.providerProfiles = { + codex: { + defaultPermissionMode: 'full', + movementPermissionOverrides: { + supervise: 'full', + }, + }, + }; + saveGlobalConfig(config); + invalidateGlobalConfigCache(); + + const reloaded = loadGlobalConfig(); + expect(reloaded.providerProfiles?.codex?.defaultPermissionMode).toBe('full'); + expect(reloaded.providerProfiles?.codex?.movementPermissionOverrides?.supervise).toBe('full'); + }); +}); diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index 913e90b..1c93dcd 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -84,7 +84,7 @@ describe('PieceConfigRawSchema', () => { expect(result.max_movements).toBe(10); }); - it('should parse movement with permission_mode', () => { + it('should parse movement with required_permission_mode', () => { const config = { name: 'test-piece', movements: [ @@ -92,7 +92,7 @@ describe('PieceConfigRawSchema', () => { name: 'implement', persona: 'coder', allowed_tools: ['Read', 'Edit', 'Write', 'Bash'], - permission_mode: 'edit', + required_permission_mode: 'edit', instruction: '{task}', rules: [ { condition: 'Done', next: 'COMPLETE' }, @@ -102,7 +102,7 @@ describe('PieceConfigRawSchema', () => { }; const result = PieceConfigRawSchema.parse(config); - expect(result.movements![0]?.permission_mode).toBe('edit'); + expect(result.movements![0]?.required_permission_mode).toBe('edit'); }); it('should parse movement with provider_options', () => { @@ -177,7 +177,7 @@ describe('PieceConfigRawSchema', () => { }); }); - it('should allow omitting permission_mode', () => { + it('should allow omitting required_permission_mode', () => { const config = { name: 'test-piece', movements: [ @@ -190,17 +190,33 @@ describe('PieceConfigRawSchema', () => { }; const result = PieceConfigRawSchema.parse(config); - expect(result.movements![0]?.permission_mode).toBeUndefined(); + expect(result.movements![0]?.required_permission_mode).toBeUndefined(); }); - it('should reject invalid permission_mode', () => { + it('should reject invalid required_permission_mode', () => { const config = { name: 'test-piece', movements: [ { name: 'step1', persona: 'coder', - permission_mode: 'superAdmin', + required_permission_mode: 'superAdmin', + instruction: '{task}', + }, + ], + }; + + expect(() => PieceConfigRawSchema.parse(config)).toThrow(); + }); + + it('should reject legacy permission_mode', () => { + const config = { + name: 'test-piece', + movements: [ + { + name: 'step1', + persona: 'coder', + permission_mode: 'edit', instruction: '{task}', }, ], diff --git a/src/__tests__/options-builder.test.ts b/src/__tests__/options-builder.test.ts index e1db23e..0a6d1f7 100644 --- a/src/__tests__/options-builder.test.ts +++ b/src/__tests__/options-builder.test.ts @@ -3,19 +3,26 @@ import { OptionsBuilder } from '../core/piece/engine/OptionsBuilder.js'; import type { PieceMovement } from '../core/models/types.js'; import type { PieceEngineOptions } from '../core/piece/types.js'; -function createMovement(): PieceMovement { +function createMovement(overrides: Partial = {}): PieceMovement { return { name: 'reviewers', personaDisplayName: 'Reviewers', instructionTemplate: 'review', passPreviousResponse: false, - permissionMode: 'full', + ...overrides, }; } -function createBuilder(step: PieceMovement): OptionsBuilder { +function createBuilder(step: PieceMovement, engineOverrides: Partial = {}): OptionsBuilder { const engineOptions: PieceEngineOptions = { projectCwd: '/project', + globalProvider: 'codex', + globalProviderProfiles: { + codex: { + defaultPermissionMode: 'full', + }, + }, + ...engineOverrides, }; return new OptionsBuilder( @@ -31,10 +38,43 @@ function createBuilder(step: PieceMovement): OptionsBuilder { ); } +describe('OptionsBuilder.buildBaseOptions', () => { + it('resolves permission mode using provider profiles', () => { + const step = createMovement(); + const builder = createBuilder(step); + + const options = builder.buildBaseOptions(step); + + expect(options.permissionMode).toBe('full'); + }); + + it('applies movement requiredPermissionMode as minimum floor', () => { + const step = createMovement({ requiredPermissionMode: 'full' }); + const builder = createBuilder(step); + + const options = builder.buildBaseOptions(step); + + expect(options.permissionMode).toBe('full'); + }); + + it('uses default profile when provider_profiles are not provided', () => { + const step = createMovement(); + const builder = createBuilder(step, { + globalProvider: undefined, + globalProviderProfiles: undefined, + projectProvider: undefined, + provider: undefined, + }); + + const options = builder.buildBaseOptions(step); + expect(options.permissionMode).toBe('edit'); + }); +}); + describe('OptionsBuilder.buildResumeOptions', () => { it('should enforce readonly permission and empty allowedTools for report/status phases', () => { // Given - const step = createMovement(); + const step = createMovement({ requiredPermissionMode: 'full' }); const builder = createBuilder(step); // When diff --git a/src/__tests__/permission-profile-resolution.test.ts b/src/__tests__/permission-profile-resolution.test.ts new file mode 100644 index 0000000..d789c02 --- /dev/null +++ b/src/__tests__/permission-profile-resolution.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveMovementPermissionMode } from '../core/piece/permission-profile-resolution.js'; + +describe('resolveMovementPermissionMode', () => { + it('applies required_permission_mode as minimum floor', () => { + const mode = resolveMovementPermissionMode({ + movementName: 'implement', + requiredPermissionMode: 'full', + provider: 'codex', + projectProviderProfiles: { + codex: { + defaultPermissionMode: 'readonly', + }, + }, + }); + + expect(mode).toBe('full'); + }); + + it('resolves by priority: project override > global override > project default > global default', () => { + const mode = resolveMovementPermissionMode({ + movementName: 'supervise', + provider: 'codex', + projectProviderProfiles: { + codex: { + defaultPermissionMode: 'edit', + movementPermissionOverrides: { + supervise: 'full', + }, + }, + }, + globalProviderProfiles: { + codex: { + defaultPermissionMode: 'readonly', + movementPermissionOverrides: { + supervise: 'edit', + }, + }, + }, + }); + + expect(mode).toBe('full'); + }); + + it('throws when unresolved', () => { + expect(() => resolveMovementPermissionMode({ + movementName: 'fix', + provider: 'codex', + })).toThrow(/Unable to resolve permission mode/); + }); + + it('resolves from required_permission_mode when provider is omitted', () => { + const mode = resolveMovementPermissionMode({ + movementName: 'fix', + requiredPermissionMode: 'edit', + }); + + expect(mode).toBe('edit'); + }); +}); diff --git a/src/__tests__/project-provider-profiles.test.ts b/src/__tests__/project-provider-profiles.test.ts new file mode 100644 index 0000000..84f7179 --- /dev/null +++ b/src/__tests__/project-provider-profiles.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { randomUUID } from 'node:crypto'; + +import { loadProjectConfig, saveProjectConfig } from '../infra/config/project/projectConfig.js'; + +describe('project provider_profiles', () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `takt-project-profile-${randomUUID()}`); + mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }); + } + }); + + it('loads provider_profiles from project config', () => { + const taktDir = join(testDir, '.takt'); + mkdirSync(taktDir, { recursive: true }); + writeFileSync( + join(taktDir, 'config.yaml'), + [ + 'piece: default', + 'provider_profiles:', + ' codex:', + ' default_permission_mode: full', + ' movement_permission_overrides:', + ' implement: full', + ].join('\n'), + 'utf-8', + ); + + const config = loadProjectConfig(testDir); + + expect(config.providerProfiles?.codex?.defaultPermissionMode).toBe('full'); + expect(config.providerProfiles?.codex?.movementPermissionOverrides?.implement).toBe('full'); + }); + + it('saves providerProfiles as provider_profiles', () => { + saveProjectConfig(testDir, { + piece: 'default', + providerProfiles: { + codex: { + defaultPermissionMode: 'full', + movementPermissionOverrides: { + fix: 'full', + }, + }, + }, + }); + + const config = loadProjectConfig(testDir); + expect(config.providerProfiles?.codex?.defaultPermissionMode).toBe('full'); + expect(config.providerProfiles?.codex?.movementPermissionOverrides?.fix).toBe('full'); + }); +}); diff --git a/src/core/models/global-config.ts b/src/core/models/global-config.ts index 126e0d5..c3c830a 100644 --- a/src/core/models/global-config.ts +++ b/src/core/models/global-config.ts @@ -3,6 +3,7 @@ */ import type { MovementProviderOptions, PieceRuntimeConfig } from './piece-types.js'; +import type { ProviderPermissionProfiles } from './provider-profiles.js'; /** Custom agent configuration */ export interface CustomAgentConfig { @@ -90,6 +91,8 @@ export interface GlobalConfig { personaProviders?: Record; /** Global provider-specific options (lowest priority) */ providerOptions?: MovementProviderOptions; + /** Provider-specific permission profiles */ + providerProfiles?: ProviderPermissionProfiles; /** Global runtime environment defaults (can be overridden by piece runtime) */ runtime?: PieceRuntimeConfig; /** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */ @@ -114,4 +117,6 @@ export interface ProjectConfig { agents?: CustomAgentConfig[]; provider?: 'claude' | 'codex' | 'opencode' | 'mock'; providerOptions?: MovementProviderOptions; + /** Provider-specific permission profiles */ + providerProfiles?: ProviderPermissionProfiles; } diff --git a/src/core/models/index.ts b/src/core/models/index.ts index 63036c7..49fca05 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -34,6 +34,9 @@ export type { PipelineConfig, GlobalConfig, ProjectConfig, + ProviderProfileName, + ProviderPermissionProfile, + ProviderPermissionProfiles, } from './types.js'; // Re-export from agent.ts diff --git a/src/core/models/piece-types.ts b/src/core/models/piece-types.ts index 5b063aa..5cdee06 100644 --- a/src/core/models/piece-types.ts +++ b/src/core/models/piece-types.ts @@ -144,8 +144,8 @@ export interface PieceMovement { provider?: 'claude' | 'codex' | 'opencode' | 'mock'; /** Model override for this movement */ model?: string; - /** Permission mode for tool execution in this movement */ - permissionMode?: PermissionMode; + /** Required minimum permission mode for tool execution in this movement */ + requiredPermissionMode?: PermissionMode; /** Provider-specific movement options */ providerOptions?: MovementProviderOptions; /** Whether this movement is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ diff --git a/src/core/models/provider-profiles.ts b/src/core/models/provider-profiles.ts new file mode 100644 index 0000000..53ded4f --- /dev/null +++ b/src/core/models/provider-profiles.ts @@ -0,0 +1,19 @@ +/** + * Provider-specific permission profile types. + */ + +import type { PermissionMode } from './status.js'; + +/** Supported providers for profile-based permission resolution. */ +export type ProviderProfileName = 'claude' | 'codex' | 'opencode' | 'mock'; + +/** Permission profile for a single provider. */ +export interface ProviderPermissionProfile { + /** Default permission mode for movements that do not have an explicit override. */ + defaultPermissionMode: PermissionMode; + /** Per-movement permission overrides keyed by movement name. */ + movementPermissionOverrides?: Record; +} + +/** Provider -> permission profile map. */ +export type ProviderPermissionProfiles = Partial>; diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 2ecd18f..1ec9a2a 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -78,6 +78,23 @@ export const MovementProviderOptionsSchema = z.object({ }).optional(), }).optional(); +/** Provider key schema for profile maps */ +export const ProviderProfileNameSchema = z.enum(['claude', 'codex', 'opencode', 'mock']); + +/** Provider permission profile schema */ +export const ProviderPermissionProfileSchema = z.object({ + default_permission_mode: PermissionModeSchema, + movement_permission_overrides: z.record(z.string(), PermissionModeSchema).optional(), +}); + +/** Provider permission profiles schema */ +export const ProviderPermissionProfilesSchema = z.object({ + claude: ProviderPermissionProfileSchema.optional(), + codex: ProviderPermissionProfileSchema.optional(), + opencode: ProviderPermissionProfileSchema.optional(), + mock: ProviderPermissionProfileSchema.optional(), +}).optional(); + /** Runtime prepare preset identifiers */ export const RuntimePreparePresetSchema = z.enum(['gradle', 'node']); /** Runtime prepare entry: preset name or script path */ @@ -240,7 +257,9 @@ export const ParallelSubMovementRawSchema = z.object({ mcp_servers: McpServersSchema, provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), model: z.string().optional(), - permission_mode: PermissionModeSchema.optional(), + /** Removed legacy field (no backward compatibility) */ + permission_mode: z.never().optional(), + required_permission_mode: PermissionModeSchema.optional(), provider_options: MovementProviderOptionsSchema, edit: z.boolean().optional(), instruction: z.string().optional(), @@ -271,8 +290,10 @@ export const PieceMovementRawSchema = z.object({ mcp_servers: McpServersSchema, provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), model: z.string().optional(), - /** Permission mode for tool execution in this movement */ - permission_mode: PermissionModeSchema.optional(), + /** Removed legacy field (no backward compatibility) */ + permission_mode: z.never().optional(), + /** Required minimum permission mode for tool execution in this movement */ + required_permission_mode: PermissionModeSchema.optional(), /** Provider-specific movement options */ provider_options: MovementProviderOptionsSchema, /** Whether this movement is allowed to edit project files */ @@ -439,6 +460,8 @@ export const GlobalConfigSchema = z.object({ persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'opencode', 'mock'])).optional(), /** Global provider-specific options (lowest priority) */ provider_options: MovementProviderOptionsSchema, + /** Provider-specific permission profiles */ + provider_profiles: ProviderPermissionProfilesSchema, /** Global runtime defaults (piece runtime overrides this) */ runtime: RuntimeConfigSchema, /** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */ @@ -469,4 +492,5 @@ export const ProjectConfigSchema = z.object({ agents: z.array(CustomAgentConfigSchema).optional(), provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), provider_options: MovementProviderOptionsSchema, + provider_profiles: ProviderPermissionProfilesSchema, }); diff --git a/src/core/models/types.ts b/src/core/models/types.ts index ec9b9f5..872ab80 100644 --- a/src/core/models/types.ts +++ b/src/core/models/types.ts @@ -52,6 +52,14 @@ export type { PieceState, } from './piece-types.js'; + +// Provider permission profiles +export type { + ProviderProfileName, + ProviderPermissionProfile, + ProviderPermissionProfiles, +} from './provider-profiles.js'; + // Configuration types (global and project) export type { CustomAgentConfig, diff --git a/src/core/piece/engine/OptionsBuilder.ts b/src/core/piece/engine/OptionsBuilder.ts index a847a22..259a572 100644 --- a/src/core/piece/engine/OptionsBuilder.ts +++ b/src/core/piece/engine/OptionsBuilder.ts @@ -5,6 +5,7 @@ import type { PhaseRunnerContext } from '../phase-runner.js'; import type { PieceEngineOptions, PhaseName } from '../types.js'; import { buildSessionKey } from '../session-key.js'; import { resolveMovementProviderModel } from '../provider-resolution.js'; +import { DEFAULT_PROVIDER_PERMISSION_PROFILES, resolveMovementPermissionMode } from '../permission-profile-resolution.js'; export class OptionsBuilder { constructor( @@ -31,6 +32,13 @@ export class OptionsBuilder { personaProviders: this.engineOptions.personaProviders, }); + const resolvedProviderForPermissions = + this.engineOptions.provider + ?? this.engineOptions.projectProvider + ?? resolved.provider + ?? this.engineOptions.globalProvider + ?? 'claude'; + return { cwd: this.getCwd(), abortSignal: this.engineOptions.abortSignal, @@ -39,7 +47,13 @@ export class OptionsBuilder { model: this.engineOptions.model, stepProvider: resolved.provider, stepModel: resolved.model, - permissionMode: step.permissionMode, + permissionMode: resolveMovementPermissionMode({ + movementName: step.name, + requiredPermissionMode: step.requiredPermissionMode, + provider: resolvedProviderForPermissions, + projectProviderProfiles: this.engineOptions.projectProviderProfiles, + globalProviderProfiles: this.engineOptions.globalProviderProfiles ?? DEFAULT_PROVIDER_PERMISSION_PROFILES, + }), providerOptions: step.providerOptions, language: this.getLanguage(), onStream: this.engineOptions.onStream, diff --git a/src/core/piece/engine/TeamLeaderRunner.ts b/src/core/piece/engine/TeamLeaderRunner.ts index 9b92eaf..eab8907 100644 --- a/src/core/piece/engine/TeamLeaderRunner.ts +++ b/src/core/piece/engine/TeamLeaderRunner.ts @@ -59,7 +59,7 @@ function createPartMovement(step: PieceMovement, part: PartDefinition): PieceMov mcpServers: step.mcpServers, provider: step.provider, model: step.model, - permissionMode: step.teamLeader.partPermissionMode ?? step.permissionMode, + requiredPermissionMode: step.teamLeader.partPermissionMode ?? step.requiredPermissionMode, edit: step.teamLeader.partEdit ?? step.edit, instructionTemplate: part.instruction, passPreviousResponse: false, diff --git a/src/core/piece/permission-profile-resolution.ts b/src/core/piece/permission-profile-resolution.ts new file mode 100644 index 0000000..41dbdc2 --- /dev/null +++ b/src/core/piece/permission-profile-resolution.ts @@ -0,0 +1,88 @@ +import type { PermissionMode } from '../models/types.js'; +import type { ProviderPermissionProfiles, ProviderProfileName } from '../models/provider-profiles.js'; + +export interface ResolvePermissionModeInput { + movementName: string; + requiredPermissionMode?: PermissionMode; + provider?: ProviderProfileName; + projectProviderProfiles?: ProviderPermissionProfiles; + globalProviderProfiles?: ProviderPermissionProfiles; +} + +export const DEFAULT_PROVIDER_PERMISSION_PROFILES: ProviderPermissionProfiles = { + claude: { defaultPermissionMode: 'edit' }, + codex: { defaultPermissionMode: 'edit' }, + opencode: { defaultPermissionMode: 'edit' }, + mock: { defaultPermissionMode: 'edit' }, +}; + +/** + * Resolve movement permission mode using provider profiles. + * + * Priority: + * 1. project provider_profiles..movement_permission_overrides. + * 2. global provider_profiles..movement_permission_overrides. + * 3. project provider_profiles..default_permission_mode + * 4. global provider_profiles..default_permission_mode + * 5. apply movement.required_permission_mode as minimum floor + * + * Throws when unresolved. + */ +export function resolveMovementPermissionMode(input: ResolvePermissionModeInput): PermissionMode { + if (!input.provider) { + if (input.requiredPermissionMode) { + return input.requiredPermissionMode; + } + throw new Error( + `Unable to resolve permission mode for movement "${input.movementName}": provider is required when movement.required_permission_mode is omitted.`, + ); + } + + const projectProfile = input.projectProviderProfiles?.[input.provider]; + const globalProfile = input.globalProviderProfiles?.[input.provider]; + + const projectOverride = projectProfile?.movementPermissionOverrides?.[input.movementName]; + if (projectOverride) { + return applyRequiredPermissionFloor(projectOverride, input.requiredPermissionMode); + } + + const globalOverride = globalProfile?.movementPermissionOverrides?.[input.movementName]; + if (globalOverride) { + return applyRequiredPermissionFloor(globalOverride, input.requiredPermissionMode); + } + + if (projectProfile?.defaultPermissionMode) { + return applyRequiredPermissionFloor(projectProfile.defaultPermissionMode, input.requiredPermissionMode); + } + + if (globalProfile?.defaultPermissionMode) { + return applyRequiredPermissionFloor(globalProfile.defaultPermissionMode, input.requiredPermissionMode); + } + + if (input.requiredPermissionMode) { + return input.requiredPermissionMode; + } + + throw new Error( + `Unable to resolve permission mode for movement "${input.movementName}" and provider "${input.provider}": ` + + 'define provider_profiles defaults/overrides or movement.required_permission_mode.', + ); +} + +const PERMISSION_MODE_RANK: Record = { + readonly: 0, + edit: 1, + full: 2, +}; + +function applyRequiredPermissionFloor( + resolvedMode: PermissionMode, + requiredMode?: PermissionMode, +): PermissionMode { + if (!requiredMode) { + return resolvedMode; + } + return PERMISSION_MODE_RANK[requiredMode] > PERMISSION_MODE_RANK[resolvedMode] + ? requiredMode + : resolvedMode; +} diff --git a/src/core/piece/types.ts b/src/core/piece/types.ts index 73b940d..8211673 100644 --- a/src/core/piece/types.ts +++ b/src/core/piece/types.ts @@ -7,6 +7,7 @@ import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; import type { PieceMovement, AgentResponse, PieceState, Language, LoopMonitorConfig } from '../models/types.js'; +import type { ProviderPermissionProfiles } from '../models/provider-profiles.js'; export type ProviderType = 'claude' | 'codex' | 'opencode' | 'mock'; @@ -177,9 +178,17 @@ export interface PieceEngineOptions { /** Language for instruction metadata. Defaults to 'en'. */ language?: Language; provider?: ProviderType; + /** Project config provider (used for provider/profile resolution parity with AgentRunner) */ + projectProvider?: ProviderType; + /** Global config provider (used for provider/profile resolution parity with AgentRunner) */ + globalProvider?: ProviderType; model?: string; /** Per-persona provider overrides (e.g., { coder: 'codex' }) */ personaProviders?: Record; + /** Project-level provider permission profiles */ + projectProviderProfiles?: ProviderPermissionProfiles; + /** Global-level provider permission profiles */ + globalProviderProfiles?: ProviderPermissionProfiles; /** Enable interactive-only rules and user-input transitions */ interactive?: boolean; /** Rule tag index detector (required for rules evaluation) */ diff --git a/src/features/tasks/execute/pieceExecution.ts b/src/features/tasks/execute/pieceExecution.ts index d8987f2..c958cc3 100644 --- a/src/features/tasks/execute/pieceExecution.ts +++ b/src/features/tasks/execute/pieceExecution.ts @@ -439,8 +439,12 @@ export async function executePiece( projectCwd, language: options.language, provider: options.provider, + projectProvider: options.projectProvider, + globalProvider: options.globalProvider, model: options.model, personaProviders: options.personaProviders, + projectProviderProfiles: options.projectProviderProfiles, + globalProviderProfiles: options.globalProviderProfiles, interactive: interactiveUserInput, detectRuleIndex, callAiJudge, diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index 2e65882..2f443cb 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -2,7 +2,7 @@ * Task execution logic */ -import { loadPieceByIdentifier, isPiecePath, loadGlobalConfig } from '../../../infra/config/index.js'; +import { loadPieceByIdentifier, isPiecePath, loadGlobalConfig, loadProjectConfig } from '../../../infra/config/index.js'; import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; import { header, @@ -72,12 +72,17 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise; + /** Project-level provider permission profiles */ + projectProviderProfiles?: ProviderPermissionProfiles; + /** Global-level provider permission profiles */ + globalProviderProfiles?: ProviderPermissionProfiles; /** Enable interactive user input during step transitions */ interactiveUserInput?: boolean; /** Interactive mode result metadata for NDJSON logging */ diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index 048a4d7..b51d140 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -9,6 +9,7 @@ import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { GlobalConfigSchema } from '../../../core/models/index.js'; import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js'; +import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js'; import { normalizeProviderOptions } from '../loaders/pieceParser.js'; import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js'; import { DEFAULT_LANGUAGE } from '../../../shared/constants.js'; @@ -41,6 +42,34 @@ function validateProviderModelCompatibility(provider: string | undefined, model: } } +function normalizeProviderProfiles( + raw: Record }> | undefined, +): ProviderPermissionProfiles | undefined { + if (!raw) return undefined; + + const entries = Object.entries(raw).map(([provider, profile]) => [provider, { + defaultPermissionMode: profile.default_permission_mode, + movementPermissionOverrides: profile.movement_permission_overrides, + }]); + + return Object.fromEntries(entries) as ProviderPermissionProfiles; +} + +function denormalizeProviderProfiles( + profiles: ProviderPermissionProfiles | undefined, +): Record }> | undefined { + if (!profiles) return undefined; + const entries = Object.entries(profiles); + if (entries.length === 0) return undefined; + + return Object.fromEntries(entries.map(([provider, profile]) => [provider, { + default_permission_mode: profile.defaultPermissionMode, + ...(profile.movementPermissionOverrides + ? { movement_permission_overrides: profile.movementPermissionOverrides } + : {}), + }])) as Record }>; +} + /** Create default global configuration (fresh instance each call) */ function createDefaultGlobalConfig(): GlobalConfig { return { @@ -126,6 +155,7 @@ export class GlobalConfigManager { pieceCategoriesFile: parsed.piece_categories_file, personaProviders: parsed.persona_providers, providerOptions: normalizeProviderOptions(parsed.provider_options), + providerProfiles: normalizeProviderProfiles(parsed.provider_profiles as Record }> | undefined), runtime: parsed.runtime?.prepare && parsed.runtime.prepare.length > 0 ? { prepare: [...new Set(parsed.runtime.prepare)] } : undefined, @@ -213,6 +243,10 @@ export class GlobalConfigManager { if (config.personaProviders && Object.keys(config.personaProviders).length > 0) { raw.persona_providers = config.personaProviders; } + const rawProviderProfiles = denormalizeProviderProfiles(config.providerProfiles); + if (rawProviderProfiles && Object.keys(rawProviderProfiles).length > 0) { + raw.provider_profiles = rawProviderProfiles; + } if (config.runtime?.prepare && config.runtime.prepare.length > 0) { raw.runtime = { prepare: [...new Set(config.runtime.prepare)], diff --git a/src/infra/config/loaders/pieceParser.ts b/src/infra/config/loaders/pieceParser.ts index 2095ebb..86d3fd6 100644 --- a/src/infra/config/loaders/pieceParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -310,7 +310,7 @@ function normalizeStepFromRaw( mcpServers: step.mcp_servers, provider: step.provider, model: step.model, - permissionMode: step.permission_mode, + requiredPermissionMode: step.required_permission_mode, providerOptions: mergeProviderOptions(inheritedProviderOptions, normalizeProviderOptions(step.provider_options)), edit: step.edit, instructionTemplate: (step.instruction_template diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index 10f23f6..83e7b8a 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -9,6 +9,7 @@ import { join, resolve } from 'node:path'; import { parse, stringify } from 'yaml'; import { copyProjectResourcesToDir } from '../../resources/index.js'; import type { PermissionMode, ProjectLocalConfig } from '../types.js'; +import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js'; export type { PermissionMode, ProjectLocalConfig }; @@ -34,6 +35,28 @@ function getConfigPath(projectDir: string): string { return join(getConfigDir(projectDir), 'config.yaml'); } +function normalizeProviderProfiles(raw: Record }> | undefined): ProviderPermissionProfiles | undefined { + if (!raw) return undefined; + return Object.fromEntries( + Object.entries(raw).map(([provider, profile]) => [provider, { + defaultPermissionMode: profile.default_permission_mode, + movementPermissionOverrides: profile.movement_permission_overrides, + }]), + ) as ProviderPermissionProfiles; +} + +function denormalizeProviderProfiles(profiles: ProviderPermissionProfiles | undefined): Record }> | undefined { + if (!profiles) return undefined; + const entries = Object.entries(profiles); + if (entries.length === 0) return undefined; + return Object.fromEntries(entries.map(([provider, profile]) => [provider, { + default_permission_mode: profile.defaultPermissionMode, + ...(profile.movementPermissionOverrides + ? { movement_permission_overrides: profile.movementPermissionOverrides } + : {}), + }])) as Record }>; +} + /** * Load project configuration from .takt/config.yaml */ @@ -46,8 +69,12 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig { try { const content = readFileSync(configPath, 'utf-8'); - const parsed = parse(content) as ProjectLocalConfig | null; - return { ...DEFAULT_PROJECT_CONFIG, ...parsed }; + const parsed = (parse(content) as ProjectLocalConfig | null) ?? {}; + return { + ...DEFAULT_PROJECT_CONFIG, + ...parsed, + providerProfiles: normalizeProviderProfiles(parsed.provider_profiles as Record }> | undefined), + }; } catch { return { ...DEFAULT_PROJECT_CONFIG }; } @@ -68,7 +95,16 @@ export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig // Copy project resources (only copies files that don't exist) copyProjectResourcesToDir(configDir); - const content = stringify(config, { indent: 2 }); + const savePayload: ProjectLocalConfig = { ...config }; + const rawProfiles = denormalizeProviderProfiles(config.providerProfiles); + if (rawProfiles && Object.keys(rawProfiles).length > 0) { + savePayload.provider_profiles = rawProfiles; + } else { + delete savePayload.provider_profiles; + } + delete savePayload.providerProfiles; + + const content = stringify(savePayload, { indent: 2 }); writeFileSync(configPath, content, 'utf-8'); } diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index 334d105..f7a31d7 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -4,6 +4,7 @@ import type { PieceCategoryConfigNode } from '../../core/models/schemas.js'; import type { MovementProviderOptions } from '../../core/models/piece-types.js'; +import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js'; /** Permission mode for the project * - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts) @@ -25,6 +26,10 @@ export interface ProjectLocalConfig { verbose?: boolean; /** Provider-specific options (overrides global, overridden by piece/movement) */ provider_options?: MovementProviderOptions; + /** Provider-specific permission profiles (project-level override) */ + provider_profiles?: ProviderPermissionProfiles; + /** Provider-specific permission profiles (camelCase alias) */ + providerProfiles?: ProviderPermissionProfiles; /** Piece categories (name -> piece list) */ piece_categories?: Record; /** Show uncategorized pieces under Others category */