mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(gateway,agent): only enforce session sendPolicy=deny when delivering (#76317)
Summary: - This PR gates gateway `agent` send-policy rejection on `request.deliver === true`, adds denied non-delivery ... plicit-delivery regression coverage, updates a gateway chat expectation, and adds a #73381 changelog entry. - Reproducibility: yes. from source inspection: current main resolves `sendPolicy` and rejects before delivery ... agent` request with `deliver` omitted or false. I did not run local tests because this review is read-only. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(gateway,agent): only enforce session sendPolicy=deny when delivering Validation: - ClawSweeper review passed for head5cfcb1c584. - Required merge gates passed before the squash merge. Prepared head SHA:5cfcb1c584Review: https://github.com/openclaw/openclaw/pull/76317#issuecomment-4364987993 Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: wenxu007 <270593229+wenxu007@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
|
||||
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.
|
||||
- Gateway/responses: emit every client tool call from `/v1/responses` JSON and SSE responses when the agent invokes multiple client tools in a single turn, so multi-tool plans, graph orchestration calls, and similar batched flows no longer drop every call but the last. Fixes #52288. Thanks @CharZhou and @bonelli.
|
||||
- Gateway/agent: enforce `session.sendPolicy=deny` on gateway agent requests only when `deliver: true`, so non-delivery smoke checks and internal agent runs are no longer rejected with `send blocked by session policy` while outbound delivery remains gated. Fixes #73381. Thanks @wenxu007.
|
||||
- Slack/reactions: treat missing no_reaction remove responses as idempotent success and route own-reaction cleanup through the remove helper, so concurrent cleanup no longer surfaces Slack race errors. Fixes #50733. (#76304) Thanks @martingarramon and @Hollychou924.
|
||||
- Control UI/Gateway: avoid full session-list reloads for locally applied message-phase session updates, carry known session keys through transcript-file update events, and defer media provider listing when explicit generation model config is present. Refs #76236, #76203, #76188, #76107, and #76166. Thanks @BunsDev.
|
||||
- Install/update: prune the obsolete `plugin-runtime-deps` state directory during packaged postinstall so upgrades from pre-2026.5.2 releases reclaim old bundled-plugin dependency caches without touching external plugin installs.
|
||||
|
||||
@@ -36,6 +36,7 @@ const mocks = vi.hoisted(() => ({
|
||||
loadConfigReturn: {} as Record<string, unknown>,
|
||||
loadVoiceWakeRoutingConfig: vi.fn(),
|
||||
resolveVoiceWakeRouteByTrigger: vi.fn(),
|
||||
resolveSendPolicy: vi.fn(() => "allow"),
|
||||
}));
|
||||
|
||||
vi.mock("../session-utils.js", async () => {
|
||||
@@ -128,7 +129,8 @@ vi.mock("../../infra/voicewake-routing.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../sessions/send-policy.js", () => ({
|
||||
resolveSendPolicy: () => "allow",
|
||||
resolveSendPolicy: (...args: unknown[]) =>
|
||||
(mocks.resolveSendPolicy as (...args: unknown[]) => unknown)(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/delivery-context.js", async () => {
|
||||
@@ -410,6 +412,7 @@ describe("gateway agent handler", () => {
|
||||
mocks.resolveExplicitAgentSessionKey.mockReset().mockReturnValue(undefined);
|
||||
mocks.resolveBareResetBootstrapFileAccess.mockReset().mockReturnValue(true);
|
||||
mocks.listAgentIds.mockReset().mockReturnValue(["main"]);
|
||||
mocks.resolveSendPolicy.mockReset().mockReturnValue("allow");
|
||||
});
|
||||
|
||||
it("preserves ACP metadata from the current stored session entry", async () => {
|
||||
@@ -2792,6 +2795,53 @@ describe("gateway agent handler", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows non-delivery agent invocations when sendPolicy is deny", async () => {
|
||||
mocks.agentCommand.mockClear();
|
||||
primeMainAgentRun();
|
||||
mocks.resolveSendPolicy.mockReturnValue("deny");
|
||||
|
||||
const respond = await runMainAgent("smoke", "non-delivery-deny");
|
||||
|
||||
expect(mocks.resolveSendPolicy).not.toHaveBeenCalled();
|
||||
expect(respond).not.toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "send blocked by session policy" }),
|
||||
);
|
||||
await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("blocks delivery agent invocations when sendPolicy is deny", async () => {
|
||||
primeMainAgentRun();
|
||||
mocks.resolveSendPolicy.mockReturnValue("deny");
|
||||
mocks.agentCommand.mockClear();
|
||||
|
||||
const respond = vi.fn();
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "smoke",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: "delivery-deny",
|
||||
deliver: true,
|
||||
},
|
||||
{ respond, reqId: "delivery-deny" },
|
||||
);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "send blocked by session policy" }),
|
||||
);
|
||||
expect(mocks.resolveSendPolicy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entry: expect.objectContaining({ sessionId: "existing-session-id" }),
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
expect(mocks.agentCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("groupId session-entry persistence validation", () => {
|
||||
async function captureGroupEntryFields(
|
||||
sessionKey: string,
|
||||
|
||||
@@ -1069,20 +1069,22 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
claudeCliSessionId: entry?.claudeCliSessionId,
|
||||
};
|
||||
sessionEntry = mergeSessionEntry(entry, nextEntryPatch);
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
entry,
|
||||
sessionKey: canonicalKey,
|
||||
channel: entry?.channel,
|
||||
chatType: entry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "send blocked by session policy"),
|
||||
);
|
||||
return;
|
||||
if (request.deliver === true) {
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
entry: sessionEntry,
|
||||
sessionKey: canonicalKey,
|
||||
channel: sessionEntry?.channel,
|
||||
chatType: sessionEntry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "send blocked by session policy"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolvedSessionId = sessionId;
|
||||
const canonicalSessionKey = canonicalKey;
|
||||
|
||||
@@ -488,15 +488,16 @@ describe("gateway server chat", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const agentBlockedRes = await rpcReq(ws, "agent", {
|
||||
vi.mocked(agentCommand).mockClear();
|
||||
const agentAllowedRes = await rpcReq(ws, "agent", {
|
||||
sessionKey: "cron:job-1",
|
||||
message: "hi",
|
||||
idempotencyKey: "idem-2",
|
||||
});
|
||||
expect(agentBlockedRes.ok).toBe(false);
|
||||
expect((agentBlockedRes.error as { message?: string } | undefined)?.message ?? "").toMatch(
|
||||
/send blocked/i,
|
||||
);
|
||||
expect(agentAllowedRes.ok).toBe(true);
|
||||
expect(agentAllowedRes.payload?.status).toBe("accepted");
|
||||
expect(agentAllowedRes.payload?.runId).toBe("idem-2");
|
||||
await vi.waitFor(() => expect(agentCommand).toHaveBeenCalled());
|
||||
|
||||
testState.sessionStorePath = undefined;
|
||||
testState.sessionConfig = undefined;
|
||||
|
||||
Reference in New Issue
Block a user