Skip to content

Commit 1d492f5

Browse files
authored
fix(gateway): honor scopes for WebChat session mutations (#110931)
* fix(gateway): authorize webchat session mutations by scope * chore: keep gateway release note in PR body
1 parent db1b932 commit 1d492f5

10 files changed

Lines changed: 92 additions & 120 deletions

docs/gateway/operator-scopes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ the concrete thing being approved or mutated:
5858
This lets lower-scope operators perform low-risk pairing actions without
5959
making all pairing approval admin-only.
6060

61+
Session mutation RPCs are authorized by their negotiated operator scopes,
62+
independent of the connecting client's `client.id` or `client.mode`. Client
63+
identity can still affect connection and device-auth policy, but it neither
64+
grants nor removes session mutation authority.
65+
6166
## Device pairing approvals
6267

6368
Device pairing records are the durable source of approved roles and scopes.

src/gateway/server-methods/sessions-compact.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,14 @@ import {
3535
import {
3636
emitSessionOperation,
3737
loadAccessorSessionEntryForGatewayTarget,
38-
rejectWebchatSessionMutation,
3938
requireSessionKey,
4039
resolveGatewaySessionTargetFromKey,
4140
} from "./sessions-shared.js";
4241
import type { GatewayRequestHandlers } from "./types.js";
4342
import { assertValidParams } from "./validation.js";
4443

4544
export const sessionCompactHandlers: GatewayRequestHandlers = {
46-
"sessions.compact": async ({ params, respond, context, client, isWebchatConnect }) => {
45+
"sessions.compact": async ({ params, respond, context }) => {
4746
if (!assertValidParams(params, validateSessionsCompactParams, "sessions.compact", respond)) {
4847
return;
4948
}
@@ -52,10 +51,6 @@ export const sessionCompactHandlers: GatewayRequestHandlers = {
5251
if (!key) {
5352
return;
5453
}
55-
if (rejectWebchatSessionMutation({ action: "compact", client, isWebchatConnect, respond })) {
56-
return;
57-
}
58-
5954
const maxLines =
6055
typeof p.maxLines === "number" && Number.isFinite(p.maxLines)
6156
? Math.max(1, Math.floor(p.maxLines))

src/gateway/server-methods/sessions-compaction-checkpoints.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { emitSessionsChanged } from "./session-change-event.js";
2424
import { interruptSessionRunIfActive } from "./sessions-messaging.js";
2525
import {
2626
loadAccessorSessionEntryForGatewayTarget,
27-
rejectWebchatSessionMutation,
2827
requireSessionKey,
2928
resolveSessionWorkerPlacementMutationError,
3029
respondSessionWorkerPlacementMutationError,
@@ -38,7 +37,7 @@ const MODEL_SELECTION_LOCKED_CHECKPOINT_MESSAGE =
3837
"Checkpoint branch and restore are unavailable while model selection is locked.";
3938

4039
export const sessionCheckpointHandlers: GatewayRequestHandlers = {
41-
"sessions.compaction.branch": async ({ params, respond, context, client, isWebchatConnect }) => {
40+
"sessions.compaction.branch": async ({ params, respond, context }) => {
4241
if (
4342
!assertValidParams(
4443
params,
@@ -54,9 +53,6 @@ export const sessionCheckpointHandlers: GatewayRequestHandlers = {
5453
if (!key) {
5554
return;
5655
}
57-
if (rejectWebchatSessionMutation({ action: "branch", client, isWebchatConnect, respond })) {
58-
return;
59-
}
6056
const checkpointId =
6157
typeof p.checkpointId === "string" && p.checkpointId.trim() ? p.checkpointId.trim() : "";
6258
if (!checkpointId) {
@@ -184,9 +180,6 @@ export const sessionCheckpointHandlers: GatewayRequestHandlers = {
184180
if (!key) {
185181
return;
186182
}
187-
if (rejectWebchatSessionMutation({ action: "restore", client, isWebchatConnect, respond })) {
188-
return;
189-
}
190183
const checkpointId =
191184
typeof p.checkpointId === "string" && p.checkpointId.trim() ? p.checkpointId.trim() : "";
192185
if (!checkpointId) {

src/gateway/server-methods/sessions-delete.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import {
3232
loadAccessorSessionEntryForGatewayTarget,
3333
loadSessionsRuntimeModule,
3434
rejectPluginRuntimeDeleteMismatch,
35-
rejectWebchatSessionMutation,
3635
requireSessionKey,
3736
resolveGatewaySessionTargetFromKey,
3837
resolveSessionWorkerPlacementMutationError,
@@ -52,10 +51,6 @@ export const sessionDeleteHandlers: GatewayRequestHandlers = {
5251
if (!key) {
5352
return;
5453
}
55-
if (rejectWebchatSessionMutation({ action: "delete", client, isWebchatConnect, respond })) {
56-
return;
57-
}
58-
5954
const cfg = context.getRuntimeConfig();
6055
const requestedAgent = resolveRequestedGlobalAgentId(cfg, key, p.agentId);
6156
if (!requestedAgent.ok) {

src/gateway/server-methods/sessions-dispatch.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,20 @@ import {
1717
import {
1818
isWorkerDispatchInputError,
1919
loadAccessorSessionEntryForGatewayTarget,
20-
rejectWebchatSessionMutation,
2120
requireSessionKey,
2221
} from "./sessions-shared.js";
2322
import type { GatewayRequestHandlers } from "./types.js";
2423
import { assertValidParams } from "./validation.js";
2524

2625
export const sessionDispatchHandlers: GatewayRequestHandlers = {
27-
"sessions.dispatch": async ({ params, respond, context, client, isWebchatConnect }) => {
26+
"sessions.dispatch": async ({ params, respond, context }) => {
2827
if (!assertValidParams(params, validateSessionsDispatchParams, "sessions.dispatch", respond)) {
2928
return;
3029
}
3130
const key = requireSessionKey(params.key, respond);
3231
if (!key) {
3332
return;
3433
}
35-
if (rejectWebchatSessionMutation({ action: "dispatch", client, isWebchatConnect, respond })) {
36-
return;
37-
}
3834
const dispatchService = context.workerPlacementDispatchService;
3935
const placementReader = context.workerSessionPlacementService;
4036
if (!dispatchService || !placementReader) {
@@ -163,17 +159,14 @@ export const sessionDispatchHandlers: GatewayRequestHandlers = {
163159
);
164160
}
165161
},
166-
"sessions.reclaim": async ({ params, respond, context, client, isWebchatConnect }) => {
162+
"sessions.reclaim": async ({ params, respond, context }) => {
167163
if (!assertValidParams(params, validateSessionsReclaimParams, "sessions.reclaim", respond)) {
168164
return;
169165
}
170166
const key = requireSessionKey(params.key, respond);
171167
if (!key) {
172168
return;
173169
}
174-
if (rejectWebchatSessionMutation({ action: "reclaim", client, isWebchatConnect, respond })) {
175-
return;
176-
}
177170
const placementService = context.workerPlacementDispatchService;
178171
const placementReader = context.workerSessionPlacementService;
179172
if (!placementService?.reclaim || !placementReader) {

src/gateway/server-methods/sessions-mutations.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import { emitSessionsChanged } from "./session-change-event.js";
3939
import {
4040
isAgentMainSessionKey,
4141
loadSessionsRuntimeModule,
42-
rejectWebchatSessionMutation,
4342
requireSessionKey,
4443
resolveGatewaySessionTargetFromKey,
4544
resolveSessionWorkerPlacementPatchError,
@@ -49,7 +48,7 @@ import type { GatewayRequestHandlers } from "./types.js";
4948
import { assertValidParams } from "./validation.js";
5049

5150
export const sessionMutationHandlers: GatewayRequestHandlers = {
52-
"sessions.patch": async ({ params, respond, context, client, isWebchatConnect }) => {
51+
"sessions.patch": async ({ params, respond, context, client }) => {
5352
if (!assertValidParams(params, validateSessionsPatchParams, "sessions.patch", respond)) {
5453
return;
5554
}
@@ -58,10 +57,6 @@ export const sessionMutationHandlers: GatewayRequestHandlers = {
5857
if (!key) {
5958
return;
6059
}
61-
if (rejectWebchatSessionMutation({ action: "patch", client, isWebchatConnect, respond })) {
62-
return;
63-
}
64-
6560
const cfg = context.getRuntimeConfig();
6661
const requestedAgent = resolveRequestedGlobalAgentId(cfg, key, p.agentId);
6762
if (!requestedAgent.ok) {
@@ -315,7 +310,7 @@ export const sessionMutationHandlers: GatewayRequestHandlers = {
315310
reason: "patch",
316311
});
317312
},
318-
"sessions.pluginPatch": async ({ params, respond, context, client, isWebchatConnect }) => {
313+
"sessions.pluginPatch": async ({ params, respond, context, client }) => {
319314
if (
320315
!assertValidParams(params, validateSessionsPluginPatchParams, "sessions.pluginPatch", respond)
321316
) {
@@ -325,9 +320,6 @@ export const sessionMutationHandlers: GatewayRequestHandlers = {
325320
if (!key) {
326321
return;
327322
}
328-
if (rejectWebchatSessionMutation({ action: "patch", client, isWebchatConnect, respond })) {
329-
return;
330-
}
331323
const scopes = Array.isArray(client?.connect.scopes) ? client.connect.scopes : [];
332324
if (!scopes.includes(ADMIN_SCOPE)) {
333325
respond(

src/gateway/server-methods/sessions-rewind.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { hasVisibleActiveSessionRun } from "./session-active-runs.js";
3131
import { emitSessionsChanged } from "./session-change-event.js";
3232
import {
3333
loadAccessorSessionEntryForGatewayTarget,
34-
rejectWebchatSessionMutation,
3534
resolveSessionWorkerPlacementMutationError,
3635
respondSessionWorkerPlacementMutationError,
3736
} from "./sessions-shared.js";
@@ -145,7 +144,7 @@ async function mutateSessionAtMessage(
145144
options: GatewayRequestHandlerOptions,
146145
action: MessageCutAction,
147146
): Promise<void> {
148-
const { params, respond, context, client, isWebchatConnect } = options;
147+
const { params, respond, context } = options;
149148
const sessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
150149
const entryId =
151150
action === "switch"
@@ -155,16 +154,6 @@ async function mutateSessionAtMessage(
155154
: typeof params.entryId === "string"
156155
? params.entryId.trim()
157156
: "";
158-
if (
159-
rejectWebchatSessionMutation({
160-
action,
161-
client,
162-
isWebchatConnect,
163-
respond,
164-
})
165-
) {
166-
return;
167-
}
168157
const cfg = context.getRuntimeConfig();
169158
const requestedAgent = resolveRequestedGlobalAgentId(
170159
cfg,

src/gateway/server-methods/sessions-shared.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Shared session-handler target resolution and mutation guards.
22
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
3-
import { GATEWAY_CLIENT_IDS } from "../../../packages/gateway-protocol/src/client-info.js";
43
import {
54
ErrorCodes,
65
errorShape,
@@ -277,39 +276,6 @@ export function emitSessionOperation(
277276
);
278277
}
279278

280-
export function rejectWebchatSessionMutation(params: {
281-
action:
282-
| "patch"
283-
| "delete"
284-
| "compact"
285-
| "branch"
286-
| "restore"
287-
| "rewind"
288-
| "fork"
289-
| "switch"
290-
| "dispatch"
291-
| "reclaim";
292-
client: GatewayClient | null;
293-
isWebchatConnect: (params: GatewayClient["connect"] | null | undefined) => boolean;
294-
respond: RespondFn;
295-
}): boolean {
296-
if (!params.client?.connect || !params.isWebchatConnect(params.client.connect)) {
297-
return false;
298-
}
299-
if (params.client.connect.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI) {
300-
return false;
301-
}
302-
params.respond(
303-
false,
304-
undefined,
305-
errorShape(
306-
ErrorCodes.INVALID_REQUEST,
307-
`webchat clients cannot ${params.action} sessions; use chat.send for session-scoped updates`,
308-
),
309-
);
310-
return true;
311-
}
312-
313279
export function isWorkerDispatchInputError(error: unknown): boolean {
314280
if (typeof error !== "object" || error === null || !("code" in error)) {
315281
return false;

0 commit comments

Comments
 (0)