diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index dcc1b108a839..e1a318288948 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -699,8 +699,8 @@ An opted-in adapter must honor the existing This optional field keeps older adapters source-compatible. An omitted or empty declaration leaves newly enabled scheduled actions denied. Existing bundled -provider-owned message-management paths keep their admission rules. To support -the new installed-plugin path, +provider-owned interactive paths keep their admission rules. To support the new +installed-plugin path, upgrade OpenClaw and the plugin, implement the request and retry checks above, declare only the covered actions, and load the updated registration. Existing direct-operator and interactive actions retain their admission rules. Upgrading diff --git a/src/channels/plugins/message-action-dispatch.ts b/src/channels/plugins/message-action-dispatch.ts index 5f87e87ea2d4..b60d05510c7c 100644 --- a/src/channels/plugins/message-action-dispatch.ts +++ b/src/channels/plugins/message-action-dispatch.ts @@ -540,15 +540,6 @@ function prepareScheduledMessageWriteContext( if (!policy || !ctx.messageActionAuthorization?.scheduled) { return undefined; } - if ( - policy === "provider" && - prepared.enforcement.kind === "provider-owned" && - prepared.enforcement.pluginTrust === "bundled" && - !prepared.plugin.actions?.writeAuthorityActions?.includes(action) - ) { - // Retain existing bundled provider admission until its adapter opts into this fence. - return undefined; - } const accountId = ctx.accountId ?? resolveChannelDefaultAccountId({ plugin: prepared.plugin, cfg: ctx.cfg }); const access = resolveScheduledMessageActionAccess({ diff --git a/test/cli-message-authority.integration.test.ts b/test/cli-message-authority.integration.test.ts index c7bcaec46ac0..bc3436f871e8 100644 --- a/test/cli-message-authority.integration.test.ts +++ b/test/cli-message-authority.integration.test.ts @@ -1480,16 +1480,49 @@ describe("CLI message authority integration", () => { ]); }); - it("preserves bundled provider-owned scheduled pins without a write declaration", async () => { + it("keeps bundled interactive pins but denies undeclared scheduled writes", async () => { registerChannelPlugins({ discordOrigin: "bundled" }); const actions = expectDefined(registeredDiscordActions, "bundled Discord actions"); delete actions.writeAuthorityActions; - const turn = await createTurn("discord", { scheduledPolicy: accountScheduledPolicy }); + const interactiveTurn = await createTurn("discord"); expectSuccess( - await turn.call({ ...messageTarget, action: "pin", target: `channel:${discordSibling}` }), + await interactiveTurn.call({ + ...messageTarget, + action: "pin", + target: `channel:${discordSibling}`, + }), ); + const requestCount = requests.length; + const mismatchedOriginPolicy: ScheduledToolPolicyContext = { + ...accountScheduledPolicy, + ownerSessionKey: `agent:main:slack:channel:${channels.slack.current}`, + ownerOrigin: { kind: "external", channel: "slack" }, + }; + const mismatchedOriginTurn = await createTurn("discord", { + scheduledPolicy: mismatchedOriginPolicy, + }); + expectDenied( + await mismatchedOriginTurn.call({ + ...messageTarget, + action: "pin", + target: `channel:${discordSibling}`, + }), + /matching recorded creator origin/, + ); + const undeclaredTurn = await createTurn("discord", { + scheduledPolicy: accountScheduledPolicy, + }); + expectDenied( + await undeclaredTurn.call({ + ...messageTarget, + action: "pin", + target: `channel:${discordSibling}`, + }), + /write authorization support/, + ); + expect(requests).toHaveLength(requestCount); expect(requests.filter((request) => request.method !== "GET")).toEqual([ expect.objectContaining({ method: "PUT",