@@ -1510,6 +1510,26 @@ describe("gateway send mirroring", () => {
15101510 } ) ;
15111511 } ) ;
15121512
1513+ it ( "keeps explicit agent ownership for legacy unscoped session keys" , async ( ) => {
1514+ mockDeliverySuccess ( "m-legacy-unscoped-agent" ) ;
1515+
1516+ await runSend ( {
1517+ to : "channel:C1" ,
1518+ message : "hello" ,
1519+ channel : "slack" ,
1520+ agentId : "work" ,
1521+ sessionKey : "legacy:slack:channel:c1" ,
1522+ idempotencyKey : "idem-legacy-unscoped-agent" ,
1523+ } ) ;
1524+
1525+ expect ( outboundRouteCall ( ) ?. agentId ) . toBe ( "work" ) ;
1526+ expect ( outboundRouteCall ( ) ?. currentSessionKey ) . toBe ( "legacy:slack:channel:c1" ) ;
1527+ expectDeliverySessionMirror ( {
1528+ agentId : "work" ,
1529+ sessionKey : "legacy:slack:channel:c1" ,
1530+ } ) ;
1531+ } ) ;
1532+
15131533 it ( "rejects a missing reserved agent-harness session before persistence or delivery" , async ( ) => {
15141534 const sessionKey = "agent:main:harness:codex:supervision:missing" ;
15151535
@@ -1606,22 +1626,84 @@ describe("gateway send mirroring", () => {
16061626 expect ( deliveryCall ( ) ?. mirror ?. agentId ) . toBe ( "work" ) ;
16071627 } ) ;
16081628
1609- it ( "prefers explicit agentId over sessionKey agent for delivery and mirror" , async ( ) => {
1610- mockDeliverySuccess ( "m-agent-precedence" ) ;
1611-
1612- await runSend ( {
1629+ it ( "rejects explicit agentId when it conflicts with the sessionKey agent" , async ( ) => {
1630+ const { respond } = await runSend ( {
16131631 to : "channel:C1" ,
16141632 message : "hello" ,
16151633 channel : "slack" ,
16161634 agentId : "work" ,
16171635 sessionKey : "agent:main:slack:channel:c1" ,
1618- idempotencyKey : "idem-agent-precedence " ,
1636+ idempotencyKey : "idem-agent-mismatch " ,
16191637 } ) ;
16201638
1621- expect ( deliveryCall ( ) ?. session ?. agentId ) . toBe ( "work" ) ;
1622- expect ( deliveryCall ( ) ?. session ?. key ) . toBe ( "agent:main:slack:channel:c1" ) ;
1623- expect ( deliveryCall ( ) ?. mirror ?. sessionKey ) . toBe ( "agent:main:slack:channel:c1" ) ;
1624- expect ( deliveryCall ( ) ?. mirror ?. agentId ) . toBe ( "work" ) ;
1639+ expect ( firstRespondCall ( respond ) ) . toMatchObject ( [
1640+ false ,
1641+ undefined ,
1642+ {
1643+ code : "INVALID_REQUEST" ,
1644+ message : 'send agentId "work" does not match session key agent "main"' ,
1645+ } ,
1646+ undefined ,
1647+ ] ) ;
1648+ expect ( mocks . getChannelPlugin ) . not . toHaveBeenCalled ( ) ;
1649+ expect ( mocks . resolveOutboundTarget ) . not . toHaveBeenCalled ( ) ;
1650+ expect ( mocks . resolveOutboundSessionRoute ) . not . toHaveBeenCalled ( ) ;
1651+ expect ( mocks . deliverOutboundPayloads ) . not . toHaveBeenCalled ( ) ;
1652+ } ) ;
1653+
1654+ it . each ( [ "agent::broken" , "agent:main" ] ) (
1655+ "rejects malformed agent-scoped send session key %s before channel work" ,
1656+ async ( sessionKey ) => {
1657+ const { respond } = await runSend ( {
1658+ to : "channel:C1" ,
1659+ message : "hello" ,
1660+ channel : "slack" ,
1661+ sessionKey,
1662+ idempotencyKey : `idem-malformed-${ sessionKey } ` ,
1663+ } ) ;
1664+
1665+ expect ( firstRespondCall ( respond ) ?. [ 2 ] ) . toMatchObject ( {
1666+ code : "INVALID_REQUEST" ,
1667+ message : expect . stringContaining ( "is malformed" ) ,
1668+ } ) ;
1669+ expect ( mocks . getChannelPlugin ) . not . toHaveBeenCalled ( ) ;
1670+ expect ( mocks . resolveOutboundTarget ) . not . toHaveBeenCalled ( ) ;
1671+ expect ( mocks . deliverOutboundPayloads ) . not . toHaveBeenCalled ( ) ;
1672+ } ,
1673+ ) ;
1674+
1675+ it ( "caches ownership validation failures under the send idempotency key" , async ( ) => {
1676+ const context = makeContext ( ) ;
1677+ const invoke = async ( ) => {
1678+ const respond = vi . fn ( ) ;
1679+ await expectDefined ( sendHandlers . send , "sendHandlers.send test invariant" ) . call (
1680+ sendHandlers ,
1681+ {
1682+ params : {
1683+ to : "channel:C1" ,
1684+ message : "hello" ,
1685+ channel : "slack" ,
1686+ agentId : "work" ,
1687+ sessionKey : "agent:main:slack:channel:c1" ,
1688+ idempotencyKey : "idem-agent-mismatch-cached" ,
1689+ } as never ,
1690+ respond,
1691+ context,
1692+ req : { type : "req" , id : "1" , method : "send" } ,
1693+ client : null as never ,
1694+ isWebchatConnect : ( ) => false ,
1695+ } ,
1696+ ) ;
1697+ return respond ;
1698+ } ;
1699+
1700+ const firstRespond = await invoke ( ) ;
1701+ const secondRespond = await invoke ( ) ;
1702+
1703+ expect ( firstRespondCall ( firstRespond ) ?. [ 2 ] ?. code ) . toBe ( "INVALID_REQUEST" ) ;
1704+ expect ( firstRespondCall ( secondRespond ) ?. [ 3 ] ) . toEqual ( { cached : true } ) ;
1705+ expect ( mocks . getChannelPlugin ) . not . toHaveBeenCalled ( ) ;
1706+ expect ( mocks . deliverOutboundPayloads ) . not . toHaveBeenCalled ( ) ;
16251707 } ) ;
16261708
16271709 it ( "ignores blank explicit agentId and falls back to sessionKey agent" , async ( ) => {
@@ -2005,6 +2087,52 @@ describe("gateway send mirroring", () => {
20052087 ) ;
20062088 } ) ;
20072089
2090+ it ( "rejects message.action agentId when it conflicts with the sessionKey agent" , async ( ) => {
2091+ const { respond } = await runMessageActionRequest (
2092+ {
2093+ channel : "whatsapp" ,
2094+ action : "react" ,
2095+ params : { messageId : "wamid.1" , emoji : "ok" } ,
2096+ sessionKey : "agent:main:whatsapp:direct:15551234567" ,
2097+ agentId : "work" ,
2098+ idempotencyKey : "idem-message-action-agent-mismatch" ,
2099+ } ,
2100+ null ,
2101+ ) ;
2102+
2103+ expect ( firstRespondCall ( respond ) ) . toMatchObject ( [
2104+ false ,
2105+ undefined ,
2106+ {
2107+ code : "INVALID_REQUEST" ,
2108+ message : 'message.action agentId "work" does not match session key agent "main"' ,
2109+ } ,
2110+ undefined ,
2111+ ] ) ;
2112+ expect ( mocks . getChannelPlugin ) . not . toHaveBeenCalled ( ) ;
2113+ expect ( mocks . dispatchChannelMessageAction ) . not . toHaveBeenCalled ( ) ;
2114+ } ) ;
2115+
2116+ it ( "rejects malformed message.action session keys before plugin discovery" , async ( ) => {
2117+ const { respond } = await runMessageActionRequest (
2118+ {
2119+ channel : "whatsapp" ,
2120+ action : "react" ,
2121+ params : { messageId : "wamid.1" , emoji : "ok" } ,
2122+ sessionKey : "agent::broken" ,
2123+ idempotencyKey : "idem-message-action-malformed-session" ,
2124+ } ,
2125+ null ,
2126+ ) ;
2127+
2128+ expect ( firstRespondCall ( respond ) ?. [ 2 ] ) . toMatchObject ( {
2129+ code : "INVALID_REQUEST" ,
2130+ message : expect . stringContaining ( "is malformed" ) ,
2131+ } ) ;
2132+ expect ( mocks . getChannelPlugin ) . not . toHaveBeenCalled ( ) ;
2133+ expect ( mocks . dispatchChannelMessageAction ) . not . toHaveBeenCalled ( ) ;
2134+ } ) ;
2135+
20082136 it ( "strips current-turn context from unauthenticated message action callers" , async ( ) => {
20092137 mocks . getChannelPlugin . mockReturnValue ( {
20102138 actions : {
0 commit comments