fix(security): harden channel auth path checks and exec approval routing

This commit is contained in:
Peter Steinberger
2026-02-26 12:45:56 +01:00
parent b096ad267e
commit da0ba1b73a
18 changed files with 314 additions and 6 deletions

View File

@@ -40,6 +40,10 @@ describe("requestExecApprovalDecision", () => {
agentId: "main",
resolvedPath: "/usr/bin/echo",
sessionKey: "session",
turnSourceChannel: "whatsapp",
turnSourceTo: "+15555550123",
turnSourceAccountId: "work",
turnSourceThreadId: "1739201675.123",
});
expect(result).toBe("allow-once");
@@ -57,6 +61,10 @@ describe("requestExecApprovalDecision", () => {
agentId: "main",
resolvedPath: "/usr/bin/echo",
sessionKey: "session",
turnSourceChannel: "whatsapp",
turnSourceTo: "+15555550123",
turnSourceAccountId: "work",
turnSourceThreadId: "1739201675.123",
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
twoPhase: true,
},

View File

@@ -17,6 +17,10 @@ export type RequestExecApprovalDecisionParams = {
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
};
type ParsedDecision = { present: boolean; value: string | null };
@@ -72,6 +76,10 @@ export async function registerExecApprovalRequest(
agentId: params.agentId,
resolvedPath: params.resolvedPath,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
twoPhase: true,
},
@@ -127,6 +135,10 @@ export async function requestExecApprovalDecisionForHost(params: {
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
}): Promise<string | null> {
return await requestExecApprovalDecision({
id: params.approvalId,
@@ -140,6 +152,10 @@ export async function requestExecApprovalDecisionForHost(params: {
agentId: params.agentId,
resolvedPath: params.resolvedPath,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
}
@@ -155,6 +171,10 @@ export async function registerExecApprovalRequestForHost(params: {
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
}): Promise<ExecApprovalRegistration> {
return await registerExecApprovalRequest({
id: params.approvalId,
@@ -168,5 +188,9 @@ export async function registerExecApprovalRequestForHost(params: {
agentId: params.agentId,
resolvedPath: params.resolvedPath,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
}

View File

@@ -44,6 +44,10 @@ export type ProcessGatewayAllowlistParams = {
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
agentId?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
scopeKey?: string;
warnings: string[];
notifySessionKey?: string;
@@ -159,6 +163,10 @@ export async function processGatewayAllowlist(
agentId: params.agentId,
resolvedPath,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
expiresAtMs = registration.expiresAtMs;
preResolvedDecision = registration.finalDecision;

View File

@@ -35,6 +35,10 @@ export type ExecuteNodeHostCommandParams = {
requestedNode?: string;
boundNode?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
agentId?: string;
security: ExecSecurity;
ask: ExecAsk;
@@ -202,6 +206,10 @@ export async function executeNodeHostCommand(
ask: hostAsk,
agentId: params.agentId,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
expiresAtMs = registration.expiresAtMs;
preResolvedDecision = registration.finalDecision;

View File

@@ -21,6 +21,9 @@ export type ExecToolDefaults = {
scopeKey?: string;
sessionKey?: string;
messageProvider?: string;
currentChannelId?: string;
currentThreadTs?: string;
accountId?: string;
notifyOnExit?: boolean;
notifyOnExitEmptySuccess?: boolean;
cwd?: string;

View File

@@ -407,6 +407,10 @@ export function createExecTool(
requestedNode: params.node?.trim(),
boundNode: defaults?.node?.trim(),
sessionKey: defaults?.sessionKey,
turnSourceChannel: defaults?.messageProvider,
turnSourceTo: defaults?.currentChannelId,
turnSourceAccountId: defaults?.accountId,
turnSourceThreadId: defaults?.currentThreadTs,
agentId,
security,
ask,
@@ -433,6 +437,10 @@ export function createExecTool(
safeBinProfiles,
agentId,
sessionKey: defaults?.sessionKey,
turnSourceChannel: defaults?.messageProvider,
turnSourceTo: defaults?.currentChannelId,
turnSourceAccountId: defaults?.accountId,
turnSourceThreadId: defaults?.currentThreadTs,
scopeKey: defaults?.scopeKey,
warnings,
notifySessionKey,

View File

@@ -116,6 +116,10 @@ export function createOpenClawTools(options?: {
createCanvasTool({ config: options?.config }),
createNodesTool({
agentSessionKey: options?.agentSessionKey,
agentChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
config: options?.config,
}),
createCronTool({

View File

@@ -401,6 +401,9 @@ export function createOpenClawCodingTools(options?: {
scopeKey,
sessionKey: options?.sessionKey,
messageProvider: options?.messageProvider,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
accountId: options?.agentAccountId,
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,
approvalRunningNoticeMs:

View File

@@ -20,6 +20,7 @@ import { parseDurationMs } from "../../cli/parse-duration.js";
import type { OpenClawConfig } from "../../config/config.js";
import { formatExecCommand } from "../../infra/system-run-command.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
@@ -128,9 +129,17 @@ const NodesToolSchema = Type.Object({
export function createNodesTool(options?: {
agentSessionKey?: string;
agentChannel?: GatewayMessageChannel;
agentAccountId?: string;
currentChannelId?: string;
currentThreadTs?: string | number;
config?: OpenClawConfig;
}): AnyAgentTool {
const sessionKey = options?.agentSessionKey?.trim() || undefined;
const turnSourceChannel = options?.agentChannel?.trim() || undefined;
const turnSourceTo = options?.currentChannelId?.trim() || undefined;
const turnSourceAccountId = options?.agentAccountId?.trim() || undefined;
const turnSourceThreadId = options?.currentThreadTs;
const agentId = resolveSessionAgentId({
sessionKey: options?.agentSessionKey,
config: options?.config,
@@ -512,6 +521,10 @@ export function createNodesTool(options?: {
host: "node",
agentId,
sessionKey,
turnSourceChannel,
turnSourceTo,
turnSourceAccountId,
turnSourceThreadId,
timeoutMs: APPROVAL_TIMEOUT_MS,
},
);