Merge branch 'develop' of https://github.com/nrslib/takt into develop

This commit is contained in:
nrslib 2026-02-13 17:48:09 +09:00
commit 8731d64c0c
63 changed files with 589 additions and 11 deletions

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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` | - | MCPModel 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
});
});
});

View File

@ -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',

View File

@ -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');
});
});

View 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 },
});
});
});

View File

@ -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,

View File

@ -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;

View File

@ -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) */

View File

@ -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 */

View File

@ -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,

View File

@ -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;

View File

@ -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) */

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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(),

View File

@ -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;