claude code がsandboxで実行されるため、テストが実行できない問題を対処できるオプションを追加
This commit is contained in:
parent
652630eeca
commit
c85f23cb6e
168
docs/provider-sandbox.md
Normal file
168
docs/provider-sandbox.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Provider Sandbox Configuration
|
||||
|
||||
TAKT supports configuring sandbox settings for AI agent providers. This document covers how sandbox isolation works across providers, how to configure it, and the security trade-offs.
|
||||
|
||||
## Overview
|
||||
|
||||
| Provider | Sandbox Mechanism | Build Tool Issues | TAKT Configuration |
|
||||
|----------|------------------|-------------------|-------------------|
|
||||
| **Claude Code** | macOS Seatbelt / Linux bubblewrap | Gradle/JVM blocked in `edit` mode | `provider_options.claude.sandbox` |
|
||||
| **Codex CLI** | macOS Seatbelt / Linux Landlock+seccomp | npm/maven/pytest failures (widespread) | `provider_options.codex.network_access` |
|
||||
| **OpenCode CLI** | None (no native sandbox) | No constraints (no security either) | N/A |
|
||||
|
||||
## Claude Code Sandbox
|
||||
|
||||
### The Problem
|
||||
|
||||
When a movement uses `permission_mode: edit` (mapped to Claude SDK's `acceptEdits`), Bash commands run inside a macOS Seatbelt sandbox. This sandbox blocks:
|
||||
|
||||
- Writes outside the working directory (e.g., `~/.gradle`)
|
||||
- Certain system calls required by JVM initialization
|
||||
- Network access (by default)
|
||||
|
||||
As a result, build tools like Gradle, Maven, or any JVM-based tool fail with `Operation not permitted`.
|
||||
|
||||
### Solution: `provider_options.claude.sandbox`
|
||||
|
||||
TAKT exposes Claude SDK's `SandboxSettings` through `provider_options.claude.sandbox` at four configuration levels.
|
||||
|
||||
#### Option A: `allow_unsandboxed_commands` (Recommended)
|
||||
|
||||
Allow all Bash commands to run outside the sandbox while keeping file edit permissions controlled:
|
||||
|
||||
```yaml
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
allow_unsandboxed_commands: true
|
||||
```
|
||||
|
||||
#### Option B: `excluded_commands`
|
||||
|
||||
Exclude only specific commands from the sandbox:
|
||||
|
||||
```yaml
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
excluded_commands:
|
||||
- ./gradlew
|
||||
- npm
|
||||
- npx
|
||||
```
|
||||
|
||||
### Configuration Levels
|
||||
|
||||
Settings are merged with the following priority (highest wins):
|
||||
|
||||
```
|
||||
Movement > Piece > Project Local > Global
|
||||
```
|
||||
|
||||
#### Global (`~/.takt/config.yaml`)
|
||||
|
||||
Applies to all projects and all pieces:
|
||||
|
||||
```yaml
|
||||
# ~/.takt/config.yaml
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
allow_unsandboxed_commands: true
|
||||
```
|
||||
|
||||
#### Project Local (`.takt/config.yaml`)
|
||||
|
||||
Applies to this project only:
|
||||
|
||||
```yaml
|
||||
# .takt/config.yaml
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
excluded_commands:
|
||||
- ./gradlew
|
||||
```
|
||||
|
||||
#### Piece (`piece_config` section)
|
||||
|
||||
Applies to all movements in this piece:
|
||||
|
||||
```yaml
|
||||
# pieces/my-piece.yaml
|
||||
piece_config:
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
allow_unsandboxed_commands: true
|
||||
```
|
||||
|
||||
#### Movement (per step)
|
||||
|
||||
Applies to a specific movement only:
|
||||
|
||||
```yaml
|
||||
movements:
|
||||
- name: implement
|
||||
permission_mode: edit
|
||||
provider_options:
|
||||
claude:
|
||||
sandbox:
|
||||
allow_unsandboxed_commands: true
|
||||
- name: review
|
||||
permission_mode: readonly
|
||||
# No sandbox config needed — readonly doesn't sandbox Bash
|
||||
```
|
||||
|
||||
### Security Risk Comparison
|
||||
|
||||
| Configuration | File Edits | Network | Bash Commands | CWD-external Writes | Risk Level |
|
||||
|--------------|-----------|---------|---------------|---------------------|------------|
|
||||
| `permission_mode: edit` (default) | Permitted | Blocked | Sandboxed | Blocked | Low |
|
||||
| `excluded_commands: [./gradlew]` | Permitted | Blocked | Only `./gradlew` unsandboxed | Only via `./gradlew` | Low |
|
||||
| `allow_unsandboxed_commands: true` | Permitted | Allowed | Unsandboxed | Allowed via Bash | **Medium** |
|
||||
| `permission_mode: full` | All permitted | Allowed | Unsandboxed | All permitted | **High** |
|
||||
|
||||
**Key difference between `allow_unsandboxed_commands` and `permission_mode: full`:**
|
||||
- `allow_unsandboxed_commands`: File edits still require Claude Code's permission check (`acceptEdits` mode). Only Bash is unsandboxed.
|
||||
- `permission_mode: full`: All permission checks are bypassed (`bypassPermissions` mode). No guardrails at all.
|
||||
|
||||
### Practical Risk Assessment
|
||||
|
||||
The "Medium" risk of `allow_unsandboxed_commands` is manageable in practice because:
|
||||
|
||||
- TAKT runs locally on the developer's machine (not a public-facing service)
|
||||
- Input comes from task instructions written by the developer
|
||||
- Agent behavior is reviewed by the supervisor movement
|
||||
- File edit operations still go through Claude Code's permission system
|
||||
|
||||
## Codex CLI Sandbox
|
||||
|
||||
Codex CLI uses macOS Seatbelt (same as Claude Code) but has **more severe compatibility issues** with build tools. Community reports show npm, Maven, pytest, and other tools frequently failing with `Operation not permitted` — even when the same commands work in Claude Code.
|
||||
|
||||
Codex sandbox is configured via `~/.codex/config.toml` (not through TAKT):
|
||||
|
||||
```toml
|
||||
# ~/.codex/config.toml
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
writable_roots = ["/Users/YOU/.gradle"]
|
||||
```
|
||||
|
||||
TAKT provides `provider_options.codex.network_access` to control network access via the Codex SDK:
|
||||
|
||||
```yaml
|
||||
provider_options:
|
||||
codex:
|
||||
network_access: true
|
||||
```
|
||||
|
||||
For other sandbox settings (writable_roots, sandbox_mode), configure directly in `~/.codex/config.toml`.
|
||||
|
||||
## OpenCode CLI Sandbox
|
||||
|
||||
OpenCode CLI does not have a native sandbox mechanism. All commands run without filesystem or network restrictions. For isolation, the community recommends Docker containers (e.g., [opencode-sandbox](https://github.com/fabianlema/opencode-sandbox)).
|
||||
|
||||
No TAKT-side sandbox configuration is needed or available for OpenCode.
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { normalizePieceConfig } from '../infra/config/loaders/pieceParser.js';
|
||||
import { normalizePieceConfig, mergeProviderOptions } from '../infra/config/loaders/pieceParser.js';
|
||||
|
||||
describe('normalizePieceConfig provider_options', () => {
|
||||
it('piece-level global を movement に継承し、movement 側で上書きできる', () => {
|
||||
@ -43,4 +43,78 @@ describe('normalizePieceConfig provider_options', () => {
|
||||
opencode: { networkAccess: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('claude sandbox を piece-level で設定し movement で上書きできる', () => {
|
||||
const raw = {
|
||||
name: 'claude-sandbox',
|
||||
piece_config: {
|
||||
provider_options: {
|
||||
claude: {
|
||||
sandbox: { allow_unsandboxed_commands: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
movements: [
|
||||
{
|
||||
name: 'inherit',
|
||||
instruction: '{task}',
|
||||
},
|
||||
{
|
||||
name: 'override',
|
||||
provider_options: {
|
||||
claude: {
|
||||
sandbox: {
|
||||
allow_unsandboxed_commands: false,
|
||||
excluded_commands: ['./gradlew'],
|
||||
},
|
||||
},
|
||||
},
|
||||
instruction: '{task}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const config = normalizePieceConfig(raw, process.cwd());
|
||||
|
||||
expect(config.providerOptions).toEqual({
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
});
|
||||
expect(config.movements[0]?.providerOptions).toEqual({
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
});
|
||||
expect(config.movements[1]?.providerOptions).toEqual({
|
||||
claude: {
|
||||
sandbox: {
|
||||
allowUnsandboxedCommands: false,
|
||||
excludedCommands: ['./gradlew'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeProviderOptions', () => {
|
||||
it('複数層を正しくマージする(後の層が優先)', () => {
|
||||
const global = {
|
||||
claude: { sandbox: { allowUnsandboxedCommands: false, excludedCommands: ['./gradlew'] } },
|
||||
codex: { networkAccess: true },
|
||||
};
|
||||
const local = {
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
};
|
||||
const step = {
|
||||
codex: { networkAccess: false },
|
||||
};
|
||||
|
||||
const result = mergeProviderOptions(global, local, step);
|
||||
|
||||
expect(result).toEqual({
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true, excludedCommands: ['./gradlew'] } },
|
||||
codex: { networkAccess: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('すべて undefined なら undefined を返す', () => {
|
||||
expect(mergeProviderOptions(undefined, undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,8 +5,9 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { basename, dirname } from 'node:path';
|
||||
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
||||
import { mergeProviderOptions } from '../infra/config/loaders/pieceParser.js';
|
||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||
import type { AgentResponse, CustomAgentConfig, MovementProviderOptions } from '../core/models/index.js';
|
||||
import { createLogger } from '../shared/utils/index.js';
|
||||
import { loadTemplate } from '../shared/prompts/index.js';
|
||||
import type { RunAgentOptions } from './types.js';
|
||||
@ -92,6 +93,24 @@ export class AgentRunner {
|
||||
return `${dir}/${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve provider options with 4-layer priority: Global < Local < Step (piece+movement merged).
|
||||
* Step already contains the piece+movement merge result from pieceParser.
|
||||
*/
|
||||
private static resolveProviderOptions(
|
||||
cwd: string,
|
||||
stepOptions?: MovementProviderOptions,
|
||||
): MovementProviderOptions | undefined {
|
||||
let globalOptions: MovementProviderOptions | undefined;
|
||||
try {
|
||||
globalOptions = loadGlobalConfig().providerOptions;
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const localOptions = loadProjectConfig(cwd).provider_options;
|
||||
|
||||
return mergeProviderOptions(globalOptions, localOptions, stepOptions);
|
||||
}
|
||||
|
||||
/** Build ProviderCallOptions from RunAgentOptions */
|
||||
private static buildCallOptions(
|
||||
resolvedProvider: ProviderType,
|
||||
@ -107,7 +126,7 @@ export class AgentRunner {
|
||||
maxTurns: options.maxTurns,
|
||||
model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig),
|
||||
permissionMode: options.permissionMode,
|
||||
providerOptions: options.providerOptions,
|
||||
providerOptions: AgentRunner.resolveProviderOptions(options.cwd, options.providerOptions),
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/types.js';
|
||||
import type { PermissionMode, Language, McpServerConfig } from '../core/models/index.js';
|
||||
import type { PermissionMode, Language, McpServerConfig, MovementProviderOptions } from '../core/models/index.js';
|
||||
|
||||
export type { StreamCallback };
|
||||
|
||||
@ -25,10 +25,7 @@ export interface RunAgentOptions {
|
||||
/** Permission mode for tool execution (from piece step) */
|
||||
permissionMode?: PermissionMode;
|
||||
/** Provider-specific movement options */
|
||||
providerOptions?: {
|
||||
codex?: { networkAccess?: boolean };
|
||||
opencode?: { networkAccess?: boolean };
|
||||
};
|
||||
providerOptions?: MovementProviderOptions;
|
||||
onStream?: StreamCallback;
|
||||
onPermissionRequest?: PermissionHandler;
|
||||
onAskUserQuestion?: AskUserQuestionHandler;
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
* Configuration types (global and project)
|
||||
*/
|
||||
|
||||
import type { MovementProviderOptions } from './piece-types.js';
|
||||
|
||||
/** Custom agent configuration */
|
||||
export interface CustomAgentConfig {
|
||||
name: string;
|
||||
@ -86,6 +88,8 @@ export interface GlobalConfig {
|
||||
pieceCategoriesFile?: string;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, 'claude' | 'codex' | 'opencode' | 'mock'>;
|
||||
/** Global provider-specific options (lowest priority) */
|
||||
providerOptions?: MovementProviderOptions;
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branchNameStrategy?: 'romaji' | 'ai';
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
@ -107,4 +111,5 @@ export interface ProjectConfig {
|
||||
piece?: string;
|
||||
agents?: CustomAgentConfig[];
|
||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||
providerOptions?: MovementProviderOptions;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export type {
|
||||
PartResult,
|
||||
TeamLeaderConfig,
|
||||
PieceRule,
|
||||
MovementProviderOptions,
|
||||
PieceMovement,
|
||||
ArpeggioMovementConfig,
|
||||
ArpeggioMergeMovementConfig,
|
||||
|
||||
@ -92,10 +92,24 @@ export interface OpenCodeProviderOptions {
|
||||
networkAccess?: boolean;
|
||||
}
|
||||
|
||||
/** Claude sandbox settings (maps to SDK SandboxSettings) */
|
||||
export interface ClaudeSandboxSettings {
|
||||
/** Allow all Bash commands to run outside the sandbox */
|
||||
allowUnsandboxedCommands?: boolean;
|
||||
/** Specific commands to exclude from sandbox (e.g., ["./gradlew", "npm test"]) */
|
||||
excludedCommands?: string[];
|
||||
}
|
||||
|
||||
/** Claude provider-specific options */
|
||||
export interface ClaudeProviderOptions {
|
||||
sandbox?: ClaudeSandboxSettings;
|
||||
}
|
||||
|
||||
/** Provider-specific movement options */
|
||||
export interface MovementProviderOptions {
|
||||
codex?: CodexProviderOptions;
|
||||
opencode?: OpenCodeProviderOptions;
|
||||
claude?: ClaudeProviderOptions;
|
||||
}
|
||||
|
||||
/** Single movement in a piece */
|
||||
|
||||
@ -59,6 +59,12 @@ export const StatusSchema = z.enum([
|
||||
|
||||
/** Permission mode schema for tool execution */
|
||||
export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']);
|
||||
/** Claude sandbox settings schema */
|
||||
export const ClaudeSandboxSchema = z.object({
|
||||
allow_unsandboxed_commands: z.boolean().optional(),
|
||||
excluded_commands: z.array(z.string()).optional(),
|
||||
}).optional();
|
||||
|
||||
/** Provider-specific movement options schema */
|
||||
export const MovementProviderOptionsSchema = z.object({
|
||||
codex: z.object({
|
||||
@ -67,6 +73,9 @@ export const MovementProviderOptionsSchema = z.object({
|
||||
opencode: z.object({
|
||||
network_access: z.boolean().optional(),
|
||||
}).optional(),
|
||||
claude: z.object({
|
||||
sandbox: ClaudeSandboxSchema,
|
||||
}).optional(),
|
||||
}).optional();
|
||||
|
||||
/** Piece-level provider options schema */
|
||||
@ -414,6 +423,8 @@ export const GlobalConfigSchema = z.object({
|
||||
piece_categories_file: z.string().optional(),
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'opencode', 'mock'])).optional(),
|
||||
/** Global provider-specific options (lowest priority) */
|
||||
provider_options: MovementProviderOptionsSchema,
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
@ -441,4 +452,5 @@ export const ProjectConfigSchema = z.object({
|
||||
piece: z.string().optional(),
|
||||
agents: z.array(CustomAgentConfigSchema).optional(),
|
||||
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(),
|
||||
provider_options: MovementProviderOptionsSchema,
|
||||
});
|
||||
|
||||
@ -37,6 +37,7 @@ export type {
|
||||
OutputContractItem,
|
||||
OutputContractEntry,
|
||||
McpServerConfig,
|
||||
MovementProviderOptions,
|
||||
PieceMovement,
|
||||
ArpeggioMovementConfig,
|
||||
ArpeggioMergeMovementConfig,
|
||||
|
||||
@ -52,6 +52,7 @@ export class ClaudeClient {
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
anthropicApiKey: options.anthropicApiKey,
|
||||
outputSchema: options.outputSchema,
|
||||
sandbox: options.sandbox,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -95,6 +95,10 @@ export class SdkOptionsBuilder {
|
||||
sdkOptions.stderr = this.options.onStderr;
|
||||
}
|
||||
|
||||
if (this.options.sandbox) {
|
||||
sdkOptions.sandbox = this.options.sandbox;
|
||||
}
|
||||
|
||||
return sdkOptions;
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
* used throughout the Claude integration layer.
|
||||
*/
|
||||
|
||||
import type { PermissionUpdate, AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PermissionUpdate, AgentDefinition, SandboxSettings } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PermissionMode, McpServerConfig } from '../../core/models/index.js';
|
||||
|
||||
export type { SandboxSettings };
|
||||
import type { PermissionResult } from '../../core/piece/index.js';
|
||||
|
||||
// Re-export PermissionResult for convenience
|
||||
@ -145,6 +147,8 @@ export interface ClaudeCallOptions {
|
||||
anthropicApiKey?: string;
|
||||
/** JSON Schema for structured output */
|
||||
outputSchema?: Record<string, unknown>;
|
||||
/** Sandbox settings for Claude SDK */
|
||||
sandbox?: SandboxSettings;
|
||||
}
|
||||
|
||||
/** Options for spawning a Claude SDK query (low-level, used by executor/process) */
|
||||
@ -176,4 +180,6 @@ export interface ClaudeSpawnOptions {
|
||||
outputSchema?: Record<string, unknown>;
|
||||
/** Callback for stderr output from the Claude Code process */
|
||||
onStderr?: (data: string) => void;
|
||||
/** Sandbox settings for Claude SDK */
|
||||
sandbox?: SandboxSettings;
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
||||
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
||||
import { normalizeProviderOptions } from '../loaders/pieceParser.js';
|
||||
import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js';
|
||||
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
||||
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
||||
@ -124,6 +125,7 @@ export class GlobalConfigManager {
|
||||
bookmarksFile: parsed.bookmarks_file,
|
||||
pieceCategoriesFile: parsed.piece_categories_file,
|
||||
personaProviders: parsed.persona_providers,
|
||||
providerOptions: normalizeProviderOptions(parsed.provider_options),
|
||||
branchNameStrategy: parsed.branch_name_strategy,
|
||||
preventSleep: parsed.prevent_sleep,
|
||||
notificationSound: parsed.notification_sound,
|
||||
|
||||
@ -24,34 +24,61 @@ import {
|
||||
|
||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||
|
||||
function normalizeProviderOptions(
|
||||
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
||||
|
||||
/** Convert raw YAML provider_options (snake_case) to internal format (camelCase). */
|
||||
export function normalizeProviderOptions(
|
||||
raw: RawStep['provider_options'],
|
||||
): PieceMovement['providerOptions'] {
|
||||
): MovementProviderOptions | undefined {
|
||||
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 } : {}) };
|
||||
const result: MovementProviderOptions = {};
|
||||
if (raw.codex?.network_access !== undefined) {
|
||||
result.codex = { networkAccess: raw.codex.network_access };
|
||||
}
|
||||
if (raw.opencode?.network_access !== undefined) {
|
||||
result.opencode = { networkAccess: raw.opencode.network_access };
|
||||
}
|
||||
if (raw.claude?.sandbox) {
|
||||
result.claude = {
|
||||
sandbox: {
|
||||
...(raw.claude.sandbox.allow_unsandboxed_commands !== undefined
|
||||
? { allowUnsandboxedCommands: raw.claude.sandbox.allow_unsandboxed_commands }
|
||||
: {}),
|
||||
...(raw.claude.sandbox.excluded_commands !== undefined
|
||||
? { excludedCommands: raw.claude.sandbox.excluded_commands }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* Deep merge provider options. Later sources override earlier ones.
|
||||
* Exported for reuse in runner.ts (4-layer resolution).
|
||||
*/
|
||||
export function mergeProviderOptions(
|
||||
...layers: (MovementProviderOptions | undefined)[]
|
||||
): MovementProviderOptions | undefined {
|
||||
const result: MovementProviderOptions = {};
|
||||
|
||||
const codex = codexNetworkAccess === undefined ? undefined : { networkAccess: codexNetworkAccess };
|
||||
const opencode = opencodeNetworkAccess === undefined ? undefined : { networkAccess: opencodeNetworkAccess };
|
||||
for (const layer of layers) {
|
||||
if (!layer) continue;
|
||||
if (layer.codex) {
|
||||
result.codex = { ...result.codex, ...layer.codex };
|
||||
}
|
||||
if (layer.opencode) {
|
||||
result.opencode = { ...result.opencode, ...layer.opencode };
|
||||
}
|
||||
if (layer.claude?.sandbox) {
|
||||
result.claude = {
|
||||
sandbox: { ...result.claude?.sandbox, ...layer.claude.sandbox },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!codex && !opencode) return undefined;
|
||||
return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) };
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
/** Check if a raw output contract item is the object form (has 'name' property). */
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { PieceCategoryConfigNode } from '../../core/models/schemas.js';
|
||||
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
||||
|
||||
/** Permission mode for the project
|
||||
* - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts)
|
||||
@ -22,6 +23,8 @@ export interface ProjectLocalConfig {
|
||||
permissionMode?: PermissionMode;
|
||||
/** Verbose output mode */
|
||||
verbose?: boolean;
|
||||
/** Provider-specific options (overrides global, overridden by piece/movement) */
|
||||
provider_options?: MovementProviderOptions;
|
||||
/** Piece categories (name -> piece list) */
|
||||
piece_categories?: Record<string, PieceCategoryConfigNode>;
|
||||
/** Show uncategorized pieces under Others category */
|
||||
|
||||
@ -9,6 +9,7 @@ import type { AgentResponse } from '../../core/models/index.js';
|
||||
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||
|
||||
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
||||
const claudeSandbox = options.providerOptions?.claude?.sandbox;
|
||||
return {
|
||||
cwd: options.cwd,
|
||||
abortSignal: options.abortSignal,
|
||||
@ -24,6 +25,10 @@ function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||
outputSchema: options.outputSchema,
|
||||
sandbox: claudeSandbox ? {
|
||||
allowUnsandboxedCommands: claudeSandbox.allowUnsandboxedCommands,
|
||||
excludedCommands: claudeSandbox.excludedCommands,
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/index.js';
|
||||
import type { AgentResponse, PermissionMode, McpServerConfig } from '../../core/models/index.js';
|
||||
import type { AgentResponse, PermissionMode, McpServerConfig, MovementProviderOptions } from '../../core/models/index.js';
|
||||
|
||||
/** Agent setup configuration — determines HOW the provider invokes the agent */
|
||||
export interface AgentSetup {
|
||||
@ -31,10 +31,7 @@ export interface ProviderCallOptions {
|
||||
/** Permission mode for tool execution (from piece step) */
|
||||
permissionMode?: PermissionMode;
|
||||
/** Provider-specific movement options */
|
||||
providerOptions?: {
|
||||
codex?: { networkAccess?: boolean };
|
||||
opencode?: { networkAccess?: boolean };
|
||||
};
|
||||
providerOptions?: MovementProviderOptions;
|
||||
onStream?: StreamCallback;
|
||||
onPermissionRequest?: PermissionHandler;
|
||||
onAskUserQuestion?: AskUserQuestionHandler;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user