mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
Prefer Codex native workspace tools (#75308)
Summary: - The PR adds Codex dynamic-tool profile config defaulting to `native-first`, filters duplicate workspace/process/planning tools from Codex app-server thread payloads, keeps managed `web_search`, updates docs/manifest/config baselines/changelog, and adds regression tests. ClawSweeper fixups: - Included follow-up commit: test(codex): pin native-first tool catalog - Included follow-up commit: chore(config): refresh generated schema baseline - Included follow-up commit: chore: add codex native-first changelog - Included follow-up commit: chore: move native-first changelog entry - Included follow-up commit: chore: refresh config baseline after rebase Validation: - ClawSweeper review passed for head30e5cecfb7. - Required merge gates passed before the squash merge. Prepared head SHA:30e5cecfb7Review: https://github.com/openclaw/openclaw/pull/75308#issuecomment-4356919781 Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: pashpashpash <nik@vault77.ai>
This commit is contained in:
committed by
GitHub
parent
ec69c07b27
commit
42aaf0c98a
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
|
||||
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
|
||||
- CLI/proxy: add `openclaw proxy validate` so operators can verify effective proxy configuration, proxy reachability, and expected allow/deny destination behavior before deploying proxy-routed OpenClaw commands. (#73438) Thanks @jesse-merhi.
|
||||
- Agents/Codex: default Codex app-server dynamic tools to native-first, keeping OpenClaw integration tools while leaving file, patch, exec, and process ownership to the Codex harness. (#75308) Thanks @pashpashpash.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
2197c0110a367c9e2adba959ff8529edad7b4d526894eec602e47189d6930d2f config-baseline.json
|
||||
1deb67d0a40456e77cb67685f6ae2f14a8ddc2c4be488d4b1a1f1127598982dd config-baseline.json
|
||||
ac7537ed5b5a2d9e7fa50977aa99f5e0babfbe1a93c7c14b93a184b36bb4f539 config-baseline.core.json
|
||||
f3326cd9490169afefe93625f63699266b75db93855ed439c9692e3c286a990c config-baseline.channel.json
|
||||
4d017161b4dc986fdc6cc68167fedbd1d415ddbcd66125a872e18aa1769cd182 config-baseline.plugin.json
|
||||
7731a0b93cb335b56fac4c807447ba659fea51ea7a6cd844dc0ef5616669ee75 config-baseline.plugin.json
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -304,6 +304,20 @@ function createMessageDynamicTool(
|
||||
};
|
||||
}
|
||||
|
||||
function createNamedDynamicTool(
|
||||
name: string,
|
||||
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
|
||||
return {
|
||||
name,
|
||||
description: `${name} test tool`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractRelayIdFromThreadRequest(params: unknown): string {
|
||||
const command = (
|
||||
params as {
|
||||
@@ -335,6 +349,94 @@ 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("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const appServer = createThreadLifecycleAppServerOptions();
|
||||
const request = vi.fn(async (method: string, _params: unknown) => {
|
||||
if (method === "thread/start") {
|
||||
return threadStartResult();
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
const dynamicTools = __testing.applyCodexDynamicToolProfile(
|
||||
[
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"exec",
|
||||
"process",
|
||||
"update_plan",
|
||||
"web_search",
|
||||
"message",
|
||||
].map(createNamedDynamicTool),
|
||||
{},
|
||||
);
|
||||
|
||||
await startOrResumeThread({
|
||||
client: { request } as never,
|
||||
params: createParams(sessionFile, workspaceDir),
|
||||
cwd: workspaceDir,
|
||||
dynamicTools,
|
||||
appServer,
|
||||
});
|
||||
|
||||
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
|
||||
const dynamicToolNames = (
|
||||
(startRequest?.[1] as { dynamicTools?: Array<{ name: string }> } | undefined)?.dynamicTools ??
|
||||
[]
|
||||
).map((tool) => tool.name);
|
||||
|
||||
expect(dynamicToolNames).toContain("message");
|
||||
expect(dynamicToolNames).toContain("web_search");
|
||||
expect(dynamicToolNames).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"exec",
|
||||
"process",
|
||||
"update_plan",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
const LOG_FIELD_MAX_LENGTH = 160;
|
||||
|
||||
type OpenClawCodingToolsOptions = NonNullable<
|
||||
@@ -319,7 +332,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;
|
||||
@@ -369,6 +383,7 @@ export async function runCodexAppServerAttempt(
|
||||
sandbox,
|
||||
runAbortController,
|
||||
sessionAgentId,
|
||||
pluginConfig,
|
||||
onYieldDetected: () => {
|
||||
yieldDetected = true;
|
||||
},
|
||||
@@ -1317,6 +1332,7 @@ type DynamicToolBuildParams = {
|
||||
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>;
|
||||
runAbortController: AbortController;
|
||||
sessionAgentId: string | undefined;
|
||||
pluginConfig: CodexPluginConfig;
|
||||
onYieldDetected: () => void;
|
||||
};
|
||||
|
||||
@@ -1372,6 +1388,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,
|
||||
@@ -1390,7 +1407,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,
|
||||
});
|
||||
@@ -1411,6 +1429,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;
|
||||
@@ -1584,6 +1622,7 @@ export const __testing = {
|
||||
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
|
||||
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
|
||||
buildCodexNativeHookRelayId,
|
||||
applyCodexDynamicToolProfile,
|
||||
filterToolsForVisionInputs,
|
||||
handleDynamicToolCallWithTimeout,
|
||||
...createCodexAppServerClientFactoryTestHooks((factory) => {
|
||||
|
||||
@@ -222,7 +222,7 @@ function stabilizeJsonValue(value: JsonValue): JsonValue {
|
||||
export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string {
|
||||
const promptOverlay = renderCodexRuntimePromptOverlay(params);
|
||||
const sections = [
|
||||
"You are running inside OpenClaw. Use OpenClaw dynamic tools for messaging, cron, sessions, and host actions when available.",
|
||||
"You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available.",
|
||||
"Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply.",
|
||||
promptOverlay,
|
||||
params.extraSystemPrompt,
|
||||
|
||||
@@ -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