refactor: share exec approval session target routing

This commit is contained in:
Peter Steinberger 2026-03-13 19:42:18 +00:00
parent c74e5210f6
commit 8473a29da7
5 changed files with 104 additions and 87 deletions

View File

@ -424,7 +424,7 @@ describe("exec approval forwarder", () => {
channel: "whatsapp",
to: "+15555550123",
accountId: "work",
threadId: "1739201675.123",
threadId: 1739201675,
}),
);
} finally {

View File

@ -1,7 +1,6 @@
import type { ReplyPayload } from "../auto-reply/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import type {
ExecApprovalForwardingConfig,
ExecApprovalForwardTarget,
@ -18,13 +17,13 @@ import {
} from "../utils/message-channel.js";
import { resolveExecApprovalCommandDisplay } from "./exec-approval-command-display.js";
import { buildExecApprovalPendingReplyPayload } from "./exec-approval-reply.js";
import { resolveExecApprovalSessionTarget } from "./exec-approval-session-target.js";
import type {
ExecApprovalDecision,
ExecApprovalRequest,
ExecApprovalResolved,
} from "./exec-approvals.js";
import { deliverOutboundPayloads } from "./outbound/deliver.js";
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
const log = createSubsystemLogger("gateway/exec-approvals");
export type { ExecApprovalRequest, ExecApprovalResolved };
@ -281,21 +280,9 @@ function defaultResolveSessionTarget(params: {
cfg: OpenClawConfig;
request: ExecApprovalRequest;
}): ExecApprovalForwardTarget | null {
const sessionKey = params.request.request.sessionKey?.trim();
if (!sessionKey) {
return null;
}
const parsed = parseAgentSessionKey(sessionKey);
const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main";
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
const store = loadSessionStore(storePath);
const entry = store[sessionKey];
if (!entry) {
return null;
}
const target = resolveSessionDeliveryTarget({
entry,
requestedChannel: "last",
const target = resolveExecApprovalSessionTarget({
cfg: params.cfg,
request: params.request,
turnSourceChannel: normalizeTurnSourceChannel(params.request.request.turnSourceChannel),
turnSourceTo: params.request.request.turnSourceTo?.trim() || undefined,
turnSourceAccountId: params.request.request.turnSourceAccountId?.trim() || undefined,

View File

@ -0,0 +1,69 @@
import type { OpenClawConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import type { ExecApprovalRequest } from "./exec-approvals.js";
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
export type ExecApprovalSessionTarget = {
channel?: string;
to: string;
accountId?: string;
threadId?: number;
};
function normalizeOptionalString(value?: string | null): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
function normalizeOptionalThreadId(value?: string | number | null): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = Number.parseInt(value, 10);
return Number.isFinite(normalized) ? normalized : undefined;
}
export function resolveExecApprovalSessionTarget(params: {
cfg: OpenClawConfig;
request: ExecApprovalRequest;
turnSourceChannel?: string | null;
turnSourceTo?: string | null;
turnSourceAccountId?: string | null;
turnSourceThreadId?: string | number | null;
}): ExecApprovalSessionTarget | null {
const sessionKey = normalizeOptionalString(params.request.request.sessionKey);
if (!sessionKey) {
return null;
}
const parsed = parseAgentSessionKey(sessionKey);
const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main";
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
const store = loadSessionStore(storePath);
const entry = store[sessionKey];
if (!entry) {
return null;
}
const target = resolveSessionDeliveryTarget({
entry,
requestedChannel: "last",
turnSourceChannel: normalizeOptionalString(params.turnSourceChannel),
turnSourceTo: normalizeOptionalString(params.turnSourceTo),
turnSourceAccountId: normalizeOptionalString(params.turnSourceAccountId),
turnSourceThreadId: normalizeOptionalThreadId(params.turnSourceThreadId),
});
if (!target.to) {
return null;
}
return {
channel: normalizeOptionalString(target.channel),
to: target.to,
accountId: normalizeOptionalString(target.accountId),
threadId: normalizeOptionalThreadId(target.threadId),
};
}

View File

@ -2,47 +2,35 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveTelegramTransport } from "../telegram/fetch.js";
import { fetchRemoteMedia } from "./fetch.js";
const undiciFetch = vi.hoisted(() => vi.fn());
const AgentCtor = vi.hoisted(() =>
vi.fn(function MockAgent(
this: { options?: Record<string, unknown> },
options?: Record<string, unknown>,
) {
this.options = options;
}),
);
const EnvHttpProxyAgentCtor = vi.hoisted(() =>
vi.fn(function MockEnvHttpProxyAgent(
this: { options?: Record<string, unknown> },
options?: Record<string, unknown>,
) {
this.options = options;
}),
);
const ProxyAgentCtor = vi.hoisted(() =>
vi.fn(function MockProxyAgent(
this: { options?: Record<string, unknown> | string },
options?: Record<string, unknown> | string,
) {
this.options = options;
}),
);
const undiciMocks = vi.hoisted(() => {
const createDispatcherCtor = <T extends Record<string, unknown> | string>() =>
vi.fn(function MockDispatcher(this: { options?: T }, options?: T) {
this.options = options;
});
return {
fetch: vi.fn(),
agentCtor: createDispatcherCtor<Record<string, unknown>>(),
envHttpProxyAgentCtor: createDispatcherCtor<Record<string, unknown>>(),
proxyAgentCtor: createDispatcherCtor<Record<string, unknown> | string>(),
};
});
vi.mock("undici", () => ({
Agent: AgentCtor,
EnvHttpProxyAgent: EnvHttpProxyAgentCtor,
ProxyAgent: ProxyAgentCtor,
fetch: undiciFetch,
Agent: undiciMocks.agentCtor,
EnvHttpProxyAgent: undiciMocks.envHttpProxyAgentCtor,
ProxyAgent: undiciMocks.proxyAgentCtor,
fetch: undiciMocks.fetch,
}));
describe("fetchRemoteMedia telegram network policy", () => {
type LookupFn = NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
afterEach(() => {
undiciFetch.mockReset();
AgentCtor.mockClear();
EnvHttpProxyAgentCtor.mockClear();
ProxyAgentCtor.mockClear();
undiciMocks.fetch.mockReset();
undiciMocks.agentCtor.mockClear();
undiciMocks.envHttpProxyAgentCtor.mockClear();
undiciMocks.proxyAgentCtor.mockClear();
vi.unstubAllEnvs();
});
@ -50,7 +38,7 @@ describe("fetchRemoteMedia telegram network policy", () => {
const lookupFn = vi.fn(async () => [
{ address: "149.154.167.220", family: 4 },
]) as unknown as LookupFn;
undiciFetch.mockResolvedValueOnce(
undiciMocks.fetch.mockResolvedValueOnce(
new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), {
status: 200,
headers: { "content-type": "image/jpeg" },
@ -76,7 +64,7 @@ describe("fetchRemoteMedia telegram network policy", () => {
},
});
const init = undiciFetch.mock.calls[0]?.[1] as
const init = undiciMocks.fetch.mock.calls[0]?.[1] as
| (RequestInit & {
dispatcher?: {
options?: {
@ -100,7 +88,7 @@ describe("fetchRemoteMedia telegram network policy", () => {
const lookupFn = vi.fn(async () => [
{ address: "149.154.167.220", family: 4 },
]) as unknown as LookupFn;
undiciFetch.mockResolvedValueOnce(
undiciMocks.fetch.mockResolvedValueOnce(
new Response(new Uint8Array([0x25, 0x50, 0x44, 0x46]), {
status: 200,
headers: { "content-type": "application/pdf" },
@ -126,7 +114,7 @@ describe("fetchRemoteMedia telegram network policy", () => {
},
});
const init = undiciFetch.mock.calls[0]?.[1] as
const init = undiciMocks.fetch.mock.calls[0]?.[1] as
| (RequestInit & {
dispatcher?: {
options?: {
@ -137,6 +125,6 @@ describe("fetchRemoteMedia telegram network policy", () => {
| undefined;
expect(init?.dispatcher?.options?.uri).toBe("http://127.0.0.1:7890");
expect(ProxyAgentCtor).toHaveBeenCalled();
expect(undiciMocks.proxyAgentCtor).toHaveBeenCalled();
});
});

View File

@ -1,5 +1,4 @@
import type { OpenClawConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { GatewayClient } from "../gateway/client.js";
import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js";
import type { EventFrame } from "../gateway/protocol/index.js";
@ -8,8 +7,8 @@ import {
buildExecApprovalPendingReplyPayload,
type ExecApprovalPendingReplyParams,
} from "../infra/exec-approval-reply.js";
import { resolveExecApprovalSessionTarget } from "../infra/exec-approval-session-target.js";
import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js";
import { resolveSessionDeliveryTarget } from "../infra/outbound/targets.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeAccountId, parseAgentSessionKey } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
@ -120,40 +119,14 @@ function resolveRequestSessionTarget(params: {
cfg: OpenClawConfig;
request: ExecApprovalRequest;
}): { to: string; accountId?: string; threadId?: number; channel?: string } | null {
const sessionKey = params.request.request.sessionKey?.trim();
if (!sessionKey) {
return null;
}
const parsed = parseAgentSessionKey(sessionKey);
const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main";
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
const store = loadSessionStore(storePath);
const entry = store[sessionKey];
if (!entry) {
return null;
}
const target = resolveSessionDeliveryTarget({
entry,
requestedChannel: "last",
return resolveExecApprovalSessionTarget({
cfg: params.cfg,
request: params.request,
turnSourceChannel: params.request.request.turnSourceChannel ?? undefined,
turnSourceTo: params.request.request.turnSourceTo ?? undefined,
turnSourceAccountId: params.request.request.turnSourceAccountId ?? undefined,
turnSourceThreadId: params.request.request.turnSourceThreadId ?? undefined,
});
if (!target.to) {
return null;
}
return {
channel: target.channel ?? undefined,
to: target.to,
accountId: target.accountId ?? undefined,
threadId:
typeof target.threadId === "number"
? target.threadId
: typeof target.threadId === "string"
? Number.parseInt(target.threadId, 10)
: undefined,
};
}
function resolveTelegramSourceTarget(params: {