Skip to content

Commit 39cdd5e

Browse files
authored
Merge branch 'main' into upstream/fix-picker-oslog
2 parents 9d2a841 + 62ccd8b commit 39cdd5e

21 files changed

Lines changed: 362 additions & 25 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,4 @@ extensions/**/.openclaw-runtime-deps-stamp.json
220220
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
221221
/.opengrep-out/
222222
/.crabbox-artifacts
223+
.comux*

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ Docs: https://docs.openclaw.ai
140140

141141
### Fixes
142142

143+
- Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev.
144+
- Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev.
145+
- Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev.
146+
- Agents/tools: avoid warning messaging-only agents about inherited global `tools.exec` or `tools.fs` sections when the agent profile did not configure those tool sections itself. Thanks @BunsDev.
147+
- Codex dynamic tools: normalize runtime `toolsAllow` entries the same way as Pi tool policy, so aliases like `bash` and `apply-patch` still expose the intended OpenClaw tools. Thanks @BunsDev.
148+
- Memory/dreaming: read OpenAI-style `output_text` assistant parts from narrative subagent transcripts, so light-phase Dream Diary entries are not dropped as empty. Thanks @BunsDev.
143149
- llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166.
144150
- Media generation: resolve slash-containing model-only overrides like `fal-ai/flux/dev` through registered provider model metadata so FAL image/video models do not get misparsed as provider `fal-ai`. Fixes #77444.
145151
- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541.

extensions/codex/src/app-server/auth-bridge.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
6969
: "");
7070
return apiKey ? { apiKey, provider: credential.provider, email: credential.email } : null;
7171
}
72+
if (credential.type !== "oauth") {
73+
return null;
74+
}
7275
let oauthCredential = credential;
7376
if ((oauthCredential.expires ?? 0) <= Date.now()) {
7477
const refreshed = await providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin({
@@ -545,6 +548,35 @@ describe("bridgeCodexAppServerStartOptions", () => {
545548
}
546549
});
547550

551+
it("rejects unsupported Codex auth profile credential types before OAuth refresh", async () => {
552+
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
553+
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
554+
try {
555+
upsertAuthProfile({
556+
agentDir,
557+
profileId: "openai-codex:aws",
558+
credential: {
559+
type: "aws-sdk",
560+
provider: "openai-codex",
561+
},
562+
});
563+
564+
await expect(
565+
applyCodexAppServerAuthProfile({
566+
client: { request } as never,
567+
agentDir,
568+
authProfileId: "openai-codex:aws",
569+
}),
570+
).rejects.toThrow(
571+
'Codex app-server auth profile "openai-codex:aws" does not contain usable credentials.',
572+
);
573+
expect(oauthMocks.refreshOpenAICodexToken).not.toHaveBeenCalled();
574+
expect(request).not.toHaveBeenCalled();
575+
} finally {
576+
await fs.rm(agentDir, { recursive: true, force: true });
577+
}
578+
});
579+
548580
it("falls back to CODEX_API_KEY when no auth profile and no Codex account is available", async () => {
549581
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
550582
const request = vi.fn(async (method: string) => {

extensions/codex/src/app-server/auth-bridge.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ async function resolveLoginParamsForCredential(
272272
? buildChatgptAuthTokensParams(profileId, credential, accessToken)
273273
: undefined;
274274
}
275+
if (credential.type !== "oauth") {
276+
return undefined;
277+
}
275278
const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, {
276279
agentDir: params.agentDir,
277280
forceRefresh: params.forceOAuthRefresh,

extensions/codex/src/app-server/dynamic-tool-profile.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ export const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [
1010
"update_plan",
1111
] as const;
1212

13+
const DYNAMIC_TOOL_NAME_ALIASES: Record<string, string> = {
14+
bash: "exec",
15+
"apply-patch": "apply_patch",
16+
};
17+
18+
export function normalizeCodexDynamicToolName(name: string): string {
19+
const normalized = name.trim().toLowerCase();
20+
return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized;
21+
}
22+
1323
export function applyCodexDynamicToolProfile<T extends { name: string }>(
1424
tools: T[],
1525
config: Pick<CodexPluginConfig, "codexDynamicToolsProfile" | "codexDynamicToolsExclude">,
@@ -22,10 +32,12 @@ export function applyCodexDynamicToolProfile<T extends { name: string }>(
2232
}
2333
}
2434
for (const name of config.codexDynamicToolsExclude ?? []) {
25-
const trimmed = name.trim();
35+
const trimmed = normalizeCodexDynamicToolName(name);
2636
if (trimmed) {
2737
excludes.add(trimmed);
2838
}
2939
}
30-
return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(tool.name));
40+
return excludes.size === 0
41+
? tools
42+
: tools.filter((tool) => !excludes.has(normalizeCodexDynamicToolName(tool.name)));
3143
}

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ describe("runCodexAppServerAttempt", () => {
504504
);
505505
});
506506

507+
it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
508+
const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name }));
509+
510+
expect(
511+
__testing
512+
.filterCodexDynamicToolsForAllowlist(tools, [" BASH ", "apply-patch", "READ"])
513+
.map((tool) => tool.name),
514+
).toEqual(["exec", "apply_patch", "read"]);
515+
});
516+
507517
it("forces the message dynamic tool for message-tool-only source replies", () => {
508518
const workspaceDir = path.join(tempDir, "workspace");
509519
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ import {
6464
type CodexPluginConfig,
6565
} from "./config.js";
6666
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
67-
import { applyCodexDynamicToolProfile } from "./dynamic-tool-profile.js";
67+
import {
68+
applyCodexDynamicToolProfile,
69+
normalizeCodexDynamicToolName,
70+
} from "./dynamic-tool-profile.js";
6871
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
6972
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
7073
import { CodexAppServerEventProjector } from "./event-projector.js";
@@ -1678,10 +1681,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
16781681
modelHasVision,
16791682
hasInboundImages: (params.images?.length ?? 0) > 0,
16801683
});
1681-
const filteredTools =
1682-
params.toolsAllow && params.toolsAllow.length > 0
1683-
? visionFilteredTools.filter((tool) => params.toolsAllow?.includes(tool.name))
1684-
: visionFilteredTools;
1684+
const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, params.toolsAllow);
16851685
return normalizeAgentRuntimeTools({
16861686
runtimePlan: params.runtimePlan,
16871687
tools: filteredTools,
@@ -1695,6 +1695,19 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
16951695
});
16961696
}
16971697

