@xmoxmo/bncr 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,17 @@
1
+ import { hasControlCommand } from 'openclaw/plugin-sdk/command-auth';
1
2
  import { emitBncrLogLine } from '../../core/logging.ts';
2
3
  import { resolveBncrChannelPolicy } from '../../core/policy.ts';
3
4
  import { formatDisplayScope } from '../../core/targets.ts';
4
5
  import {
6
+ readBncrSessionEntry,
5
7
  recordBncrInboundSession,
6
8
  resolveBncrInboundSessionStorePath,
7
9
  resolveBncrPinnedMainDmOwnerFromAllowlist,
8
10
  } from '../../openclaw/inbound-session-runtime.ts';
9
11
  import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
10
12
  import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
13
+ import { performBncrGatewaySessionReset } from '../../openclaw/session-reset-runtime.ts';
14
+ import { mergeBncrOwnerAllowFromIntoConfig } from './command-owner.ts';
11
15
  import type {
12
16
  BncrEnqueueFromReply,
13
17
  BncrInboundApi,
@@ -18,8 +22,13 @@ import type {
18
22
  import { assertResolvedAgentRoute, type ParsedInbound } from './dispatch-prep.ts';
19
23
  import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
20
24
  import {
25
+ BNCR_OPENCLAW_NATIVE_COMMAND,
26
+ BNCR_SELF_SERVICE_COMMANDS,
27
+ isBncrStopCommandText,
28
+ isBncrWhitelistBareCommandText,
21
29
  parseBncrNativeCommand,
22
30
  parseBncrUnsupportedDirectCommand,
31
+ resolveBncrNativeCommandParseOptions,
23
32
  resolveBncrNativeHelpCommand,
24
33
  resolveBncrNativeSessionResetCommand,
25
34
  resolveBncrNativeStatusCommand,
@@ -108,14 +117,49 @@ export async function handleBncrNativeCommand(params: {
108
117
  enqueueFromReply,
109
118
  } = params;
110
119
  const { accountId, route, peer, clientId, extracted, msgId } = parsed;
111
- const command = parseBncrNativeCommand(extracted.text, {
112
- allowBareWhoami: parsed.isAdmin !== true,
113
- allowBareStatus: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
114
- allowBareSessionReset: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
115
- });
116
- if (!command && parsed.peer.kind === 'direct') {
117
- const unsupportedDirectCommand = parseBncrUnsupportedDirectCommand(extracted.text);
118
- if (unsupportedDirectCommand) {
120
+
121
+ // Stop always goes through the unified stop fast path in dispatch.ts.
122
+ // It is not a bncr native command and must never be processed here.
123
+ if (isBncrStopCommandText(extracted.text)) {
124
+ return { handled: false };
125
+ }
126
+
127
+ const whitelistBareCommand =
128
+ !parsed.isAdmin && parsed.peer.kind === 'direct'
129
+ ? isBncrWhitelistBareCommandText(extracted.text)
130
+ : null;
131
+ let dispatchToOpenClaw =
132
+ whitelistBareCommand === 'whoami' ||
133
+ whitelistBareCommand === 'status' ||
134
+ whitelistBareCommand === 'model' ||
135
+ whitelistBareCommand === 'verbose';
136
+ const effectiveIsAdmin = whitelistBareCommand ? true : parsed.isAdmin;
137
+ let command: ReturnType<typeof parseBncrNativeCommand>;
138
+ if (whitelistBareCommand) {
139
+ // Private non-admin whitelist commands are temporarily elevated to admin
140
+ // for the local session-reset handlers or handed to the OpenClaw native
141
+ // parser. They never fall back to the agent so the agent cannot observe
142
+ // a conflicting identity.
143
+ const raw = String(extracted.text || '').trim();
144
+ command = { command: whitelistBareCommand, raw, body: raw, argsText: '' };
145
+ const argsTextStart = raw.indexOf(' ');
146
+ if (argsTextStart !== -1) {
147
+ command.argsText = raw.slice(argsTextStart + 1).trim();
148
+ }
149
+ } else {
150
+ command = parseBncrNativeCommand(
151
+ extracted.text,
152
+ resolveBncrNativeCommandParseOptions({
153
+ isAdmin: parsed.isAdmin,
154
+ peerKind: parsed.peer.kind as 'direct' | 'group',
155
+ }),
156
+ );
157
+ }
158
+ if (!command) {
159
+ const unsupportedCommand = parseBncrUnsupportedDirectCommand(extracted.text);
160
+ // Unknown /bncr subcommands are rejected by bncr without agent fallback,
161
+ // for both admin and non-admin callers and in private or group chat.
162
+ if (unsupportedCommand?.command.startsWith('bncr ')) {
119
163
  const rejectedRoute = buildBncrNativeCommandResolvedRoute({
120
164
  api,
121
165
  cfg,
@@ -133,7 +177,7 @@ export async function handleBncrNativeCommand(params: {
133
177
  logBncrNativeCommandSummary(
134
178
  buildBncrNativeCommandSummary({
135
179
  kind: 'unsupported',
136
- command: unsupportedDirectCommand.command,
180
+ command: unsupportedCommand.command,
137
181
  accountId,
138
182
  to: displayTo,
139
183
  msgId: msgId || null,
@@ -145,20 +189,43 @@ export async function handleBncrNativeCommand(params: {
145
189
  sessionKey: baseSessionKey,
146
190
  route,
147
191
  payload: {
148
- text: `Unsupported private-chat command: /${unsupportedDirectCommand.command}`,
192
+ text: `Unsupported command: /${unsupportedCommand.command}`,
149
193
  replyToId: msgId || undefined,
150
194
  },
151
195
  });
152
- return { handled: true, command: unsupportedDirectCommand.command, sessionKey };
196
+ return { handled: true, command: unsupportedCommand.command, sessionKey };
197
+ }
198
+ // Non-bncr bare commands fall through to the OpenClaw parser. Admin
199
+ // callers may fall back to the agent; non-admin callers keep their
200
+ // original identity when they do.
201
+
202
+ // Admin bare OpenClaw native commands are routed to the OpenClaw parser
203
+ // and handled there, preventing a second agent turn from producing a
204
+ // duplicate reply. This mirrors the non-admin whitelist elevation path.
205
+ if (
206
+ !command &&
207
+ parsed.isAdmin &&
208
+ hasControlCommand(extracted.text, cfg as Parameters<typeof hasControlCommand>[1])
209
+ ) {
210
+ const raw = String(extracted.text || '').trim();
211
+ command = { command: BNCR_OPENCLAW_NATIVE_COMMAND, raw, body: raw, argsText: '' };
212
+ const argsTextStart = raw.indexOf(' ');
213
+ if (argsTextStart !== -1) {
214
+ command.argsText = raw.slice(argsTextStart + 1).trim();
215
+ }
216
+ dispatchToOpenClaw = true;
153
217
  }
154
218
  }
155
219
  if (!command) return { handled: false };
156
220
  const nativeCommandDebugEnabled = resolveNativeCommandDebugEnabled({ cfg, channelId });
157
221
 
222
+ const displayCommand =
223
+ command.command === BNCR_OPENCLAW_NATIVE_COMMAND ? command.raw : command.command;
224
+
158
225
  logBncrNativeCommandEvent(
159
226
  'detected',
160
227
  {
161
- command: command.command,
228
+ command: displayCommand,
162
229
  accountId,
163
230
  to: formatDisplayScope(route),
164
231
  msgId: msgId || null,
@@ -198,6 +265,9 @@ export async function handleBncrNativeCommand(params: {
198
265
  agentId: resolvedRoute.agentId,
199
266
  });
200
267
 
268
+ const dispatchOwnerAllowFrom =
269
+ dispatchToOpenClaw && senderIdForContext ? [senderIdForContext] : undefined;
270
+
201
271
  const ctxPayload = await Promise.resolve(
202
272
  createNativeCommandTurnContext({
203
273
  api,
@@ -207,6 +277,7 @@ export async function handleBncrNativeCommand(params: {
207
277
  peer,
208
278
  resolvedRoute,
209
279
  sessionKey,
280
+ ownerAllowFrom: dispatchOwnerAllowFrom,
210
281
  displayTo,
211
282
  originatingTo,
212
283
  senderIdForContext,
@@ -223,9 +294,16 @@ export async function handleBncrNativeCommand(params: {
223
294
  senderId: senderIdForContext,
224
295
  });
225
296
 
226
- const nativeVerbose = resolveBncrNativeVerboseCommand(command);
297
+ const currentVerboseLevel = storePath
298
+ ? ((await readBncrSessionEntry({ storePath, sessionKey }))?.verboseLevel as
299
+ | 'on'
300
+ | 'off'
301
+ | 'full'
302
+ | undefined)
303
+ : undefined;
304
+ const nativeVerbose = resolveBncrNativeVerboseCommand(command, currentVerboseLevel);
227
305
  const nativeHelp = resolveBncrNativeHelpCommand(command, {
228
- isAdmin: parsed.isAdmin,
306
+ isAdmin: effectiveIsAdmin,
229
307
  peerKind: parsed.peer.kind as 'direct' | 'group',
230
308
  });
231
309
  const nativeWhoami = resolveBncrNativeWhoamiCommand({
@@ -236,7 +314,7 @@ export async function handleBncrNativeCommand(params: {
236
314
  userId: parsed.userId,
237
315
  userName: parsed.userName,
238
316
  isGroup: parsed.isGroup,
239
- isAdmin: parsed.isAdmin,
317
+ isAdmin: effectiveIsAdmin,
240
318
  });
241
319
  const nativeStatus = resolveBncrNativeStatusCommand({
242
320
  command,
@@ -251,107 +329,16 @@ export async function handleBncrNativeCommand(params: {
251
329
  command,
252
330
  peerKind: parsed.peer.kind as 'direct' | 'group',
253
331
  });
254
- if (nativeHelp) {
255
- logBncrNativeCommandSummary(
256
- buildBncrNativeCommandSummary({
257
- kind: 'help',
258
- command: command.command,
259
- accountId,
260
- to: displayTo,
261
- msgId: msgId || null,
262
- result: 'handled',
263
- }),
264
- );
265
- await recordAndPatchBncrInboundSessionEntry({
266
- storePath,
267
- sessionKey,
268
- ctx: ctxPayload,
269
- patch: sessionIdentityPatch,
270
- });
271
- rememberSessionRoute(baseSessionKey, accountId, route);
272
- await enqueueFromReply({
273
- accountId,
274
- sessionKey,
275
- route,
276
- payload: {
277
- text: nativeHelp.text,
278
- replyToId: msgId || undefined,
279
- },
280
- });
281
- return { handled: true, command: command.command, sessionKey };
282
- }
283
-
284
- if (nativeWhoami) {
285
- logBncrNativeCommandSummary(
286
- buildBncrNativeCommandSummary({
287
- kind: 'whoami',
288
- command: command.command,
289
- accountId,
290
- to: displayTo,
291
- msgId: msgId || null,
292
- result: 'handled',
293
- }),
294
- );
295
- await recordAndPatchBncrInboundSessionEntry({
296
- storePath,
297
- sessionKey,
298
- ctx: ctxPayload,
299
- patch: sessionIdentityPatch,
300
- });
301
- rememberSessionRoute(baseSessionKey, accountId, route);
302
- await enqueueFromReply({
303
- accountId,
304
- sessionKey,
305
- route,
306
- payload: {
307
- text: nativeWhoami.text,
308
- replyToId: msgId || undefined,
309
- },
310
- });
311
- return { handled: true, command: command.command, sessionKey };
312
- }
313
-
314
- if (nativeStatus) {
315
- logBncrNativeCommandSummary(
316
- buildBncrNativeCommandSummary({
317
- kind: 'status',
318
- command: command.command,
319
- accountId,
320
- to: displayTo,
321
- msgId: msgId || null,
322
- result: 'handled',
323
- }),
324
- );
325
- await recordAndPatchBncrInboundSessionEntry({
326
- storePath,
327
- sessionKey,
328
- ctx: ctxPayload,
329
- patch: sessionIdentityPatch,
330
- });
331
- rememberSessionRoute(baseSessionKey, accountId, route);
332
- await enqueueFromReply({
333
- accountId,
334
- sessionKey,
335
- route,
336
- payload: {
337
- text: nativeStatus.text,
338
- replyToId: msgId || undefined,
339
- },
340
- });
341
- return { handled: true, command: command.command, sessionKey };
342
- }
343
-
344
- if (nativeSessionReset) {
345
- const allowLocalSessionReset = parsed.peer.kind === 'direct' || parsed.isAdmin;
346
- if (!allowLocalSessionReset) {
332
+ if (!dispatchToOpenClaw) {
333
+ if (nativeHelp) {
347
334
  logBncrNativeCommandSummary(
348
335
  buildBncrNativeCommandSummary({
349
- kind: nativeSessionReset.reason,
336
+ kind: 'help',
350
337
  command: command.command,
351
338
  accountId,
352
339
  to: displayTo,
353
340
  msgId: msgId || null,
354
- result: 'rejected',
341
+ result: 'handled',
355
342
  }),
356
343
  );
357
344
  await recordAndPatchBncrInboundSessionEntry({
@@ -361,63 +348,57 @@ export async function handleBncrNativeCommand(params: {
361
348
  patch: sessionIdentityPatch,
362
349
  });
363
350
  rememberSessionRoute(baseSessionKey, accountId, route);
351
+ await enqueueFromReply({
352
+ accountId,
353
+ sessionKey,
354
+ route,
355
+ payload: {
356
+ text: nativeHelp.text,
357
+ replyToId: msgId || undefined,
358
+ },
359
+ });
364
360
  return { handled: true, command: command.command, sessionKey };
365
361
  }
366
362
 
367
- const requestedAgentId = resolvedRoute.agentId || canonicalAgentId;
368
- const requestApi = api as BncrInboundApi & {
369
- request?: (method: string, params: Record<string, unknown>) => Promise<unknown>;
370
- };
371
- if (typeof requestApi.request !== 'function') {
372
- throw new Error('OpenClaw plugin api.request is unavailable for sessions.reset');
373
- }
374
- await recordAndPatchBncrInboundSessionEntry({
375
- storePath,
376
- sessionKey,
377
- ctx: ctxPayload,
378
- patch: sessionIdentityPatch,
379
- });
380
- const resetResult = (await requestApi.request('sessions.reset', {
381
- key: baseSessionKey,
382
- reason: nativeSessionReset.reason,
383
- ...(requestedAgentId ? { agentId: requestedAgentId } : {}),
384
- })) as { ok?: boolean } | undefined;
385
- if (resetResult?.ok === false) {
386
- throw new Error(`sessions.reset failed for ${baseSessionKey}`);
387
- }
388
- logBncrNativeCommandSummary(
389
- buildBncrNativeCommandSummary({
390
- kind: nativeSessionReset.reason,
391
- command: command.command,
363
+ if (nativeWhoami) {
364
+ logBncrNativeCommandSummary(
365
+ buildBncrNativeCommandSummary({
366
+ kind: 'whoami',
367
+ command: command.command,
368
+ accountId,
369
+ to: displayTo,
370
+ msgId: msgId || null,
371
+ result: 'handled',
372
+ }),
373
+ );
374
+ await recordAndPatchBncrInboundSessionEntry({
375
+ storePath,
376
+ sessionKey,
377
+ ctx: ctxPayload,
378
+ patch: sessionIdentityPatch,
379
+ });
380
+ rememberSessionRoute(baseSessionKey, accountId, route);
381
+ await enqueueFromReply({
392
382
  accountId,
393
- to: displayTo,
394
- msgId: msgId || null,
395
- result: 'handled',
396
- }),
397
- );
398
- rememberSessionRoute(baseSessionKey, accountId, route);
399
- await enqueueFromReply({
400
- accountId,
401
- sessionKey: baseSessionKey,
402
- route,
403
- payload: {
404
- text: nativeSessionReset.text,
405
- replyToId: msgId || undefined,
406
- },
407
- });
408
- return { handled: true, command: command.command, sessionKey: baseSessionKey };
409
- }
383
+ sessionKey,
384
+ route,
385
+ payload: {
386
+ text: nativeWhoami.text,
387
+ replyToId: msgId || undefined,
388
+ },
389
+ });
390
+ return { handled: true, command: command.command, sessionKey };
391
+ }
410
392
 
411
- if (nativeVerbose) {
412
- if (!parsed.isAdmin) {
393
+ if (nativeStatus) {
413
394
  logBncrNativeCommandSummary(
414
395
  buildBncrNativeCommandSummary({
415
- kind: 'verbose',
396
+ kind: 'status',
416
397
  command: command.command,
417
398
  accountId,
418
399
  to: displayTo,
419
400
  msgId: msgId || null,
420
- result: 'rejected',
401
+ result: 'handled',
421
402
  }),
422
403
  );
423
404
  await recordAndPatchBncrInboundSessionEntry({
@@ -427,96 +408,258 @@ export async function handleBncrNativeCommand(params: {
427
408
  patch: sessionIdentityPatch,
428
409
  });
429
410
  rememberSessionRoute(baseSessionKey, accountId, route);
430
- if (parsed.peer.kind !== 'group') {
411
+ await enqueueFromReply({
412
+ accountId,
413
+ sessionKey,
414
+ route,
415
+ payload: {
416
+ text: nativeStatus.text,
417
+ replyToId: msgId || undefined,
418
+ },
419
+ });
420
+ return { handled: true, command: command.command, sessionKey };
421
+ }
422
+
423
+ if (nativeSessionReset) {
424
+ const allowLocalSessionReset = parsed.peer.kind === 'direct' || effectiveIsAdmin;
425
+ if (!allowLocalSessionReset) {
426
+ logBncrNativeCommandSummary(
427
+ buildBncrNativeCommandSummary({
428
+ kind: nativeSessionReset.reason,
429
+ command: command.command,
430
+ accountId,
431
+ to: displayTo,
432
+ msgId: msgId || null,
433
+ result: 'rejected',
434
+ }),
435
+ );
436
+ await recordAndPatchBncrInboundSessionEntry({
437
+ storePath,
438
+ sessionKey,
439
+ ctx: ctxPayload,
440
+ patch: sessionIdentityPatch,
441
+ });
442
+ rememberSessionRoute(baseSessionKey, accountId, route);
443
+ return { handled: true, command: command.command, sessionKey };
444
+ }
445
+
446
+ const requestedAgentId = resolvedRoute.agentId || canonicalAgentId;
447
+ await recordAndPatchBncrInboundSessionEntry({
448
+ storePath,
449
+ sessionKey,
450
+ ctx: ctxPayload,
451
+ patch: sessionIdentityPatch,
452
+ });
453
+ let resetResult: { ok?: boolean } | undefined;
454
+ try {
455
+ resetResult = await performBncrGatewaySessionReset({
456
+ key: baseSessionKey,
457
+ reason: nativeSessionReset.reason,
458
+ ...(requestedAgentId ? { agentId: requestedAgentId } : {}),
459
+ commandSource: 'bncr:native-command',
460
+ });
461
+ } catch (resetError) {
462
+ emitBncrLogLine(
463
+ 'error',
464
+ `[bncr] native-command sessions.reset failed|key=${baseSessionKey}|err=${resetError instanceof Error ? resetError.message : String(resetError)}`,
465
+ );
431
466
  await enqueueFromReply({
432
467
  accountId,
433
- sessionKey,
468
+ sessionKey: baseSessionKey,
434
469
  route,
435
470
  payload: {
436
- text: 'Admin permission required.',
471
+ text: `Session reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}`,
437
472
  replyToId: msgId || undefined,
438
473
  },
439
474
  });
475
+ return { handled: true, command: command.command, sessionKey: baseSessionKey };
440
476
  }
441
- return { handled: true, command: command.command, sessionKey };
477
+ if (resetResult?.ok === false) {
478
+ await enqueueFromReply({
479
+ accountId,
480
+ sessionKey: baseSessionKey,
481
+ route,
482
+ payload: {
483
+ text: 'Session reset was rejected by the gateway.',
484
+ replyToId: msgId || undefined,
485
+ },
486
+ });
487
+ return { handled: true, command: command.command, sessionKey: baseSessionKey };
488
+ }
489
+ logBncrNativeCommandSummary(
490
+ buildBncrNativeCommandSummary({
491
+ kind: nativeSessionReset.reason,
492
+ command: command.command,
493
+ accountId,
494
+ to: displayTo,
495
+ msgId: msgId || null,
496
+ result: 'handled',
497
+ }),
498
+ );
499
+ rememberSessionRoute(baseSessionKey, accountId, route);
500
+ await enqueueFromReply({
501
+ accountId,
502
+ sessionKey: baseSessionKey,
503
+ route,
504
+ payload: {
505
+ text: nativeSessionReset.text,
506
+ replyToId: msgId || undefined,
507
+ },
508
+ });
509
+ return { handled: true, command: command.command, sessionKey: baseSessionKey };
442
510
  }
443
511
 
444
- logBncrNativeCommandSummary(
445
- buildBncrNativeCommandSummary({
446
- kind: 'verbose',
447
- command: command.command,
448
- accountId,
449
- to: displayTo,
450
- msgId: msgId || null,
451
- result: 'handled',
452
- }),
453
- );
454
- logBncrNativeCommandEvent(
455
- 'handled-verbose',
456
- {
457
- command: command.command,
458
- accountId,
459
- sessionKey,
460
- to: displayTo,
461
- msgId: msgId || null,
462
- fallbackToAgent: false,
463
- },
464
- { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
465
- );
466
- await recordAndPatchBncrInboundSessionEntry({
467
- storePath,
468
- sessionKey,
469
- ctx: ctxPayload,
470
- patch: {
471
- ...sessionIdentityPatch,
472
- ...(nativeVerbose.verboseLevel ? { verboseLevel: nativeVerbose.verboseLevel } : {}),
473
- },
474
- });
475
- rememberSessionRoute(baseSessionKey, accountId, route);
476
- await enqueueFromReply({
477
- accountId,
478
- sessionKey,
479
- route,
480
- payload: {
481
- text: nativeVerbose.text,
482
- replyToId: msgId || undefined,
483
- },
484
- });
485
- return { handled: true, command: command.command, sessionKey };
486
- }
512
+ if (nativeVerbose) {
513
+ if (!effectiveIsAdmin && parsed.peer.kind !== 'direct') {
514
+ logBncrNativeCommandSummary(
515
+ buildBncrNativeCommandSummary({
516
+ kind: 'verbose',
517
+ command: command.command,
518
+ accountId,
519
+ to: displayTo,
520
+ msgId: msgId || null,
521
+ result: 'rejected',
522
+ }),
523
+ );
524
+ await recordAndPatchBncrInboundSessionEntry({
525
+ storePath,
526
+ sessionKey,
527
+ ctx: ctxPayload,
528
+ patch: sessionIdentityPatch,
529
+ });
530
+ rememberSessionRoute(baseSessionKey, accountId, route);
531
+ if (parsed.peer.kind !== 'group') {
532
+ await enqueueFromReply({
533
+ accountId,
534
+ sessionKey,
535
+ route,
536
+ payload: {
537
+ text: 'Admin permission required.',
538
+ replyToId: msgId || undefined,
539
+ },
540
+ });
541
+ }
542
+ return { handled: true, command: command.command, sessionKey };
543
+ }
487
544
 
488
- const sceneAdmin = parseSceneAdminCommand(command);
489
- if (sceneAdmin.matched) {
490
- if (!parsed.isAdmin) {
491
545
  logBncrNativeCommandSummary(
492
546
  buildBncrNativeCommandSummary({
493
- kind: 'scene-admin',
547
+ kind: 'verbose',
494
548
  command: command.command,
495
549
  accountId,
496
550
  to: displayTo,
497
- msgId: msgId ?? null,
498
- result: 'rejected',
551
+ msgId: msgId || null,
552
+ result: 'handled',
499
553
  }),
500
554
  );
555
+ logBncrNativeCommandEvent(
556
+ 'handled-verbose',
557
+ {
558
+ command: command.command,
559
+ accountId,
560
+ sessionKey,
561
+ to: displayTo,
562
+ msgId: msgId || null,
563
+ fallbackToAgent: false,
564
+ },
565
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
566
+ );
501
567
  await recordAndPatchBncrInboundSessionEntry({
502
568
  storePath,
503
569
  sessionKey,
504
570
  ctx: ctxPayload,
505
- patch: sessionIdentityPatch,
571
+ patch: {
572
+ ...sessionIdentityPatch,
573
+ ...(nativeVerbose.verboseLevel ? { verboseLevel: nativeVerbose.verboseLevel } : {}),
574
+ },
506
575
  });
576
+ rememberSessionRoute(baseSessionKey, accountId, route);
507
577
  await enqueueFromReply({
508
578
  accountId,
509
579
  sessionKey,
510
580
  route,
511
581
  payload: {
512
- text: 'Admin permission required.',
582
+ text: nativeVerbose.text,
513
583
  replyToId: msgId || undefined,
514
584
  },
515
585
  });
516
586
  return { handled: true, command: command.command, sessionKey };
517
587
  }
518
588
 
519
- if (!sceneAdmin.valid) {
589
+ const sceneAdmin = parseSceneAdminCommand(command);
590
+ if (sceneAdmin.matched) {
591
+ const directSelfAdminCommand =
592
+ parsed.peer.kind === 'direct' &&
593
+ new Set(['history-help', 'history-limit', 'history-force', 'download-media']).has(
594
+ command.command,
595
+ );
596
+ if (!effectiveIsAdmin && !directSelfAdminCommand) {
597
+ logBncrNativeCommandSummary(
598
+ buildBncrNativeCommandSummary({
599
+ kind: 'scene-admin',
600
+ command: command.command,
601
+ accountId,
602
+ to: displayTo,
603
+ msgId: msgId ?? null,
604
+ result: 'rejected',
605
+ }),
606
+ );
607
+ await recordAndPatchBncrInboundSessionEntry({
608
+ storePath,
609
+ sessionKey,
610
+ ctx: ctxPayload,
611
+ patch: sessionIdentityPatch,
612
+ });
613
+ await enqueueFromReply({
614
+ accountId,
615
+ sessionKey,
616
+ route,
617
+ payload: {
618
+ text: 'Admin permission required.',
619
+ replyToId: msgId || undefined,
620
+ },
621
+ });
622
+ return { handled: true, command: command.command, sessionKey };
623
+ }
624
+
625
+ if (!sceneAdmin.valid) {
626
+ logBncrNativeCommandSummary(
627
+ buildBncrNativeCommandSummary({
628
+ kind: 'scene-admin',
629
+ command: command.command,
630
+ accountId,
631
+ to: displayTo,
632
+ msgId: msgId ?? null,
633
+ result: 'rejected',
634
+ }),
635
+ );
636
+ await recordAndPatchBncrInboundSessionEntry({
637
+ storePath,
638
+ sessionKey,
639
+ ctx: ctxPayload,
640
+ patch: sessionIdentityPatch,
641
+ });
642
+ await enqueueFromReply({
643
+ accountId,
644
+ sessionKey,
645
+ route,
646
+ payload: {
647
+ text: sceneAdmin.text,
648
+ replyToId: msgId || undefined,
649
+ },
650
+ });
651
+ return { handled: true, command: command.command, sessionKey };
652
+ }
653
+
654
+ const outcome = executeSceneAdminCommand({
655
+ parsed,
656
+ command: sceneAdmin.command,
657
+ sceneRegistry,
658
+ defaultAdminAgentId,
659
+ defaultPublicAgentId,
660
+ now,
661
+ allowNonAdminSelfAdmin: !!directSelfAdminCommand,
662
+ });
520
663
  logBncrNativeCommandSummary(
521
664
  buildBncrNativeCommandSummary({
522
665
  kind: 'scene-admin',
@@ -524,7 +667,7 @@ export async function handleBncrNativeCommand(params: {
524
667
  accountId,
525
668
  to: displayTo,
526
669
  msgId: msgId ?? null,
527
- result: 'rejected',
670
+ result: outcome.ok ? 'handled' : 'rejected',
528
671
  }),
529
672
  );
530
673
  await recordAndPatchBncrInboundSessionEntry({
@@ -538,47 +681,12 @@ export async function handleBncrNativeCommand(params: {
538
681
  sessionKey,
539
682
  route,
540
683
  payload: {
541
- text: sceneAdmin.text,
684
+ text: outcome.text,
542
685
  replyToId: msgId || undefined,
543
686
  },
544
687
  });
545
688
  return { handled: true, command: command.command, sessionKey };
546
689
  }
547
-
548
- const outcome = executeSceneAdminCommand({
549
- parsed,
550
- command: sceneAdmin.command,
551
- sceneRegistry,
552
- defaultAdminAgentId,
553
- defaultPublicAgentId,
554
- now,
555
- });
556
- logBncrNativeCommandSummary(
557
- buildBncrNativeCommandSummary({
558
- kind: 'scene-admin',
559
- command: command.command,
560
- accountId,
561
- to: displayTo,
562
- msgId: msgId ?? null,
563
- result: outcome.ok ? 'handled' : 'rejected',
564
- }),
565
- );
566
- await recordAndPatchBncrInboundSessionEntry({
567
- storePath,
568
- sessionKey,
569
- ctx: ctxPayload,
570
- patch: sessionIdentityPatch,
571
- });
572
- await enqueueFromReply({
573
- accountId,
574
- sessionKey,
575
- route,
576
- payload: {
577
- text: outcome.text,
578
- replyToId: msgId || undefined,
579
- },
580
- });
581
- return { handled: true, command: command.command, sessionKey };
582
690
  }
583
691
 
584
692
  await recordAndPatchBncrInboundSessionEntry({
@@ -588,7 +696,11 @@ export async function handleBncrNativeCommand(params: {
588
696
  patch: sessionIdentityPatch,
589
697
  });
590
698
 
591
- const effectiveReply = buildBncrReplyConfig(cfg);
699
+ const dispatchCfgForElevated =
700
+ dispatchToOpenClaw && senderIdForContext
701
+ ? mergeBncrOwnerAllowFromIntoConfig({ cfg, senderIdForContext })
702
+ : cfg;
703
+ const effectiveReply = buildBncrReplyConfig(dispatchCfgForElevated);
592
704
  const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
593
705
  const pinnedMainDmOwner =
594
706
  peer.kind === 'direct'
@@ -691,13 +803,35 @@ export async function handleBncrNativeCommand(params: {
691
803
  });
692
804
 
693
805
  if (!responded) {
806
+ // Self-service commands must never fall back to the agent — doing so
807
+ // would let the agent see a message whose author identity was
808
+ // temporarily elevated, causing role confusion.
809
+ if (BNCR_SELF_SERVICE_COMMANDS.has(command.command)) {
810
+ emitBncrLogLine(
811
+ 'warn',
812
+ `[bncr] self-service command=${displayCommand} produced no payload; refusing agent fallback`,
813
+ );
814
+ await enqueueFromReply({
815
+ accountId,
816
+ sessionKey,
817
+ route,
818
+ payload: {
819
+ text: `Command /${command.command} failed.`,
820
+ replyToId: msgId || undefined,
821
+ },
822
+ });
823
+ return buildNativeCommandHandledResult({
824
+ command: command.command,
825
+ sessionKey,
826
+ });
827
+ }
694
828
  logBncrNativeCommandSummary(
695
- `fallback command=${command.command}|accountId=${accountId}|to=${displayTo}|msgId=${msgId || '-'}|reason=no-payload`,
829
+ `fallback command=${displayCommand}|accountId=${accountId}|to=${displayTo}|msgId=${msgId || '-'}|reason=no-payload`,
696
830
  );
697
831
  logBncrNativeCommandEvent(
698
832
  'no-payload-fallback-to-agent',
699
833
  {
700
- command: command.command,
834
+ command: displayCommand,
701
835
  accountId,
702
836
  sessionKey,
703
837
  to: displayTo,