fix(gateway): block webchat session compaction mutations (#70716)

* fix(gateway): block webchat session compaction mutations

* docs(changelog): note webchat compaction guard (#70716)
This commit is contained in:
Devin Robison
2026-04-23 14:50:35 -06:00
committed by GitHub
parent 8af3d91668
commit 873dce178d
3 changed files with 51 additions and 4 deletions

View File

@@ -241,6 +241,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Control UI: require authenticated Control UI read access before serving `/__openclaw/control-ui-config.json` when `gateway.auth` is enabled, so unauthenticated callers can no longer read bootstrap metadata. (#70247) Thanks @drobison00.
- Gateway/restart: default session-scoped restart sentinels to a one-shot agent continuation, so chat-initiated Gateway restarts acknowledge successful boot automatically. (#70269) Thanks @obviyus.
- Build/npm publish: fail postpublish verification when root `dist/*` files import bundled plugin runtime dependencies without mirroring them in the root package manifest, so Slack-style plugin deps cannot silently ship on the wrong module-resolution path again. (#60112) thanks @medns.
- Gateway/sessions: extend the webchat session-mutation guard to `sessions.compact` and `sessions.compaction.restore`, so `WEBCHAT_UI` clients are rejected from compaction-side session mutations consistently with the existing patch/delete guards. (#70716) Thanks @drobison00.
## 2026.4.21

View File

@@ -223,7 +223,7 @@ function emitSessionsChanged(
}
function rejectWebchatSessionMutation(params: {
action: "patch" | "delete";
action: "patch" | "delete" | "compact" | "restore";
client: GatewayClient | null;
isWebchatConnect: (params: GatewayClient["connect"] | null | undefined) => boolean;
respond: RespondFn;
@@ -1101,6 +1101,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
if (!key) {
return;
}
if (rejectWebchatSessionMutation({ action: "restore", client, isWebchatConnect, respond })) {
return;
}
const checkpointId =
typeof p.checkpointId === "string" && p.checkpointId.trim() ? p.checkpointId.trim() : "";
if (!checkpointId) {
@@ -1495,6 +1498,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
if (!key) {
return;
}
if (rejectWebchatSessionMutation({ action: "compact", client, isWebchatConnect, respond })) {
return;
}
const maxLines =
typeof p.maxLines === "number" && Number.isFinite(p.maxLines)

View File

@@ -3329,14 +3329,40 @@ describe("gateway server sessions", () => {
ws.close();
});
test("webchat clients cannot patch or delete sessions", async () => {
await createSessionStoreDir();
test("webchat clients cannot patch, delete, compact, or restore sessions", async () => {
const { dir } = await createSessionStoreDir();
const fixture = await createCheckpointFixture(dir);
await writeSessionStore({
entries: {
main: {
sessionId: "sess-main",
sessionId: fixture.sessionId,
sessionFile: fixture.sessionFile,
updatedAt: Date.now(),
compactionCheckpoints: [
{
checkpointId: "checkpoint-1",
sessionKey: "agent:main:main",
sessionId: fixture.sessionId,
createdAt: Date.now(),
reason: "manual",
tokensBefore: 123,
tokensAfter: 45,
summary: "checkpoint summary",
firstKeptEntryId: fixture.preCompactionLeafId,
preCompaction: {
sessionId: fixture.preCompactionSession.getSessionId(),
sessionFile: fixture.preCompactionSessionFile,
leafId: fixture.preCompactionLeafId,
},
postCompaction: {
sessionId: fixture.sessionId,
sessionFile: fixture.sessionFile,
leafId: fixture.postCompactionLeafId,
entryId: fixture.postCompactionLeafId,
},
},
],
},
"discord:group:dev": {
sessionId: "sess-group",
@@ -3373,6 +3399,20 @@ describe("gateway server sessions", () => {
expect(deleted.ok).toBe(false);
expect(deleted.error?.message ?? "").toMatch(/webchat clients cannot delete sessions/i);
const compacted = await rpcReq(ws, "sessions.compact", {
key: "main",
maxLines: 3,
});
expect(compacted.ok).toBe(false);
expect(compacted.error?.message ?? "").toMatch(/webchat clients cannot compact sessions/i);
const restored = await rpcReq(ws, "sessions.compaction.restore", {
key: "main",
checkpointId: "checkpoint-1",
});
expect(restored.ok).toBe(false);
expect(restored.error?.message ?? "").toMatch(/webchat clients cannot restore sessions/i);
ws.close();
});