1698+
function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
1699+
tools: T[],
1700+
toolsAllow?: string[],
1701+
): T[] {
1702+
if (!toolsAllow || toolsAllow.length === 0) {
1703+
return tools;
1704+
}
1705+
const allowSet = new Set(
1706+
toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean),
1707+
);
1708+
return tools.filter((tool) => allowSet.has(normalizeCodexDynamicToolName(tool.name)));
1709+
}
1710+
16981711
function shouldForceMessageTool(params: EmbeddedRunAttemptParams): boolean {
16991712
return params.sourceReplyDeliveryMode === "message_tool_only";
17001713
}
@@ -2117,6 +2130,7 @@ export const __testing = {
21172130
buildCodexNativeHookRelayId,
21182131
applyCodexDynamicToolProfile,
21192132
buildDynamicTools,
2133+
filterCodexDynamicToolsForAllowlist,
21202134
filterToolsForVisionInputs,
21212135
handleDynamicToolCallWithTimeout,
21222136
resolveOpenClawCodingToolsSessionKeys,

extensions/memory-core/src/dreaming-narrative.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ describe("extractNarrativeText", () => {
101101
expect(extractNarrativeText(messages)).toBe("First paragraph.\nSecond paragraph.");
102102
});
103103

104+
it("extracts from OpenAI output_text assistant parts", () => {
105+
const messages = [
106+
{
107+
role: "assistant",
108+
content: [{ type: "output_text", text: "The light phase found a diary thread." }],
109+
},
110+
];
111+
expect(extractNarrativeText(messages)).toBe("The light phase found a diary thread.");
112+
});
113+
104114
it("returns null when no assistant message exists", () => {
105115
const messages = [{ role: "user", content: "hello" }];
106116
expect(extractNarrativeText(messages)).toBeNull();

extensions/memory-core/src/dreaming-narrative.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ export function extractNarrativeText(messages: unknown[]): string | null {
304304
part &&
305305
typeof part === "object" &&
306306
!Array.isArray(part) &&
307-
(part as Record<string, unknown>).type === "text" &&
307+
((part as Record<string, unknown>).type === "text" ||
308+
(part as Record<string, unknown>).type === "output_text") &&
308309
typeof (part as Record<string, unknown>).text === "string",
309310
)
310311
.map((part) => (part as { text: string }).text)

extensions/microsoft-foundry/index.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,40 @@ describe("microsoft-foundry plugin", () => {
698698
]);
699699
});
700700

701+
it("keeps Foundry profile selection compatible with unrelated AWS SDK profile modes", async () => {
702+
const provider = registerProvider();
703+
const config: OpenClawConfig = {
704+
...buildFoundryConfig({
705+
profileIds: ["microsoft-foundry:entra"],
706+
orderedProfileIds: ["microsoft-foundry:entra"],
707+
}),
708+
auth: {
709+
profiles: {
710+
"amazon-bedrock:default": {
711+
provider: "amazon-bedrock",
712+
mode: "aws-sdk",
713+
},
714+
"microsoft-foundry:entra": {
715+
provider: "microsoft-foundry",
716+
mode: "api_key",
717+
},
718+
},
719+
order: {
720+
"microsoft-foundry": ["microsoft-foundry:entra"],
721+
},
722+
},
723+
};
724+
725+
await provider.onModelSelected?.({
726+
config,
727+
model: "microsoft-foundry/gpt-5.4",
728+
prompter: {} as never,
729+
agentDir: defaultFoundryAgentDir,
730+
});
731+
732+
expect(config.auth?.order?.["microsoft-foundry"]).toEqual(["microsoft-foundry:entra"]);
733+
});
734+
701735
it("persists discovered deployments alongside the selected default model", () => {
702736
const result = buildFoundryAuthResult({
703737
profileId: "microsoft-foundry:entra",

0 commit comments

Comments
 (0)