Skip to content

Commit 281bc84

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/ios-interface-cleanup
2 parents 73c4d95 + c1eee1a commit 281bc84

21 files changed

Lines changed: 856 additions & 79 deletions

.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function sectionFor(changelog, version) {
209209
function referencesIn(text) {
210210
const references = [];
211211
for (const match of text.matchAll(
212-
/(?<![A-Za-z0-9_.-])(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<name>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g,
212+
/(?<![A-Za-z0-9_.&-])(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<name>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g,
213213
)) {
214214
const qualifiedRepository = match.groups?.owner
215215
? `${match.groups.owner}/${match.groups.name}`.toLowerCase()

.github/workflows/ci.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,12 +1732,25 @@ jobs:
17321732
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
17331733
with:
17341734
path: apps/macos/.build
1735-
key: ${{ runner.os }}-swift-build-v2-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/macos/Sources/**', 'apps/macos/Tests/**', 'apps/shared/OpenClawKit/Package.swift', 'apps/shared/OpenClawKit/Sources/**', 'apps/swabble/Package.swift', 'apps/swabble/Sources/**') }}
1735+
key: ${{ runner.os }}-swift-build-v3-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/macos/Sources/**', 'apps/macos/Tests/**', 'apps/shared/OpenClawKit/Package.swift', 'apps/shared/OpenClawKit/Sources/**', 'apps/swabble/Package.swift', 'apps/swabble/Sources/**') }}
17361736
restore-keys: |
1737-
${{ runner.os }}-swift-build-v2-${{ steps.swift-toolchain.outputs.key }}-
1737+
${{ runner.os }}-swift-build-v3-${{ steps.swift-toolchain.outputs.key }}-
1738+
1739+
- name: Validate Swift build cache
1740+
id: validate-swift-build-cache
1741+
run: |
1742+
set -euo pipefail
1743+
cache_valid=true
1744+
sparkle_info="apps/macos/.build/artifacts/sparkle/Sparkle/Sparkle.xcframework/Info.plist"
1745+
if [[ -d apps/macos/.build && ! -f "$sparkle_info" ]]; then
1746+
echo "::warning::Swift build cache is missing Sparkle; resetting the local SwiftPM build directory."
1747+
swift package --package-path apps/macos reset
1748+
cache_valid=false
1749+
fi
1750+
echo "cache-valid=$cache_valid" >> "$GITHUB_OUTPUT"
17381751
17391752
- name: Preserve Swift build cache hit
1740-
if: steps.swift-build-cache.outputs.cache-hit == 'true'
1753+
if: steps.swift-build-cache.outputs.cache-hit == 'true' && steps.validate-swift-build-cache.outputs.cache-valid == 'true'
17411754
run: |
17421755
set -euo pipefail
17431756
# Exact source-hash cache hits already match these inputs; checkout

extensions/browser/src/browser/client.test.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,7 @@ describe("browser client", () => {
6969
});
7070

7171
it("surfaces non-2xx responses with body text", async () => {
72-
vi.stubGlobal(
73-
"fetch",
74-
vi.fn().mockResolvedValue({
75-
ok: false,
76-
status: 409,
77-
text: async () => "conflict",
78-
} as unknown as Response),
79-
);
72+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("conflict", { status: 409 })));
8073

8174
await expect(
8275
browserSnapshot("http://127.0.0.1:18791", { format: "aria", limit: 1 }),

extensions/codex/src/app-server/side-question.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,110 @@ describe("runCodexAppServerSideQuestion", () => {
585585
expect(toolOptions).toHaveProperty("requireExplicitMessageTarget", true);
586586
});
587587

588+
it("replays app-scoped reviewer policy into side-thread forks", async () => {
589+
const client = createFakeClient();
590+
getSharedCodexAppServerClientMock.mockResolvedValue(client);
591+
readCodexAppServerBindingMock.mockResolvedValue({
592+
schemaVersion: 2,
593+
threadId: "parent-thread",
594+
sessionFile: "/tmp/session-1.jsonl",
595+
cwd: "/tmp/workspace",
596+
authProfileId: "openai:work",
597+
model: "gpt-5.5",
598+
approvalPolicy: "on-request",
599+
sandbox: "workspace-write",
600+
pluginAppPolicyContext: {
601+
fingerprint: "mixed-plugin-policy",
602+
apps: {
603+
"ask-app": {
604+
configKey: "ask",
605+
marketplaceName: "openai",
606+
pluginName: "ask",
607+
allowDestructiveActions: true,
608+
destructiveApprovalMode: "ask",
609+
mcpServerNames: ["ask"],
610+
},
611+
"true-app": {
612+
configKey: "true",
613+
marketplaceName: "openai",
614+
pluginName: "true",
615+
allowDestructiveActions: true,
616+
destructiveApprovalMode: "allow",
617+
mcpServerNames: ["true"],
618+
},
619+
"false-app": {
620+
configKey: "false",
621+
marketplaceName: "openai",
622+
pluginName: "false",
623+
allowDestructiveActions: false,
624+
destructiveApprovalMode: "deny",
625+
mcpServerNames: ["false"],
626+
},
627+
"auto-app": {
628+
configKey: "auto",
629+
marketplaceName: "openai",
630+
pluginName: "auto",
631+
allowDestructiveActions: true,
632+
destructiveApprovalMode: "auto",
633+
mcpServerNames: ["auto"],
634+
},
635+
},
636+
pluginAppIds: {
637+
ask: ["ask-app"],
638+
true: ["true-app"],
639+
false: ["false-app"],
640+
auto: ["auto-app"],
641+
},
642+
},
643+
createdAt: new Date(0).toISOString(),
644+
updatedAt: new Date(0).toISOString(),
645+
});
646+
647+
await expect(
648+
runCodexAppServerSideQuestion(sideParams(), {
649+
pluginConfig: { appServer: { mode: "guardian" } },
650+
}),
651+
).resolves.toEqual({ text: "Side answer." });
652+
653+
const forkParams = mockCall(client.request)[1] as Record<string, unknown> | undefined;
654+
expect(forkParams?.approvalsReviewer).toBe("auto_review");
655+
const config = forkParams?.config as Record<string, unknown> | undefined;
656+
expect(config).not.toHaveProperty("approvals_reviewer");
657+
expect(config?.["features.code_mode"]).toBe(true);
658+
expect(config?.apps).toEqual({
659+
_default: {
660+
enabled: false,
661+
destructive_enabled: false,
662+
open_world_enabled: false,
663+
},
664+
"ask-app": {
665+
enabled: true,
666+
approvals_reviewer: "user",
667+
destructive_enabled: true,
668+
open_world_enabled: true,
669+
default_tools_approval_mode: "auto",
670+
},
671+
"auto-app": {
672+
enabled: true,
673+
destructive_enabled: true,
674+
open_world_enabled: true,
675+
default_tools_approval_mode: "auto",
676+
},
677+
"false-app": {
678+
enabled: true,
679+
destructive_enabled: false,
680+
open_world_enabled: true,
681+
default_tools_approval_mode: "auto",
682+
},
683+
"true-app": {
684+
enabled: true,
685+
destructive_enabled: true,
686+
open_world_enabled: true,
687+
default_tools_approval_mode: "auto",
688+
},
689+
});
690+
});
691+
588692
it("disables hosted search when side-question sender policy removes managed web_search", async () => {
589693
createOpenClawCodingToolsMock.mockImplementation((options: { senderId?: string }) =>
590694
options.senderId === "restricted-sender"

extensions/codex/src/app-server/side-question.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ import {
5656
readCodexNotificationThreadId,
5757
readCodexNotificationTurnId,
5858
} from "./notification-correlation.js";
59-
import { mergeCodexThreadConfigs } from "./plugin-thread-config.js";
59+
import {
60+
buildCodexPluginAppsConfigPatchFromPolicyContext,
61+
mergeCodexThreadConfigs,
62+
} from "./plugin-thread-config.js";
6063
import {
6164
assertCodexThreadForkResponse,
6265
assertCodexTurnStartResponse,
@@ -420,10 +423,16 @@ export async function runCodexAppServerSideQuestion(
420423
nativeCodeModeEnabled: nativeToolSurfaceEnabled,
421424
nativeCodeModeOnlyEnabled: appServer.codeModeOnly,
422425
});
426+
// Codex reloads config for thread/fork, so replay the persisted app policy or
427+
// app-scoped reviewers disappear while sibling apps inherit the thread reviewer.
428+
const pluginAppsConfigPatch = binding.pluginAppPolicyContext
429+
? buildCodexPluginAppsConfigPatchFromPolicyContext(binding.pluginAppPolicyContext)
430+
: undefined;
423431
const threadConfig =
424432
mergeCodexThreadConfigs(
425433
nativeHookRelayConfig,
426434
runtimeThreadConfig,
435+
pluginAppsConfigPatch,
427436
modelScopedAppServer.networkProxy?.configPatch,
428437
) ?? runtimeThreadConfig;
429438
const forkResponse = assertCodexThreadForkResponse(

extensions/discord/src/api.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,24 @@ describe("fetchDiscord", () => {
321321
expect(request?.signal).toBe(timeoutController.signal);
322322
});
323323

324+
it("throws DiscordApiError on malformed JSON success response body", async () => {
325+
const fetcher = withFetchPreconnect(
326+
async () => new Response("NOT JSON {{{", { status: 200 }),
327+
);
328+
329+
let error: unknown;
330+
try {
331+
await fetchDiscord("/users/@me/guilds", "test", fetcher, {
332+
retry: { attempts: 1 },
333+
});
334+
} catch (err) {
335+
error = err;
336+
}
337+
338+
expect(error).toBeInstanceOf(DiscordApiError);
339+
expect(String(error)).toContain("Discord API /users/@me/guilds returned malformed JSON");
340+
});
341+
324342
it("returns under-cap requestDiscord responses from a real loopback HTTP server", async () => {
325343
const payload = { id: "channel-42", name: "loopback", type: 0 };
326344
let contentLength: string | null | undefined;

extensions/discord/src/api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,14 @@ export async function requestDiscord<T>(
203203
if (!text.trim()) {
204204
return undefined as T;
205205
}
206-
return JSON.parse(text) as T;
206+
try {
207+
return JSON.parse(text) as T;
208+
} catch {
209+
throw new DiscordApiError(
210+
`Discord API ${path} returned malformed JSON`,
211+
0,
212+
);
213+
}
207214
},
208215
{
209216
...retryConfig,

extensions/memory-wiki/src/apply.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
withTrailingNewline,
66
} from "openclaw/plugin-sdk/memory-host-markdown";
77
import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
8-
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
8+
import { FsSafeError, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
99
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
1010
import { compileMemoryWikiVault, type CompileMemoryWikiResult } from "./compile.js";
1111
import type { ResolvedMemoryWikiConfig } from "./config.js";
@@ -191,6 +191,27 @@ function buildSynthesisBody(params: {
191191
return ensureHumanNotesBlock(withGenerated);
192192
}
193193

194+
type VaultRoot = Awaited<ReturnType<typeof fsRoot>>;
195+
196+
function isMissingWikiPageError(error: unknown): boolean {
197+
return error instanceof FsSafeError && error.code === "not-found";
198+
}
199+
200+
async function readExistingWikiPage(root: VaultRoot, pagePath: string): Promise<string> {
201+
try {
202+
return await root.readText(pagePath);
203+
} catch {
204+
try {
205+
return await root.readText(pagePath);
206+
} catch (retryError) {
207+
if (isMissingWikiPageError(retryError)) {
208+
return "";
209+
}
210+
throw retryError;
211+
}
212+
}
213+
}
214+
194215
async function writeWikiPage(params: {
195216
rootDir: string;
196217
relativePath: string;
@@ -204,7 +225,7 @@ async function writeWikiPage(params: {
204225
body: params.body,
205226
}),
206227
);
207-
const existing = await root.readText(params.relativePath).catch(() => "");
228+
const existing = await readExistingWikiPage(root, params.relativePath);
208229
if (existing === rendered) {
209230
return false;
210231
}
@@ -228,7 +249,7 @@ async function applyCreateSynthesisMutation(params: {
228249
const pageStem = slugifyWikiPageStem(params.mutation.title);
229250
const pagePath = path.join("syntheses", `${pageStem}.md`).replace(/\\/g, "/");
230251
const root = await fsRoot(params.config.vault.path);
231-
const existing = await root.readText(pagePath).catch(() => "");
252+
const existing = await readExistingWikiPage(root, pagePath);
232253
const parsed = parseWikiMarkdown(existing);
233254
const pageId =
234255
(typeof parsed.frontmatter.id === "string" && parsed.frontmatter.id.trim()) ||

extensions/memory-wiki/src/chatgpt-import.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ function normalizeWhitespace(value: string): string {
136136
return value.trim().replace(/\s+/g, " ");
137137
}
138138

139+
function isMissingConversationPageError(error: unknown): boolean {
140+
return asRecord(error)?.code === "ENOENT";
141+
}
142+
143+
async function readExistingConversationPage(absolutePath: string): Promise<string> {
144+
try {
145+
return await fs.readFile(absolutePath, "utf8");
146+
} catch {
147+
try {
148+
return await fs.readFile(absolutePath, "utf8");
149+
} catch (retryError) {
150+
if (isMissingConversationPageError(retryError)) {
151+
return "";
152+
}
153+
throw retryError;
154+
}
155+
}
156+
}
157+
139158
function resolveConversationSourcePath(exportInputPath: string): {
140159
exportPath: string;
141160
conversationsPath: string;
@@ -737,7 +756,7 @@ export async function importChatGptConversations(params: {
737756
for (const record of records) {
738757
const rendered = renderConversationPage(record);
739758
const absolutePath = path.join(params.config.vault.path, record.pagePath);
740-
const existing = await fs.readFile(absolutePath, "utf8").catch(() => "");
759+
const existing = await readExistingConversationPage(absolutePath);
741760
const stabilized = preserveExistingPageBlocks(rendered, existing);
742761
const operation: ChatGptImportOperation =
743762
existing === stabilized ? "skip" : existing ? "update" : "create";

0 commit comments

Comments
 (0)