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 { 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', () => {
|
describe('normalizePieceConfig provider_options', () => {
|
||||||
it('piece-level global を movement に継承し、movement 側で上書きできる', () => {
|
it('piece-level global を movement に継承し、movement 側で上書きできる', () => {
|
||||||
@ -43,4 +43,78 @@ describe('normalizePieceConfig provider_options', () => {
|
|||||||
opencode: { networkAccess: false },
|
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 { existsSync, readFileSync } from 'node:fs';
|
||||||
import { basename, dirname } from 'node:path';
|
import { basename, dirname } from 'node:path';
|
||||||
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
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 { 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 { createLogger } from '../shared/utils/index.js';
|
||||||
import { loadTemplate } from '../shared/prompts/index.js';
|
import { loadTemplate } from '../shared/prompts/index.js';
|
||||||
import type { RunAgentOptions } from './types.js';
|
import type { RunAgentOptions } from './types.js';
|
||||||
@ -92,6 +93,24 @@ export class AgentRunner {
|
|||||||
return `${dir}/${name}`;
|
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 */
|
/** Build ProviderCallOptions from RunAgentOptions */
|
||||||
private static buildCallOptions(
|
private static buildCallOptions(
|
||||||
resolvedProvider: ProviderType,
|
resolvedProvider: ProviderType,
|
||||||
@ -107,7 +126,7 @@ export class AgentRunner {
|
|||||||
maxTurns: options.maxTurns,
|
maxTurns: options.maxTurns,
|
||||||
model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig),
|
model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig),
|
||||||
permissionMode: options.permissionMode,
|
permissionMode: options.permissionMode,
|
||||||
providerOptions: options.providerOptions,
|
providerOptions: AgentRunner.resolveProviderOptions(options.cwd, options.providerOptions),
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/types.js';
|
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 };
|
export type { StreamCallback };
|
||||||
|
|
||||||
@ -25,10 +25,7 @@ export interface RunAgentOptions {
|
|||||||
/** Permission mode for tool execution (from piece step) */
|
/** Permission mode for tool execution (from piece step) */
|
||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
/** Provider-specific movement options */
|
/** Provider-specific movement options */
|
||||||
providerOptions?: {
|
providerOptions?: MovementProviderOptions;
|
||||||
codex?: { networkAccess?: boolean };
|
|
||||||
opencode?: { networkAccess?: boolean };
|
|
||||||
};
|
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
* Configuration types (global and project)
|
* Configuration types (global and project)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { MovementProviderOptions } from './piece-types.js';
|
||||||
|
|
||||||
/** Custom agent configuration */
|
/** Custom agent configuration */
|
||||||
export interface CustomAgentConfig {
|
export interface CustomAgentConfig {
|
||||||
name: string;
|
name: string;
|
||||||
@ -86,6 +88,8 @@ export interface GlobalConfig {
|
|||||||
pieceCategoriesFile?: string;
|
pieceCategoriesFile?: string;
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
personaProviders?: Record<string, 'claude' | 'codex' | 'opencode' | 'mock'>;
|
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) */
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branchNameStrategy?: 'romaji' | 'ai';
|
branchNameStrategy?: 'romaji' | 'ai';
|
||||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||||
@ -107,4 +111,5 @@ export interface ProjectConfig {
|
|||||||
piece?: string;
|
piece?: string;
|
||||||
agents?: CustomAgentConfig[];
|
agents?: CustomAgentConfig[];
|
||||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||||
|
providerOptions?: MovementProviderOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type {
|
|||||||
PartResult,
|
PartResult,
|
||||||
TeamLeaderConfig,
|
TeamLeaderConfig,
|
||||||
PieceRule,
|
PieceRule,
|
||||||
|
MovementProviderOptions,
|
||||||
PieceMovement,
|
PieceMovement,
|
||||||
ArpeggioMovementConfig,
|
ArpeggioMovementConfig,
|
||||||
ArpeggioMergeMovementConfig,
|
ArpeggioMergeMovementConfig,
|
||||||
|
|||||||
@ -92,10 +92,24 @@ export interface OpenCodeProviderOptions {
|
|||||||
networkAccess?: boolean;
|
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 */
|
/** Provider-specific movement options */
|
||||||
export interface MovementProviderOptions {
|
export interface MovementProviderOptions {
|
||||||
codex?: CodexProviderOptions;
|
codex?: CodexProviderOptions;
|
||||||
opencode?: OpenCodeProviderOptions;
|
opencode?: OpenCodeProviderOptions;
|
||||||
|
claude?: ClaudeProviderOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single movement in a piece */
|
/** Single movement in a piece */
|
||||||
|
|||||||
@ -59,6 +59,12 @@ export const StatusSchema = z.enum([
|
|||||||
|
|
||||||
/** Permission mode schema for tool execution */
|
/** Permission mode schema for tool execution */
|
||||||
export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']);
|
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 */
|
/** Provider-specific movement options schema */
|
||||||
export const MovementProviderOptionsSchema = z.object({
|
export const MovementProviderOptionsSchema = z.object({
|
||||||
codex: z.object({
|
codex: z.object({
|
||||||
@ -67,6 +73,9 @@ export const MovementProviderOptionsSchema = z.object({
|
|||||||
opencode: z.object({
|
opencode: z.object({
|
||||||
network_access: z.boolean().optional(),
|
network_access: z.boolean().optional(),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
|
claude: z.object({
|
||||||
|
sandbox: ClaudeSandboxSchema,
|
||||||
|
}).optional(),
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
/** Piece-level provider options schema */
|
/** Piece-level provider options schema */
|
||||||
@ -414,6 +423,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
piece_categories_file: z.string().optional(),
|
piece_categories_file: z.string().optional(),
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'opencode', 'mock'])).optional(),
|
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 generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
||||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||||
@ -441,4 +452,5 @@ export const ProjectConfigSchema = z.object({
|
|||||||
piece: z.string().optional(),
|
piece: z.string().optional(),
|
||||||
agents: z.array(CustomAgentConfigSchema).optional(),
|
agents: z.array(CustomAgentConfigSchema).optional(),
|
||||||
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(),
|
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(),
|
||||||
|
provider_options: MovementProviderOptionsSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export type {
|
|||||||
OutputContractItem,
|
OutputContractItem,
|
||||||
OutputContractEntry,
|
OutputContractEntry,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
|
MovementProviderOptions,
|
||||||
PieceMovement,
|
PieceMovement,
|
||||||
ArpeggioMovementConfig,
|
ArpeggioMovementConfig,
|
||||||
ArpeggioMergeMovementConfig,
|
ArpeggioMergeMovementConfig,
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export class ClaudeClient {
|
|||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
anthropicApiKey: options.anthropicApiKey,
|
anthropicApiKey: options.anthropicApiKey,
|
||||||
outputSchema: options.outputSchema,
|
outputSchema: options.outputSchema,
|
||||||
|
sandbox: options.sandbox,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,6 +95,10 @@ export class SdkOptionsBuilder {
|
|||||||
sdkOptions.stderr = this.options.onStderr;
|
sdkOptions.stderr = this.options.onStderr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.sandbox) {
|
||||||
|
sdkOptions.sandbox = this.options.sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
return sdkOptions;
|
return sdkOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,10 @@
|
|||||||
* used throughout the Claude integration layer.
|
* 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';
|
import type { PermissionMode, McpServerConfig } from '../../core/models/index.js';
|
||||||
|
|
||||||
|
export type { SandboxSettings };
|
||||||
import type { PermissionResult } from '../../core/piece/index.js';
|
import type { PermissionResult } from '../../core/piece/index.js';
|
||||||
|
|
||||||
// Re-export PermissionResult for convenience
|
// Re-export PermissionResult for convenience
|
||||||
@ -145,6 +147,8 @@ export interface ClaudeCallOptions {
|
|||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
/** JSON Schema for structured output */
|
/** JSON Schema for structured output */
|
||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
|
/** Sandbox settings for Claude SDK */
|
||||||
|
sandbox?: SandboxSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for spawning a Claude SDK query (low-level, used by executor/process) */
|
/** Options for spawning a Claude SDK query (low-level, used by executor/process) */
|
||||||
@ -176,4 +180,6 @@ export interface ClaudeSpawnOptions {
|
|||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
/** Callback for stderr output from the Claude Code process */
|
/** Callback for stderr output from the Claude Code process */
|
||||||
onStderr?: (data: string) => void;
|
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 { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||||
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
||||||
import type { GlobalConfig, DebugConfig, Language } 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 { getGlobalConfigPath, getProjectConfigPath } from '../paths.js';
|
||||||
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
||||||
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
||||||
@ -124,6 +125,7 @@ export class GlobalConfigManager {
|
|||||||
bookmarksFile: parsed.bookmarks_file,
|
bookmarksFile: parsed.bookmarks_file,
|
||||||
pieceCategoriesFile: parsed.piece_categories_file,
|
pieceCategoriesFile: parsed.piece_categories_file,
|
||||||
personaProviders: parsed.persona_providers,
|
personaProviders: parsed.persona_providers,
|
||||||
|
providerOptions: normalizeProviderOptions(parsed.provider_options),
|
||||||
branchNameStrategy: parsed.branch_name_strategy,
|
branchNameStrategy: parsed.branch_name_strategy,
|
||||||
preventSleep: parsed.prevent_sleep,
|
preventSleep: parsed.prevent_sleep,
|
||||||
notificationSound: parsed.notification_sound,
|
notificationSound: parsed.notification_sound,
|
||||||
|
|||||||
@ -24,34 +24,61 @@ import {
|
|||||||
|
|
||||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
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'],
|
raw: RawStep['provider_options'],
|
||||||
): PieceMovement['providerOptions'] {
|
): MovementProviderOptions | undefined {
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
|
|
||||||
const codex = raw.codex?.network_access === undefined
|
const result: MovementProviderOptions = {};
|
||||||
? undefined
|
if (raw.codex?.network_access !== undefined) {
|
||||||
: { networkAccess: raw.codex.network_access };
|
result.codex = { networkAccess: raw.codex.network_access };
|
||||||
const opencode = raw.opencode?.network_access === undefined
|
}
|
||||||
? undefined
|
if (raw.opencode?.network_access !== undefined) {
|
||||||
: { networkAccess: raw.opencode.network_access };
|
result.opencode = { networkAccess: raw.opencode.network_access };
|
||||||
|
}
|
||||||
if (!codex && !opencode) return undefined;
|
if (raw.claude?.sandbox) {
|
||||||
return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) };
|
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'],
|
* Deep merge provider options. Later sources override earlier ones.
|
||||||
override: PieceMovement['providerOptions'],
|
* Exported for reuse in runner.ts (4-layer resolution).
|
||||||
): PieceMovement['providerOptions'] {
|
*/
|
||||||
const codexNetworkAccess = override?.codex?.networkAccess ?? base?.codex?.networkAccess;
|
export function mergeProviderOptions(
|
||||||
const opencodeNetworkAccess = override?.opencode?.networkAccess ?? base?.opencode?.networkAccess;
|
...layers: (MovementProviderOptions | undefined)[]
|
||||||
|
): MovementProviderOptions | undefined {
|
||||||
|
const result: MovementProviderOptions = {};
|
||||||
|
|
||||||
const codex = codexNetworkAccess === undefined ? undefined : { networkAccess: codexNetworkAccess };
|
for (const layer of layers) {
|
||||||
const opencode = opencodeNetworkAccess === undefined ? undefined : { networkAccess: opencodeNetworkAccess };
|
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 Object.keys(result).length > 0 ? result : undefined;
|
||||||
return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a raw output contract item is the object form (has 'name' property). */
|
/** 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 { PieceCategoryConfigNode } from '../../core/models/schemas.js';
|
||||||
|
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
||||||
|
|
||||||
/** Permission mode for the project
|
/** Permission mode for the project
|
||||||
* - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts)
|
* - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts)
|
||||||
@ -22,6 +23,8 @@ export interface ProjectLocalConfig {
|
|||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
/** Verbose output mode */
|
/** Verbose output mode */
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
|
/** Provider-specific options (overrides global, overridden by piece/movement) */
|
||||||
|
provider_options?: MovementProviderOptions;
|
||||||
/** Piece categories (name -> piece list) */
|
/** Piece categories (name -> piece list) */
|
||||||
piece_categories?: Record<string, PieceCategoryConfigNode>;
|
piece_categories?: Record<string, PieceCategoryConfigNode>;
|
||||||
/** Show uncategorized pieces under Others category */
|
/** 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';
|
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
||||||
|
const claudeSandbox = options.providerOptions?.claude?.sandbox;
|
||||||
return {
|
return {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
abortSignal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
@ -24,6 +25,10 @@ function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
|||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||||
outputSchema: options.outputSchema,
|
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 { 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 */
|
/** Agent setup configuration — determines HOW the provider invokes the agent */
|
||||||
export interface AgentSetup {
|
export interface AgentSetup {
|
||||||
@ -31,10 +31,7 @@ export interface ProviderCallOptions {
|
|||||||
/** Permission mode for tool execution (from piece step) */
|
/** Permission mode for tool execution (from piece step) */
|
||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
/** Provider-specific movement options */
|
/** Provider-specific movement options */
|
||||||
providerOptions?: {
|
providerOptions?: MovementProviderOptions;
|
||||||
codex?: { networkAccess?: boolean };
|
|
||||||
opencode?: { networkAccess?: boolean };
|
|
||||||
};
|
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user