@@ -5360,6 +5360,7 @@ describe("dispatchReplyFromConfig", () => {
53605360 hookMocks . runner . runInboundClaimForPluginOutcome . mockResolvedValue ( {
53615361 status : "no_handler" ,
53625362 } ) ;
5363+ hookMocks . runner . runInboundClaimForPluginOutcome . mockClear ( ) ;
53635364 sessionBindingMocks . resolveByConversation . mockReturnValue ( {
53645365 bindingId : "binding-no-handler-1" ,
53655366 targetSessionKey : "plugin-binding:codex:nohandler123" ,
@@ -6781,6 +6782,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
67816782 hookMocks . runner . runInboundClaimForPluginOutcome . mockResolvedValue ( {
67826783 status : "no_handler" ,
67836784 } ) ;
6785+ hookMocks . runner . runInboundClaimForPluginOutcome . mockClear ( ) ;
67846786 sessionBindingMocks . resolveByConversation . mockReturnValue ( {
67856787 bindingId : "binding-message-tool-fallback" ,
67866788 targetSessionKey : "plugin-binding:codex:abc123" ,
@@ -6852,6 +6854,231 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
68526854 expect ( dispatcher . sendFinalReply ) . not . toHaveBeenCalled ( ) ;
68536855 } ) ;
68546856
6857+ it ( "lets authorized control commands without CommandSource escape plugin-bound fallback" , async ( ) => {
6858+ setNoAbort ( ) ;
6859+ hookMocks . runner . hasHooks . mockImplementation (
6860+ ( ( hookName ?: string ) =>
6861+ hookName === "inbound_claim" || hookName === "message_received" ) as ( ) => boolean ,
6862+ ) ;
6863+ hookMocks . registry . plugins = [ { id : "openclaw-codex-app-server" , status : "loaded" } ] ;
6864+ hookMocks . runner . runInboundClaimForPluginOutcome . mockResolvedValue ( {
6865+ status : "no_handler" ,
6866+ } ) ;
6867+ hookMocks . runner . runInboundClaimForPluginOutcome . mockClear ( ) ;
6868+ sessionBindingMocks . resolveByConversation . mockReturnValue ( {
6869+ bindingId : "binding-message-tool-command" ,
6870+ targetSessionKey : "plugin-binding:codex:abc123" ,
6871+ targetKind : "session" ,
6872+ conversation : {
6873+ channel : "telegram" ,
6874+ accountId : "default" ,
6875+ conversationId : "-1001234567890:topic:11" ,
6876+ parentConversationId : "-1001234567890" ,
6877+ } ,
6878+ status : "active" ,
6879+ boundAt : 1710000000000 ,
6880+ metadata : {
6881+ pluginBindingOwner : "plugin" ,
6882+ pluginId : "openclaw-codex-app-server" ,
6883+ pluginRoot : "/tmp/plugin" ,
6884+ } ,
6885+ } satisfies SessionBindingRecord ) ;
6886+ sessionStoreMocks . currentEntry = {
6887+ sessionId : "s1" ,
6888+ updatedAt : 0 ,
6889+ sendPolicy : "allow" ,
6890+ } ;
6891+ const cfg = { messages : { visibleReplies : "message_tool" } } as OpenClawConfig ;
6892+ const dispatcher = createDispatcher ( ) ;
6893+ const replyResolver = vi . fn ( async ( ) => ( { text : "reset ack" } ) satisfies ReplyPayload ) ;
6894+ const ctx = buildTestCtx ( {
6895+ Provider : "telegram" ,
6896+ Surface : "telegram" ,
6897+ OriginatingChannel : "telegram" ,
6898+ OriginatingTo : "telegram:-1001234567890" ,
6899+ To : "telegram:-1001234567890" ,
6900+ AccountId : "default" ,
6901+ MessageThreadId : 11 ,
6902+ SessionKey : "agent:main:telegram:group:-1001234567890:topic:11" ,
6903+ ChatType : "group" ,
6904+ GroupSubject : "Dev" ,
6905+ Body : "/reset@openclaw" ,
6906+ RawBody : "/reset@openclaw" ,
6907+ CommandBody : "/reset" ,
6908+ CommandSource : undefined ,
6909+ CommandAuthorized : true ,
6910+ WasMentioned : false ,
6911+ } ) ;
6912+
6913+ await dispatchReplyFromConfig ( {
6914+ ctx,
6915+ cfg,
6916+ dispatcher,
6917+ replyResolver,
6918+ } ) ;
6919+
6920+ expect ( hookMocks . runner . runInboundClaimForPluginOutcome ) . not . toHaveBeenCalled ( ) ;
6921+ expect ( replyResolver ) . toHaveBeenCalledTimes ( 1 ) ;
6922+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledWith ( { text : "reset ack" } ) ;
6923+ } ) ;
6924+
6925+ it ( "keeps unauthorized native commands on the plugin-bound claim path" , async ( ) => {
6926+ setNoAbort ( ) ;
6927+ hookMocks . runner . hasHooks . mockImplementation (
6928+ ( ( hookName ?: string ) =>
6929+ hookName === "inbound_claim" || hookName === "message_received" ) as ( ) => boolean ,
6930+ ) ;
6931+ hookMocks . registry . plugins = [ { id : "openclaw-codex-app-server" , status : "loaded" } ] ;
6932+ hookMocks . runner . runInboundClaimForPluginOutcome . mockResolvedValue ( {
6933+ status : "handled" ,
6934+ result : { } ,
6935+ } ) ;
6936+ hookMocks . runner . runInboundClaimForPluginOutcome . mockClear ( ) ;
6937+ sessionBindingMocks . resolveByConversation . mockReturnValue ( {
6938+ bindingId : "binding-native-unauthorized" ,
6939+ targetSessionKey : "plugin-binding:codex:abc123" ,
6940+ targetKind : "session" ,
6941+ conversation : {
6942+ channel : "telegram" ,
6943+ accountId : "default" ,
6944+ conversationId : "-1001234567890:topic:11" ,
6945+ parentConversationId : "-1001234567890" ,
6946+ } ,
6947+ status : "active" ,
6948+ boundAt : 1710000000000 ,
6949+ metadata : {
6950+ pluginBindingOwner : "plugin" ,
6951+ pluginId : "openclaw-codex-app-server" ,
6952+ pluginRoot : "/tmp/plugin" ,
6953+ } ,
6954+ } satisfies SessionBindingRecord ) ;
6955+ sessionStoreMocks . currentEntry = {
6956+ sessionId : "s1" ,
6957+ updatedAt : 0 ,
6958+ sendPolicy : "allow" ,
6959+ } ;
6960+ const dispatcher = createDispatcher ( ) ;
6961+ const replyResolver = vi . fn ( async ( ) => ( { text : "core reply" } ) satisfies ReplyPayload ) ;
6962+ const ctx = buildTestCtx ( {
6963+ Provider : "telegram" ,
6964+ Surface : "telegram" ,
6965+ OriginatingChannel : "telegram" ,
6966+ OriginatingTo : "telegram:-1001234567890" ,
6967+ To : "telegram:-1001234567890" ,
6968+ AccountId : "default" ,
6969+ MessageThreadId : 11 ,
6970+ SessionKey : "agent:main:telegram:group:-1001234567890:topic:11" ,
6971+ ChatType : "group" ,
6972+ GroupSubject : "Dev" ,
6973+ Body : "/status" ,
6974+ RawBody : "/status" ,
6975+ CommandBody : "/status" ,
6976+ CommandSource : "native" ,
6977+ CommandAuthorized : false ,
6978+ WasMentioned : false ,
6979+ } ) ;
6980+
6981+ await dispatchReplyFromConfig ( {
6982+ ctx,
6983+ cfg : emptyConfig ,
6984+ dispatcher,
6985+ replyResolver,
6986+ } ) ;
6987+
6988+ expect ( hookMocks . runner . runInboundClaimForPluginOutcome ) . toHaveBeenCalledWith (
6989+ "openclaw-codex-app-server" ,
6990+ expect . objectContaining ( {
6991+ channel : "telegram" ,
6992+ content : "/status" ,
6993+ } ) ,
6994+ expect . objectContaining ( {
6995+ pluginBinding : expect . objectContaining ( { bindingId : "binding-native-unauthorized" } ) ,
6996+ } ) ,
6997+ ) ;
6998+ expect ( replyResolver ) . not . toHaveBeenCalled ( ) ;
6999+ } ) ;
7000+
7001+ it ( "keeps structured normal command turns on the plugin-bound claim path" , async ( ) => {
7002+ setNoAbort ( ) ;
7003+ hookMocks . runner . hasHooks . mockImplementation (
7004+ ( ( hookName ?: string ) =>
7005+ hookName === "inbound_claim" || hookName === "message_received" ) as ( ) => boolean ,
7006+ ) ;
7007+ hookMocks . registry . plugins = [ { id : "openclaw-codex-app-server" , status : "loaded" } ] ;
7008+ hookMocks . runner . runInboundClaimForPluginOutcome . mockResolvedValue ( {
7009+ status : "handled" ,
7010+ result : { } ,
7011+ } ) ;
7012+ hookMocks . runner . runInboundClaimForPluginOutcome . mockClear ( ) ;
7013+ sessionBindingMocks . resolveByConversation . mockReturnValue ( {
7014+ bindingId : "binding-structured-normal-turn" ,
7015+ targetSessionKey : "plugin-binding:codex:abc123" ,
7016+ targetKind : "session" ,
7017+ conversation : {
7018+ channel : "telegram" ,
7019+ accountId : "default" ,
7020+ conversationId : "-1001234567890:topic:11" ,
7021+ parentConversationId : "-1001234567890" ,
7022+ } ,
7023+ status : "active" ,
7024+ boundAt : 1710000000000 ,
7025+ metadata : {
7026+ pluginBindingOwner : "plugin" ,
7027+ pluginId : "openclaw-codex-app-server" ,
7028+ pluginRoot : "/tmp/plugin" ,
7029+ } ,
7030+ } satisfies SessionBindingRecord ) ;
7031+ sessionStoreMocks . currentEntry = {
7032+ sessionId : "s1" ,
7033+ updatedAt : 0 ,
7034+ sendPolicy : "allow" ,
7035+ } ;
7036+ const dispatcher = createDispatcher ( ) ;
7037+ const replyResolver = vi . fn ( async ( ) => ( { text : "core reply" } ) satisfies ReplyPayload ) ;
7038+ const ctx = buildTestCtx ( {
7039+ Provider : "telegram" ,
7040+ Surface : "telegram" ,
7041+ OriginatingChannel : "telegram" ,
7042+ OriginatingTo : "telegram:-1001234567890" ,
7043+ To : "telegram:-1001234567890" ,
7044+ AccountId : "default" ,
7045+ MessageThreadId : 11 ,
7046+ SessionKey : "agent:main:telegram:group:-1001234567890:topic:11" ,
7047+ ChatType : "group" ,
7048+ GroupSubject : "Dev" ,
7049+ Body : "through this" ,
7050+ RawBody : "through this" ,
7051+ CommandBody : "/think high through this" ,
7052+ CommandAuthorized : true ,
7053+ CommandTurn : {
7054+ kind : "normal" ,
7055+ source : "message" ,
7056+ authorized : false ,
7057+ body : "/think high through this" ,
7058+ } ,
7059+ WasMentioned : false ,
7060+ } ) ;
7061+
7062+ await dispatchReplyFromConfig ( {
7063+ ctx,
7064+ cfg : emptyConfig ,
7065+ dispatcher,
7066+ replyResolver,
7067+ } ) ;
7068+
7069+ expect ( hookMocks . runner . runInboundClaimForPluginOutcome ) . toHaveBeenCalledWith (
7070+ "openclaw-codex-app-server" ,
7071+ expect . objectContaining ( {
7072+ channel : "telegram" ,
7073+ content : "/think high through this" ,
7074+ } ) ,
7075+ expect . objectContaining ( {
7076+ pluginBinding : expect . objectContaining ( { bindingId : "binding-structured-normal-turn" } ) ,
7077+ } ) ,
7078+ ) ;
7079+ expect ( replyResolver ) . not . toHaveBeenCalled ( ) ;
7080+ } ) ;
7081+
68557082 it ( "keeps message-tool-only source delivery private while still processing the turn" , async ( ) => {
68567083 setNoAbort ( ) ;
68577084 sessionStoreMocks . currentEntry = {
0 commit comments