Merge pull request #1831 from paperclipai/pr/pap-891-opencode-headless-prompts
fix(opencode): support headless permission prompt configuration
This commit is contained in:
commit
aa5b2be907
9 changed files with 652 additions and 430 deletions
|
|
@ -22,6 +22,7 @@ Core fields:
|
||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||||
- model (string, required): OpenCode model id in provider/model format (for example anthropic/claude-sonnet-4-5)
|
- model (string, required): OpenCode model id in provider/model format (for example anthropic/claude-sonnet-4-5)
|
||||||
- variant (string, optional): provider-specific model variant (for example minimal|low|medium|high|max)
|
- variant (string, optional): provider-specific model variant (for example minimal|low|medium|high|max)
|
||||||
|
- dangerouslySkipPermissions (boolean, optional): inject a runtime OpenCode config that allows \`external_directory\` access without interactive prompts; defaults to true for unattended Paperclip runs
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
- command (string, optional): defaults to "opencode"
|
- command (string, optional): defaults to "opencode"
|
||||||
- extraArgs (string[], optional): additional CLI args
|
- extraArgs (string[], optional): additional CLI args
|
||||||
|
|
@ -40,4 +41,7 @@ Notes:
|
||||||
- The adapter sets OPENCODE_DISABLE_PROJECT_CONFIG=true to prevent OpenCode from \
|
- The adapter sets OPENCODE_DISABLE_PROJECT_CONFIG=true to prevent OpenCode from \
|
||||||
writing an opencode.json config file into the project working directory. Model \
|
writing an opencode.json config file into the project working directory. Model \
|
||||||
selection is passed via the --model CLI flag instead.
|
selection is passed via the --model CLI flag instead.
|
||||||
|
- When \`dangerouslySkipPermissions\` is enabled, Paperclip injects a temporary \
|
||||||
|
runtime config with \`permission.external_directory=allow\` so headless runs do \
|
||||||
|
not stall on approval prompts.
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { isOpenCodeUnknownSessionError, parseOpenCodeJsonl } from "./parse.js";
|
import { isOpenCodeUnknownSessionError, parseOpenCodeJsonl } from "./parse.js";
|
||||||
import { ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
import { ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||||
import { removeMaintainerOnlySkillSymlinks } from "@paperclipai/adapter-utils/server-utils";
|
import { removeMaintainerOnlySkillSymlinks } from "@paperclipai/adapter-utils/server-utils";
|
||||||
|
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
|
@ -177,8 +178,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (!hasExplicitApiKey && authToken) {
|
if (!hasExplicitApiKey && authToken) {
|
||||||
env.PAPERCLIP_API_KEY = authToken;
|
env.PAPERCLIP_API_KEY = authToken;
|
||||||
}
|
}
|
||||||
|
const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({ env, config });
|
||||||
|
try {
|
||||||
const runtimeEnv = Object.fromEntries(
|
const runtimeEnv = Object.fromEntries(
|
||||||
Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter(
|
Object.entries(ensurePathInEnv({ ...process.env, ...preparedRuntimeConfig.env })).filter(
|
||||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -236,16 +239,19 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandNotes = (() => {
|
const commandNotes = (() => {
|
||||||
if (!resolvedInstructionsFilePath) return [] as string[];
|
const notes = [...preparedRuntimeConfig.notes];
|
||||||
|
if (!resolvedInstructionsFilePath) return notes;
|
||||||
if (instructionsPrefix.length > 0) {
|
if (instructionsPrefix.length > 0) {
|
||||||
return [
|
notes.push(`Loaded agent instructions from ${resolvedInstructionsFilePath}`);
|
||||||
`Loaded agent instructions from ${resolvedInstructionsFilePath}`,
|
notes.push(
|
||||||
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
||||||
];
|
);
|
||||||
|
return notes;
|
||||||
}
|
}
|
||||||
return [
|
notes.push(
|
||||||
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
||||||
];
|
);
|
||||||
|
return notes;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
||||||
|
|
@ -296,7 +302,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: [...args, `<stdin prompt ${prompt.length} chars>`],
|
commandArgs: [...args, `<stdin prompt ${prompt.length} chars>`],
|
||||||
env: redactEnvForLogs(env),
|
env: redactEnvForLogs(preparedRuntimeConfig.env),
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
@ -404,4 +410,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
}
|
}
|
||||||
|
|
||||||
return toResult(initial);
|
return toResult(initial);
|
||||||
|
} finally {
|
||||||
|
await preparedRuntimeConfig.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||||
|
|
||||||
|
const cleanupPaths = new Set<string>();
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
[...cleanupPaths].map(async (filepath) => {
|
||||||
|
await fs.rm(filepath, { recursive: true, force: true });
|
||||||
|
cleanupPaths.delete(filepath);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function makeConfigHome(initialConfig?: Record<string, unknown>) {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-test-"));
|
||||||
|
cleanupPaths.add(root);
|
||||||
|
const configDir = path.join(root, "opencode");
|
||||||
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
|
if (initialConfig) {
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir, "opencode.json"),
|
||||||
|
`${JSON.stringify(initialConfig, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("prepareOpenCodeRuntimeConfig", () => {
|
||||||
|
it("injects an external_directory allow rule by default", async () => {
|
||||||
|
const configHome = await makeConfigHome({
|
||||||
|
permission: {
|
||||||
|
read: "allow",
|
||||||
|
},
|
||||||
|
theme: "system",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepared = await prepareOpenCodeRuntimeConfig({
|
||||||
|
env: { XDG_CONFIG_HOME: configHome },
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
|
||||||
|
|
||||||
|
expect(prepared.env.XDG_CONFIG_HOME).not.toBe(configHome);
|
||||||
|
const runtimeConfig = JSON.parse(
|
||||||
|
await fs.readFile(
|
||||||
|
path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
) as Record<string, unknown>;
|
||||||
|
expect(runtimeConfig).toMatchObject({
|
||||||
|
theme: "system",
|
||||||
|
permission: {
|
||||||
|
read: "allow",
|
||||||
|
external_directory: "allow",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prepared.cleanup();
|
||||||
|
cleanupPaths.delete(prepared.env.XDG_CONFIG_HOME);
|
||||||
|
await expect(fs.access(prepared.env.XDG_CONFIG_HOME)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects explicit opt-out", async () => {
|
||||||
|
const configHome = await makeConfigHome();
|
||||||
|
const prepared = await prepareOpenCodeRuntimeConfig({
|
||||||
|
env: { XDG_CONFIG_HOME: configHome },
|
||||||
|
config: { dangerouslySkipPermissions: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prepared.env).toEqual({ XDG_CONFIG_HOME: configHome });
|
||||||
|
expect(prepared.notes).toEqual([]);
|
||||||
|
await prepared.cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { asBoolean } from "@paperclipai/adapter-utils/server-utils";
|
||||||
|
|
||||||
|
type PreparedOpenCodeRuntimeConfig = {
|
||||||
|
env: Record<string, string>;
|
||||||
|
notes: string[];
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveXdgConfigHome(env: Record<string, string>): string {
|
||||||
|
return (
|
||||||
|
(typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()) ||
|
||||||
|
(typeof process.env.XDG_CONFIG_HOME === "string" && process.env.XDG_CONFIG_HOME.trim()) ||
|
||||||
|
path.join(os.homedir(), ".config")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonObject(filepath: string): Promise<Record<string, unknown>> {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filepath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return isPlainObject(parsed) ? parsed : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareOpenCodeRuntimeConfig(input: {
|
||||||
|
env: Record<string, string>;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
}): Promise<PreparedOpenCodeRuntimeConfig> {
|
||||||
|
const skipPermissions = asBoolean(input.config.dangerouslySkipPermissions, true);
|
||||||
|
if (!skipPermissions) {
|
||||||
|
return {
|
||||||
|
env: input.env,
|
||||||
|
notes: [],
|
||||||
|
cleanup: async () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceConfigDir = path.join(resolveXdgConfigHome(input.env), "opencode");
|
||||||
|
const runtimeConfigHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-config-"));
|
||||||
|
const runtimeConfigDir = path.join(runtimeConfigHome, "opencode");
|
||||||
|
const runtimeConfigPath = path.join(runtimeConfigDir, "opencode.json");
|
||||||
|
|
||||||
|
await fs.mkdir(runtimeConfigDir, { recursive: true });
|
||||||
|
try {
|
||||||
|
await fs.cp(sourceConfigDir, runtimeConfigDir, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
dereference: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as NodeJS.ErrnoException | null)?.code !== "ENOENT") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConfig = await readJsonObject(runtimeConfigPath);
|
||||||
|
const existingPermission = isPlainObject(existingConfig.permission)
|
||||||
|
? existingConfig.permission
|
||||||
|
: {};
|
||||||
|
const nextConfig = {
|
||||||
|
...existingConfig,
|
||||||
|
permission: {
|
||||||
|
...existingPermission,
|
||||||
|
external_directory: "allow",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.writeFile(runtimeConfigPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
||||||
|
|
||||||
|
return {
|
||||||
|
env: {
|
||||||
|
...input.env,
|
||||||
|
XDG_CONFIG_HOME: runtimeConfigHome,
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
"Injected runtime OpenCode config with permission.external_directory=allow to avoid headless approval prompts.",
|
||||||
|
],
|
||||||
|
cleanup: async () => {
|
||||||
|
await fs.rm(runtimeConfigHome, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
AdapterEnvironmentTestResult,
|
AdapterEnvironmentTestResult,
|
||||||
} from "@paperclipai/adapter-utils";
|
} from "@paperclipai/adapter-utils";
|
||||||
import {
|
import {
|
||||||
|
asBoolean,
|
||||||
asString,
|
asString,
|
||||||
asStringArray,
|
asStringArray,
|
||||||
parseObject,
|
parseObject,
|
||||||
|
|
@ -14,6 +15,7 @@ import {
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||||
import { parseOpenCodeJsonl } from "./parse.js";
|
import { parseOpenCodeJsonl } from "./parse.js";
|
||||||
|
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||||
|
|
||||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||||
if (checks.some((check) => check.level === "error")) return "fail";
|
if (checks.some((check) => check.level === "error")) return "fail";
|
||||||
|
|
@ -92,7 +94,16 @@ export async function testEnvironment(
|
||||||
|
|
||||||
// Prevent OpenCode from writing an opencode.json into the working directory.
|
// Prevent OpenCode from writing an opencode.json into the working directory.
|
||||||
env.OPENCODE_DISABLE_PROJECT_CONFIG = "true";
|
env.OPENCODE_DISABLE_PROJECT_CONFIG = "true";
|
||||||
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env }));
|
const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({ env, config });
|
||||||
|
if (asBoolean(config.dangerouslySkipPermissions, true)) {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_headless_permissions_enabled",
|
||||||
|
level: "info",
|
||||||
|
message: "Headless OpenCode external-directory permissions are auto-approved for unattended runs.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...preparedRuntimeConfig.env }));
|
||||||
|
|
||||||
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
|
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
|
||||||
if (cwdInvalid) {
|
if (cwdInvalid) {
|
||||||
|
|
@ -311,6 +322,9 @@ export async function testEnvironment(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await preparedRuntimeConfig.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
adapterType: ctx.adapterType,
|
adapterType: ctx.adapterType,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record<string,
|
||||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||||
if (v.model) ac.model = v.model;
|
if (v.model) ac.model = v.model;
|
||||||
if (v.thinkingEffort) ac.variant = v.thinkingEffort;
|
if (v.thinkingEffort) ac.variant = v.thinkingEffort;
|
||||||
|
ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions;
|
||||||
// OpenCode sessions can run until the CLI exits naturally; keep timeout disabled (0)
|
// OpenCode sessions can run until the CLI exits naturally; keep timeout disabled (0)
|
||||||
// and rely on graceSec for termination handling when a timeout is configured elsewhere.
|
// and rely on graceSec for termination handling when a timeout is configured elsewhere.
|
||||||
ac.timeoutSec = 0;
|
ac.timeoutSec = 0;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import type { AdapterConfigFieldsProps } from "../types";
|
import type { AdapterConfigFieldsProps } from "../types";
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
|
ToggleField,
|
||||||
DraftInput,
|
DraftInput,
|
||||||
|
help,
|
||||||
} from "../../components/agent-config-primitives";
|
} from "../../components/agent-config-primitives";
|
||||||
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
||||||
|
|
||||||
|
|
@ -19,8 +21,9 @@ export function OpenCodeLocalConfigFields({
|
||||||
mark,
|
mark,
|
||||||
hideInstructionsFile,
|
hideInstructionsFile,
|
||||||
}: AdapterConfigFieldsProps) {
|
}: AdapterConfigFieldsProps) {
|
||||||
if (hideInstructionsFile) return null;
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{!hideInstructionsFile && (
|
||||||
<Field label="Agent instructions file" hint={instructionsFileHint}>
|
<Field label="Agent instructions file" hint={instructionsFileHint}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DraftInput
|
<DraftInput
|
||||||
|
|
@ -45,5 +48,25 @@ export function OpenCodeLocalConfigFields({
|
||||||
<ChoosePathButton />
|
<ChoosePathButton />
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
)}
|
||||||
|
<ToggleField
|
||||||
|
label="Skip permissions"
|
||||||
|
hint={help.dangerouslySkipPermissions}
|
||||||
|
checked={
|
||||||
|
isCreate
|
||||||
|
? values!.dangerouslySkipPermissions
|
||||||
|
: eff(
|
||||||
|
"adapterConfig",
|
||||||
|
"dangerouslySkipPermissions",
|
||||||
|
config.dangerouslySkipPermissions !== false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(v) =>
|
||||||
|
isCreate
|
||||||
|
? set!({ dangerouslySkipPermissions: v })
|
||||||
|
: mark("adapterConfig", "dangerouslySkipPermissions", v)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,8 @@ export function OnboardingWizard() {
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
url,
|
url,
|
||||||
dangerouslySkipPermissions: adapterType === "claude_local",
|
dangerouslySkipPermissions:
|
||||||
|
adapterType === "claude_local" || adapterType === "opencode_local",
|
||||||
dangerouslyBypassSandbox:
|
dangerouslyBypassSandbox:
|
||||||
adapterType === "codex_local"
|
adapterType === "codex_local"
|
||||||
? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX
|
? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const help: Record<string, string> = {
|
||||||
model: "Override the default model used by the adapter.",
|
model: "Override the default model used by the adapter.",
|
||||||
thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.",
|
thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.",
|
||||||
chrome: "Enable Claude's Chrome integration by passing --chrome.",
|
chrome: "Enable Claude's Chrome integration by passing --chrome.",
|
||||||
dangerouslySkipPermissions: "Run Claude without permission prompts. Required for unattended operation.",
|
dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.",
|
||||||
dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.",
|
dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.",
|
||||||
search: "Enable Codex web search capability during runs.",
|
search: "Enable Codex web search capability during runs.",
|
||||||
workspaceStrategy: "How Paperclip should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.",
|
workspaceStrategy: "How Paperclip should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue