Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
464
test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts
Normal file
464
test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts
Normal file
@@ -0,0 +1,464 @@
|
||||
// MCP channels Docker client drives the QA-owned channel bridge smoke.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import {
|
||||
assert,
|
||||
ClaudeChannelNotificationSchema,
|
||||
ClaudePermissionNotificationSchema,
|
||||
connectGateway,
|
||||
connectMcpClient,
|
||||
extractTextFromGatewayPayload,
|
||||
type ClaudeChannelNotification,
|
||||
type GatewayRpcClient,
|
||||
maybeApprovePendingBridgePairing,
|
||||
waitFor,
|
||||
} from "./mcp-channels.fixture.ts";
|
||||
import {
|
||||
connectMcpClientWithPairingReconnect,
|
||||
createMcpClientTempState,
|
||||
} from "./mcp-client-temp-state.fixture.ts";
|
||||
|
||||
function summarizeSessionRows(rows: Array<Record<string, unknown>> | undefined) {
|
||||
return (rows ?? []).map((entry) => ({
|
||||
key: entry.key,
|
||||
channel: entry.channel,
|
||||
deliveryContext: entry.deliveryContext,
|
||||
lastChannel: entry.lastChannel,
|
||||
lastTo: entry.lastTo,
|
||||
lastAccountId: entry.lastAccountId,
|
||||
lastThreadId: entry.lastThreadId,
|
||||
}));
|
||||
}
|
||||
|
||||
function findEventByText(events: Array<Record<string, unknown>> | undefined, text: string) {
|
||||
return (events ?? []).find((entry) => entry.text === text);
|
||||
}
|
||||
|
||||
const NON_OWNER_PERMISSION_QUIET_WINDOW_MS = 1_000;
|
||||
|
||||
function formatUnknownError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (error === undefined || error === null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
||||
return `${error}`;
|
||||
}
|
||||
if (typeof error === "symbol") {
|
||||
return error.description ?? "symbol";
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error) ?? "";
|
||||
} catch {
|
||||
return Object.prototype.toString.call(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForGatewaySeededConversation(gateway: GatewayRpcClient) {
|
||||
let lastList: { sessions?: Array<Record<string, unknown>> } | undefined;
|
||||
let lastError: unknown;
|
||||
try {
|
||||
return await waitFor(
|
||||
"seeded conversation in gateway sessions.list",
|
||||
async () => {
|
||||
try {
|
||||
lastList = await gateway.request<{ sessions?: Array<Record<string, unknown>> }>(
|
||||
"sessions.list",
|
||||
{ limit: 50, includeDerivedTitles: false, includeLastMessage: false },
|
||||
);
|
||||
lastError = undefined;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
return undefined;
|
||||
}
|
||||
return lastList.sessions?.find((entry) => entry.key === "agent:main:main");
|
||||
},
|
||||
180_000,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`gateway sessions.list did not include seeded conversation: ${JSON.stringify(
|
||||
{
|
||||
count: lastList?.sessions?.length ?? 0,
|
||||
sessions: summarizeSessionRows(lastList?.sessions),
|
||||
lastError: formatUnknownError(lastError),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const gatewayUrl = process.env.GW_URL?.trim();
|
||||
const gatewayToken = process.env.GW_TOKEN?.trim();
|
||||
assert(gatewayUrl, "missing GW_URL");
|
||||
assert(gatewayToken, "missing GW_TOKEN");
|
||||
|
||||
const gateway = await connectGateway({ url: gatewayUrl, token: gatewayToken });
|
||||
let nonOwnerGateway: GatewayRpcClient | undefined;
|
||||
let mcpHandle: Awaited<ReturnType<typeof connectMcpClient>> | undefined;
|
||||
const mcpTempState = createMcpClientTempState({ gatewayToken });
|
||||
|
||||
try {
|
||||
const gatewayConversation = await waitForGatewaySeededConversation(gateway);
|
||||
assert(
|
||||
(gatewayConversation.deliveryContext as { channel?: unknown } | undefined)?.channel ===
|
||||
"imessage",
|
||||
"expected seeded gateway deliveryContext channel",
|
||||
);
|
||||
assert(
|
||||
(gatewayConversation.deliveryContext as { to?: unknown } | undefined)?.to === "+15551234567",
|
||||
"expected seeded gateway deliveryContext target",
|
||||
);
|
||||
|
||||
mcpHandle = await connectMcpClientWithPairingReconnect({
|
||||
tempState: mcpTempState,
|
||||
connect: (tempState) =>
|
||||
connectMcpClient({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
tempState,
|
||||
}),
|
||||
maybeApprovePairing: () => maybeApprovePendingBridgePairing(gateway),
|
||||
});
|
||||
const mcp = mcpHandle.client;
|
||||
const callTool = <T>(params: Parameters<typeof mcp.callTool>[0]) =>
|
||||
mcp.callTool(params, undefined, { timeout: 240_000 }) as Promise<T>;
|
||||
|
||||
let lastMcpConversationList: unknown;
|
||||
const conversation = await waitFor(
|
||||
"seeded conversation in conversations_list",
|
||||
async () => {
|
||||
const listed = await callTool<{
|
||||
structuredContent?: { conversations?: Array<Record<string, unknown>> };
|
||||
}>({
|
||||
name: "conversations_list",
|
||||
arguments: {
|
||||
includeDerivedTitles: false,
|
||||
includeLastMessage: false,
|
||||
},
|
||||
});
|
||||
lastMcpConversationList = listed;
|
||||
return listed.structuredContent?.conversations?.find(
|
||||
(entry) => entry.sessionKey === "agent:main:main",
|
||||
);
|
||||
},
|
||||
240_000,
|
||||
).catch((error: unknown) => {
|
||||
throw new Error(
|
||||
`timeout waiting for seeded MCP conversation: ${JSON.stringify(
|
||||
lastMcpConversationList,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
});
|
||||
assert(conversation.channel === "imessage", "expected seeded channel");
|
||||
assert(conversation.to === "+15551234567", "expected seeded target");
|
||||
|
||||
const fetched = await callTool<{
|
||||
structuredContent?: { conversation?: Record<string, unknown> };
|
||||
isError?: boolean;
|
||||
}>({
|
||||
name: "conversation_get",
|
||||
arguments: { session_key: "agent:main:main" },
|
||||
});
|
||||
assert(!fetched.isError, "conversation_get should succeed");
|
||||
assert(
|
||||
fetched.structuredContent?.conversation?.sessionKey === "agent:main:main",
|
||||
"conversation_get returned wrong session",
|
||||
);
|
||||
|
||||
let lastHistory: unknown;
|
||||
const messages = await waitFor(
|
||||
"seeded transcript messages",
|
||||
async () => {
|
||||
const history = await callTool<{
|
||||
structuredContent?: { messages?: Array<Record<string, unknown>> };
|
||||
}>({
|
||||
name: "messages_read",
|
||||
arguments: { session_key: "agent:main:main", limit: 10 },
|
||||
});
|
||||
lastHistory = history;
|
||||
const currentMessages = history.structuredContent?.messages ?? [];
|
||||
return currentMessages.length >= 2 ? currentMessages : undefined;
|
||||
},
|
||||
240_000,
|
||||
).catch((error: unknown) => {
|
||||
throw new Error(
|
||||
`timeout waiting for seeded transcript messages: ${JSON.stringify(lastHistory, null, 2)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
});
|
||||
await waitFor(
|
||||
"seeded attachment message",
|
||||
() =>
|
||||
messages.find((entry) => {
|
||||
const raw = entry["__openclaw"];
|
||||
return (
|
||||
raw && typeof raw === "object" && (raw as { id?: unknown }).id === "msg-attachment"
|
||||
);
|
||||
}),
|
||||
240_000,
|
||||
);
|
||||
|
||||
const attachments = await callTool<{
|
||||
structuredContent?: { attachments?: Array<Record<string, unknown>> };
|
||||
isError?: boolean;
|
||||
}>({
|
||||
name: "attachments_fetch",
|
||||
arguments: { session_key: "agent:main:main", message_id: "msg-attachment" },
|
||||
});
|
||||
assert(!attachments.isError, "attachments_fetch should succeed");
|
||||
assert(
|
||||
(attachments.structuredContent?.attachments?.length ?? 0) === 1,
|
||||
"expected one seeded attachment",
|
||||
);
|
||||
|
||||
const waited = (await Promise.all([
|
||||
callTool<{
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
}>({
|
||||
name: "events_wait",
|
||||
arguments: {
|
||||
session_key: "agent:main:main",
|
||||
after_cursor: 0,
|
||||
timeout_ms: 10_000,
|
||||
},
|
||||
}),
|
||||
gateway.request("chat.inject", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "assistant live event",
|
||||
}),
|
||||
]).then(([result]) => result)) as {
|
||||
structuredContent?: { event?: Record<string, unknown> };
|
||||
};
|
||||
const assistantEvent = waited.structuredContent?.event;
|
||||
assert(assistantEvent, "expected events_wait result");
|
||||
assert(assistantEvent.type === "message", "expected message event");
|
||||
assert(assistantEvent.role === "assistant", "expected assistant event role");
|
||||
assert(assistantEvent.text === "assistant live event", "expected assistant event text");
|
||||
const assistantCursor = typeof assistantEvent.cursor === "number" ? assistantEvent.cursor : 0;
|
||||
|
||||
const polled = await callTool<{
|
||||
structuredContent?: { events?: Array<Record<string, unknown>> };
|
||||
}>({
|
||||
name: "events_poll",
|
||||
arguments: { session_key: "agent:main:main", after_cursor: 0, limit: 10 },
|
||||
});
|
||||
assert(
|
||||
(polled.structuredContent?.events ?? []).some(
|
||||
(entry) => entry.text === "assistant live event",
|
||||
),
|
||||
"expected assistant event in events_poll",
|
||||
);
|
||||
|
||||
const channelMessage = `hello from docker ${randomUUID()}`;
|
||||
await gateway.request("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: channelMessage,
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
const rawGatewayUserMessage = await waitFor(
|
||||
"raw gateway user session.message",
|
||||
() =>
|
||||
gateway.events.find(
|
||||
(entry) =>
|
||||
entry.event === "session.message" &&
|
||||
entry.payload.sessionKey === "agent:main:main" &&
|
||||
extractTextFromGatewayPayload(entry.payload) === channelMessage,
|
||||
),
|
||||
10_000,
|
||||
).catch(() => undefined);
|
||||
let userEvent = await waitFor(
|
||||
"MCP user session.message event",
|
||||
async () => {
|
||||
const polledValue = await callTool<{
|
||||
structuredContent?: { events?: Array<Record<string, unknown>> };
|
||||
}>({
|
||||
name: "events_poll",
|
||||
arguments: { session_key: "agent:main:main", after_cursor: assistantCursor, limit: 50 },
|
||||
});
|
||||
return findEventByText(polledValue.structuredContent?.events, channelMessage);
|
||||
},
|
||||
60_000,
|
||||
).catch(() => undefined);
|
||||
let finalPolledEvents: Array<Record<string, unknown>> | undefined;
|
||||
if (userEvent?.text !== channelMessage) {
|
||||
const polledLocal = await callTool<{
|
||||
structuredContent?: { events?: Array<Record<string, unknown>> };
|
||||
}>({
|
||||
name: "events_poll",
|
||||
arguments: { session_key: "agent:main:main", after_cursor: assistantCursor, limit: 50 },
|
||||
});
|
||||
finalPolledEvents = polledLocal.structuredContent?.events ?? [];
|
||||
const finalUserEvent = findEventByText(finalPolledEvents, channelMessage);
|
||||
if (finalUserEvent?.text === channelMessage) {
|
||||
userEvent = finalUserEvent;
|
||||
}
|
||||
}
|
||||
if (userEvent?.text !== channelMessage) {
|
||||
throw new Error(
|
||||
`expected user event after chat.send: ${JSON.stringify(
|
||||
{
|
||||
userEvent: userEvent ?? null,
|
||||
rawGatewayUserMessage: rawGatewayUserMessage ?? null,
|
||||
mcpEventsAfterAssistant: finalPolledEvents ?? [],
|
||||
recentGatewayEvents: gateway.events.slice(-10).map((entry) => ({
|
||||
event: entry.event,
|
||||
sessionKey: entry.payload.sessionKey,
|
||||
text: extractTextFromGatewayPayload(entry.payload),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let helpNotification: ClaudeChannelNotification;
|
||||
try {
|
||||
helpNotification = await waitFor(
|
||||
"Claude channel notification",
|
||||
() =>
|
||||
mcpHandle.rawMessages
|
||||
.map((entry) => ClaudeChannelNotificationSchema.safeParse(entry))
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.success &&
|
||||
entry.data.params.meta.session_key === "agent:main:main" &&
|
||||
entry.data.params.content === channelMessage,
|
||||
)?.data.params,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`timeout waiting for Claude channel notification: ${JSON.stringify(
|
||||
{
|
||||
rawMessages: mcpHandle.rawMessages.slice(-10),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
assert(helpNotification.content === channelMessage, "expected Claude channel content");
|
||||
|
||||
await mcp.notification({
|
||||
method: "notifications/claude/channel/permission_request",
|
||||
params: {
|
||||
request_id: "abcde",
|
||||
tool_name: "Bash",
|
||||
description: "run npm test",
|
||||
input_preview: '{"cmd":"npm test"}',
|
||||
},
|
||||
});
|
||||
|
||||
nonOwnerGateway = await connectGateway({
|
||||
url: gatewayUrl,
|
||||
token: gatewayToken,
|
||||
scopes: ["operator.read", "operator.write"],
|
||||
});
|
||||
await nonOwnerGateway.request("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "yes abcde",
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
await waitFor(
|
||||
"non-owner reply forwarded as an ordinary Claude channel message",
|
||||
() =>
|
||||
mcpHandle.rawMessages
|
||||
.map((entry) => ClaudeChannelNotificationSchema.safeParse(entry))
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.success &&
|
||||
entry.data.params.meta.session_key === "agent:main:main" &&
|
||||
entry.data.params.content === "yes abcde",
|
||||
)?.data.params,
|
||||
60_000,
|
||||
);
|
||||
await delay(NON_OWNER_PERMISSION_QUIET_WINDOW_MS);
|
||||
const nonOwnerPermission = mcpHandle.rawMessages
|
||||
.map((entry) => ClaudePermissionNotificationSchema.safeParse(entry))
|
||||
.find((entry) => entry.success && entry.data.params.request_id === "abcde");
|
||||
assert(!nonOwnerPermission, "non-owner reply must not resolve the Claude permission");
|
||||
|
||||
const ownerNotificationStart = mcpHandle.rawMessages.length;
|
||||
await gateway.request("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "yes abcde",
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
let permission: { request_id: string; behavior: "allow" | "deny" };
|
||||
try {
|
||||
permission = await waitFor(
|
||||
"Claude permission notification",
|
||||
() =>
|
||||
mcpHandle.rawMessages
|
||||
.slice(ownerNotificationStart)
|
||||
.map((entry) => ClaudePermissionNotificationSchema.safeParse(entry))
|
||||
.find((entry) => entry.success && entry.data.params.request_id === "abcde")?.data
|
||||
.params,
|
||||
60_000,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`timeout waiting for Claude permission notification: ${JSON.stringify(
|
||||
{
|
||||
rawMessages: mcpHandle.rawMessages.slice(-10),
|
||||
recentGatewayEvents: gateway.events.slice(-10).map((entry) => ({
|
||||
event: entry.event,
|
||||
sessionKey: entry.payload.sessionKey,
|
||||
text: extractTextFromGatewayPayload(entry.payload),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
assert(permission.behavior === "allow", "expected allow permission reply");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
sessionKey: "agent:main:main",
|
||||
nonOwnerReplyForwarded: true,
|
||||
nonOwnerPermissionBlocked: true,
|
||||
ownerPermissionAllowed: permission.behavior === "allow",
|
||||
rawNotifications: mcpHandle.rawMessages.filter(
|
||||
(entry) =>
|
||||
ClaudeChannelNotificationSchema.safeParse(entry).success ||
|
||||
ClaudePermissionNotificationSchema.safeParse(entry).success,
|
||||
).length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
} finally {
|
||||
const closeTasks: Array<Promise<unknown>> = [gateway.close()];
|
||||
if (nonOwnerGateway) {
|
||||
closeTasks.push(nonOwnerGateway.close());
|
||||
}
|
||||
if (mcpHandle) {
|
||||
closeTasks.push(mcpHandle.client.close(), mcpHandle.transport.close());
|
||||
}
|
||||
await Promise.allSettled(closeTasks);
|
||||
mcpHandle?.cleanup();
|
||||
mcpTempState.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user