mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 11:21:07 +00:00
Gateway: align chat.send reset scope checks (#56009)
* Gateway: align chat.send reset scope checks * Gateway: tighten chat.send reset regression test * Gateway: honor internal provider reset scope
This commit is contained in:
@@ -1405,6 +1405,43 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
|
||||
}
|
||||
});
|
||||
|
||||
it("requires operator.admin when Provider is internal even if Surface carries external metadata", async () => {
|
||||
const storePath = await createStorePath("openclaw-internal-reset-provider-authoritative-");
|
||||
const sessionKey = "agent:main:telegram:dm:provider-authoritative";
|
||||
const existingSessionId = "existing-session-provider-authoritative";
|
||||
|
||||
await seedSessionStoreWithOverrides({
|
||||
storePath,
|
||||
sessionKey,
|
||||
sessionId: existingSessionId,
|
||||
overrides: {},
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { store: storePath, idleMinutes: 999 },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: "/reset",
|
||||
RawBody: "/reset",
|
||||
CommandBody: "/reset",
|
||||
Provider: "webchat",
|
||||
Surface: "telegram",
|
||||
OriginatingChannel: "telegram",
|
||||
GatewayClientScopes: ["operator.write"],
|
||||
ChatType: "direct",
|
||||
SessionKey: sessionKey,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.resetTriggered).toBe(false);
|
||||
expect(result.isNewSession).toBe(false);
|
||||
expect(result.sessionId).toBe(existingSessionId);
|
||||
});
|
||||
|
||||
it("archives the old session store entry on /new", async () => {
|
||||
const storePath = await createStorePath("openclaw-archive-old-");
|
||||
const sessionKey = "agent:main:telegram:dm:user-archive";
|
||||
|
||||
@@ -35,6 +35,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { resolveEffectiveResetTargetSessionKey } from "./acp-reset-target.js";
|
||||
@@ -78,6 +79,29 @@ export type SessionInitResult = {
|
||||
triggerBodyNormalized: string;
|
||||
};
|
||||
|
||||
function isResetAuthorizedForContext(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: OpenClawConfig;
|
||||
commandAuthorized: boolean;
|
||||
}): boolean {
|
||||
const auth = resolveCommandAuthorization(params);
|
||||
if (!auth.isAuthorizedSender) {
|
||||
return false;
|
||||
}
|
||||
const provider = params.ctx.Provider;
|
||||
const internalGatewayCaller = provider
|
||||
? isInternalMessageChannel(provider)
|
||||
: isInternalMessageChannel(params.ctx.Surface);
|
||||
if (!internalGatewayCaller) {
|
||||
return true;
|
||||
}
|
||||
const scopes = params.ctx.GatewayClientScopes;
|
||||
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return scopes.includes("operator.admin");
|
||||
}
|
||||
|
||||
function resolveAcpResetBindingContext(ctx: MsgContext): {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
@@ -251,11 +275,11 @@ export async function initSessionState(params: {
|
||||
// Use CommandBody/RawBody for reset trigger matching (clean message without structural context).
|
||||
const rawBody = commandSource;
|
||||
const trimmedBody = rawBody.trim();
|
||||
const resetAuthorized = resolveCommandAuthorization({
|
||||
const resetAuthorized = isResetAuthorizedForContext({
|
||||
ctx,
|
||||
cfg,
|
||||
commandAuthorized,
|
||||
}).isAuthorizedSender;
|
||||
});
|
||||
// Timestamp/message prefixes (e.g. "[Dec 4 17:35] ") are added by the
|
||||
// web inbox before we get here. They prevented reset triggers like "/new"
|
||||
// from matching, so strip structural wrappers when checking for resets.
|
||||
|
||||
@@ -726,6 +726,47 @@ describe("gateway server chat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("chat.send does not rotate sessions for operator.write reset triggers", async () => {
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
await withMainSessionStore(async () => {
|
||||
let scopedWs: WebSocket | undefined;
|
||||
|
||||
try {
|
||||
scopedWs = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
trackConnectChallengeNonce(scopedWs);
|
||||
await new Promise<void>((resolve) => scopedWs?.once("open", resolve));
|
||||
await connectOk(scopedWs, {
|
||||
scopes: ["operator.write"],
|
||||
});
|
||||
|
||||
const sendRes = await rpcReq(scopedWs, "chat.send", {
|
||||
sessionKey: "main",
|
||||
message: "/reset",
|
||||
idempotencyKey: "idem-write-scope-reset-no-rotate",
|
||||
});
|
||||
expect(sendRes.ok).toBe(true);
|
||||
|
||||
const waitRes = await rpcReq(scopedWs, "agent.wait", {
|
||||
runId: "idem-write-scope-reset-no-rotate",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
expect(waitRes.ok).toBe(true);
|
||||
expect(waitRes.payload?.status).toBe("ok");
|
||||
|
||||
const raw = await fs.readFile(testState.sessionStorePath!, "utf-8");
|
||||
const stored = JSON.parse(raw) as {
|
||||
"agent:main:main"?: {
|
||||
sessionId?: string;
|
||||
};
|
||||
};
|
||||
expect(stored["agent:main:main"]?.sessionId).toBe("sess-main");
|
||||
} finally {
|
||||
scopedWs?.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("agent.wait resolves chat.send runs that finish without lifecycle events", async () => {
|
||||
await withMainSessionStore(async () => {
|
||||
const runId = "idem-wait-chat-1";
|
||||
|
||||
Reference in New Issue
Block a user