11// MCP channels Docker client drives the QA-owned channel bridge smoke.
22import { randomUUID } from "node:crypto" ;
3+ import { setTimeout as delay } from "node:timers/promises" ;
34import {
45 assert ,
56 ClaudeChannelNotificationSchema ,
@@ -29,6 +30,8 @@ function summarizeSessionRows(rows: Array<Record<string, unknown>> | undefined)
2930 } ) ) ;
3031}
3132
33+ const NON_OWNER_PERMISSION_QUIET_WINDOW_MS = 1_000 ;
34+
3235function formatUnknownError ( error : unknown ) : string {
3336 if ( error instanceof Error ) {
3437 return error . message ;
@@ -96,6 +99,7 @@ async function main() {
9699 assert ( gatewayToken , "missing GW_TOKEN" ) ;
97100
98101 const gateway = await connectGateway ( { url : gatewayUrl , token : gatewayToken } ) ;
102+ let nonOwnerGateway : GatewayRpcClient | undefined ;
99103 let mcpHandle : Awaited < ReturnType < typeof connectMcpClient > > | undefined ;
100104 const mcpTempState = createMcpClientTempState ( { gatewayToken } ) ;
101105
@@ -349,6 +353,36 @@ async function main() {
349353 } ,
350354 } ) ;
351355
356+ nonOwnerGateway = await connectGateway ( {
357+ url : gatewayUrl ,
358+ token : gatewayToken ,
359+ scopes : [ "operator.read" , "operator.write" ] ,
360+ } ) ;
361+ await nonOwnerGateway . request ( "chat.send" , {
362+ sessionKey : "agent:main:main" ,
363+ message : "yes abcde" ,
364+ idempotencyKey : randomUUID ( ) ,
365+ } ) ;
366+ await waitFor (
367+ "non-owner reply forwarded as an ordinary Claude channel message" ,
368+ ( ) =>
369+ mcpHandle . rawMessages
370+ . map ( ( entry ) => ClaudeChannelNotificationSchema . safeParse ( entry ) )
371+ . find (
372+ ( entry ) =>
373+ entry . success &&
374+ entry . data . params . meta . session_key === "agent:main:main" &&
375+ entry . data . params . content === "yes abcde" ,
376+ ) ?. data . params ,
377+ 60_000 ,
378+ ) ;
379+ await delay ( NON_OWNER_PERMISSION_QUIET_WINDOW_MS ) ;
380+ const nonOwnerPermission = mcpHandle . rawMessages
381+ . map ( ( entry ) => ClaudePermissionNotificationSchema . safeParse ( entry ) )
382+ . find ( ( entry ) => entry . success && entry . data . params . request_id === "abcde" ) ;
383+ assert ( ! nonOwnerPermission , "non-owner reply must not resolve the Claude permission" ) ;
384+
385+ const ownerNotificationStart = mcpHandle . rawMessages . length ;
352386 await gateway . request ( "chat.send" , {
353387 sessionKey : "agent:main:main" ,
354388 message : "yes abcde" ,
@@ -360,6 +394,7 @@ async function main() {
360394 "Claude permission notification" ,
361395 ( ) =>
362396 mcpHandle . rawMessages
397+ . slice ( ownerNotificationStart )
363398 . map ( ( entry ) => ClaudePermissionNotificationSchema . safeParse ( entry ) )
364399 . find ( ( entry ) => entry . success && entry . data . params . request_id === "abcde" ) ?. data
365400 . params ,
@@ -389,6 +424,9 @@ async function main() {
389424 {
390425 ok : true ,
391426 sessionKey : "agent:main:main" ,
427+ nonOwnerReplyForwarded : true ,
428+ nonOwnerPermissionBlocked : true ,
429+ ownerPermissionAllowed : permission . behavior === "allow" ,
392430 rawNotifications : mcpHandle . rawMessages . filter (
393431 ( entry ) =>
394432 ClaudeChannelNotificationSchema . safeParse ( entry ) . success ||
@@ -401,6 +439,9 @@ async function main() {
401439 ) ;
402440 } finally {
403441 const closeTasks : Array < Promise < unknown > > = [ gateway . close ( ) ] ;
442+ if ( nonOwnerGateway ) {
443+ closeTasks . push ( nonOwnerGateway . close ( ) ) ;
444+ }
404445 if ( mcpHandle ) {
405446 closeTasks . push ( mcpHandle . client . close ( ) , mcpHandle . transport . close ( ) ) ;
406447 }
0 commit comments