providerごとに通信を許可する
This commit is contained in:
parent
6bd698941c
commit
479ee7ec25
12
README.md
12
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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -14,11 +14,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
// ===== Codex SDK mock =====
|
||||
|
||||
let mockEvents: Array<Record<string, unknown>> = [];
|
||||
let lastThreadOptions: Record<string, unknown> | undefined;
|
||||
|
||||
vi.mock('@openai/codex-sdk', () => {
|
||||
return {
|
||||
Codex: class MockCodex {
|
||||
async startThread() {
|
||||
async startThread(options?: Record<string, unknown>) {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
46
src/__tests__/provider-options-piece-parser.test.ts
Normal file
46
src/__tests__/provider-options-piece-parser.test.ts
Normal file
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<string, string>;
|
||||
/** Resolved policy definitions — map of name to file content (resolved at parse time) */
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) */
|
||||
|
||||
@ -24,6 +24,36 @@ import {
|
||||
|
||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||
|
||||
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,
|
||||
|
||||
@ -311,7 +311,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,
|
||||
@ -332,7 +332,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;
|
||||
|
||||
@ -100,14 +100,38 @@ function buildPermissionMap(mode?: PermissionMode): OpenCodePermissionMap {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOpenCodePermissionConfig(mode?: PermissionMode): OpenCodePermissionAction | Record<string, OpenCodePermissionAction> {
|
||||
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;
|
||||
}
|
||||
|
||||
export function buildOpenCodePermissionRuleset(mode?: PermissionMode): Array<{ permission: string; pattern: string; action: OpenCodePermissionAction }> {
|
||||
const permissionMap = buildPermissionMap(mode);
|
||||
const action: OpenCodePermissionAction = networkAccess ? 'allow' : 'deny';
|
||||
return {
|
||||
...map,
|
||||
webfetch: action,
|
||||
websearch: action,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOpenCodePermissionConfig(
|
||||
mode?: PermissionMode,
|
||||
networkAccess?: boolean,
|
||||
): OpenCodePermissionAction | Record<string, OpenCodePermissionAction> {
|
||||
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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user