mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 19:50:24 +00:00
fix(qa): expose codex tools for runtime parity
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { CodexPluginConfig } from "./config.js";
|
||||
import type { CodexDynamicToolsLoading, CodexPluginConfig } from "./config.js";
|
||||
|
||||
export const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [
|
||||
"read",
|
||||
@@ -19,18 +19,44 @@ const DYNAMIC_TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
"apply-patch": "apply_patch",
|
||||
};
|
||||
|
||||
type CodexDynamicToolProfileEnv = {
|
||||
OPENCLAW_BUILD_PRIVATE_QA?: string;
|
||||
OPENCLAW_QA_FORCE_RUNTIME?: string;
|
||||
};
|
||||
|
||||
export function normalizeCodexDynamicToolName(name: string): string {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
export function isForcedPrivateQaCodexRuntime(
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): boolean {
|
||||
return (
|
||||
env.OPENCLAW_BUILD_PRIVATE_QA === "1" &&
|
||||
env.OPENCLAW_QA_FORCE_RUNTIME?.trim().toLowerCase() === "codex"
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCodexDynamicToolsLoading(
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsLoading">,
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): CodexDynamicToolsLoading {
|
||||
return isForcedPrivateQaCodexRuntime(env)
|
||||
? "direct"
|
||||
: (config.codexDynamicToolsLoading ?? "searchable");
|
||||
}
|
||||
|
||||
export function filterCodexDynamicTools<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): T[] {
|
||||
const excludes = new Set<string>();
|
||||
for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
|
||||
excludes.add(name);
|
||||
if (!isForcedPrivateQaCodexRuntime(env)) {
|
||||
for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
|
||||
excludes.add(name);
|
||||
}
|
||||
}
|
||||
for (const name of config.codexDynamicToolsExclude ?? []) {
|
||||
const trimmed = normalizeCodexDynamicToolName(name);
|
||||
|
||||
@@ -646,6 +646,21 @@ describe("runCodexAppServerAttempt", () => {
|
||||
).toEqual(["message"]);
|
||||
});
|
||||
|
||||
it("exposes app-server-owned tools directly for forced private QA Codex runtime", () => {
|
||||
const tools = ["read", "write", "image_generate", "message"].map((name) => ({ name }));
|
||||
const privateQaCodexEnv = {
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
||||
OPENCLAW_QA_FORCE_RUNTIME: "codex",
|
||||
};
|
||||
|
||||
expect(
|
||||
__testing
|
||||
.filterCodexDynamicTools(tools, {}, privateQaCodexEnv)
|
||||
.map((tool) => tool.name),
|
||||
).toEqual(["read", "write", "image_generate", "message"]);
|
||||
expect(__testing.resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -897,6 +912,38 @@ describe("runCodexAppServerAttempt", () => {
|
||||
expect((factoryOptions[0] as { modelApi?: unknown }).modelApi).toBe("openai-responses");
|
||||
});
|
||||
|
||||
it("enables gateway subagent binding for forced private QA Codex runs", async () => {
|
||||
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", "1");
|
||||
vi.stubEnv("OPENCLAW_QA_FORCE_RUNTIME", "codex");
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
__testing.setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
factoryOptions.push(options);
|
||||
return [createRuntimeDynamicTool("sessions_spawn")];
|
||||
});
|
||||
|
||||
const tools = await __testing.buildDynamicTools({
|
||||
params,
|
||||
resolvedWorkspace: workspaceDir,
|
||||
effectiveWorkspace: workspaceDir,
|
||||
sandboxSessionKey: params.sessionKey!,
|
||||
sandbox: null as never,
|
||||
runAbortController: new AbortController(),
|
||||
sessionAgentId: "main",
|
||||
pluginConfig: {},
|
||||
onYieldDetected: () => undefined,
|
||||
});
|
||||
|
||||
expect(factoryOptions).toHaveLength(1);
|
||||
const factoryOption = factoryOptions[0] as { allowGatewaySubagentBinding?: unknown };
|
||||
expect(factoryOption.allowGatewaySubagentBinding).toBe(true);
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["sessions_spawn"]);
|
||||
});
|
||||
|
||||
it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
|
||||
const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name }));
|
||||
|
||||
|
||||
@@ -78,7 +78,12 @@ import {
|
||||
resolveCodexContextEngineProjectionMaxChars,
|
||||
resolveCodexContextEngineProjectionReserveTokens,
|
||||
} from "./context-engine-projection.js";
|
||||
import { filterCodexDynamicTools, normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
isForcedPrivateQaCodexRuntime,
|
||||
normalizeCodexDynamicToolName,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
|
||||
import { CodexAppServerEventProjector } from "./event-projector.js";
|
||||
@@ -618,7 +623,7 @@ export async function runCodexAppServerAttempt(
|
||||
const toolBridge = createCodexDynamicToolBridge({
|
||||
tools,
|
||||
signal: runAbortController.signal,
|
||||
loading: pluginConfig.codexDynamicToolsLoading ?? "searchable",
|
||||
loading: resolveCodexDynamicToolsLoading(pluginConfig),
|
||||
directToolNames: shouldForceMessageTool(params) ? ["message"] : [],
|
||||
hookContext: {
|
||||
agentId: sessionAgentId,
|
||||
@@ -2748,7 +2753,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
||||
allowGatewaySubagentBinding:
|
||||
params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
|
||||
...sessionKeys,
|
||||
sessionId: params.sessionId,
|
||||
runId: params.runId,
|
||||
@@ -3933,6 +3939,7 @@ export const __testing = {
|
||||
isInvalidCodexImagePayloadError,
|
||||
remapCodexContextFilePath,
|
||||
resolveDynamicToolCallTimeoutMs,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
restrictCodexAppServerSandboxForOpenClawSandbox,
|
||||
resolveCodexAppServerForOpenClawToolPolicy,
|
||||
resolveOpenClawCodingToolsSessionKeys,
|
||||
|
||||
@@ -16,7 +16,10 @@ import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
|
||||
import { refreshCodexAppServerAuthTokens } from "./auth-bridge.js";
|
||||
import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js";
|
||||
import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
|
||||
import { filterCodexDynamicTools } from "./dynamic-tool-profile.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
|
||||
import {
|
||||
@@ -378,7 +381,7 @@ async function createCodexSideToolBridge(input: {
|
||||
return createCodexDynamicToolBridge({
|
||||
tools,
|
||||
signal: input.signal,
|
||||
loading: input.pluginConfig.codexDynamicToolsLoading ?? "searchable",
|
||||
loading: resolveCodexDynamicToolsLoading(input.pluginConfig),
|
||||
hookContext: {
|
||||
agentId: input.sessionAgentId,
|
||||
config: input.params.cfg,
|
||||
|
||||
Reference in New Issue
Block a user