diff --git a/README.md b/README.md index 0838200..21ef03e 100644 --- a/README.md +++ b/README.md @@ -803,10 +803,22 @@ Special `next` values: `COMPLETE` (success), `ABORT` (failure) | `provider` | - | Override provider for this movement (`claude`, `codex`, or `opencode`) | | `model` | - | Override model for this movement | | `permission_mode` | - | Permission mode: `readonly`, `edit`, `full` (provider-independent) | +| `provider_options` | - | Provider-specific options (e.g. `codex.network_access`, `opencode.network_access`) | | `output_contracts` | - | Output contract definitions for report files | | `quality_gates` | - | AI directives for movement completion requirements | | `mcp_servers` | - | MCP (Model Context Protocol) server configuration (stdio/SSE/HTTP) | +Piece-level defaults can be set with `piece_config.provider_options`, and movement-level `provider_options` overrides them. + +```yaml +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true +``` + ## API Usage Example ```typescript diff --git a/builtins/en/pieces/backend-cqrs.yaml b/builtins/en/pieces/backend-cqrs.yaml index aa4a112..e5c8d54 100644 --- a/builtins/en/pieces/backend-cqrs.yaml +++ b/builtins/en/pieces/backend-cqrs.yaml @@ -1,5 +1,12 @@ name: backend-cqrs description: CQRS+ES, Security, QA Expert Review +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/en/pieces/backend.yaml b/builtins/en/pieces/backend.yaml index d3d7522..8719355 100644 --- a/builtins/en/pieces/backend.yaml +++ b/builtins/en/pieces/backend.yaml @@ -1,5 +1,12 @@ name: backend description: Backend, Security, QA Expert Review +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/en/pieces/coding.yaml b/builtins/en/pieces/coding.yaml index ba28892..642614a 100644 --- a/builtins/en/pieces/coding.yaml +++ b/builtins/en/pieces/coding.yaml @@ -1,5 +1,12 @@ name: coding description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan movements: diff --git a/builtins/en/pieces/compound-eye.yaml b/builtins/en/pieces/compound-eye.yaml index 8c37938..b1e65b6 100644 --- a/builtins/en/pieces/compound-eye.yaml +++ b/builtins/en/pieces/compound-eye.yaml @@ -1,5 +1,12 @@ name: compound-eye description: Multi-model review - send the same instruction to Claude and Codex simultaneously, synthesize both responses +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: evaluate movements: diff --git a/builtins/en/pieces/default.yaml b/builtins/en/pieces/default.yaml index 05afa4f..270880f 100644 --- a/builtins/en/pieces/default.yaml +++ b/builtins/en/pieces/default.yaml @@ -1,5 +1,12 @@ name: default description: Standard development piece with planning and specialized reviews +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan loop_monitors: diff --git a/builtins/en/pieces/e2e-test.yaml b/builtins/en/pieces/e2e-test.yaml index ae582e9..3de5d75 100644 --- a/builtins/en/pieces/e2e-test.yaml +++ b/builtins/en/pieces/e2e-test.yaml @@ -1,5 +1,12 @@ name: e2e-test description: E2E test focused piece (E2E analysis → E2E implementation → review → fix) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan_test loop_monitors: diff --git a/builtins/en/pieces/expert-cqrs.yaml b/builtins/en/pieces/expert-cqrs.yaml index 041f5d9..bced204 100644 --- a/builtins/en/pieces/expert-cqrs.yaml +++ b/builtins/en/pieces/expert-cqrs.yaml @@ -1,5 +1,12 @@ name: expert-cqrs description: CQRS+ES, Frontend, Security, QA Expert Review +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/en/pieces/expert.yaml b/builtins/en/pieces/expert.yaml index 65a006e..0ea5818 100644 --- a/builtins/en/pieces/expert.yaml +++ b/builtins/en/pieces/expert.yaml @@ -1,5 +1,12 @@ name: expert description: Architecture, Frontend, Security, QA Expert Review +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/en/pieces/frontend.yaml b/builtins/en/pieces/frontend.yaml index 31e3eb6..077c6c8 100644 --- a/builtins/en/pieces/frontend.yaml +++ b/builtins/en/pieces/frontend.yaml @@ -1,5 +1,12 @@ name: frontend description: Frontend, Security, QA Expert Review +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/en/pieces/magi.yaml b/builtins/en/pieces/magi.yaml index 76631ed..7bb7d80 100644 --- a/builtins/en/pieces/magi.yaml +++ b/builtins/en/pieces/magi.yaml @@ -1,5 +1,12 @@ name: magi description: MAGI Deliberation System - Analyze from 3 perspectives and decide by majority +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 5 initial_movement: melchior movements: diff --git a/builtins/en/pieces/minimal.yaml b/builtins/en/pieces/minimal.yaml index 344e95d..0512ef3 100644 --- a/builtins/en/pieces/minimal.yaml +++ b/builtins/en/pieces/minimal.yaml @@ -1,5 +1,12 @@ name: minimal description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: implement movements: diff --git a/builtins/en/pieces/passthrough.yaml b/builtins/en/pieces/passthrough.yaml index f4fbae5..2d730b7 100644 --- a/builtins/en/pieces/passthrough.yaml +++ b/builtins/en/pieces/passthrough.yaml @@ -1,5 +1,12 @@ name: passthrough description: Single-agent thin wrapper. Pass task directly to coder as-is. +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: execute movements: diff --git a/builtins/en/pieces/research.yaml b/builtins/en/pieces/research.yaml index 75ec5c0..0d9af43 100644 --- a/builtins/en/pieces/research.yaml +++ b/builtins/en/pieces/research.yaml @@ -1,5 +1,12 @@ name: research description: Research piece - autonomously executes research without asking questions +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: plan movements: diff --git a/builtins/en/pieces/review-fix-minimal.yaml b/builtins/en/pieces/review-fix-minimal.yaml index 26051de..5c3a936 100644 --- a/builtins/en/pieces/review-fix-minimal.yaml +++ b/builtins/en/pieces/review-fix-minimal.yaml @@ -1,5 +1,12 @@ name: review-fix-minimal description: Review and fix piece for existing code (starts with review, no implementation) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: reviewers movements: diff --git a/builtins/en/pieces/review-only.yaml b/builtins/en/pieces/review-only.yaml index da4fed8..a665b86 100644 --- a/builtins/en/pieces/review-only.yaml +++ b/builtins/en/pieces/review-only.yaml @@ -1,5 +1,12 @@ name: review-only description: Review-only piece - reviews code without making edits +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: plan movements: diff --git a/builtins/en/pieces/structural-reform.yaml b/builtins/en/pieces/structural-reform.yaml index 8a268f4..11d57ba 100644 --- a/builtins/en/pieces/structural-reform.yaml +++ b/builtins/en/pieces/structural-reform.yaml @@ -1,5 +1,12 @@ name: structural-reform description: Full project review and structural reform - iterative codebase restructuring with staged file splits +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 50 initial_movement: review loop_monitors: diff --git a/builtins/en/pieces/unit-test.yaml b/builtins/en/pieces/unit-test.yaml index 819e8b4..b1037ee 100644 --- a/builtins/en/pieces/unit-test.yaml +++ b/builtins/en/pieces/unit-test.yaml @@ -1,5 +1,12 @@ name: unit-test description: Unit test focused piece (test analysis → test implementation → review → fix) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan_test loop_monitors: diff --git a/builtins/ja/pieces/backend-cqrs.yaml b/builtins/ja/pieces/backend-cqrs.yaml index efd269b..bf6b7ea 100644 --- a/builtins/ja/pieces/backend-cqrs.yaml +++ b/builtins/ja/pieces/backend-cqrs.yaml @@ -1,5 +1,12 @@ name: backend-cqrs description: CQRS+ES・セキュリティ・QA専門家レビュー +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/ja/pieces/backend.yaml b/builtins/ja/pieces/backend.yaml index 1ac6ea6..fa7a395 100644 --- a/builtins/ja/pieces/backend.yaml +++ b/builtins/ja/pieces/backend.yaml @@ -1,5 +1,12 @@ name: backend description: バックエンド・セキュリティ・QA専門家レビュー +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/ja/pieces/coding.yaml b/builtins/ja/pieces/coding.yaml index 990ff1b..4a6ead8 100644 --- a/builtins/ja/pieces/coding.yaml +++ b/builtins/ja/pieces/coding.yaml @@ -1,5 +1,12 @@ name: coding description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan movements: diff --git a/builtins/ja/pieces/compound-eye.yaml b/builtins/ja/pieces/compound-eye.yaml index 8a94f41..fd0f300 100644 --- a/builtins/ja/pieces/compound-eye.yaml +++ b/builtins/ja/pieces/compound-eye.yaml @@ -1,5 +1,12 @@ name: compound-eye description: 複眼レビュー - 同じ指示を Claude と Codex に同時に投げ、両者の回答を統合する +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: evaluate diff --git a/builtins/ja/pieces/default.yaml b/builtins/ja/pieces/default.yaml index 258cdb0..d281dc3 100644 --- a/builtins/ja/pieces/default.yaml +++ b/builtins/ja/pieces/default.yaml @@ -1,5 +1,12 @@ name: default description: Standard development piece with planning and specialized reviews +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan loop_monitors: diff --git a/builtins/ja/pieces/e2e-test.yaml b/builtins/ja/pieces/e2e-test.yaml index 6096e98..11a8673 100644 --- a/builtins/ja/pieces/e2e-test.yaml +++ b/builtins/ja/pieces/e2e-test.yaml @@ -1,5 +1,12 @@ name: e2e-test description: E2Eテスト追加に特化したピース(E2E分析→E2E実装→レビュー→修正) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan_test loop_monitors: diff --git a/builtins/ja/pieces/expert-cqrs.yaml b/builtins/ja/pieces/expert-cqrs.yaml index 8c2a2b6..f24448a 100644 --- a/builtins/ja/pieces/expert-cqrs.yaml +++ b/builtins/ja/pieces/expert-cqrs.yaml @@ -1,5 +1,12 @@ name: expert-cqrs description: CQRS+ES・フロントエンド・セキュリティ・QA専門家レビュー +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/ja/pieces/expert.yaml b/builtins/ja/pieces/expert.yaml index 9f36c60..8f5e5bf 100644 --- a/builtins/ja/pieces/expert.yaml +++ b/builtins/ja/pieces/expert.yaml @@ -1,5 +1,12 @@ name: expert description: アーキテクチャ・フロントエンド・セキュリティ・QA専門家レビュー +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/ja/pieces/frontend.yaml b/builtins/ja/pieces/frontend.yaml index 6bcbcb2..19aa1f1 100644 --- a/builtins/ja/pieces/frontend.yaml +++ b/builtins/ja/pieces/frontend.yaml @@ -1,5 +1,12 @@ name: frontend description: フロントエンド・セキュリティ・QA専門家レビュー +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 30 initial_movement: plan movements: diff --git a/builtins/ja/pieces/magi.yaml b/builtins/ja/pieces/magi.yaml index 83614cb..f722c3d 100644 --- a/builtins/ja/pieces/magi.yaml +++ b/builtins/ja/pieces/magi.yaml @@ -1,5 +1,12 @@ name: magi description: MAGI合議システム - 3つの観点から分析し多数決で判定 +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 5 initial_movement: melchior movements: diff --git a/builtins/ja/pieces/minimal.yaml b/builtins/ja/pieces/minimal.yaml index c190ac9..ea90200 100644 --- a/builtins/ja/pieces/minimal.yaml +++ b/builtins/ja/pieces/minimal.yaml @@ -1,5 +1,12 @@ name: minimal description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: implement movements: diff --git a/builtins/ja/pieces/passthrough.yaml b/builtins/ja/pieces/passthrough.yaml index ac4cb8b..b4d11ea 100644 --- a/builtins/ja/pieces/passthrough.yaml +++ b/builtins/ja/pieces/passthrough.yaml @@ -1,5 +1,12 @@ name: passthrough description: Single-agent thin wrapper. Pass task directly to coder as-is. +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: execute movements: diff --git a/builtins/ja/pieces/research.yaml b/builtins/ja/pieces/research.yaml index 78f2521..708601c 100644 --- a/builtins/ja/pieces/research.yaml +++ b/builtins/ja/pieces/research.yaml @@ -1,5 +1,12 @@ name: research description: 調査ピース - 質問せずに自律的に調査を実行 +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: plan movements: diff --git a/builtins/ja/pieces/review-fix-minimal.yaml b/builtins/ja/pieces/review-fix-minimal.yaml index f9f3300..fa12b33 100644 --- a/builtins/ja/pieces/review-fix-minimal.yaml +++ b/builtins/ja/pieces/review-fix-minimal.yaml @@ -1,5 +1,12 @@ name: review-fix-minimal description: 既存コードのレビューと修正ピース(レビュー開始、実装なし) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: reviewers movements: diff --git a/builtins/ja/pieces/review-only.yaml b/builtins/ja/pieces/review-only.yaml index 6a8a0d9..c9eeb06 100644 --- a/builtins/ja/pieces/review-only.yaml +++ b/builtins/ja/pieces/review-only.yaml @@ -1,5 +1,12 @@ name: review-only description: レビュー専用ピース - コードをレビューするだけで編集は行わない +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 initial_movement: plan movements: diff --git a/builtins/ja/pieces/structural-reform.yaml b/builtins/ja/pieces/structural-reform.yaml index 3cb0419..7903163 100644 --- a/builtins/ja/pieces/structural-reform.yaml +++ b/builtins/ja/pieces/structural-reform.yaml @@ -1,5 +1,12 @@ name: structural-reform description: プロジェクト全体レビューと構造改革 - 段階的なファイル分割による反復的コードベース再構築 +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 50 initial_movement: review loop_monitors: diff --git a/builtins/ja/pieces/unit-test.yaml b/builtins/ja/pieces/unit-test.yaml index 3c5e94c..89f408c 100644 --- a/builtins/ja/pieces/unit-test.yaml +++ b/builtins/ja/pieces/unit-test.yaml @@ -1,5 +1,12 @@ name: unit-test description: 単体テスト追加に特化したピース(テスト分析→テスト実装→レビュー→修正) +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: plan_test loop_monitors: diff --git a/docs/README.ja.md b/docs/README.ja.md index c67bb76..4ce8796 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -803,10 +803,22 @@ rules: | `provider` | - | このムーブメントのプロバイダーを上書き(`claude`、`codex`、または`opencode`) | | `model` | - | このムーブメントのモデルを上書き | | `permission_mode` | - | パーミッションモード: `readonly`、`edit`、`full`(プロバイダー非依存) | +| `provider_options` | - | プロバイダー固有オプション(例: `codex.network_access`、`opencode.network_access`) | | `output_contracts` | - | レポートファイルの出力契約定義 | | `quality_gates` | - | ムーブメント完了要件のAIディレクティブ | | `mcp_servers` | - | MCP(Model Context Protocol)サーバー設定(stdio/SSE/HTTP) | +ピース全体のデフォルトは `piece_config.provider_options` で設定でき、ムーブメント側 `provider_options` で上書きできます。 + +```yaml +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true +``` + ## API使用例 ```typescript diff --git a/e2e/fixtures/pieces/mock-cycle-detect.yaml b/e2e/fixtures/pieces/mock-cycle-detect.yaml index c98ea48..addd846 100644 --- a/e2e/fixtures/pieces/mock-cycle-detect.yaml +++ b/e2e/fixtures/pieces/mock-cycle-detect.yaml @@ -1,5 +1,12 @@ name: e2e-cycle-detect description: Piece with loop_monitors for cycle detection E2E testing +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 initial_movement: review diff --git a/e2e/fixtures/pieces/mock-max-iter.yaml b/e2e/fixtures/pieces/mock-max-iter.yaml index 1912fde..632bbf9 100644 --- a/e2e/fixtures/pieces/mock-max-iter.yaml +++ b/e2e/fixtures/pieces/mock-max-iter.yaml @@ -1,5 +1,12 @@ name: e2e-mock-max-iter description: Piece with max_movements=2 that loops between two steps +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 2 diff --git a/e2e/fixtures/pieces/mock-no-match.yaml b/e2e/fixtures/pieces/mock-no-match.yaml index 493bd88..142ee36 100644 --- a/e2e/fixtures/pieces/mock-no-match.yaml +++ b/e2e/fixtures/pieces/mock-no-match.yaml @@ -1,5 +1,12 @@ name: e2e-mock-no-match description: Piece with a strict rule condition that will not match mock output +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 3 diff --git a/e2e/fixtures/pieces/mock-single-step.yaml b/e2e/fixtures/pieces/mock-single-step.yaml index 087869c..b2703ee 100644 --- a/e2e/fixtures/pieces/mock-single-step.yaml +++ b/e2e/fixtures/pieces/mock-single-step.yaml @@ -1,5 +1,12 @@ name: e2e-mock-single description: Minimal mock-only piece for CLI E2E +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 3 diff --git a/e2e/fixtures/pieces/mock-slow-multi-step.yaml b/e2e/fixtures/pieces/mock-slow-multi-step.yaml index 8da8f3f..0921826 100644 --- a/e2e/fixtures/pieces/mock-slow-multi-step.yaml +++ b/e2e/fixtures/pieces/mock-slow-multi-step.yaml @@ -1,5 +1,12 @@ name: e2e-mock-slow-multi-step description: Multi-step mock piece to keep tasks in-flight long enough for SIGINT E2E +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 20 diff --git a/e2e/fixtures/pieces/mock-two-step.yaml b/e2e/fixtures/pieces/mock-two-step.yaml index 8090cf4..73e565e 100644 --- a/e2e/fixtures/pieces/mock-two-step.yaml +++ b/e2e/fixtures/pieces/mock-two-step.yaml @@ -1,5 +1,12 @@ name: e2e-mock-two-step description: Two-step sequential piece for E2E testing +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 5 diff --git a/e2e/fixtures/pieces/multi-step-parallel.yaml b/e2e/fixtures/pieces/multi-step-parallel.yaml index 2b25b3e..236b354 100644 --- a/e2e/fixtures/pieces/multi-step-parallel.yaml +++ b/e2e/fixtures/pieces/multi-step-parallel.yaml @@ -1,5 +1,12 @@ name: e2e-multi-step-parallel description: Multi-step piece with parallel sub-movements for E2E testing +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 10 diff --git a/e2e/fixtures/pieces/report-judge.yaml b/e2e/fixtures/pieces/report-judge.yaml index d855609..b0a18b8 100644 --- a/e2e/fixtures/pieces/report-judge.yaml +++ b/e2e/fixtures/pieces/report-judge.yaml @@ -1,5 +1,12 @@ name: e2e-report-judge description: E2E piece that exercises report + judge phases +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 3 diff --git a/e2e/fixtures/pieces/simple.yaml b/e2e/fixtures/pieces/simple.yaml index c8f813e..1afd38f 100644 --- a/e2e/fixtures/pieces/simple.yaml +++ b/e2e/fixtures/pieces/simple.yaml @@ -1,5 +1,12 @@ name: e2e-simple description: Minimal E2E test piece +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 5 diff --git a/e2e/fixtures/pieces/structured-output.yaml b/e2e/fixtures/pieces/structured-output.yaml index fcb7280..d4df485 100644 --- a/e2e/fixtures/pieces/structured-output.yaml +++ b/e2e/fixtures/pieces/structured-output.yaml @@ -1,5 +1,12 @@ name: e2e-structured-output description: E2E piece to verify structured output rule matching +piece_config: + provider_options: + codex: + network_access: true + opencode: + network_access: true + max_movements: 5 diff --git a/src/__tests__/codex-structured-output.test.ts b/src/__tests__/codex-structured-output.test.ts index a262b14..a7a6a28 100644 --- a/src/__tests__/codex-structured-output.test.ts +++ b/src/__tests__/codex-structured-output.test.ts @@ -14,11 +14,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; // ===== Codex SDK mock ===== let mockEvents: Array> = []; +let lastThreadOptions: Record | undefined; vi.mock('@openai/codex-sdk', () => { return { Codex: class MockCodex { - async startThread() { + async startThread(options?: Record) { + lastThreadOptions = options; return { id: 'thread-mock', runStreamed: async () => ({ @@ -44,6 +46,7 @@ describe('CodexClient — structuredOutput 抽出', () => { beforeEach(() => { vi.clearAllMocks(); mockEvents = []; + lastThreadOptions = undefined; }); it('outputSchema 指定時に agent_message の JSON テキストを structuredOutput として返す', async () => { @@ -149,4 +152,21 @@ describe('CodexClient — structuredOutput 抽出', () => { expect(result.structuredOutput).toEqual({ step: 1 }); }); + + it('provider_options.codex.network_access が ThreadOptions に反映される', async () => { + mockEvents = [ + { type: 'thread.started', thread_id: 'thread-1' }, + { type: 'turn.completed', usage: { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 } }, + ]; + + const client = new CodexClient(); + await client.call('coder', 'prompt', { + cwd: '/tmp', + networkAccess: true, + }); + + expect(lastThreadOptions).toMatchObject({ + networkAccessEnabled: true, + }); + }); }); diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index 0709d02..1d8b423 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -105,6 +105,54 @@ describe('PieceConfigRawSchema', () => { expect(result.movements![0]?.permission_mode).toBe('edit'); }); + it('should parse movement with provider_options', () => { + const config = { + name: 'test-piece', + movements: [ + { + name: 'implement', + provider: 'codex', + provider_options: { + codex: { network_access: true }, + opencode: { network_access: false }, + }, + instruction: '{task}', + }, + ], + }; + + const result = PieceConfigRawSchema.parse(config); + expect(result.movements![0]?.provider_options).toEqual({ + codex: { network_access: true }, + opencode: { network_access: false }, + }); + }); + + it('should parse piece-level piece_config.provider_options', () => { + const config = { + name: 'test-piece', + piece_config: { + provider_options: { + codex: { network_access: true }, + }, + }, + movements: [ + { + name: 'implement', + provider: 'codex', + instruction: '{task}', + }, + ], + }; + + const result = PieceConfigRawSchema.parse(config); + expect(result.piece_config).toEqual({ + provider_options: { + codex: { network_access: true }, + }, + }); + }); + it('should allow omitting permission_mode', () => { const config = { name: 'test-piece', diff --git a/src/__tests__/opencode-types.test.ts b/src/__tests__/opencode-types.test.ts index f661940..bf3f492 100644 --- a/src/__tests__/opencode-types.test.ts +++ b/src/__tests__/opencode-types.test.ts @@ -84,4 +84,16 @@ describe('OpenCode permissions', () => { action: 'deny', }); }); + + it('should force allow web tools when networkAccess=true', () => { + const ruleset = buildOpenCodePermissionRuleset('readonly', true); + expect(ruleset.find((rule) => rule.permission === 'webfetch')?.action).toBe('allow'); + expect(ruleset.find((rule) => rule.permission === 'websearch')?.action).toBe('allow'); + }); + + it('should force deny web tools when networkAccess=false', () => { + const ruleset = buildOpenCodePermissionRuleset('full', false); + expect(ruleset.find((rule) => rule.permission === 'webfetch')?.action).toBe('deny'); + expect(ruleset.find((rule) => rule.permission === 'websearch')?.action).toBe('deny'); + }); }); diff --git a/src/__tests__/provider-options-piece-parser.test.ts b/src/__tests__/provider-options-piece-parser.test.ts new file mode 100644 index 0000000..45d7adc --- /dev/null +++ b/src/__tests__/provider-options-piece-parser.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { normalizePieceConfig } from '../infra/config/loaders/pieceParser.js'; + +describe('normalizePieceConfig provider_options', () => { + it('piece-level global を movement に継承し、movement 側で上書きできる', () => { + const raw = { + name: 'provider-options', + piece_config: { + provider_options: { + codex: { network_access: true }, + opencode: { network_access: false }, + }, + }, + movements: [ + { + name: 'codex-default', + provider: 'codex', + instruction: '{task}', + }, + { + name: 'codex-override', + provider: 'codex', + provider_options: { + codex: { network_access: false }, + }, + instruction: '{task}', + }, + ], + }; + + const config = normalizePieceConfig(raw, process.cwd()); + + expect(config.providerOptions).toEqual({ + codex: { networkAccess: true }, + opencode: { networkAccess: false }, + }); + expect(config.movements[0]?.providerOptions).toEqual({ + codex: { networkAccess: true }, + opencode: { networkAccess: false }, + }); + expect(config.movements[1]?.providerOptions).toEqual({ + codex: { networkAccess: false }, + opencode: { networkAccess: false }, + }); + }); +}); diff --git a/src/agents/runner.ts b/src/agents/runner.ts index 5b126d0..80952cd 100644 --- a/src/agents/runner.ts +++ b/src/agents/runner.ts @@ -107,6 +107,7 @@ export class AgentRunner { maxTurns: options.maxTurns, model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig), permissionMode: options.permissionMode, + providerOptions: options.providerOptions, onStream: options.onStream, onPermissionRequest: options.onPermissionRequest, onAskUserQuestion: options.onAskUserQuestion, diff --git a/src/agents/types.ts b/src/agents/types.ts index dad5d17..da7cea0 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -24,6 +24,11 @@ export interface RunAgentOptions { maxTurns?: number; /** Permission mode for tool execution (from piece step) */ permissionMode?: PermissionMode; + /** Provider-specific movement options */ + providerOptions?: { + codex?: { networkAccess?: boolean }; + opencode?: { networkAccess?: boolean }; + }; onStream?: StreamCallback; onPermissionRequest?: PermissionHandler; onAskUserQuestion?: AskUserQuestionHandler; diff --git a/src/core/models/piece-types.ts b/src/core/models/piece-types.ts index 49deeec..5a02088 100644 --- a/src/core/models/piece-types.ts +++ b/src/core/models/piece-types.ts @@ -80,6 +80,24 @@ export interface McpHttpServerConfig { /** MCP server configuration (union of all YAML-configurable transports) */ export type McpServerConfig = McpStdioServerConfig | McpSseServerConfig | McpHttpServerConfig; +/** Codex provider-specific options */ +export interface CodexProviderOptions { + /** Enable network access for Codex workspace-write sandbox */ + networkAccess?: boolean; +} + +/** OpenCode provider-specific options */ +export interface OpenCodeProviderOptions { + /** Enable/disable network tools (webfetch/websearch) */ + networkAccess?: boolean; +} + +/** Provider-specific movement options */ +export interface MovementProviderOptions { + codex?: CodexProviderOptions; + opencode?: OpenCodeProviderOptions; +} + /** Single movement in a piece */ export interface PieceMovement { name: string; @@ -103,6 +121,8 @@ export interface PieceMovement { model?: string; /** Permission mode for tool execution in this movement */ permissionMode?: PermissionMode; + /** Provider-specific movement options */ + providerOptions?: MovementProviderOptions; /** Whether this movement is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ edit?: boolean; instructionTemplate: string; @@ -201,6 +221,8 @@ export interface LoopMonitorConfig { export interface PieceConfig { name: string; description?: string; + /** Piece-level default provider options (used as movement defaults) */ + providerOptions?: MovementProviderOptions; /** Persona definitions — map of name to file path or inline content (raw, not content-resolved) */ personas?: Record; /** Resolved policy definitions — map of name to file content (resolved at parse time) */ diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 0c0f812..82a3dde 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -59,6 +59,20 @@ export const StatusSchema = z.enum([ /** Permission mode schema for tool execution */ export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']); +/** Provider-specific movement options schema */ +export const MovementProviderOptionsSchema = z.object({ + codex: z.object({ + network_access: z.boolean().optional(), + }).optional(), + opencode: z.object({ + network_access: z.boolean().optional(), + }).optional(), +}).optional(); + +/** Piece-level provider options schema */ +export const PieceProviderOptionsSchema = z.object({ + provider_options: MovementProviderOptionsSchema, +}).optional(); /** * Output contract item schema (new structured format). @@ -204,6 +218,7 @@ export const ParallelSubMovementRawSchema = z.object({ provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), model: z.string().optional(), permission_mode: PermissionModeSchema.optional(), + provider_options: MovementProviderOptionsSchema, edit: z.boolean().optional(), instruction: z.string().optional(), instruction_template: z.string().optional(), @@ -235,6 +250,8 @@ export const PieceMovementRawSchema = z.object({ model: z.string().optional(), /** Permission mode for tool execution in this movement */ permission_mode: PermissionModeSchema.optional(), + /** Provider-specific movement options */ + provider_options: MovementProviderOptionsSchema, /** Whether this movement is allowed to edit project files */ edit: z.boolean().optional(), instruction: z.string().optional(), @@ -295,6 +312,7 @@ export const InteractiveModeSchema = z.enum(INTERACTIVE_MODES); export const PieceConfigRawSchema = z.object({ name: z.string().min(1), description: z.string().optional(), + piece_config: PieceProviderOptionsSchema, /** Piece-level persona definitions — map of name to .md file path or inline content */ personas: z.record(z.string(), z.string()).optional(), /** Piece-level policy definitions — map of name to .md file path or inline content */ diff --git a/src/core/piece/engine/OptionsBuilder.ts b/src/core/piece/engine/OptionsBuilder.ts index 1b11ddc..535cacf 100644 --- a/src/core/piece/engine/OptionsBuilder.ts +++ b/src/core/piece/engine/OptionsBuilder.ts @@ -38,6 +38,7 @@ export class OptionsBuilder { provider: resolved.provider, model: resolved.model, permissionMode: step.permissionMode, + providerOptions: step.providerOptions, language: this.getLanguage(), onStream: this.engineOptions.onStream, onPermissionRequest: this.engineOptions.onPermissionRequest, diff --git a/src/infra/codex/client.ts b/src/infra/codex/client.ts index 0a0e045..ceb49c5 100644 --- a/src/infra/codex/client.ts +++ b/src/infra/codex/client.ts @@ -95,6 +95,7 @@ export class CodexClient { ...(options.model ? { model: options.model } : {}), workingDirectory: options.cwd, sandboxMode, + ...(options.networkAccess === undefined ? {} : { networkAccessEnabled: options.networkAccess }), }; let threadId = options.sessionId; diff --git a/src/infra/codex/types.ts b/src/infra/codex/types.ts index 097eed6..102c79e 100644 --- a/src/infra/codex/types.ts +++ b/src/infra/codex/types.ts @@ -27,6 +27,8 @@ export interface CodexCallOptions { systemPrompt?: string; /** Permission mode for sandbox configuration */ permissionMode?: PermissionMode; + /** Enable network access for workspace-write sandbox */ + networkAccess?: boolean; /** Enable streaming mode with callback (best-effort) */ onStream?: StreamCallback; /** OpenAI API key (bypasses CLI auth) */ diff --git a/src/infra/config/loaders/pieceParser.ts b/src/infra/config/loaders/pieceParser.ts index 1616800..83df5dc 100644 --- a/src/infra/config/loaders/pieceParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -24,6 +24,36 @@ import { type RawStep = z.output; +function normalizeProviderOptions( + raw: RawStep['provider_options'], +): PieceMovement['providerOptions'] { + if (!raw) return undefined; + + const codex = raw.codex?.network_access === undefined + ? undefined + : { networkAccess: raw.codex.network_access }; + const opencode = raw.opencode?.network_access === undefined + ? undefined + : { networkAccess: raw.opencode.network_access }; + + if (!codex && !opencode) return undefined; + return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) }; +} + +function mergeProviderOptions( + base: PieceMovement['providerOptions'], + override: PieceMovement['providerOptions'], +): PieceMovement['providerOptions'] { + const codexNetworkAccess = override?.codex?.networkAccess ?? base?.codex?.networkAccess; + const opencodeNetworkAccess = override?.opencode?.networkAccess ?? base?.opencode?.networkAccess; + + const codex = codexNetworkAccess === undefined ? undefined : { networkAccess: codexNetworkAccess }; + const opencode = opencodeNetworkAccess === undefined ? undefined : { networkAccess: opencodeNetworkAccess }; + + if (!codex && !opencode) return undefined; + return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) }; +} + /** Check if a raw output contract item is the object form (has 'name' property). */ function isOutputContractItem(raw: unknown): raw is { name: string; order?: string; format?: string } { return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw; @@ -209,6 +239,7 @@ function normalizeStepFromRaw( step: RawStep, pieceDir: string, sections: PieceSections, + inheritedProviderOptions?: PieceMovement['providerOptions'], context?: FacetResolutionContext, ): PieceMovement { const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule); @@ -241,6 +272,7 @@ function normalizeStepFromRaw( provider: step.provider, model: step.model, permissionMode: step.permission_mode, + providerOptions: mergeProviderOptions(inheritedProviderOptions, normalizeProviderOptions(step.provider_options)), edit: step.edit, instructionTemplate: (step.instruction_template ? resolveRefToContent(step.instruction_template, sections.resolvedInstructions, pieceDir, 'instructions', context) @@ -254,7 +286,9 @@ function normalizeStepFromRaw( }; if (step.parallel && step.parallel.length > 0) { - result.parallel = step.parallel.map((sub: RawStep) => normalizeStepFromRaw(sub, pieceDir, sections, context)); + result.parallel = step.parallel.map((sub: RawStep) => + normalizeStepFromRaw(sub, pieceDir, sections, inheritedProviderOptions, context), + ); } const arpeggioConfig = normalizeArpeggio(step.arpeggio, pieceDir); @@ -327,8 +361,10 @@ export function normalizePieceConfig( resolvedReportFormats, }; + const pieceProviderOptions = normalizeProviderOptions(parsed.piece_config?.provider_options as RawStep['provider_options']); + const movements: PieceMovement[] = parsed.movements.map((step) => - normalizeStepFromRaw(step, pieceDir, sections, context), + normalizeStepFromRaw(step, pieceDir, sections, pieceProviderOptions, context), ); // Schema guarantees movements.min(1) @@ -337,6 +373,7 @@ export function normalizePieceConfig( return { name: parsed.name, description: parsed.description, + providerOptions: pieceProviderOptions, personas: parsed.personas, policies: resolvedPolicies, knowledge: resolvedKnowledge, diff --git a/src/infra/opencode/client.ts b/src/infra/opencode/client.ts index 632ea9f..1f33038 100644 --- a/src/infra/opencode/client.ts +++ b/src/infra/opencode/client.ts @@ -312,7 +312,7 @@ export class OpenCodeClient { const parsedModel = parseProviderModel(options.model, 'OpenCode model'); const fullModel = `${parsedModel.providerID}/${parsedModel.modelID}`; const port = await getFreePort(); - const permission = buildOpenCodePermissionConfig(options.permissionMode); + const permission = buildOpenCodePermissionConfig(options.permissionMode, options.networkAccess); const config = { model: fullModel, small_model: fullModel, @@ -334,7 +334,7 @@ export class OpenCodeClient { ? { data: { id: options.sessionId } } : await client.session.create({ directory: options.cwd, - permission: buildOpenCodePermissionRuleset(options.permissionMode), + permission: buildOpenCodePermissionRuleset(options.permissionMode, options.networkAccess), }); const sessionId = sessionResult.data?.id; diff --git a/src/infra/opencode/types.ts b/src/infra/opencode/types.ts index d981247..e407d6b 100644 --- a/src/infra/opencode/types.ts +++ b/src/infra/opencode/types.ts @@ -100,14 +100,38 @@ function buildPermissionMap(mode?: PermissionMode): OpenCodePermissionMap { }; } -export function buildOpenCodePermissionConfig(mode?: PermissionMode): OpenCodePermissionAction | Record { - if (mode === 'readonly') return 'deny'; - if (mode === 'full') return 'allow'; - return buildPermissionMap(mode); +function applyNetworkAccessOverride( + map: OpenCodePermissionMap, + networkAccess?: boolean, +): OpenCodePermissionMap { + if (networkAccess === undefined) { + return map; + } + + const action: OpenCodePermissionAction = networkAccess ? 'allow' : 'deny'; + return { + ...map, + webfetch: action, + websearch: action, + }; } -export function buildOpenCodePermissionRuleset(mode?: PermissionMode): Array<{ permission: string; pattern: string; action: OpenCodePermissionAction }> { - const permissionMap = buildPermissionMap(mode); +export function buildOpenCodePermissionConfig( + mode?: PermissionMode, + networkAccess?: boolean, +): OpenCodePermissionAction | Record { + if (networkAccess === undefined) { + if (mode === 'readonly') return 'deny'; + if (mode === 'full') return 'allow'; + } + return applyNetworkAccessOverride(buildPermissionMap(mode), networkAccess); +} + +export function buildOpenCodePermissionRuleset( + mode?: PermissionMode, + networkAccess?: boolean, +): Array<{ permission: string; pattern: string; action: OpenCodePermissionAction }> { + const permissionMap = applyNetworkAccessOverride(buildPermissionMap(mode), networkAccess); return OPEN_CODE_PERMISSION_KEYS.map((permission) => ({ permission, pattern: '**', @@ -165,6 +189,8 @@ export interface OpenCodeCallOptions { allowedTools?: string[]; /** Permission mode for automatic permission handling */ permissionMode?: PermissionMode; + /** Override network access (webfetch/websearch) */ + networkAccess?: boolean; /** Enable streaming mode with callback (best-effort) */ onStream?: StreamCallback; onAskUserQuestion?: AskUserQuestionHandler; diff --git a/src/infra/providers/codex.ts b/src/infra/providers/codex.ts index 47a4de8..c124f94 100644 --- a/src/infra/providers/codex.ts +++ b/src/infra/providers/codex.ts @@ -31,6 +31,7 @@ function toCodexOptions(options: ProviderCallOptions): CodexCallOptions { sessionId: options.sessionId, model: options.model, permissionMode: options.permissionMode, + networkAccess: options.providerOptions?.codex?.networkAccess, onStream: options.onStream, openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(), outputSchema: options.outputSchema, diff --git a/src/infra/providers/opencode.ts b/src/infra/providers/opencode.ts index 3243158..3c80878 100644 --- a/src/infra/providers/opencode.ts +++ b/src/infra/providers/opencode.ts @@ -19,6 +19,7 @@ function toOpenCodeOptions(options: ProviderCallOptions): OpenCodeCallOptions { model: options.model, allowedTools: options.allowedTools, permissionMode: options.permissionMode, + networkAccess: options.providerOptions?.opencode?.networkAccess, onStream: options.onStream, onAskUserQuestion: options.onAskUserQuestion, opencodeApiKey: options.opencodeApiKey ?? resolveOpencodeApiKey(), diff --git a/src/infra/providers/types.ts b/src/infra/providers/types.ts index e9214b8..b47c2e8 100644 --- a/src/infra/providers/types.ts +++ b/src/infra/providers/types.ts @@ -30,6 +30,11 @@ export interface ProviderCallOptions { maxTurns?: number; /** Permission mode for tool execution (from piece step) */ permissionMode?: PermissionMode; + /** Provider-specific movement options */ + providerOptions?: { + codex?: { networkAccess?: boolean }; + opencode?: { networkAccess?: boolean }; + }; onStream?: StreamCallback; onPermissionRequest?: PermissionHandler; onAskUserQuestion?: AskUserQuestionHandler;