Implement granular tool permissions (#82)

* Granular tool permissions

* Fix build

* Start on UI

* Fixes

* Fmt

* Go ham on UI

* Update migrations

* Considate wiki files

* Clean up

* More tweaks. Docs.

* Consolidate collab and noncollab

* Fix build

* Restrict for non-collaborators
This commit is contained in:
Colin McDonnell
2026-01-15 08:05:30 +00:00
committed by pullfrog[bot]
parent 4547b0032e
commit 97dce099c1
29 changed files with 800 additions and 2052 deletions
+23 -25
View File
@@ -3,7 +3,7 @@ import type { Effort } from "../external.ts";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, createAgentEnv, installFromNpmTarball } from "./shared.ts";
import { agent, createAgentEnv, installFromNpmTarball, type ToolPermissions } from "./shared.ts";
// Model selection based on effort level
// Note: mini uses Haiku for speed, auto uses opusplan for balance, max uses Opus for capability
@@ -19,6 +19,20 @@ const claudeEffortModels: Record<Effort, string> = {
// See: https://platform.claude.com/docs/en/build-with-claude/effort
// This approach could replace model selection if effort proves effective for controlling capability.
/**
* Build disallowedTools list from ToolPermissions.
*/
function buildDisallowedTools(tools: ToolPermissions): string[] {
const disallowed: string[] = [];
if (tools.web === "disabled") disallowed.push("WebFetch");
if (tools.search === "disabled") disallowed.push("WebSearch");
if (tools.write === "disabled") disallowed.push("Write");
// both "disabled" and "restricted" block native bash
// "restricted" means use MCP bash tool instead
if (tools.bash !== "enabled") disallowed.push("Bash");
return disallowed;
}
export const claude = agent({
name: "claude",
install: async () => {
@@ -29,44 +43,28 @@ export const claude = agent({
executablePath: "cli.js",
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => {
// Ensure API key is NOT in process.env - only pass via SDK's env option
delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions({ payload, repo });
const prompt = addInstructions({ payload, repo, tools });
log.group("Full prompt", () => log.info(prompt));
// select model based on effort level
const model = claudeEffortModels[effort];
log.info(`Using model: ${model} (effort: ${effort})`);
// SECURITY: For PUBLIC repos, Claude Code spawns subprocesses with full process.env, leaking API keys.
// disable native Bash; agents use MCP bash tool which filters secrets.
// for private repos, native Bash is allowed since secrets are less exposed.
const disallowedTools = repo.isPublic ? ["Bash"] : [];
const sandboxOptions: Options = payload.sandbox
? {
permissionMode: "default",
disallowedTools: ["Bash", "WebSearch", "WebFetch", "Write"],
async canUseTool(toolName, input, _options) {
if (toolName.startsWith("mcp__gh_pullfrog__"))
return { behavior: "allow", updatedInput: input, updatedPermissions: [] };
return { behavior: "deny", message: "tool not allowed in sandbox mode" };
},
}
: {
permissionMode: "bypassPermissions" as const,
disallowedTools,
};
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
// build disallowedTools based on tool permissions
const disallowedTools = buildDisallowedTools(tools);
if (disallowedTools.length > 0) {
log.info(`🔒 disallowed tools: ${disallowedTools.join(", ")}`);
}
// Pass secrets via SDK's env option only (not process.env)
// This ensures secrets are only available to Claude Code subprocess, not user code
const queryOptions: Options = {
...sandboxOptions,
permissionMode: "bypassPermissions" as const,
disallowedTools,
mcpServers,
model,
pathToClaudeCodeExecutable: cliPath,
+40 -42
View File
@@ -11,7 +11,12 @@ import {
import type { Effort } from "../external.ts";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
import {
agent,
installFromNpmTarball,
setupProcessAgentEnv,
type ToolPermissions,
} from "./shared.ts";
// model configuration based on effort level
const codexModel: Record<Effort, string> = {
@@ -33,10 +38,10 @@ const codexReasoningEffort: Record<Effort, ModelReasoningEffort | undefined> = {
interface WriteCodexConfigParams {
tempHome: string;
mcpServers: Record<string, McpHttpServerConfig>;
isPublicRepo: boolean;
tools: ToolPermissions;
}
function writeCodexConfig({ tempHome, mcpServers, isPublicRepo }: WriteCodexConfigParams): string {
function writeCodexConfig({ tempHome, mcpServers, tools }: WriteCodexConfigParams): string {
const codexDir = join(tempHome, ".codex");
mkdirSync(codexDir, { recursive: true });
const configPath = join(codexDir, "config.toml");
@@ -45,32 +50,32 @@ function writeCodexConfig({ tempHome, mcpServers, isPublicRepo }: WriteCodexConf
const mcpServerSections: string[] = [];
for (const [name, config] of Object.entries(mcpServers)) {
if (config.type !== "http") continue;
log.info(`» Adding MCP server '${name}' at ${config.url}`);
log.info(`» adding MCP server '${name}' at ${config.url}`);
mcpServerSections.push(`[mcp_servers.${name}]\nurl = "${config.url}"`);
}
// SECURITY: for public repos, enforce env filtering via shell_environment_policy
// this prevents vuln if user's ~/.codex/config.toml has ignore_default_excludes=true
// for private repos, no filtering - agents use native shell with full env access
const shellPolicy = isPublicRepo
? `[shell_environment_policy]
ignore_default_excludes = false`
: "";
// build features section for tool control
// disable native shell if bash is "disabled" or "restricted"
// when "restricted", agent uses MCP bash tool which filters secrets
const features: string[] = [];
if (tools.bash !== "enabled") {
features.push("shell_command_tool = false");
features.push("unified_exec = false");
}
const featuresSection = features.length > 0 ? `[features]\n${features.join("\n")}` : "";
writeFileSync(
configPath,
`# written by pullfrog
${shellPolicy}
${featuresSection}
${mcpServerSections.join("\n\n")}
`.trim() + "\n"
);
if (isPublicRepo) {
log.info(`» Codex config written to ${configPath} (env filtering: enabled)`);
} else {
log.info(`» Codex config written to ${configPath} (private repo: no env filtering)`);
}
log.info(
`» Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})`
);
return codexDir;
}
@@ -84,7 +89,7 @@ export const codex = agent({
executablePath: "bin/codex.js",
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR!;
// create config directory for codex before setting HOME
@@ -94,7 +99,7 @@ export const codex = agent({
const codexDir = writeCodexConfig({
tempHome,
mcpServers,
isPublicRepo: repo.isPublic,
tools,
});
setupProcessAgentEnv({
@@ -117,36 +122,29 @@ export const codex = agent({
codexPathOverride: cliPath,
};
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
}
const codex = new Codex(codexOptions);
// Build thread options with model and optional model_reasoning_effort
const baseThreadOptions = payload.sandbox
? {
model,
approvalPolicy: "never" as const,
sandboxMode: "read-only" as const,
networkAccessEnabled: false,
}
: {
model,
approvalPolicy: "never" as const,
// use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
sandboxMode: "danger-full-access" as const,
networkAccessEnabled: true,
};
// build thread options based on tool permissions
const threadOptions: ThreadOptions = {
model,
approvalPolicy: "never" as const,
// write: "disabled" → read-only sandbox, otherwise full access for git ops
sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access",
// web: controls network access
networkAccessEnabled: tools.web !== "disabled",
// search: controls web search
webSearchEnabled: tools.search !== "disabled",
...(modelReasoningEffort && { modelReasoningEffort }),
};
const threadOptions: ThreadOptions = modelReasoningEffort
? { ...baseThreadOptions, modelReasoningEffort }
: baseThreadOptions;
log.info(
`🔧 Codex options: sandboxMode=${threadOptions.sandboxMode}, networkAccessEnabled=${threadOptions.networkAccessEnabled}, webSearchEnabled=${threadOptions.webSearchEnabled}`
);
const thread = codex.startThread(threadOptions);
try {
const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo }));
const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools }));
let finalOutput = "";
for await (const event of streamedTurn.events) {
+39 -42
View File
@@ -10,6 +10,7 @@ import {
type ConfigureMcpServersParams,
createAgentEnv,
installFromCurl,
type ToolPermissions,
} from "./shared.ts";
// effort configuration for Cursor
@@ -100,9 +101,9 @@ export const cursor = agent({
executableName: "cursor-agent",
});
},
run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort }) => {
run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => {
configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false, isPublicRepo: repo.isPublic });
configureCursorTools({ tools });
// determine model based on effort level
// respect project's .cursor/cli.json if it specifies a model
@@ -201,11 +202,10 @@ export const cursor = agent({
};
try {
const fullPrompt = addInstructions({ payload, repo });
const fullPrompt = addInstructions({ payload, repo, tools });
log.group("Full prompt", () => log.info(fullPrompt));
// configure sandbox mode if enabled
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
// build CLI args
const baseArgs = ["--print", fullPrompt, "--output-format", "stream-json", "--approve-mcps"];
// add model flag if we have an override
@@ -213,13 +213,8 @@ export const cursor = agent({
baseArgs.push("--model", modelOverride);
}
const cursorArgs = payload.sandbox
? baseArgs // --force removed in sandbox mode to enforce safety checks
: [...baseArgs, "--force"];
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
}
// always use --force since permissions are controlled via cli-config.json
const cursorArgs = [...baseArgs, "--force"];
log.info("Running Cursor CLI...");
@@ -347,48 +342,50 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
log.info(`» MCP config written to ${mcpConfigPath}`);
}
interface ConfigureCursorToolsParams {
tools: ToolPermissions;
}
/**
* Configure Cursor CLI sandbox mode via cli-config.json.
*
* SECURITY: For PUBLIC repos, Cursor spawns subprocesses with full process.env, leaking API keys.
* We deny native Shell via Shell(*) rule, forcing use of MCP bash tool which
* filters secrets. Note: Shell(**) does NOT work, must use Shell(*).
* For private repos, native Shell is allowed.
* Configure Cursor CLI tool permissions via cli-config.json.
*
* Config path: $XDG_CONFIG_HOME/cursor/ (not ~/.cursor/) because createAgentEnv
* sets XDG_CONFIG_HOME=$HOME/.config. See issues/cursor-perms.md.
* sets XDG_CONFIG_HOME=$HOME/.config.
*/
function configureCursorSandbox({
sandbox,
isPublicRepo,
}: {
sandbox: boolean;
isPublicRepo: boolean;
}): void {
function configureCursorTools({ tools }: ConfigureCursorToolsParams): void {
const realHome = homedir();
const cursorConfigDir = join(realHome, ".config", "cursor");
const cliConfigPath = join(cursorConfigDir, "cli-config.json");
mkdirSync(cursorConfigDir, { recursive: true });
// deny native shell for public repos to prevent secret leakage
const denyShell = isPublicRepo ? ["Shell(*)"] : [];
// build deny list based on tool permissions
const deny: string[] = [];
if (tools.search === "disabled") deny.push("WebSearch");
if (tools.write === "disabled") deny.push("Write(**)");
// both "disabled" and "restricted" block native shell
if (tools.bash !== "enabled") deny.push("Shell(*)");
const config = sandbox
? {
permissions: {
allow: ["Read(**)"],
deny: ["Write(**)", ...denyShell],
},
}
: {
permissions: {
allow: ["Read(**)", "Write(**)"],
deny: denyShell,
},
};
// web: "disabled" requires sandbox with network blocking
// sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt
const needsSandbox = tools.web === "disabled";
const config: Record<string, unknown> = {
permissions: {
allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"],
deny,
},
};
if (needsSandbox) {
config.sandbox = {
mode: "enabled",
networkAccess: "allowlist",
};
}
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
log.info(`» CLI config written to ${cliConfigPath}`);
log.info(
`» CLI config written to ${cliConfigPath} (sandbox: ${sandbox}, isPublicRepo: ${isPublicRepo})`
`🔧 Cursor permissions: allow=${(config.permissions as { allow: string[] }).allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}`
);
}
+27 -44
View File
@@ -10,6 +10,7 @@ import {
type ConfigureMcpServersParams,
createAgentEnv,
installFromGithub,
type ToolPermissions,
} from "./shared.ts";
// effort configuration: model + thinking level
@@ -171,48 +172,23 @@ export const gemini = agent({
...(githubInstallationToken && { githubInstallationToken }),
});
},
run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort }) => {
run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => {
// get model and thinking level based on effort
const { model, thinkingLevel } = geminiEffortConfig[effort];
log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`);
configureGeminiSettings({ mcpServers, isPublicRepo: repo.isPublic, thinkingLevel });
configureGeminiSettings({ mcpServers, tools, thinkingLevel });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
const sessionPrompt = addInstructions({ payload, repo });
const sessionPrompt = addInstructions({ payload, repo, tools });
log.group("Full prompt", () => log.info(sessionPrompt));
// build CLI args based on sandbox mode
// for public repos, native shell is disabled via excludeTools in settings.json
let args: string[];
if (payload.sandbox) {
// sandbox mode: read-only tools only
args = [
"--model",
model,
"--allowed-tools",
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
"--allowed-mcp-server-names",
"gh_pullfrog",
"--output-format=stream-json",
"-p",
sessionPrompt,
];
} else {
// normal mode: --yolo for auto-approval
// for public repos, shell is excluded via settings.json excludeTools
args = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt];
if (repo.isPublic) {
log.info("🔒 public repo: native shell disabled via excludeTools, using MCP bash");
}
}
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
}
// build CLI args - --yolo for auto-approval
// tool restrictions handled via settings.json tools.exclude
const args = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt];
let finalOutput = "";
let stdoutBuffer = "";
@@ -294,23 +270,23 @@ export const gemini = agent({
},
});
type ConfigureGeminiParams = {
interface ConfigureGeminiParams {
mcpServers: ConfigureMcpServersParams["mcpServers"];
isPublicRepo: boolean;
tools: ToolPermissions;
thinkingLevel: string;
};
}
/**
* Configure Gemini CLI settings by writing to settings.json.
* - MCP servers: uses `httpUrl` for HTTP/streamable transport
* - thinkingLevel: configured via modelConfig.generateContentConfig.thinkingConfig
* - For public repos, excludeTools disables native shell
* - tools.exclude: disables native tools based on ToolPermissions (v0.3.0+ format)
*
* See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md
*/
function configureGeminiSettings({
mcpServers,
isPublicRepo,
tools,
thinkingLevel,
}: ConfigureGeminiParams): void {
const realHome = homedir();
@@ -328,7 +304,7 @@ function configureGeminiSettings({
}
// convert to Gemini's expected format (httpUrl for HTTP transport, no type field)
type GeminiMcpServerConfig = {
interface GeminiMcpServerConfig {
command?: string;
args?: string[];
env?: Record<string, string>;
@@ -341,7 +317,7 @@ function configureGeminiSettings({
description?: string;
includeTools?: string[];
excludeTools?: string[];
};
}
const geminiMcpServers: Record<string, GeminiMcpServerConfig> = {};
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (serverConfig.type !== "http") {
@@ -353,9 +329,16 @@ function configureGeminiSettings({
httpUrl: serverConfig.url,
trust: true, // trust our own MCP server to avoid confirmation prompts
};
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
log.info(`adding MCP server '${serverName}' at ${serverConfig.url}...`);
}
// build tools.exclude based on permissions (v0.3.0+ nested format)
const exclude: string[] = [];
if (tools.bash !== "enabled") exclude.push("run_shell_command");
if (tools.write === "disabled") exclude.push("write_file");
if (tools.web === "disabled") exclude.push("web_fetch");
if (tools.search === "disabled") exclude.push("google_web_search");
// merge with existing settings, overwriting mcpServers and modelConfig
const newSettings: Record<string, unknown> = {
...existingSettings,
@@ -369,13 +352,13 @@ function configureGeminiSettings({
},
},
},
// v0.3.0+ nested format
...(exclude.length > 0 && { tools: { exclude } }),
};
// for public repos, exclude native shell tool to prevent secret leakage via env
if (isPublicRepo) {
newSettings.excludeTools = ["run_shell_command"];
}
writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8");
log.info(`» Gemini settings written to ${settingsPath}`);
if (exclude.length > 0) {
log.info(`🔒 excluded tools: ${exclude.join(", ")}`);
}
}
+22 -9
View File
@@ -3,6 +3,7 @@ import { encode as toonEncode } from "@toon-format/toon";
import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts";
import { getModes } from "../modes.ts";
import type { ToolPermissions } from "./shared.ts";
interface RepoInfo {
owner: string;
@@ -54,12 +55,28 @@ function buildRuntimeContext(repo: RepoInfo): string {
interface AddInstructionsParams {
payload: Payload;
repo: RepoInfo;
tools: ToolPermissions;
}
export const addInstructions = ({ payload, repo }: AddInstructionsParams) => {
// for public repos, always use MCP bash for security (filters secrets)
// for private repos, agents can use their native bash
const useNativeBash = !repo.isPublic;
/**
* Generate shell instructions based on bash permission level.
*/
function getShellInstructions(bash: ToolPermissions["bash"]): string {
switch (bash) {
case "disabled":
return `**Shell commands**: Shell command execution is DISABLED. Do not attempt to run shell commands.`;
case "restricted":
return `**Shell commands**: Use the \`${ghPullfrogMcpName}/bash\` MCP tool for all shell command execution. This tool provides a secure environment with filtered credentials. Do NOT use any native shell/bash tool - it is disabled for security.`;
case "enabled":
return `**Shell commands**: Use your native bash/shell tool for shell command execution.`;
default: {
const _exhaustive: never = bash;
return _exhaustive satisfies never;
}
}
}
export const addInstructions = ({ payload, repo, tools }: AddInstructionsParams) => {
let encodedEvent = "";
const eventKeys = Object.keys(payload.event);
@@ -142,11 +159,7 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Efficiency**: Trust the tools - do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error.
${
useNativeBash
? `**Shell commands**: Use your native bash/shell tool for shell command execution.`
: `**Shell commands**: Use the \`${ghPullfrogMcpName}/bash\` MCP tool for all shell command execution. This tool provides a secure environment with filtered credentials. Do NOT use any native shell/bash tool - it is disabled for security.`
}
${getShellInstructions(tools.bash)}
**Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished.
+19 -34
View File
@@ -1,5 +1,4 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { log } from "../utils/cli.ts";
import { spawn } from "../utils/subprocess.ts";
@@ -10,6 +9,7 @@ import {
createAgentEnv,
installFromNpmTarball,
setupProcessAgentEnv,
type ToolPermissions,
} from "./shared.ts";
export const opencode = agent({
@@ -30,28 +30,21 @@ export const opencode = agent({
cliPath,
repo,
effort: _effort,
tools,
}) => {
// 1. configure home/config directory
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "opencode");
mkdirSync(configDir, { recursive: true });
configureOpenCode({
mcpServers,
sandbox: payload.sandbox ?? false,
isPublicRepo: repo.isPublic,
});
configureOpenCode({ mcpServers, tools });
const prompt = addInstructions({ payload, repo });
const prompt = addInstructions({ payload, repo, tools });
log.group("Full prompt", () => log.info(prompt));
// message positional must come right after "run", before flags
const args = ["run", prompt, "--format", "json"];
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
}
// 6. set up environment
setupProcessAgentEnv({ HOME: tempHome });
@@ -200,15 +193,14 @@ export const opencode = agent({
interface ConfigureOpenCodeParams {
mcpServers: ConfigureMcpServersParams["mcpServers"];
sandbox: boolean;
isPublicRepo: boolean;
tools: ToolPermissions;
}
/**
* Configure OpenCode via opencode.json config file.
* Builds complete config with MCP servers and permissions in a single write to avoid race conditions.
*/
function configureOpenCode({ mcpServers, sandbox, isPublicRepo }: ConfigureOpenCodeParams): void {
function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void {
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "opencode");
mkdirSync(configDir, { recursive: true });
@@ -232,25 +224,15 @@ function configureOpenCode({ mcpServers, sandbox, isPublicRepo }: ConfigureOpenC
};
}
// SECURITY: For PUBLIC repos, OpenCode spawns subprocesses with full process.env, leaking API keys.
// disable native bash; agents use MCP bash tool which filters secrets.
// for private repos, native bash is allowed.
const bashPermission = isPublicRepo ? "deny" : "allow";
const permission = sandbox
? {
edit: "deny",
bash: "deny",
webfetch: "deny",
doom_loop: "allow",
external_directory: "allow",
}
: {
edit: "allow",
bash: bashPermission,
webfetch: "allow",
doom_loop: "allow",
external_directory: "allow",
};
// build permission object based on tool permissions
// note: OpenCode has no built-in web search tool
const permission = {
edit: tools.write === "disabled" ? "deny" : "allow",
bash: tools.bash !== "enabled" ? "deny" : "allow",
webfetch: tools.web === "disabled" ? "deny" : "allow",
doom_loop: "allow",
external_directory: "allow",
};
// build complete config in one object
const config = {
@@ -268,7 +250,10 @@ function configureOpenCode({ mcpServers, sandbox, isPublicRepo }: ConfigureOpenC
throw error;
}
log.info(`» OpenCode config written to ${configPath} (sandbox: ${sandbox})`);
log.info(`» OpenCode config written to ${configPath}`);
log.info(
`🔧 OpenCode permissions: edit=${permission.edit}, bash=${permission.bash}, webfetch=${permission.webfetch}`
);
log.debug(`OpenCode config contents:\n${configJson}`);
}
+17
View File
@@ -36,6 +36,22 @@ export interface RepoInfo {
isPublic: boolean;
}
/**
* Tool permission levels
*/
export type ToolPermission = "disabled" | "enabled";
export type BashPermission = "disabled" | "restricted" | "enabled";
/**
* Granular tool permissions for agents
*/
export interface ToolPermissions {
web: ToolPermission;
search: ToolPermission;
write: ToolPermission;
bash: BashPermission;
}
/**
* Configuration for agent creation
*/
@@ -47,6 +63,7 @@ export interface AgentConfig {
cliPath: string;
repo: RepoInfo;
effort: Effort;
tools: ToolPermissions;
}
/**