From 4c614c230dd64af7eb8ccd67d016d0231bff6064 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 20 Mar 2026 15:53:03 +0000 Subject: [PATCH 01/10] fix: restore local gate --- docs/install/azure.md | 2 ++ extensions/msteams/src/graph-upload.test.ts | 2 +- extensions/msteams/src/messenger.test.ts | 11 +++++++++++ .../msteams/src/monitor-handler.file-consent.test.ts | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/install/azure.md b/docs/install/azure.md index 7c6abae64fe..012434bc43f 100644 --- a/docs/install/azure.md +++ b/docs/install/azure.md @@ -284,10 +284,12 @@ Azure Bastion Standard SKU runs approximately **\$140/month** and the VM (Standa To reduce costs: - **Deallocate the VM** when not in use (stops compute billing; disk charges remain). The OpenClaw Gateway will not be reachable while the VM is deallocated — restart it when you need it live again: + ```bash az vm deallocate -g "${RG}" -n "${VM_NAME}" az vm start -g "${RG}" -n "${VM_NAME}" # restart later ``` + - **Delete Bastion when not needed** and recreate it when you need SSH access. Bastion is the largest cost component and takes only a few minutes to provision. - **Use the Basic Bastion SKU** (~\$38/month) if you only need Portal-based SSH and don't require CLI tunneling (`az network bastion ssh`). diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index 9da78c1ed61..43a66e95c3f 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -141,7 +141,7 @@ describe("resolveGraphChatId", () => { }), ); // Should filter by user AAD object ID - const callUrl = (fetchFn.mock.calls[0] as [string, unknown])[0]; + const callUrl = (fetchFn.mock.calls[0] as unknown as [string, unknown])[0]; expect(callUrl).toContain("user-aad-object-id-123"); expect(result).toBe("19:dm-chat-id@unq.gbl.spaces"); }); diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 2644092f127..92f161341de 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -50,9 +50,14 @@ const runtimeStub: PluginRuntime = createPluginRuntimeMock({ }, }); +const noopUpdateActivity = async () => {}; +const noopDeleteActivity = async () => {}; + const createNoopAdapter = (): MSTeamsAdapter => ({ continueConversation: async () => {}, process: async () => {}, + updateActivity: noopUpdateActivity, + deleteActivity: noopDeleteActivity, }); const createRecordedSendActivity = ( @@ -81,6 +86,8 @@ const createFallbackAdapter = (proactiveSent: string[]): MSTeamsAdapter => ({ }); }, process: async () => {}, + updateActivity: noopUpdateActivity, + deleteActivity: noopDeleteActivity, }); describe("msteams messenger", () => { @@ -195,6 +202,8 @@ describe("msteams messenger", () => { }); }, process: async () => {}, + updateActivity: noopUpdateActivity, + deleteActivity: noopDeleteActivity, }; const ids = await sendMSTeamsMessages({ @@ -366,6 +375,8 @@ describe("msteams messenger", () => { await logic({ sendActivity: createRecordedSendActivity(attempts, 503) }); }, process: async () => {}, + updateActivity: noopUpdateActivity, + deleteActivity: noopDeleteActivity, }; const ids = await sendMSTeamsMessages({ diff --git a/extensions/msteams/src/monitor-handler.file-consent.test.ts b/extensions/msteams/src/monitor-handler.file-consent.test.ts index 5e610bfcfa6..39b6ea1b1ff 100644 --- a/extensions/msteams/src/monitor-handler.file-consent.test.ts +++ b/extensions/msteams/src/monitor-handler.file-consent.test.ts @@ -42,6 +42,8 @@ function createDeps(): MSTeamsMessageHandlerDeps { const adapter: MSTeamsAdapter = { continueConversation: async () => {}, process: async () => {}, + updateActivity: async () => {}, + deleteActivity: async () => {}, }; const conversationStore: MSTeamsConversationStore = { upsert: async () => {}, @@ -82,6 +84,8 @@ function createActivityHandler(): MSTeamsActivityHandler { handler = { onMessage: () => handler, onMembersAdded: () => handler, + onReactionsAdded: () => handler, + onReactionsRemoved: () => handler, run: async () => {}, }; return handler; From cb89325cd8754225f7f4c42805bc95465ffb9181 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 20 Mar 2026 16:29:00 +0000 Subject: [PATCH 02/10] fix: restore latest main gate --- src/infra/archive.test.ts | 55 ++++++++++++++++++++++++++------- src/plugin-sdk/acp-runtime.ts | 6 ++++ src/plugin-sdk/core.ts | 2 ++ src/plugin-sdk/provider-auth.ts | 4 +++ 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/infra/archive.test.ts b/src/infra/archive.test.ts index d77b1e0bdb4..5f62200314e 100644 --- a/src/infra/archive.test.ts +++ b/src/infra/archive.test.ts @@ -11,6 +11,7 @@ import { extractArchive, resolvePackedRootDir } from "./archive.js"; let fixtureRoot = ""; let fixtureCount = 0; const directorySymlinkType = process.platform === "win32" ? "junction" : undefined; +const ARCHIVE_EXTRACT_TIMEOUT_MS = 15_000; async function makeTempDir(prefix = "case") { const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`); @@ -67,7 +68,7 @@ async function expectExtractedSizeBudgetExceeded(params: { extractArchive({ archivePath: params.archivePath, destDir: params.destDir, - timeoutMs: params.timeoutMs ?? 5_000, + timeoutMs: params.timeoutMs ?? ARCHIVE_EXTRACT_TIMEOUT_MS, limits: { maxExtractedBytes: params.maxExtractedBytes }, }), ).rejects.toThrow("archive extracted size exceeds limit"); @@ -93,7 +94,11 @@ describe("archive utils", () => { fileName: "hello.txt", content: "hi", }); - await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }); + await extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }); const rootDir = await resolvePackedRootDir(extractDir); const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8"); expect(content).toBe("hi"); @@ -118,7 +123,11 @@ describe("archive utils", () => { await createDirectorySymlink(realExtractDir, extractDir); await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toMatchObject({ code: "destination-symlink", } satisfies Partial); @@ -135,7 +144,11 @@ describe("archive utils", () => { await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toThrow(/(escapes destination|absolute)/i); }); }); @@ -151,7 +164,11 @@ describe("archive utils", () => { await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toMatchObject({ code: "destination-symlink-traversal", } satisfies Partial); @@ -186,7 +203,11 @@ describe("archive utils", () => { timing: "after-realpath", run: async () => { await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toMatchObject({ code: "destination-symlink-traversal", } satisfies Partial); @@ -222,7 +243,11 @@ describe("archive utils", () => { try { await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toMatchObject({ code: "destination-symlink-traversal", } satisfies Partial); @@ -245,7 +270,11 @@ describe("archive utils", () => { await tar.c({ cwd: insideDir, file: archivePath }, ["../outside.txt"]); await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toThrow(/escapes destination/i); }); }); @@ -261,7 +290,11 @@ describe("archive utils", () => { await tar.c({ cwd: archiveRoot, file: archivePath }, ["escape"]); await expect( - extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }), + extractArchive({ + archivePath, + destDir: extractDir, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, + }), ).rejects.toMatchObject({ code: "destination-symlink-traversal", } satisfies Partial); @@ -308,7 +341,7 @@ describe("archive utils", () => { extractArchive({ archivePath, destDir: extractDir, - timeoutMs: 5_000, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, limits: { maxArchiveBytes: Math.max(1, stat.size - 1) }, }), ).rejects.toThrow("archive size exceeds limit"); @@ -328,7 +361,7 @@ describe("archive utils", () => { extractArchive({ archivePath, destDir: extractDir, - timeoutMs: 5_000, + timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, }), ).rejects.toThrow(/absolute|drive path|escapes destination/i); }); diff --git a/src/plugin-sdk/acp-runtime.ts b/src/plugin-sdk/acp-runtime.ts index 7767d042f9b..88088867b2a 100644 --- a/src/plugin-sdk/acp-runtime.ts +++ b/src/plugin-sdk/acp-runtime.ts @@ -4,6 +4,12 @@ export { getAcpSessionManager } from "../acp/control-plane/manager.js"; export { AcpRuntimeError, isAcpRuntimeError } from "../acp/runtime/errors.js"; export { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../acp/runtime/registry.js"; export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js"; +export { + getAcpRuntimeBackend, + registerAcpRuntimeBackend, + requireAcpRuntimeBackend, + unregisterAcpRuntimeBackend, +} from "../acp/runtime/registry.js"; export type { AcpRuntime, AcpRuntimeCapabilities, diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index c8c7980fbd2..b5b149e25b0 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -58,6 +58,8 @@ export type { PluginCommandContext, PluginLogger, PluginInteractiveTelegramHandlerContext, + OpenClawPluginToolContext, + OpenClawPluginToolFactory, } from "../plugins/types.js"; export type { OpenClawConfig } from "../config/config.js"; export { isSecretRef } from "../config/types.secrets.js"; diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 13125b7704c..bdc73f50793 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -43,3 +43,7 @@ export { normalizeOptionalSecretInput, normalizeSecretInput, } from "../utils/normalize-secret-input.js"; +export { + listKnownProviderAuthEnvVarNames, + omitEnvKeysCaseInsensitive, +} from "../secrets/provider-env-vars.js"; From 18fa2992f92b216654a8f27b2d828d08dc725f92 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 20 Mar 2026 16:45:35 +0000 Subject: [PATCH 03/10] fix: restore plugin sdk runtime barrels --- extensions/feishu/runtime-api.ts | 2 +- extensions/googlechat/runtime-api.ts | 2 +- extensions/irc/src/runtime-api.ts | 2 +- extensions/line/runtime-api.ts | 6 +- extensions/line/src/config-adapter.ts | 2 +- extensions/line/src/group-policy.ts | 2 +- extensions/line/src/setup-core.ts | 4 +- extensions/line/src/setup-surface.ts | 4 +- extensions/mattermost/runtime-api.ts | 2 +- extensions/msteams/runtime-api.ts | 2 +- extensions/nextcloud-talk/runtime-api.ts | 2 +- extensions/nostr/runtime-api.ts | 2 +- extensions/signal/src/runtime-api.ts | 2 +- extensions/tlon/runtime-api.ts | 2 +- extensions/twitch/runtime-api.ts | 2 +- extensions/voice-call/runtime-api.ts | 2 +- extensions/zalo/runtime-api.ts | 2 +- extensions/zalouser/runtime-api.ts | 2 +- package.json | 60 +++++++++++++++++++ scripts/lib/plugin-sdk-entrypoints.json | 15 +++++ src/plugin-sdk/line-core.ts | 2 +- src/plugin-sdk/root-alias.cjs | 9 +++ src/plugin-sdk/root-alias.test.ts | 6 +- src/plugin-sdk/runtime-api-guardrails.test.ts | 4 +- src/plugin-sdk/subpaths.test.ts | 15 ----- 25 files changed, 114 insertions(+), 41 deletions(-) diff --git a/extensions/feishu/runtime-api.ts b/extensions/feishu/runtime-api.ts index cde6bbf5569..ece8df41cca 100644 --- a/extensions/feishu/runtime-api.ts +++ b/extensions/feishu/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Feishu extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/feishu.js"; +export * from "openclaw/plugin-sdk/feishu"; diff --git a/extensions/googlechat/runtime-api.ts b/extensions/googlechat/runtime-api.ts index cd47c0e56c7..df946f8ec4a 100644 --- a/extensions/googlechat/runtime-api.ts +++ b/extensions/googlechat/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Google Chat extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/googlechat.js"; +export * from "openclaw/plugin-sdk/googlechat"; diff --git a/extensions/irc/src/runtime-api.ts b/extensions/irc/src/runtime-api.ts index 96e4bdbbe90..40f35e1ad53 100644 --- a/extensions/irc/src/runtime-api.ts +++ b/extensions/irc/src/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled IRC extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../../src/plugin-sdk/irc.js"; +export * from "openclaw/plugin-sdk/irc"; diff --git a/extensions/line/runtime-api.ts b/extensions/line/runtime-api.ts index 53f1be0c51c..b40e5c76e0e 100644 --- a/extensions/line/runtime-api.ts +++ b/extensions/line/runtime-api.ts @@ -1,12 +1,12 @@ // Private runtime barrel for the bundled LINE extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/line.js"; -export { resolveExactLineGroupConfigKey } from "../../src/plugin-sdk/line-core.js"; +export * from "openclaw/plugin-sdk/line"; +export { resolveExactLineGroupConfigKey } from "openclaw/plugin-sdk/line-core"; export { formatDocsLink, setSetupChannelEnabled, splitSetupEntries, type ChannelSetupDmPolicy, type ChannelSetupWizard, -} from "../../src/plugin-sdk/line-core.js"; +} from "openclaw/plugin-sdk/line-core"; diff --git a/extensions/line/src/config-adapter.ts b/extensions/line/src/config-adapter.ts index 1b10989b45c..3894210f0a6 100644 --- a/extensions/line/src/config-adapter.ts +++ b/extensions/line/src/config-adapter.ts @@ -5,7 +5,7 @@ import { resolveLineAccount, type OpenClawConfig, type ResolvedLineAccount, -} from "../runtime-api.js"; +} from "openclaw/plugin-sdk/line-core"; export function normalizeLineAllowFrom(entry: string): string { return entry.replace(/^line:(?:user:)?/i, ""); diff --git a/extensions/line/src/group-policy.ts b/extensions/line/src/group-policy.ts index eaf30e04cf7..e6b4fa0ba95 100644 --- a/extensions/line/src/group-policy.ts +++ b/extensions/line/src/group-policy.ts @@ -1,5 +1,5 @@ import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy"; -import { resolveExactLineGroupConfigKey, type OpenClawConfig } from "../runtime-api.js"; +import { resolveExactLineGroupConfigKey, type OpenClawConfig } from "openclaw/plugin-sdk/line-core"; type LineGroupContext = { cfg: OpenClawConfig; diff --git a/extensions/line/src/setup-core.ts b/extensions/line/src/setup-core.ts index 7e894d2b87a..363b4dcb2a1 100644 --- a/extensions/line/src/setup-core.ts +++ b/extensions/line/src/setup-core.ts @@ -1,11 +1,11 @@ -import type { ChannelSetupAdapter, OpenClawConfig } from "openclaw/plugin-sdk/setup"; import { DEFAULT_ACCOUNT_ID, listLineAccountIds, normalizeAccountId, resolveLineAccount, type LineConfig, -} from "../runtime-api.js"; +} from "openclaw/plugin-sdk/line-core"; +import type { ChannelSetupAdapter, OpenClawConfig } from "openclaw/plugin-sdk/setup"; const channel = "line" as const; diff --git a/extensions/line/src/setup-surface.ts b/extensions/line/src/setup-surface.ts index 6f46cc92217..640ad3812b8 100644 --- a/extensions/line/src/setup-surface.ts +++ b/extensions/line/src/setup-surface.ts @@ -1,4 +1,3 @@ -import { createAllowFromSection, createTopLevelChannelDmPolicy } from "openclaw/plugin-sdk/setup"; import { DEFAULT_ACCOUNT_ID, formatDocsLink, @@ -7,7 +6,8 @@ import { splitSetupEntries, type ChannelSetupDmPolicy, type ChannelSetupWizard, -} from "../runtime-api.js"; +} from "openclaw/plugin-sdk/line-core"; +import { createAllowFromSection, createTopLevelChannelDmPolicy } from "openclaw/plugin-sdk/setup"; import { isLineConfigured, listLineAccountIds, diff --git a/extensions/mattermost/runtime-api.ts b/extensions/mattermost/runtime-api.ts index 2bc65439262..d4e591c8c1e 100644 --- a/extensions/mattermost/runtime-api.ts +++ b/extensions/mattermost/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Mattermost extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/mattermost.js"; +export * from "openclaw/plugin-sdk/mattermost"; diff --git a/extensions/msteams/runtime-api.ts b/extensions/msteams/runtime-api.ts index e2b75780399..d32cb7b65d5 100644 --- a/extensions/msteams/runtime-api.ts +++ b/extensions/msteams/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Microsoft Teams extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/msteams.js"; +export * from "openclaw/plugin-sdk/msteams"; diff --git a/extensions/nextcloud-talk/runtime-api.ts b/extensions/nextcloud-talk/runtime-api.ts index 80bc1b1dc7b..b2093a7a057 100644 --- a/extensions/nextcloud-talk/runtime-api.ts +++ b/extensions/nextcloud-talk/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Nextcloud Talk extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/nextcloud-talk.js"; +export * from "openclaw/plugin-sdk/nextcloud-talk"; diff --git a/extensions/nostr/runtime-api.ts b/extensions/nostr/runtime-api.ts index 602b0ac81b7..29825771891 100644 --- a/extensions/nostr/runtime-api.ts +++ b/extensions/nostr/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Nostr extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/nostr.js"; +export * from "openclaw/plugin-sdk/nostr"; diff --git a/extensions/signal/src/runtime-api.ts b/extensions/signal/src/runtime-api.ts index 172943641f8..6aeeef0adb1 100644 --- a/extensions/signal/src/runtime-api.ts +++ b/extensions/signal/src/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Signal extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../../src/plugin-sdk/signal.js"; +export * from "openclaw/plugin-sdk/signal"; diff --git a/extensions/tlon/runtime-api.ts b/extensions/tlon/runtime-api.ts index 3ba9718868f..3c2c83655c5 100644 --- a/extensions/tlon/runtime-api.ts +++ b/extensions/tlon/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Tlon extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/tlon.js"; +export * from "openclaw/plugin-sdk/tlon"; diff --git a/extensions/twitch/runtime-api.ts b/extensions/twitch/runtime-api.ts index 9d055202a39..87433b1997f 100644 --- a/extensions/twitch/runtime-api.ts +++ b/extensions/twitch/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Twitch extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/twitch.js"; +export * from "openclaw/plugin-sdk/twitch"; diff --git a/extensions/voice-call/runtime-api.ts b/extensions/voice-call/runtime-api.ts index f0b32548645..9dd4fb0f3bc 100644 --- a/extensions/voice-call/runtime-api.ts +++ b/extensions/voice-call/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Voice Call extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/voice-call.js"; +export * from "openclaw/plugin-sdk/voice-call"; diff --git a/extensions/zalo/runtime-api.ts b/extensions/zalo/runtime-api.ts index 082f65d43b8..90ced0da803 100644 --- a/extensions/zalo/runtime-api.ts +++ b/extensions/zalo/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Zalo extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/zalo.js"; +export * from "openclaw/plugin-sdk/zalo"; diff --git a/extensions/zalouser/runtime-api.ts b/extensions/zalouser/runtime-api.ts index 1b63edaea42..7d931f2d118 100644 --- a/extensions/zalouser/runtime-api.ts +++ b/extensions/zalouser/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Zalo Personal extension. // Keep this barrel thin and aligned with the local extension surface. -export * from "../../src/plugin-sdk/zalouser.js"; +export * from "openclaw/plugin-sdk/zalouser"; diff --git a/package.json b/package.json index a522ced8380..91abc6172a7 100644 --- a/package.json +++ b/package.json @@ -193,10 +193,50 @@ "types": "./dist/plugin-sdk/discord-core.d.ts", "default": "./dist/plugin-sdk/discord-core.js" }, + "./plugin-sdk/feishu": { + "types": "./dist/plugin-sdk/feishu.d.ts", + "default": "./dist/plugin-sdk/feishu.js" + }, + "./plugin-sdk/googlechat": { + "types": "./dist/plugin-sdk/googlechat.d.ts", + "default": "./dist/plugin-sdk/googlechat.js" + }, + "./plugin-sdk/irc": { + "types": "./dist/plugin-sdk/irc.d.ts", + "default": "./dist/plugin-sdk/irc.js" + }, + "./plugin-sdk/line": { + "types": "./dist/plugin-sdk/line.d.ts", + "default": "./dist/plugin-sdk/line.js" + }, + "./plugin-sdk/line-core": { + "types": "./dist/plugin-sdk/line-core.d.ts", + "default": "./dist/plugin-sdk/line-core.js" + }, "./plugin-sdk/matrix": { "types": "./dist/plugin-sdk/matrix.d.ts", "default": "./dist/plugin-sdk/matrix.js" }, + "./plugin-sdk/mattermost": { + "types": "./dist/plugin-sdk/mattermost.d.ts", + "default": "./dist/plugin-sdk/mattermost.js" + }, + "./plugin-sdk/msteams": { + "types": "./dist/plugin-sdk/msteams.d.ts", + "default": "./dist/plugin-sdk/msteams.js" + }, + "./plugin-sdk/nextcloud-talk": { + "types": "./dist/plugin-sdk/nextcloud-talk.d.ts", + "default": "./dist/plugin-sdk/nextcloud-talk.js" + }, + "./plugin-sdk/nostr": { + "types": "./dist/plugin-sdk/nostr.d.ts", + "default": "./dist/plugin-sdk/nostr.js" + }, + "./plugin-sdk/signal": { + "types": "./dist/plugin-sdk/signal.d.ts", + "default": "./dist/plugin-sdk/signal.js" + }, "./plugin-sdk/slack": { "types": "./dist/plugin-sdk/slack.d.ts", "default": "./dist/plugin-sdk/slack.js" @@ -205,6 +245,26 @@ "types": "./dist/plugin-sdk/slack-core.d.ts", "default": "./dist/plugin-sdk/slack-core.js" }, + "./plugin-sdk/tlon": { + "types": "./dist/plugin-sdk/tlon.d.ts", + "default": "./dist/plugin-sdk/tlon.js" + }, + "./plugin-sdk/twitch": { + "types": "./dist/plugin-sdk/twitch.d.ts", + "default": "./dist/plugin-sdk/twitch.js" + }, + "./plugin-sdk/voice-call": { + "types": "./dist/plugin-sdk/voice-call.d.ts", + "default": "./dist/plugin-sdk/voice-call.js" + }, + "./plugin-sdk/zalo": { + "types": "./dist/plugin-sdk/zalo.d.ts", + "default": "./dist/plugin-sdk/zalo.js" + }, + "./plugin-sdk/zalouser": { + "types": "./dist/plugin-sdk/zalouser.d.ts", + "default": "./dist/plugin-sdk/zalouser.js" + }, "./plugin-sdk/imessage": { "types": "./dist/plugin-sdk/imessage.d.ts", "default": "./dist/plugin-sdk/imessage.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index e5dad4777eb..1dc306bd9b7 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -38,9 +38,24 @@ "telegram-core", "discord", "discord-core", + "feishu", + "googlechat", + "irc", + "line", + "line-core", "matrix", + "mattermost", + "msteams", + "nextcloud-talk", + "nostr", + "signal", "slack", "slack-core", + "tlon", + "twitch", + "voice-call", + "zalo", + "zalouser", "imessage", "imessage-core", "whatsapp", diff --git a/src/plugin-sdk/line-core.ts b/src/plugin-sdk/line-core.ts index 04b2950a50d..596593fc8f4 100644 --- a/src/plugin-sdk/line-core.ts +++ b/src/plugin-sdk/line-core.ts @@ -3,11 +3,11 @@ export type { LineConfig } from "../line/types.js"; export { createTopLevelChannelDmPolicy, DEFAULT_ACCOUNT_ID, - formatDocsLink, setSetupChannelEnabled, setTopLevelChannelDmPolicyWithAllowFrom, splitSetupEntries, } from "./setup.js"; +export { formatDocsLink } from "../terminal/links.js"; export type { ChannelSetupAdapter, ChannelSetupDmPolicy, ChannelSetupWizard } from "./setup.js"; export { listLineAccountIds, diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 669586bb80c..11ffc459ef2 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -62,6 +62,14 @@ function resolveControlCommandGate(params) { return { commandAuthorized, shouldBlock }; } +function onDiagnosticEvent(listener) { + const monolithic = loadMonolithicSdk(); + if (!monolithic || typeof monolithic.onDiagnosticEvent !== "function") { + throw new Error("openclaw/plugin-sdk root alias could not resolve onDiagnosticEvent"); + } + return monolithic.onDiagnosticEvent(listener); +} + function getPackageRoot() { return path.resolve(__dirname, "..", ".."); } @@ -152,6 +160,7 @@ function tryLoadMonolithicSdk() { const fastExports = { emptyPluginConfigSchema, + onDiagnosticEvent, resolveControlCommandGate, }; diff --git a/src/plugin-sdk/root-alias.test.ts b/src/plugin-sdk/root-alias.test.ts index 48ae4a7b43c..37072f9ded7 100644 --- a/src/plugin-sdk/root-alias.test.ts +++ b/src/plugin-sdk/root-alias.test.ts @@ -180,7 +180,11 @@ describe("plugin-sdk root alias", () => { const lazyRootSdk = lazyModule.moduleExports; expect(typeof lazyRootSdk.onDiagnosticEvent).toBe("function"); - expect(lazyRootSdk.onDiagnosticEvent).toBe(onDiagnosticEvent); + expect( + typeof (lazyRootSdk.onDiagnosticEvent as (listener: () => void) => () => void)( + () => undefined, + ), + ).toBe("function"); expect("onDiagnosticEvent" in lazyRootSdk).toBe(true); }); diff --git a/src/plugin-sdk/runtime-api-guardrails.test.ts b/src/plugin-sdk/runtime-api-guardrails.test.ts index 2158edff7d0..afa32af0b7f 100644 --- a/src/plugin-sdk/runtime-api-guardrails.test.ts +++ b/src/plugin-sdk/runtime-api-guardrails.test.ts @@ -34,14 +34,14 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { probeIMessage } from "./src/probe.js";', 'export { sendMessageIMessage } from "./src/send.js";', ], - "extensions/googlechat/runtime-api.ts": ['export * from "../../src/plugin-sdk/googlechat.js";'], + "extensions/googlechat/runtime-api.ts": ['export * from "openclaw/plugin-sdk/googlechat";'], "extensions/matrix/runtime-api.ts": [ 'export * from "./src/auth-precedence.js";', 'export * from "./helper-api.js";', 'export * from "./thread-bindings-runtime.js";', ], "extensions/nextcloud-talk/runtime-api.ts": [ - 'export * from "../../src/plugin-sdk/nextcloud-talk.js";', + 'export * from "openclaw/plugin-sdk/nextcloud-talk";', ], "extensions/signal/runtime-api.ts": ['export * from "./src/runtime-api.js";'], "extensions/slack/runtime-api.ts": [ diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index ab8c16d71f7..566dc6645e1 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -61,30 +61,15 @@ describe("plugin-sdk subpath exports", () => { expect(pluginSdkSubpaths).not.toContain("acpx"); expect(pluginSdkSubpaths).not.toContain("compat"); expect(pluginSdkSubpaths).not.toContain("device-pair"); - expect(pluginSdkSubpaths).not.toContain("feishu"); expect(pluginSdkSubpaths).not.toContain("google"); - expect(pluginSdkSubpaths).not.toContain("googlechat"); - expect(pluginSdkSubpaths).not.toContain("irc"); - expect(pluginSdkSubpaths).not.toContain("line"); - expect(pluginSdkSubpaths).not.toContain("line-core"); expect(pluginSdkSubpaths).not.toContain("lobster"); - expect(pluginSdkSubpaths).not.toContain("mattermost"); - expect(pluginSdkSubpaths).not.toContain("msteams"); - expect(pluginSdkSubpaths).not.toContain("nextcloud-talk"); - expect(pluginSdkSubpaths).not.toContain("nostr"); expect(pluginSdkSubpaths).not.toContain("pairing-access"); expect(pluginSdkSubpaths).not.toContain("qwen-portal-auth"); expect(pluginSdkSubpaths).not.toContain("reply-prefix"); - expect(pluginSdkSubpaths).not.toContain("signal"); expect(pluginSdkSubpaths).not.toContain("signal-core"); expect(pluginSdkSubpaths).not.toContain("synology-chat"); - expect(pluginSdkSubpaths).not.toContain("tlon"); - expect(pluginSdkSubpaths).not.toContain("twitch"); expect(pluginSdkSubpaths).not.toContain("typing"); - expect(pluginSdkSubpaths).not.toContain("voice-call"); - expect(pluginSdkSubpaths).not.toContain("zalo"); expect(pluginSdkSubpaths).not.toContain("zai"); - expect(pluginSdkSubpaths).not.toContain("zalouser"); expect(pluginSdkSubpaths).not.toContain("provider-model-definitions"); }); From fcabecc9a4fbeb47b07d770bb734b4580cecc783 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 20 Mar 2026 16:52:10 +0000 Subject: [PATCH 04/10] fix: remove duplicate plugin sdk exports --- src/plugin-sdk/acp-runtime.ts | 1 - src/plugin-sdk/core.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/plugin-sdk/acp-runtime.ts b/src/plugin-sdk/acp-runtime.ts index 88088867b2a..1657cb7cace 100644 --- a/src/plugin-sdk/acp-runtime.ts +++ b/src/plugin-sdk/acp-runtime.ts @@ -2,7 +2,6 @@ export { getAcpSessionManager } from "../acp/control-plane/manager.js"; export { AcpRuntimeError, isAcpRuntimeError } from "../acp/runtime/errors.js"; -export { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../acp/runtime/registry.js"; export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js"; export { getAcpRuntimeBackend, diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index b5b149e25b0..c8c7980fbd2 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -58,8 +58,6 @@ export type { PluginCommandContext, PluginLogger, PluginInteractiveTelegramHandlerContext, - OpenClawPluginToolContext, - OpenClawPluginToolFactory, } from "../plugins/types.js"; export type { OpenClawConfig } from "../config/config.js"; export { isSecretRef } from "../config/types.secrets.js"; From 87eeab703465eb42481946b130b9122c3e0fb587 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 20 Mar 2026 10:04:11 -0700 Subject: [PATCH 05/10] docs: add plugin SDK migration guide, link deprecation warning to docs --- docs/docs.json | 1 + docs/plugins/sdk-migration.md | 144 ++++++++++++++++++++++++++++++++++ src/plugin-sdk/compat.ts | 4 +- 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 docs/plugins/sdk-migration.md diff --git a/docs/docs.json b/docs/docs.json index c9df5c4f0cc..65e4ed25c1b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1076,6 +1076,7 @@ "group": "Extensions", "pages": [ "plugins/building-extensions", + "plugins/sdk-migration", "plugins/architecture", "plugins/community", "plugins/bundles", diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md new file mode 100644 index 00000000000..7ae4e514c94 --- /dev/null +++ b/docs/plugins/sdk-migration.md @@ -0,0 +1,144 @@ +--- +title: "Plugin SDK Migration" +summary: "Migrate from openclaw/plugin-sdk/compat to focused subpath imports" +read_when: + - You see the OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED warning + - You are updating a plugin from the monolithic plugin-sdk import to scoped subpaths + - You maintain an external OpenClaw plugin +--- + +# Plugin SDK Migration + +OpenClaw is migrating from a single monolithic `openclaw/plugin-sdk/compat` barrel +to **focused subpath imports** (`openclaw/plugin-sdk/`). This page explains +what changed, why, and how to migrate. + +## Why this change + +The monolithic compat barrel re-exported everything from a single entry point. +This caused: + +- **Slow startup**: importing one helper pulled in dozens of unrelated modules. +- **Circular dependency risk**: broad re-exports made it easy to create import cycles. +- **Unclear API surface**: no way to tell which exports were stable vs internal. + +Focused subpaths fix all three: each subpath is a small, self-contained module +with a clear purpose. + +## What triggers the warning + +If your plugin imports from the compat barrel, you will see: + +``` +[OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED] Warning: openclaw/plugin-sdk/compat is +deprecated for new plugins. Migrate to focused openclaw/plugin-sdk/ imports. +``` + +The compat barrel still works at runtime. This is a deprecation warning, not an +error. But new plugins **must not** use it, and existing plugins should migrate +before compat is removed. + +## How to migrate + +### Step 1: Find compat imports + +Search your extension for imports from the compat path: + +```bash +grep -r "plugin-sdk/compat" extensions/my-plugin/ +``` + +### Step 2: Replace with focused subpaths + +Each export from compat maps to a specific subpath. Replace the import source: + +```typescript +// Before (compat barrel) +import { + createChannelReplyPipeline, + createPluginRuntimeStore, + resolveControlCommandGate, +} from "openclaw/plugin-sdk/compat"; + +// After (focused subpaths) +import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; +import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; +import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth"; +``` + +### Step 3: Verify + +Run the build and tests: + +```bash +pnpm build +pnpm test -- extensions/my-plugin/ +``` + +## Subpath reference + +| Subpath | Purpose | Key exports | +| ----------------------------------- | ------------------------------------ | ---------------------------------------------------------------------- | +| `plugin-sdk/core` | Plugin entry definitions, base types | `defineChannelPluginEntry`, `definePluginEntry` | +| `plugin-sdk/channel-setup` | Setup wizard adapters | `createOptionalChannelSetupSurface` | +| `plugin-sdk/channel-pairing` | DM pairing primitives | `createChannelPairingController` | +| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring | `createChannelReplyPipeline` | +| `plugin-sdk/channel-config-helpers` | Config adapter factories | `createHybridChannelConfigAdapter`, `createScopedChannelConfigAdapter` | +| `plugin-sdk/channel-config-schema` | Config schema builders | Channel config schema types | +| `plugin-sdk/channel-policy` | Group/DM policy resolution | `resolveChannelGroupRequireMention` | +| `plugin-sdk/channel-lifecycle` | Account status tracking | `createAccountStatusSink` | +| `plugin-sdk/channel-runtime` | Runtime wiring helpers | Channel runtime utilities | +| `plugin-sdk/channel-send-result` | Send result types | Reply result types | +| `plugin-sdk/runtime-store` | Persistent plugin storage | `createPluginRuntimeStore` | +| `plugin-sdk/allow-from` | Allowlist formatting | `formatAllowFromLowercase`, `formatNormalizedAllowFromEntries` | +| `plugin-sdk/allowlist-resolution` | Allowlist input mapping | `mapAllowlistResolutionInputs` | +| `plugin-sdk/command-auth` | Command gating | `resolveControlCommandGate` | +| `plugin-sdk/secret-input` | Secret input parsing | Secret input helpers | +| `plugin-sdk/webhook-ingress` | Webhook request helpers | Webhook target utilities | +| `plugin-sdk/reply-payload` | Message reply types | Reply payload types | +| `plugin-sdk/provider-onboard` | Provider onboarding patches | Onboarding config helpers | +| `plugin-sdk/keyed-async-queue` | Ordered async queue | `KeyedAsyncQueue` | +| `plugin-sdk/testing` | Test utilities | Test helpers and mocks | + +Use the narrowest subpath that has what you need. If you cannot find an export, +check the source at `src/plugin-sdk/` or ask in Discord. + +## Compat barrel removal timeline + +- **Now**: compat barrel emits a deprecation warning at runtime. +- **Next major release**: compat barrel will be removed. Plugins still using it will + fail to import. + +Bundled plugins (under `extensions/`) have already been migrated. External plugins +should migrate before the next major release. + +## Suppressing the warning temporarily + +If you need to suppress the warning while migrating: + +```bash +OPENCLAW_SUPPRESS_PLUGIN_SDK_COMPAT_WARNING=1 openclaw gateway run +``` + +This is a temporary escape hatch, not a permanent solution. + +## Internal barrel pattern + +Within your extension, use local barrel files (`api.ts`, `runtime-api.ts`) for +internal code sharing instead of importing through the plugin SDK: + +```typescript +// extensions/my-plugin/api.ts — public contract for this extension +export { MyConfig } from "./src/config.js"; +export { MyRuntime } from "./src/runtime.js"; +``` + +Never import your own extension back through `openclaw/plugin-sdk/` +from production files. That path is for external consumers only. See +[Building Extensions](/plugins/building-extensions#step-4-use-local-barrels-for-internal-imports). + +## Related + +- [Building Extensions](/plugins/building-extensions) +- [Plugin Architecture](/plugins/architecture) +- [Plugin Manifest](/plugins/manifest) diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index 99e2066633c..643557f0960 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -8,11 +8,11 @@ const shouldWarnCompatImport = if (shouldWarnCompatImport) { process.emitWarning( - "openclaw/plugin-sdk/compat is deprecated for new plugins. Migrate to focused openclaw/plugin-sdk/ imports.", + "openclaw/plugin-sdk/compat is deprecated for new plugins. Migrate to focused openclaw/plugin-sdk/ imports. See https://docs.openclaw.ai/plugins/sdk-migration", { code: "OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED", detail: - "Bundled plugins must use scoped plugin-sdk subpaths. External plugins may keep compat temporarily while migrating.", + "Bundled plugins must use scoped plugin-sdk subpaths. External plugins may keep compat temporarily while migrating. Migration guide: https://docs.openclaw.ai/plugins/sdk-migration", }, ); } From 93fbe26adbbcf15fec0b2ddd395478e9100de41e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 20 Mar 2026 10:10:57 -0700 Subject: [PATCH 06/10] fix(config): tighten json and json5 parsing paths (#51153) --- CHANGELOG.md | 1 + src/agents/subagent-depth.test.ts | 27 ++++++++++++++++++++++++++ src/agents/subagent-depth.ts | 10 +++++++++- src/cli/config-cli.test.ts | 11 +++++++++++ src/cli/config-cli.ts | 8 ++++---- src/config/paths.ts | 2 +- src/cron/store.test.ts | 32 +++++++++++++++++++++++++++++++ src/cron/store.ts | 10 +++++++++- ui/src/ui/views/config.ts | 4 ++-- 9 files changed, 96 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b95fe247361..8e33a2d82a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/config: make `config set --strict-json` enforce real JSON, prefer `JSON.parse` with JSON5 fallback for machine-written cron/subagent stores, and relabel raw config surfaces as `JSON/JSON5` to match actual compatibility. Related: #48415, #43127, #14529, #21332. Thanks @adhitShet and @vincentkoc. - CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD. - Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev. - Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev. diff --git a/src/agents/subagent-depth.test.ts b/src/agents/subagent-depth.test.ts index 5d9427b7818..2c62432a692 100644 --- a/src/agents/subagent-depth.test.ts +++ b/src/agents/subagent-depth.test.ts @@ -76,6 +76,33 @@ describe("getSubagentDepthFromSessionStore", () => { expect(depth).toBe(2); }); + it("accepts JSON5 syntax in the on-disk depth store for backward compatibility", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-depth-json5-")); + const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json"); + const storePath = storeTemplate.replaceAll("{agentId}", "main"); + fs.writeFileSync( + storePath, + `{ + // hand-edited legacy store + "agent:main:subagent:flat": { + sessionId: "subagent-flat", + spawnDepth: 2, + }, + }`, + "utf-8", + ); + + const depth = getSubagentDepthFromSessionStore("subagent:flat", { + cfg: { + session: { + store: storeTemplate, + }, + }, + }); + + expect(depth).toBe(2); + }); + it("falls back to session-key segment counting when metadata is missing", () => { const key = "agent:main:subagent:flat"; const depth = getSubagentDepthFromSessionStore(key, { diff --git a/src/agents/subagent-depth.ts b/src/agents/subagent-depth.ts index 8b62539ac45..9ad03bbbc91 100644 --- a/src/agents/subagent-depth.ts +++ b/src/agents/subagent-depth.ts @@ -11,6 +11,14 @@ type SessionDepthEntry = { spawnedBy?: unknown; }; +function parseSessionDepthStore(raw: string): unknown { + try { + return JSON.parse(raw); + } catch { + return JSON5.parse(raw); + } +} + function normalizeSpawnDepth(value: unknown): number | undefined { if (typeof value === "number") { return Number.isInteger(value) && value >= 0 ? value : undefined; @@ -37,7 +45,7 @@ function normalizeSessionKey(value: unknown): string | undefined { function readSessionStore(storePath: string): Record { try { const raw = fs.readFileSync(storePath, "utf-8"); - const parsed = JSON5.parse(raw); + const parsed = parseSessionDepthStore(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return parsed as Record; } diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index d30a476004d..6e9cc07bf7e 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -442,6 +442,15 @@ describe("config cli", () => { expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled(); }); + it("rejects JSON5-only object syntax when strict parsing is enabled", async () => { + await expect( + runConfigCommand(["config", "set", "gateway.auth", "{mode:'token'}", "--strict-json"]), + ).rejects.toThrow("__exit__:1"); + + expect(mockWriteConfigFile).not.toHaveBeenCalled(); + expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled(); + }); + it("accepts --strict-json with batch mode and applies batch payload", async () => { const resolved: OpenClawConfig = { gateway: { port: 18789 } }; setSnapshot(resolved, resolved); @@ -470,6 +479,8 @@ describe("config cli", () => { expect(helpText).toContain("--strict-json"); expect(helpText).toContain("--json"); expect(helpText).toContain("Legacy alias for --strict-json"); + expect(helpText).toContain("Value (JSON/JSON5 or raw string)"); + expect(helpText).toContain("Strict JSON parsing (error instead of"); expect(helpText).toContain("--ref-provider"); expect(helpText).toContain("--provider-source"); expect(helpText).toContain("--batch-json"); diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index 604e27666c9..e7a94ae99ab 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -159,9 +159,9 @@ function parseValue(raw: string, opts: ConfigSetParseOpts): unknown { const trimmed = raw.trim(); if (opts.strictJson) { try { - return JSON5.parse(trimmed); + return JSON.parse(trimmed); } catch (err) { - throw new Error(`Failed to parse JSON5 value: ${String(err)}`, { cause: err }); + throw new Error(`Failed to parse JSON value: ${String(err)}`, { cause: err }); } } @@ -1280,8 +1280,8 @@ export function registerConfigCli(program: Command) { .command("set") .description(CONFIG_SET_DESCRIPTION) .argument("[path]", "Config path (dot or bracket notation)") - .argument("[value]", "Value (JSON5 or raw string)") - .option("--strict-json", "Strict JSON5 parsing (error instead of raw string fallback)", false) + .argument("[value]", "Value (JSON/JSON5 or raw string)") + .option("--strict-json", "Strict JSON parsing (error instead of raw string fallback)", false) .option("--json", "Legacy alias for --strict-json", false) .option( "--dry-run", diff --git a/src/config/paths.ts b/src/config/paths.ts index 84c27749bcf..a35a1a3d03d 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -99,7 +99,7 @@ function resolveUserPath( export const STATE_DIR = resolveStateDir(); /** - * Config file path (JSON5). + * Config file path (JSON or JSON5). * Can be overridden via OPENCLAW_CONFIG_PATH. * Default: ~/.openclaw/openclaw.json (or $OPENCLAW_STATE_DIR/openclaw.json) */ diff --git a/src/cron/store.test.ts b/src/cron/store.test.ts index f511636fb85..405d04cbe60 100644 --- a/src/cron/store.test.ts +++ b/src/cron/store.test.ts @@ -56,6 +56,38 @@ describe("cron store", () => { await expect(loadCronStore(store.storePath)).rejects.toThrow(/Failed to parse cron store/i); }); + it("accepts JSON5 syntax when loading an existing cron store", async () => { + const store = await makeStorePath(); + await fs.mkdir(path.dirname(store.storePath), { recursive: true }); + await fs.writeFile( + store.storePath, + `{ + // hand-edited legacy store + version: 1, + jobs: [ + { + id: 'job-1', + name: 'Job 1', + enabled: true, + createdAtMs: 1, + updatedAtMs: 1, + schedule: { kind: 'every', everyMs: 60000 }, + sessionTarget: 'main', + wakeMode: 'next-heartbeat', + payload: { kind: 'systemEvent', text: 'tick-job-1' }, + state: {}, + }, + ], + }`, + "utf-8", + ); + + await expect(loadCronStore(store.storePath)).resolves.toMatchObject({ + version: 1, + jobs: [{ id: "job-1", enabled: true }], + }); + }); + it("does not create a backup file when saving unchanged content", async () => { const store = await makeStorePath(); const payload = makeStore("job-1", true); diff --git a/src/cron/store.ts b/src/cron/store.ts index 8e8f0440f35..551a1f3cb64 100644 --- a/src/cron/store.ts +++ b/src/cron/store.ts @@ -10,6 +10,14 @@ export const DEFAULT_CRON_DIR = path.join(CONFIG_DIR, "cron"); export const DEFAULT_CRON_STORE_PATH = path.join(DEFAULT_CRON_DIR, "jobs.json"); const serializedStoreCache = new Map(); +function parseCronStoreRaw(raw: string): unknown { + try { + return JSON.parse(raw); + } catch { + return JSON5.parse(raw); + } +} + export function resolveCronStorePath(storePath?: string) { if (storePath?.trim()) { const raw = storePath.trim(); @@ -26,7 +34,7 @@ export async function loadCronStore(storePath: string): Promise { const raw = await fs.promises.readFile(storePath, "utf-8"); let parsed: unknown; try { - parsed = JSON5.parse(raw); + parsed = parseCronStoreRaw(raw); } catch (err) { throw new Error(`Failed to parse cron store at ${storePath}: ${String(err)}`, { cause: err, diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 7c1121e6bb8..6e3db2c6a67 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -1062,7 +1062,7 @@ export function renderConfig(props: ConfigProps) { }
- Raw JSON5 + Raw config (JSON/JSON5) ${ sensitiveCount > 0 ? html` @@ -1087,7 +1087,7 @@ export function renderConfig(props: ConfigProps) {