fix: handle permissions=skip correctly in codex plugin (#337)
* fix: handle permissions=skip correctly in codex plugin * refactor: rename permission mode to permissionless and align agent mappings --------- Co-authored-by: Jayesh Sharma <wjayesh@outlook.com>
This commit is contained in:
parent
8801502871
commit
ef40c529f8
|
|
@ -45,7 +45,11 @@ projects:
|
|||
|
||||
# Agent-specific config
|
||||
# agentConfig:
|
||||
# permissions: skip # --dangerously-skip-permissions
|
||||
# permissions: permissionless # modes: permissionless | default | auto-edit | suggest
|
||||
# # - permissionless: no interactive permission prompts
|
||||
# # - default: agent defaults
|
||||
# # - auto-edit: auto-approve edits where supported
|
||||
# # - suggest: conservative/untrusted-approval mode where supported
|
||||
# model: opus
|
||||
|
||||
# Inline rules included in every agent prompt for this project
|
||||
|
|
|
|||
|
|
@ -51,9 +51,14 @@ const NotifierConfigSchema = z
|
|||
})
|
||||
.passthrough();
|
||||
|
||||
const AgentPermissionSchema = z
|
||||
.enum(["permissionless", "default", "auto-edit", "suggest", "skip"])
|
||||
.default("permissionless")
|
||||
.transform((value) => (value === "skip" ? "permissionless" : value));
|
||||
|
||||
const AgentSpecificConfigSchema = z
|
||||
.object({
|
||||
permissions: z.enum(["skip", "default"]).default("skip"),
|
||||
permissions: AgentPermissionSchema,
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
|
|||
|
|
@ -718,11 +718,11 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
|
|||
}
|
||||
|
||||
// Get agent launch config — uses systemPromptFile, no issue/tracker interaction.
|
||||
// Orchestrator ALWAYS gets skip permissions — it must run ao CLI commands autonomously.
|
||||
// Orchestrator ALWAYS gets permissionless mode — it must run ao CLI commands autonomously.
|
||||
const agentLaunchConfig = {
|
||||
sessionId,
|
||||
projectConfig: project,
|
||||
permissions: "skip" as const,
|
||||
permissions: "permissionless" as const,
|
||||
model: project.agentConfig?.model,
|
||||
systemPromptFile,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ export interface AgentLaunchConfig {
|
|||
projectConfig: ProjectConfig;
|
||||
issueId?: string;
|
||||
prompt?: string;
|
||||
permissions?: "skip" | "default";
|
||||
permissions?: AgentPermissionMode;
|
||||
model?: string;
|
||||
/**
|
||||
* System prompt to pass to the agent for orchestrator context.
|
||||
|
|
@ -922,11 +922,43 @@ export interface NotifierConfig {
|
|||
}
|
||||
|
||||
export interface AgentSpecificConfig {
|
||||
permissions?: "skip" | "default";
|
||||
permissions?: AgentPermissionMode;
|
||||
model?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical cross-agent permission policy mode.
|
||||
*
|
||||
* Semantics:
|
||||
* - permissionless: run without interactive permission prompts (most permissive mode).
|
||||
* - default: use the agent's normal/default permission model.
|
||||
* - auto-edit: automatically approve edit actions where the agent supports granular approval policies.
|
||||
* - suggest: conservative mode that asks for approval on higher-risk/untrusted actions where supported.
|
||||
*
|
||||
* Note: Not every agent exposes all granular policies; plugins map these modes to
|
||||
* their closest supported behavior.
|
||||
*/
|
||||
export type AgentPermissionMode = "permissionless" | "default" | "auto-edit" | "suggest";
|
||||
|
||||
/** Backward-compatible legacy alias accepted in config parsing. */
|
||||
export type LegacyAgentPermissionMode = "skip";
|
||||
|
||||
/** Raw permission input (supports legacy aliases). */
|
||||
export type AgentPermissionInput = AgentPermissionMode | LegacyAgentPermissionMode;
|
||||
|
||||
/** Normalize legacy aliases to canonical permission modes. */
|
||||
export function normalizeAgentPermissionMode(
|
||||
mode: string | undefined,
|
||||
): AgentPermissionMode | undefined {
|
||||
if (!mode) return undefined;
|
||||
if (mode !== "permissionless" && mode !== "default" && mode !== "auto-edit" && mode !== "suggest") {
|
||||
if (mode === "skip") return "permissionless";
|
||||
return undefined;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PLUGIN SYSTEM
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -114,8 +114,20 @@ describe("getLaunchCommand", () => {
|
|||
expect(agent.getLaunchCommand(makeLaunchConfig())).toBe("aider");
|
||||
});
|
||||
|
||||
it("includes --yes when permissions=skip", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "skip" }));
|
||||
it("includes --yes when permissions=permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "permissionless" }));
|
||||
expect(cmd).toContain("--yes");
|
||||
});
|
||||
|
||||
it("treats legacy permissions=skip as permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "skip" as unknown as AgentLaunchConfig["permissions"] }),
|
||||
);
|
||||
expect(cmd).toContain("--yes");
|
||||
});
|
||||
|
||||
it("maps permissions=auto-edit to no-prompt mode on Aider", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "auto-edit" }));
|
||||
expect(cmd).toContain("--yes");
|
||||
});
|
||||
|
||||
|
|
@ -131,7 +143,7 @@ describe("getLaunchCommand", () => {
|
|||
|
||||
it("combines all options", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "skip", model: "sonnet", prompt: "Go" }),
|
||||
makeLaunchConfig({ permissions: "permissionless", model: "sonnet", prompt: "Go" }),
|
||||
);
|
||||
expect(cmd).toBe("aider --yes --model 'sonnet' --message 'Go'");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ import { constants } from "node:fs";
|
|||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function normalizePermissionMode(mode: string | undefined): "permissionless" | "default" | "auto-edit" | "suggest" | undefined {
|
||||
if (!mode) return undefined;
|
||||
if (mode === "skip") return "permissionless";
|
||||
if (mode === "permissionless" || mode === "default" || mode === "auto-edit" || mode === "suggest") {
|
||||
return mode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Aider Activity Detection Helpers
|
||||
// =============================================================================
|
||||
|
|
@ -75,7 +84,8 @@ function createAiderAgent(): Agent {
|
|||
getLaunchCommand(config: AgentLaunchConfig): string {
|
||||
const parts: string[] = ["aider"];
|
||||
|
||||
if (config.permissions === "skip") {
|
||||
const permissionMode = normalizePermissionMode(config.permissions);
|
||||
if (permissionMode === "permissionless" || permissionMode === "auto-edit") {
|
||||
parts.push("--yes");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,20 @@ describe("getLaunchCommand", () => {
|
|||
expect(cmd).not.toContain("unset");
|
||||
});
|
||||
|
||||
it("includes --dangerously-skip-permissions when permissions=skip", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "skip" }));
|
||||
it("includes --dangerously-skip-permissions when permissions=permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "permissionless" }));
|
||||
expect(cmd).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
it("treats legacy permissions=skip as permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "skip" as unknown as AgentLaunchConfig["permissions"] }),
|
||||
);
|
||||
expect(cmd).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
it("maps permissions=auto-edit to no-prompt mode on Claude", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "auto-edit" }));
|
||||
expect(cmd).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
|
|
@ -167,7 +179,7 @@ describe("getLaunchCommand", () => {
|
|||
|
||||
it("combines all options without prompt", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "skip", model: "opus", prompt: "Hello" }),
|
||||
makeLaunchConfig({ permissions: "permissionless", model: "opus", prompt: "Hello" }),
|
||||
);
|
||||
expect(cmd).toBe("claude --dangerously-skip-permissions --model 'opus'");
|
||||
});
|
||||
|
|
@ -658,4 +670,3 @@ describe("getSessionInfo", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ import { promisify } from "node:util";
|
|||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function normalizePermissionMode(mode: string | undefined): "permissionless" | "default" | "auto-edit" | "suggest" | undefined {
|
||||
if (!mode) return undefined;
|
||||
if (mode === "skip") return "permissionless";
|
||||
if (mode === "permissionless" || mode === "default" || mode === "auto-edit" || mode === "suggest") {
|
||||
return mode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Metadata Updater Hook Script
|
||||
// =============================================================================
|
||||
|
|
@ -635,7 +644,8 @@ function createClaudeCodeAgent(): Agent {
|
|||
// This command must be safe for both shell and execFile contexts.
|
||||
const parts: string[] = ["claude"];
|
||||
|
||||
if (config.permissions === "skip") {
|
||||
const permissionMode = normalizePermissionMode(config.permissions);
|
||||
if (permissionMode === "permissionless" || permissionMode === "auto-edit") {
|
||||
parts.push("--dangerously-skip-permissions");
|
||||
}
|
||||
|
||||
|
|
@ -794,7 +804,8 @@ function createClaudeCodeAgent(): Agent {
|
|||
// Build resume command
|
||||
const parts: string[] = ["claude", "--resume", shellEscape(sessionUuid)];
|
||||
|
||||
if (project.agentConfig?.permissions === "skip") {
|
||||
const permissionMode = normalizePermissionMode(project.agentConfig?.permissions);
|
||||
if (permissionMode === "permissionless" || permissionMode === "auto-edit") {
|
||||
parts.push("--dangerously-skip-permissions");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { Session, RuntimeHandle, AgentLaunchConfig } from "@composio/ao-core";
|
||||
import type { Session, RuntimeHandle, AgentLaunchConfig, AgentSpecificConfig } from "@composio/ao-core";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hoisted mocks — available inside vi.mock factories
|
||||
|
|
@ -213,24 +213,27 @@ describe("getLaunchCommand", () => {
|
|||
expect(agent.getLaunchCommand(makeLaunchConfig())).toBe("'codex'");
|
||||
});
|
||||
|
||||
it("includes --dangerously-bypass-approvals-and-sandbox when permissions=skip", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "skip" }));
|
||||
it("includes bypass flag when permissions=permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "permissionless" }));
|
||||
expect(cmd).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
expect(cmd).not.toContain("--ask-for-approval");
|
||||
expect(cmd).not.toContain("--full-auto");
|
||||
});
|
||||
|
||||
it("includes --ask-for-approval never when permissions=auto-edit", () => {
|
||||
// Cast needed: "auto-edit" not yet in AgentLaunchConfig type union
|
||||
it("treats legacy permissions=skip as permissionless", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "auto-edit" as AgentLaunchConfig["permissions"] }),
|
||||
makeLaunchConfig({ permissions: "skip" as unknown as AgentLaunchConfig["permissions"] }),
|
||||
);
|
||||
expect(cmd).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
});
|
||||
|
||||
it("includes --ask-for-approval never when permissions=auto-edit", () => {
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "auto-edit" }));
|
||||
expect(cmd).toContain("--ask-for-approval never");
|
||||
});
|
||||
|
||||
it("includes --ask-for-approval untrusted when permissions=suggest", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "suggest" as AgentLaunchConfig["permissions"] }),
|
||||
);
|
||||
const cmd = agent.getLaunchCommand(makeLaunchConfig({ permissions: "suggest" }));
|
||||
expect(cmd).toContain("--ask-for-approval untrusted");
|
||||
});
|
||||
|
||||
|
|
@ -253,7 +256,7 @@ describe("getLaunchCommand", () => {
|
|||
|
||||
it("combines all options", () => {
|
||||
const cmd = agent.getLaunchCommand(
|
||||
makeLaunchConfig({ permissions: "skip", model: "o3", prompt: "Go" }),
|
||||
makeLaunchConfig({ permissions: "permissionless", model: "o3", prompt: "Go" }),
|
||||
);
|
||||
expect(cmd).toBe("'codex' --dangerously-bypass-approvals-and-sandbox --model 'o3' -c model_reasoning_effort=high -- 'Go'");
|
||||
});
|
||||
|
|
@ -965,7 +968,7 @@ describe("getRestoreCommand", () => {
|
|||
expect(cmd).toContain("thread-abc-123");
|
||||
});
|
||||
|
||||
it("includes --dangerously-bypass-approvals-and-sandbox from project config", async () => {
|
||||
it("includes bypass flag when project config permissions=permissionless", async () => {
|
||||
const content = jsonl(
|
||||
{ type: "session_meta", cwd: "/workspace/test", model: "gpt-4o" },
|
||||
{ threadId: "thread-1" },
|
||||
|
|
@ -978,7 +981,27 @@ describe("getRestoreCommand", () => {
|
|||
|
||||
const session = makeSession({ workspacePath: "/workspace/test" });
|
||||
const cmd = await agent.getRestoreCommand!(session, makeProjectConfig({
|
||||
agentConfig: { permissions: "skip" },
|
||||
agentConfig: { permissions: "permissionless" },
|
||||
}));
|
||||
|
||||
expect(cmd).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
expect(cmd).not.toContain("--ask-for-approval");
|
||||
});
|
||||
|
||||
it("treats legacy project config permissions=skip as permissionless", async () => {
|
||||
const content = jsonl(
|
||||
{ type: "session_meta", cwd: "/workspace/test", model: "gpt-4o" },
|
||||
{ threadId: "thread-1" },
|
||||
);
|
||||
mockReaddir.mockResolvedValue(["sess.jsonl"]);
|
||||
setupMockOpen(content);
|
||||
setupMockStream(content);
|
||||
mockReadFile.mockResolvedValue(content);
|
||||
mockStat.mockResolvedValue({ mtimeMs: 1000 });
|
||||
|
||||
const session = makeSession({ workspacePath: "/workspace/test" });
|
||||
const cmd = await agent.getRestoreCommand!(session, makeProjectConfig({
|
||||
agentConfig: { permissions: "skip" as unknown as AgentSpecificConfig["permissions"] },
|
||||
}));
|
||||
|
||||
expect(cmd).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ import { randomBytes } from "node:crypto";
|
|||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function normalizePermissionMode(mode: string | undefined): "permissionless" | "default" | "auto-edit" | "suggest" | undefined {
|
||||
if (!mode) return undefined;
|
||||
if (mode === "skip") return "permissionless";
|
||||
if (mode === "permissionless" || mode === "default" || mode === "auto-edit" || mode === "suggest") {
|
||||
return mode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Shared bin directory for ao shell wrappers (prepended to PATH) */
|
||||
const AO_BIN_DIR = join(homedir(), ".ao", "bin");
|
||||
|
||||
|
|
@ -516,11 +525,12 @@ export async function resolveCodexBinary(): Promise<string> {
|
|||
|
||||
/** Append approval-policy flags to a command parts array */
|
||||
function appendApprovalFlags(parts: string[], permissions: string | undefined): void {
|
||||
if (permissions === "skip") {
|
||||
const mode = normalizePermissionMode(permissions);
|
||||
if (mode === "permissionless") {
|
||||
parts.push("--dangerously-bypass-approvals-and-sandbox");
|
||||
} else if (permissions === "auto-edit") {
|
||||
} else if (mode === "auto-edit") {
|
||||
parts.push("--ask-for-approval", "never");
|
||||
} else if (permissions === "suggest") {
|
||||
} else if (mode === "suggest") {
|
||||
parts.push("--ask-for-approval", "untrusted");
|
||||
}
|
||||
}
|
||||
|
|
@ -571,7 +581,7 @@ function createCodexAgent(): Agent {
|
|||
const binary = resolvedBinary ?? "codex";
|
||||
const parts: string[] = [shellEscape(binary)];
|
||||
|
||||
appendApprovalFlags(parts, config.permissions as string | undefined);
|
||||
appendApprovalFlags(parts, config.permissions);
|
||||
appendModelFlags(parts, config.model);
|
||||
|
||||
if (config.systemPromptFile) {
|
||||
|
|
@ -760,7 +770,7 @@ function createCodexAgent(): Agent {
|
|||
const binary = resolvedBinary ?? "codex";
|
||||
const parts: string[] = [shellEscape(binary), "resume"];
|
||||
|
||||
appendApprovalFlags(parts, project.agentConfig?.permissions as string | undefined);
|
||||
appendApprovalFlags(parts, project.agentConfig?.permissions);
|
||||
const effectiveModel = (project.agentConfig?.model ?? data.model) as string | undefined;
|
||||
appendModelFlags(parts, effectiveModel ?? undefined);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue