This commit is contained in:
Colin McDonnell
2026-01-16 18:43:09 +00:00
committed by pullfrog[bot]
parent 101c666610
commit 69b9b96ddd
37 changed files with 1005 additions and 1005 deletions
+8 -7
View File
@@ -21,16 +21,17 @@ const claudeEffortModels: Record<Effort, string> = {
// This approach could replace model selection if effort proves effective for controlling capability.
/**
* Build disallowedTools list from ToolPermissions.
* Build disallowedTools list from payload permissions.
*/
function buildDisallowedTools(ctx: AgentRunContext): string[] {
const disallowed: string[] = [];
if (ctx.tools.web === "disabled") disallowed.push("WebFetch");
if (ctx.tools.search === "disabled") disallowed.push("WebSearch");
if (ctx.tools.write === "disabled") disallowed.push("Write");
if (ctx.payload.web === "disabled") disallowed.push("WebFetch");
if (ctx.payload.search === "disabled") disallowed.push("WebSearch");
if (ctx.payload.write === "disabled") disallowed.push("Write");
// both "disabled" and "restricted" block native bash
// "restricted" means use MCP bash tool instead
if (ctx.tools.bash !== "enabled") disallowed.push("Bash");
const bash = ctx.payload.bash;
if (bash !== "enabled") disallowed.push("Bash");
return disallowed;
}
@@ -51,8 +52,8 @@ export const claude = agent({
const cliPath = await installClaude();
// select model based on effort level
const model = claudeEffortModels[ctx.effort];
log.info(`» using model: ${model} (effort: ${ctx.effort})`);
const model = claudeEffortModels[ctx.payload.effort];
log.info(`» using model: ${model} (effort: ${ctx.payload.effort})`);
// build disallowedTools based on tool permissions
const disallowedTools = buildDisallowedTools(ctx);
+14 -10
View File
@@ -42,8 +42,9 @@ function writeCodexConfig(ctx: AgentRunContext): string {
// 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 bash = ctx.payload.bash;
const features: string[] = [];
if (ctx.tools.bash !== "enabled") {
if (bash !== "enabled") {
features.push("shell_command_tool = false");
features.push("unified_exec = false");
}
@@ -58,9 +59,7 @@ ${mcpServerSections.join("\n\n")}
`.trim() + "\n"
);
log.info(
`» Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})`
);
log.info(`» Codex config written to ${configPath} (shell: ${bash === "enabled" ? "enabled" : "disabled"})`);
return codexDir;
}
@@ -90,16 +89,21 @@ export const codex = agent({
process.env.CODEX_HOME = codexDir;
// get model and reasoning effort based on effort level
const model = codexModel[ctx.effort];
const modelReasoningEffort = codexReasoningEffort[ctx.effort];
const model = codexModel[ctx.payload.effort];
const modelReasoningEffort = codexReasoningEffort[ctx.payload.effort];
log.info(`» using model: ${model}`);
if (modelReasoningEffort) {
log.info(`» using modelReasoningEffort: ${modelReasoningEffort}`);
}
// Configure Codex
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY is required for codex agent");
}
const codexOptions: CodexOptions = {
apiKey: ctx.apiKey,
apiKey,
codexPathOverride: cliPath,
};
@@ -110,11 +114,11 @@ export const codex = agent({
model,
approvalPolicy: "never" as const,
// write: "disabled" → read-only sandbox, otherwise full access for git ops
sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access",
sandboxMode: ctx.payload.write === "disabled" ? "read-only" : "danger-full-access",
// web: controls network access
networkAccessEnabled: ctx.tools.web !== "disabled",
networkAccessEnabled: ctx.payload.web !== "disabled",
// search: controls web search
webSearchEnabled: ctx.tools.search !== "disabled",
webSearchEnabled: ctx.payload.search !== "disabled",
...(modelReasoningEffort && { modelReasoningEffort }),
};
+11 -10
View File
@@ -116,19 +116,19 @@ export const cursor = agent({
if (projectConfig.model) {
log.info(`» using model from project .cursor/cli.json: ${projectConfig.model}`);
} else {
modelOverride = cursorEffortModels[ctx.effort];
modelOverride = cursorEffortModels[ctx.payload.effort];
}
} catch {
modelOverride = cursorEffortModels[ctx.effort];
modelOverride = cursorEffortModels[ctx.payload.effort];
}
} else {
modelOverride = cursorEffortModels[ctx.effort];
modelOverride = cursorEffortModels[ctx.payload.effort];
}
if (modelOverride) {
log.info(`» using model: ${modelOverride}, effort=${ctx.effort}`);
log.info(`» using model: ${modelOverride}, effort=${ctx.payload.effort}`);
} else if (!existsSync(projectCliConfigPath)) {
log.info(`» using default model, effort=${ctx.effort}`);
log.info(`» using default model, effort=${ctx.payload.effort}`);
}
// track logged model_call_ids to avoid duplicates
@@ -354,22 +354,23 @@ function configureCursorTools(ctx: AgentRunContext): void {
mkdirSync(cursorConfigDir, { recursive: true });
// build deny list based on tool permissions
const bash = ctx.payload.bash;
const deny: string[] = [];
if (ctx.tools.search === "disabled") deny.push("WebSearch");
if (ctx.tools.write === "disabled") deny.push("Write(**)");
if (ctx.payload.search === "disabled") deny.push("WebSearch");
if (ctx.payload.write === "disabled") deny.push("Write(**)");
// both "disabled" and "restricted" block native shell
if (ctx.tools.bash !== "enabled") deny.push("Shell(*)");
if (bash !== "enabled") deny.push("Shell(*)");
const config: CursorCliConfig = {
permissions: {
allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"],
allow: ctx.payload.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"],
deny,
},
};
// web: "disabled" requires sandbox with network blocking
// sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt
if (ctx.tools.web === "disabled") {
if (ctx.payload.web === "disabled") {
config.sandbox = {
mode: "enabled",
networkAccess: "allowlist",
+8 -7
View File
@@ -176,8 +176,8 @@ export const gemini = agent({
const model = configureGeminiSettings(ctx);
if (!ctx.apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
if (!process.env.GOOGLE_API_KEY && !process.env.GEMINI_API_KEY) {
throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required for gemini agent");
}
// build CLI args - --yolo for auto-approval
@@ -278,7 +278,7 @@ export const gemini = agent({
* See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md
*/
function configureGeminiSettings(ctx: AgentRunContext): string {
const { model, thinkingLevel } = geminiEffortConfig[ctx.effort];
const { model, thinkingLevel } = geminiEffortConfig[ctx.payload.effort];
log.info(`» using model: ${model}, thinkingLevel: ${thinkingLevel}`);
const realHome = homedir();
@@ -319,11 +319,12 @@ function configureGeminiSettings(ctx: AgentRunContext): string {
};
// build tools.exclude based on permissions (v0.3.0+ nested format)
const bash = ctx.payload.bash;
const exclude: string[] = [];
if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command");
if (ctx.tools.write === "disabled") exclude.push("write_file");
if (ctx.tools.web === "disabled") exclude.push("web_fetch");
if (ctx.tools.search === "disabled") exclude.push("google_web_search");
if (bash !== "enabled") exclude.push("run_shell_command");
if (ctx.payload.write === "disabled") exclude.push("write_file");
if (ctx.payload.web === "disabled") exclude.push("web_fetch");
if (ctx.payload.search === "disabled") exclude.push("google_web_search");
// merge with existing settings, overwriting mcpServers and modelConfig
const newSettings: Record<string, unknown> = {
+4 -3
View File
@@ -186,10 +186,11 @@ function configureOpenCode(ctx: AgentRunContext): void {
// build permission object based on tool permissions
// note: OpenCode has no built-in web search tool
const bash = ctx.payload.bash;
const permission = {
edit: ctx.tools.write === "disabled" ? "deny" : "allow",
bash: ctx.tools.bash !== "enabled" ? "deny" : "allow",
webfetch: ctx.tools.web === "disabled" ? "deny" : "allow",
edit: ctx.payload.write === "disabled" ? "deny" : "allow",
bash: bash !== "enabled" ? "deny" : "allow",
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
doom_loop: "allow",
external_directory: "allow",
};
+9 -25
View File
@@ -1,6 +1,7 @@
import type { show } from "@ark/util";
import { type AgentManifest, type AgentName, agentsManifest, type Effort } from "../external.ts";
import { type AgentManifest, type AgentName, agentsManifest } from "../external.ts";
import { log } from "../utils/cli.ts";
import type { ResolvedPayload } from "../utils/payload.ts";
/**
* Result returned by agent execution
@@ -12,44 +13,27 @@ export interface AgentResult {
metadata?: Record<string, unknown>;
}
/**
* 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;
}
/**
* Minimal context passed to agent.run()
*/
export interface AgentRunContext {
effort: Effort;
tools: ToolPermissions;
payload: ResolvedPayload;
mcpServerUrl: string;
tmpdir: string;
instructions: string;
apiKey: string;
apiKeys: Record<string, string>;
}
export const agent = <const input extends AgentInput>(input: input): defineAgent<input> => {
return {
...input,
run: async (ctx: AgentRunContext): Promise<AgentResult> => {
log.info(`» running ${input.name} with effort=${ctx.effort}...`);
const bash = ctx.payload.bash;
const web = ctx.payload.web;
const search = ctx.payload.search;
const write = ctx.payload.write;
log.info(`» running ${input.name} with effort=${ctx.payload.effort}...`);
log.box(ctx.instructions, { title: "Instructions" });
log.info(
`» tool permissions: web=${ctx.tools.web}, search=${ctx.tools.search}, write=${ctx.tools.write}, bash=${ctx.tools.bash}`
);
log.info(`» tool permissions: web=${web}, search=${search}, write=${write}, bash=${bash}`);
return input.run(ctx);
},
...agentsManifest[input.name],