@@ -33,7 +33,6 @@ import {
3333 detectPluginInstallPathIssue ,
3434 formatPluginInstallPathIssue ,
3535} from "../infra/plugin-install-path-warnings.js" ;
36- import { readChannelAllowFromStore } from "../pairing/pairing-store.js" ;
3736import {
3837 formatChannelAccountsDefaultPath ,
3938 formatSetExplicitDefaultInstruction ,
@@ -62,6 +61,7 @@ import {
6261 maybeRepairTelegramAllowFromUsernames ,
6362 scanTelegramAllowFromUsernameEntries ,
6463} from "./doctor/providers/telegram.js" ;
64+ import { maybeRepairAllowlistPolicyAllowFrom } from "./doctor/shared/allowlist-policy-repair.js" ;
6565import { hasAllowFromEntries } from "./doctor/shared/allowlist.js" ;
6666import { collectEmptyAllowlistPolicyWarningsForAccount } from "./doctor/shared/empty-allowlist-policy.js" ;
6767import { scanMutableAllowlistEntries } from "./doctor/shared/mutable-allowlist.js" ;
@@ -261,159 +261,6 @@ async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<st
261261 } ) . map ( ( entry ) => `- ${ entry } ` ) ;
262262}
263263
264- async function maybeRepairAllowlistPolicyAllowFrom ( cfg : OpenClawConfig ) : Promise < {
265- config : OpenClawConfig ;
266- changes : string [ ] ;
267- } > {
268- const channels = cfg . channels ;
269- if ( ! channels || typeof channels !== "object" ) {
270- return { config : cfg , changes : [ ] } ;
271- }
272-
273- type AllowFromMode = "topOnly" | "topOrNested" | "nestedOnly" ;
274-
275- const resolveAllowFromMode = ( channelName : string ) : AllowFromMode => {
276- if ( channelName === "googlechat" ) {
277- return "nestedOnly" ;
278- }
279- if ( channelName === "discord" || channelName === "slack" ) {
280- return "topOrNested" ;
281- }
282- return "topOnly" ;
283- } ;
284-
285- const next = structuredClone ( cfg ) ;
286- const changes : string [ ] = [ ] ;
287-
288- const applyRecoveredAllowFrom = ( params : {
289- account : Record < string , unknown > ;
290- allowFrom : string [ ] ;
291- mode : AllowFromMode ;
292- prefix : string ;
293- } ) => {
294- const count = params . allowFrom . length ;
295- const noun = count === 1 ? "entry" : "entries" ;
296-
297- if ( params . mode === "nestedOnly" ) {
298- const dmEntry = params . account . dm ;
299- const dm =
300- dmEntry && typeof dmEntry === "object" && ! Array . isArray ( dmEntry )
301- ? ( dmEntry as Record < string , unknown > )
302- : { } ;
303- dm . allowFrom = params . allowFrom ;
304- params . account . dm = dm ;
305- changes . push (
306- `- ${ params . prefix } .dm.allowFrom: restored ${ count } sender ${ noun } from pairing store (dmPolicy="allowlist").` ,
307- ) ;
308- return ;
309- }
310-
311- if ( params . mode === "topOrNested" ) {
312- const dmEntry = params . account . dm ;
313- const dm =
314- dmEntry && typeof dmEntry === "object" && ! Array . isArray ( dmEntry )
315- ? ( dmEntry as Record < string , unknown > )
316- : undefined ;
317- const nestedAllowFrom = dm ?. allowFrom as Array < string | number > | undefined ;
318- if ( dm && ! Array . isArray ( params . account . allowFrom ) && Array . isArray ( nestedAllowFrom ) ) {
319- dm . allowFrom = params . allowFrom ;
320- changes . push (
321- `- ${ params . prefix } .dm.allowFrom: restored ${ count } sender ${ noun } from pairing store (dmPolicy="allowlist").` ,
322- ) ;
323- return ;
324- }
325- }
326-
327- params . account . allowFrom = params . allowFrom ;
328- changes . push (
329- `- ${ params . prefix } .allowFrom: restored ${ count } sender ${ noun } from pairing store (dmPolicy="allowlist").` ,
330- ) ;
331- } ;
332-
333- const recoverAllowFromForAccount = async ( params : {
334- channelName : string ;
335- account : Record < string , unknown > ;
336- accountId ?: string ;
337- prefix : string ;
338- } ) => {
339- const dmEntry = params . account . dm ;
340- const dm =
341- dmEntry && typeof dmEntry === "object" && ! Array . isArray ( dmEntry )
342- ? ( dmEntry as Record < string , unknown > )
343- : undefined ;
344- const dmPolicy =
345- ( params . account . dmPolicy as string | undefined ) ?? ( dm ?. policy as string | undefined ) ;
346- if ( dmPolicy !== "allowlist" ) {
347- return ;
348- }
349-
350- const topAllowFrom = params . account . allowFrom as Array < string | number > | undefined ;
351- const nestedAllowFrom = dm ?. allowFrom as Array < string | number > | undefined ;
352- if ( hasAllowFromEntries ( topAllowFrom ) || hasAllowFromEntries ( nestedAllowFrom ) ) {
353- return ;
354- }
355-
356- const normalizedChannelId = ( normalizeChatChannelId ( params . channelName ) ?? params . channelName )
357- . trim ( )
358- . toLowerCase ( ) ;
359- if ( ! normalizedChannelId ) {
360- return ;
361- }
362- const normalizedAccountId = normalizeAccountId ( params . accountId ) || DEFAULT_ACCOUNT_ID ;
363- const fromStore = await readChannelAllowFromStore (
364- normalizedChannelId ,
365- process . env ,
366- normalizedAccountId ,
367- ) . catch ( ( ) => [ ] ) ;
368- const recovered = Array . from ( new Set ( fromStore . map ( ( entry ) => String ( entry ) . trim ( ) ) ) ) . filter (
369- Boolean ,
370- ) ;
371- if ( recovered . length === 0 ) {
372- return ;
373- }
374-
375- applyRecoveredAllowFrom ( {
376- account : params . account ,
377- allowFrom : recovered ,
378- mode : resolveAllowFromMode ( params . channelName ) ,
379- prefix : params . prefix ,
380- } ) ;
381- } ;
382-
383- const nextChannels = next . channels as Record < string , Record < string , unknown > > ;
384- for ( const [ channelName , channelConfig ] of Object . entries ( nextChannels ) ) {
385- if ( ! channelConfig || typeof channelConfig !== "object" ) {
386- continue ;
387- }
388- await recoverAllowFromForAccount ( {
389- channelName,
390- account : channelConfig ,
391- prefix : `channels.${ channelName } ` ,
392- } ) ;
393-
394- const accounts = channelConfig . accounts as Record < string , Record < string , unknown > > | undefined ;
395- if ( ! accounts || typeof accounts !== "object" ) {
396- continue ;
397- }
398- for ( const [ accountId , accountConfig ] of Object . entries ( accounts ) ) {
399- if ( ! accountConfig || typeof accountConfig !== "object" ) {
400- continue ;
401- }
402- await recoverAllowFromForAccount ( {
403- channelName,
404- account : accountConfig ,
405- accountId,
406- prefix : `channels.${ channelName } .accounts.${ accountId } ` ,
407- } ) ;
408- }
409- }
410-
411- if ( changes . length === 0 ) {
412- return { config : cfg , changes : [ ] } ;
413- }
414- return { config : next , changes } ;
415- }
416-
417264/**
418265 * Scan all channel configs for dmPolicy="allowlist" without any allowFrom entries.
419266 * This configuration blocks all DMs because no sender can match the empty
0 commit comments