mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
fix(codex): prefer native dynamic tools
This commit is contained in:
committed by
pashpashpash
parent
ec1b96cdfa
commit
6eb5ce5de7
@@ -579,6 +579,19 @@ If a deployment needs additional environment isolation, add those variables to
|
||||
|
||||
`appServer.clearEnv` only affects the spawned Codex app-server child process.
|
||||
|
||||
Codex dynamic tools default to the `native-first` profile. In that mode,
|
||||
OpenClaw does not expose dynamic tools that duplicate Codex-native workspace
|
||||
operations: `read`, `write`, `edit`, `apply_patch`, `exec`, `process`, and
|
||||
`update_plan`. OpenClaw integration tools such as messaging, sessions, media,
|
||||
cron, browser, nodes, gateway, and `web_search` remain available.
|
||||
|
||||
Supported top-level Codex plugin fields:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| -------------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `codexDynamicToolsProfile` | `"native-first"` | Use `"openclaw-compat"` to expose the full OpenClaw dynamic tool set to Codex app-server. |
|
||||
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
|
||||
|
||||
Supported `appServer` fields:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|
||||
@@ -33,6 +33,16 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"codexDynamicToolsProfile": {
|
||||
"type": "string",
|
||||
"enum": ["native-first", "openclaw-compat"],
|
||||
"default": "native-first"
|
||||
},
|
||||
"codexDynamicToolsExclude": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": []
|
||||
},
|
||||
"discovery": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -141,6 +151,16 @@
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"codexDynamicToolsProfile": {
|
||||
"label": "Dynamic Tools Profile",
|
||||
"help": "Select which OpenClaw dynamic tools are exposed to Codex app-server. native-first omits tools Codex already owns.",
|
||||
"advanced": true
|
||||
},
|
||||
"codexDynamicToolsExclude": {
|
||||
"label": "Dynamic Tool Excludes",
|
||||
"help": "Additional OpenClaw dynamic tool names to omit from Codex app-server turns.",
|
||||
"advanced": true
|
||||
},
|
||||
"discovery": {
|
||||
"label": "Model Discovery",
|
||||
"help": "Plugin-owned controls for discovering Codex app-server models."
|
||||
|
||||
@@ -138,6 +138,18 @@ describe("Codex app-server config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("parses dynamic tool profile controls", () => {
|
||||
expect(
|
||||
readCodexPluginConfig({
|
||||
codexDynamicToolsProfile: "openclaw-compat",
|
||||
codexDynamicToolsExclude: ["custom_tool"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
codexDynamicToolsProfile: "openclaw-compat",
|
||||
codexDynamicToolsExclude: ["custom_tool"],
|
||||
});
|
||||
});
|
||||
|
||||
it("treats configured and environment commands as explicit overrides", () => {
|
||||
expect(
|
||||
resolveCodexAppServerRuntimeOptions({
|
||||
|
||||
@@ -10,6 +10,7 @@ export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure"
|
||||
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
||||
export type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
|
||||
export type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
|
||||
export type CodexDynamicToolsProfile = "native-first" | "openclaw-compat";
|
||||
|
||||
export type CodexComputerUseConfig = {
|
||||
enabled?: boolean;
|
||||
@@ -55,6 +56,8 @@ export type CodexAppServerRuntimeOptions = {
|
||||
};
|
||||
|
||||
export type CodexPluginConfig = {
|
||||
codexDynamicToolsProfile?: CodexDynamicToolsProfile;
|
||||
codexDynamicToolsExclude?: string[];
|
||||
discovery?: {
|
||||
enabled?: boolean;
|
||||
timeoutMs?: number;
|
||||
@@ -120,6 +123,7 @@ const codexAppServerApprovalPolicySchema = z.enum([
|
||||
]);
|
||||
const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]);
|
||||
const codexAppServerApprovalsReviewerSchema = z.enum(["user", "auto_review", "guardian_subagent"]);
|
||||
const codexDynamicToolsProfileSchema = z.enum(["native-first", "openclaw-compat"]);
|
||||
const codexAppServerServiceTierSchema = z
|
||||
.preprocess(
|
||||
(value) => (value === null ? null : resolveServiceTier(value)),
|
||||
@@ -129,6 +133,8 @@ const codexAppServerServiceTierSchema = z
|
||||
|
||||
const codexPluginConfigSchema = z
|
||||
.object({
|
||||
codexDynamicToolsProfile: codexDynamicToolsProfileSchema.optional(),
|
||||
codexDynamicToolsExclude: z.array(z.string()).optional(),
|
||||
discovery: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
@@ -334,6 +334,40 @@ describe("runCodexAppServerAttempt", () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("defaults Codex dynamic tools to the native-first profile", () => {
|
||||
const tools = [
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"exec",
|
||||
"process",
|
||||
"update_plan",
|
||||
"web_search",
|
||||
"message",
|
||||
"sessions_spawn",
|
||||
].map((name) => ({ name }));
|
||||
|
||||
expect(__testing.applyCodexDynamicToolProfile(tools, {}).map((tool) => tool.name)).toEqual([
|
||||
"web_search",
|
||||
"message",
|
||||
"sessions_spawn",
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows Codex dynamic tool filtering to opt back into OpenClaw compatibility", () => {
|
||||
const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name }));
|
||||
|
||||
expect(
|
||||
__testing
|
||||
.applyCodexDynamicToolProfile(tools, {
|
||||
codexDynamicToolsProfile: "openclaw-compat",
|
||||
codexDynamicToolsExclude: ["custom_tool"],
|
||||
})
|
||||
.map((tool) => tool.name),
|
||||
).toEqual(["read", "exec", "message"]);
|
||||
});
|
||||
|
||||
it("returns a failed dynamic tool response when an app-server tool call exceeds the deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
let capturedSignal: AbortSignal | undefined;
|
||||
|
||||
@@ -44,7 +44,11 @@ import {
|
||||
} from "./client-factory.js";
|
||||
import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js";
|
||||
import { ensureCodexComputerUse } from "./computer-use.js";
|
||||
import { resolveCodexAppServerRuntimeOptions } from "./config.js";
|
||||
import {
|
||||
readCodexPluginConfig,
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
type CodexPluginConfig,
|
||||
} from "./config.js";
|
||||
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
|
||||
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
|
||||
@@ -89,6 +93,15 @@ const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 30_000;
|
||||
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
|
||||
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
|
||||
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
|
||||
const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"exec",
|
||||
"process",
|
||||
"update_plan",
|
||||
] as const;
|
||||
|
||||
type OpenClawCodingToolsOptions = NonNullable<
|
||||
Parameters<(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]>[0]
|
||||
@@ -231,7 +244,8 @@ export async function runCodexAppServerAttempt(
|
||||
} = {},
|
||||
): Promise<EmbeddedRunAttemptResult> {
|
||||
const attemptStartedAt = Date.now();
|
||||
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
|
||||
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
|
||||
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
|
||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
||||
@@ -281,6 +295,7 @@ export async function runCodexAppServerAttempt(
|
||||
sandbox,
|
||||
runAbortController,
|
||||
sessionAgentId,
|
||||
pluginConfig,
|
||||
onYieldDetected: () => {
|
||||
yieldDetected = true;
|
||||
},
|
||||
@@ -1232,6 +1247,7 @@ type DynamicToolBuildParams = {
|
||||
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>;
|
||||
runAbortController: AbortController;
|
||||
sessionAgentId: string | undefined;
|
||||
pluginConfig: CodexPluginConfig;
|
||||
onYieldDetected: () => void;
|
||||
};
|
||||
|
||||
@@ -1287,6 +1303,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
|
||||
workspaceDir: input.effectiveWorkspace,
|
||||
}),
|
||||
suppressManagedWebSearch: false,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
@@ -1305,7 +1322,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
input.runAbortController.abort("sessions_yield");
|
||||
},
|
||||
});
|
||||
const visionFilteredTools = filterToolsForVisionInputs(allTools, {
|
||||
const profiledTools = applyCodexDynamicToolProfile(allTools, input.pluginConfig);
|
||||
const visionFilteredTools = filterToolsForVisionInputs(profiledTools, {
|
||||
modelHasVision,
|
||||
hasInboundImages: (params.images?.length ?? 0) > 0,
|
||||
});
|
||||
@@ -1326,6 +1344,26 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
});
|
||||
}
|
||||
|
||||
function applyCodexDynamicToolProfile<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: CodexPluginConfig,
|
||||
): T[] {
|
||||
const excludes = new Set<string>();
|
||||
const profile = config.codexDynamicToolsProfile ?? "native-first";
|
||||
if (profile === "native-first") {
|
||||
for (const name of CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES) {
|
||||
excludes.add(name);
|
||||
}
|
||||
}
|
||||
for (const name of config.codexDynamicToolsExclude ?? []) {
|
||||
const trimmed = name.trim();
|
||||
if (trimmed) {
|
||||
excludes.add(trimmed);
|
||||
}
|
||||
}
|
||||
return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(tool.name));
|
||||
}
|
||||
|
||||
async function withCodexStartupTimeout<T>(params: {
|
||||
timeoutMs: number;
|
||||
timeoutFloorMs?: number;
|
||||
@@ -1499,6 +1537,7 @@ export const __testing = {
|
||||
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
|
||||
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
|
||||
buildCodexNativeHookRelayId,
|
||||
applyCodexDynamicToolProfile,
|
||||
filterToolsForVisionInputs,
|
||||
handleDynamicToolCallWithTimeout,
|
||||
...createCodexAppServerClientFactoryTestHooks((factory) => {
|
||||
|
||||
@@ -68,6 +68,27 @@ describe("applyModelProviderToolPolicy", () => {
|
||||
expect(toolNames(filtered)).toEqual(["read", "exec"]);
|
||||
});
|
||||
|
||||
it("can keep managed web_search for Codex app-server dynamic tools", () => {
|
||||
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: true,
|
||||
openaiCodex: { enabled: true, mode: "cached" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modelProvider: "gateway",
|
||||
modelApi: "openai-codex-responses",
|
||||
modelId: "gpt-5.4",
|
||||
suppressManagedWebSearch: false,
|
||||
});
|
||||
|
||||
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
|
||||
});
|
||||
|
||||
it("removes managed web_search for direct Codex models when auth is available", () => {
|
||||
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
|
||||
config: {
|
||||
|
||||
@@ -143,6 +143,7 @@ function applyModelProviderToolPolicy(
|
||||
modelId?: string;
|
||||
agentDir?: string;
|
||||
modelCompat?: ModelCompatConfig;
|
||||
suppressManagedWebSearch?: boolean;
|
||||
},
|
||||
): AnyAgentTool[] {
|
||||
if (params?.config?.agents?.defaults?.experimental?.localModelLean === true) {
|
||||
@@ -151,6 +152,7 @@ function applyModelProviderToolPolicy(
|
||||
}
|
||||
|
||||
if (
|
||||
params?.suppressManagedWebSearch !== false &&
|
||||
shouldSuppressManagedWebSearchTool({
|
||||
config: params?.config,
|
||||
modelProvider: params?.modelProvider,
|
||||
@@ -302,6 +304,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
modelContextWindowTokens?: number;
|
||||
/** Resolved runtime model compatibility hints. */
|
||||
modelCompat?: ModelCompatConfig;
|
||||
/** If false, keep OpenClaw web_search even when a provider-native search tool is active. */
|
||||
suppressManagedWebSearch?: boolean;
|
||||
/**
|
||||
* Auth mode for the current provider. We only need this for Anthropic OAuth
|
||||
* tool-name blocking quirks.
|
||||
@@ -685,6 +689,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
modelId: options?.modelId,
|
||||
agentDir: options?.agentDir,
|
||||
modelCompat: options?.modelCompat,
|
||||
suppressManagedWebSearch: options?.suppressManagedWebSearch,
|
||||
});
|
||||
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
||||
const senderIsOwner = options?.senderIsOwner === true;
|
||||
|
||||
Reference in New Issue
Block a user