refactor: share exec host approval helpers

This commit is contained in:
Peter Steinberger 2026-03-14 02:37:43 +00:00
parent 3bc9d9177d
commit 6720bf5be0
3 changed files with 279 additions and 226 deletions

View File

@ -1,5 +1,4 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
import { import {
addAllowlistEntry, addAllowlistEntry,
type ExecAsk, type ExecAsk,
@ -14,20 +13,22 @@ import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js"; import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
import { logInfo } from "../logger.js"; import { logInfo } from "../logger.js";
import { markBackgrounded, tail } from "./bash-process-registry.js"; import { markBackgrounded, tail } from "./bash-process-registry.js";
import { sendExecApprovalFollowup } from "./bash-tools.exec-approval-followup.js";
import { import {
buildExecApprovalRequesterContext, buildExecApprovalRequesterContext,
buildExecApprovalTurnSourceContext, buildExecApprovalTurnSourceContext,
registerExecApprovalRequestForHostOrThrow, registerExecApprovalRequestForHostOrThrow,
} from "./bash-tools.exec-approval-request.js"; } from "./bash-tools.exec-approval-request.js";
import { import {
buildDefaultExecApprovalRequestArgs,
buildExecApprovalFollowupTarget,
buildExecApprovalPendingToolResult,
createExecApprovalDecisionState,
createAndRegisterDefaultExecApprovalRequest, createAndRegisterDefaultExecApprovalRequest,
resolveBaseExecApprovalDecision,
resolveApprovalDecisionOrUndefined, resolveApprovalDecisionOrUndefined,
resolveExecHostApprovalContext, resolveExecHostApprovalContext,
sendExecApprovalFollowupResult,
} from "./bash-tools.exec-host-shared.js"; } from "./bash-tools.exec-host-shared.js";
import { import {
buildApprovalPendingMessage,
DEFAULT_NOTIFY_TAIL_CHARS, DEFAULT_NOTIFY_TAIL_CHARS,
createApprovalSlug, createApprovalSlug,
normalizeNotifyOutput, normalizeNotifyOutput,
@ -140,6 +141,28 @@ export async function processGatewayAllowlist(
} }
if (requiresAsk) { if (requiresAsk) {
const requestArgs = buildDefaultExecApprovalRequestArgs({
warnings: params.warnings,
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
createApprovalSlug,
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
});
const registerGatewayApproval = async (approvalId: string) =>
await registerExecApprovalRequestForHostOrThrow({
approvalId,
command: params.command,
workdir: params.workdir,
host: "gateway",
security: hostSecurity,
ask: hostAsk,
...buildExecApprovalRequesterContext({
agentId: params.agentId,
sessionKey: params.sessionKey,
}),
resolvedPath: allowlistEval.segments[0]?.resolution?.resolvedPath,
...buildExecApprovalTurnSourceContext(params),
});
const { const {
approvalId, approvalId,
approvalSlug, approvalSlug,
@ -150,57 +173,46 @@ export async function processGatewayAllowlist(
sentApproverDms, sentApproverDms,
unavailableReason, unavailableReason,
} = await createAndRegisterDefaultExecApprovalRequest({ } = await createAndRegisterDefaultExecApprovalRequest({
warnings: params.warnings, ...requestArgs,
approvalRunningNoticeMs: params.approvalRunningNoticeMs, register: registerGatewayApproval,
createApprovalSlug,
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
register: async (approvalId) =>
await registerExecApprovalRequestForHostOrThrow({
approvalId,
command: params.command,
workdir: params.workdir,
host: "gateway",
security: hostSecurity,
ask: hostAsk,
...buildExecApprovalRequesterContext({
agentId: params.agentId,
sessionKey: params.sessionKey,
}),
resolvedPath: allowlistEval.segments[0]?.resolution?.resolvedPath,
...buildExecApprovalTurnSourceContext(params),
}),
}); });
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath; const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
const effectiveTimeout = const effectiveTimeout =
typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec; typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec;
const followupTarget = buildExecApprovalFollowupTarget({
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
void (async () => { void (async () => {
const decision = await resolveApprovalDecisionOrUndefined({ const decision = await resolveApprovalDecisionOrUndefined({
approvalId, approvalId,
preResolvedDecision, preResolvedDecision,
onFailure: () => onFailure: () =>
void sendExecApprovalFollowup({ void sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (gateway id=${approvalId}, approval-request-failed): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, ),
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (gateway id=${approvalId}, approval-request-failed): ${params.command}`,
}),
}); });
if (decision === undefined) { if (decision === undefined) {
return; return;
} }
const baseDecision = resolveBaseExecApprovalDecision({ const {
baseDecision,
approvedByAsk: initialApprovedByAsk,
deniedReason: initialDeniedReason,
} = createExecApprovalDecisionState({
decision, decision,
askFallback, askFallback,
obfuscationDetected: obfuscation.detected, obfuscationDetected: obfuscation.detected,
}); });
let approvedByAsk = baseDecision.approvedByAsk; let approvedByAsk = initialApprovedByAsk;
let deniedReason = baseDecision.deniedReason; let deniedReason = initialDeniedReason;
if (baseDecision.timedOut && askFallback === "allowlist") { if (baseDecision.timedOut && askFallback === "allowlist") {
if (!analysisOk || !allowlistSatisfied) { if (!analysisOk || !allowlistSatisfied) {
@ -232,15 +244,10 @@ export async function processGatewayAllowlist(
} }
if (deniedReason) { if (deniedReason) {
await sendExecApprovalFollowup({ await sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (gateway id=${approvalId}, ${deniedReason}): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, );
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (gateway id=${approvalId}, ${deniedReason}): ${params.command}`,
}).catch(() => {});
return; return;
} }
@ -266,15 +273,10 @@ export async function processGatewayAllowlist(
timeoutSec: effectiveTimeout, timeoutSec: effectiveTimeout,
}); });
} catch { } catch {
await sendExecApprovalFollowup({ await sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (gateway id=${approvalId}, spawn-failed): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, );
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (gateway id=${approvalId}, spawn-failed): ${params.command}`,
}).catch(() => {});
return; return;
} }
@ -288,63 +290,22 @@ export async function processGatewayAllowlist(
const summary = output const summary = output
? `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})\n${output}` ? `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})\n${output}`
: `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})`; : `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})`;
await sendExecApprovalFollowup({ await sendExecApprovalFollowupResult(followupTarget, summary);
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: summary,
}).catch(() => {});
})(); })();
return { return {
pendingResult: { pendingResult: buildExecApprovalPendingToolResult({
content: [ host: "gateway",
{ command: params.command,
type: "text", cwd: params.workdir,
text: warningText,
unavailableReason !== null approvalId,
? (buildExecApprovalUnavailableReplyPayload({ approvalSlug,
warningText, expiresAtMs,
reason: unavailableReason, initiatingSurface,
channelLabel: initiatingSurface.channelLabel, sentApproverDms,
sentApproverDms, unavailableReason,
}).text ?? "") }),
: buildApprovalPendingMessage({
warningText,
approvalSlug,
approvalId,
command: params.command,
cwd: params.workdir,
host: "gateway",
}),
},
],
details:
unavailableReason !== null
? ({
status: "approval-unavailable",
reason: unavailableReason,
channelLabel: initiatingSurface.channelLabel,
sentApproverDms,
host: "gateway",
command: params.command,
cwd: params.workdir,
warningText,
} satisfies ExecToolDetails)
: ({
status: "approval-pending",
approvalId,
approvalSlug,
expiresAtMs,
host: "gateway",
command: params.command,
cwd: params.workdir,
warningText,
} satisfies ExecToolDetails),
},
}; };
} }

View File

@ -1,6 +1,5 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
import { import {
type ExecApprovalsFile, type ExecApprovalsFile,
type ExecAsk, type ExecAsk,
@ -13,20 +12,13 @@ import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
import { buildNodeShellCommand } from "../infra/node-shell.js"; import { buildNodeShellCommand } from "../infra/node-shell.js";
import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js"; import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js";
import { logInfo } from "../logger.js"; import { logInfo } from "../logger.js";
import { sendExecApprovalFollowup } from "./bash-tools.exec-approval-followup.js";
import { import {
buildExecApprovalRequesterContext, buildExecApprovalRequesterContext,
buildExecApprovalTurnSourceContext, buildExecApprovalTurnSourceContext,
registerExecApprovalRequestForHostOrThrow, registerExecApprovalRequestForHostOrThrow,
} from "./bash-tools.exec-approval-request.js"; } from "./bash-tools.exec-approval-request.js";
import * as execHostShared from "./bash-tools.exec-host-shared.js";
import { import {
createAndRegisterDefaultExecApprovalRequest,
resolveBaseExecApprovalDecision,
resolveApprovalDecisionOrUndefined,
resolveExecHostApprovalContext,
} from "./bash-tools.exec-host-shared.js";
import {
buildApprovalPendingMessage,
DEFAULT_NOTIFY_TAIL_CHARS, DEFAULT_NOTIFY_TAIL_CHARS,
createApprovalSlug, createApprovalSlug,
normalizeNotifyOutput, normalizeNotifyOutput,
@ -61,7 +53,7 @@ export type ExecuteNodeHostCommandParams = {
export async function executeNodeHostCommand( export async function executeNodeHostCommand(
params: ExecuteNodeHostCommandParams, params: ExecuteNodeHostCommandParams,
): Promise<AgentToolResult<ExecToolDetails>> { ): Promise<AgentToolResult<ExecToolDetails>> {
const { hostSecurity, hostAsk, askFallback } = resolveExecHostApprovalContext({ const { hostSecurity, hostAsk, askFallback } = execHostShared.resolveExecHostApprovalContext({
agentId: params.agentId, agentId: params.agentId,
security: params.security, security: params.security,
ask: params.ask, ask: params.ask,
@ -216,6 +208,29 @@ export async function executeNodeHostCommand(
}) satisfies Record<string, unknown>; }) satisfies Record<string, unknown>;
if (requiresAsk) { if (requiresAsk) {
const requestArgs = execHostShared.buildDefaultExecApprovalRequestArgs({
warnings: params.warnings,
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
createApprovalSlug,
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
});
const registerNodeApproval = async (approvalId: string) =>
await registerExecApprovalRequestForHostOrThrow({
approvalId,
systemRunPlan: prepared.plan,
env: nodeEnv,
workdir: runCwd,
host: "node",
nodeId,
security: hostSecurity,
ask: hostAsk,
...buildExecApprovalRequesterContext({
agentId: runAgentId,
sessionKey: runSessionKey,
}),
...buildExecApprovalTurnSourceContext(params),
});
const { const {
approvalId, approvalId,
approvalSlug, approvalSlug,
@ -225,57 +240,45 @@ export async function executeNodeHostCommand(
initiatingSurface, initiatingSurface,
sentApproverDms, sentApproverDms,
unavailableReason, unavailableReason,
} = await createAndRegisterDefaultExecApprovalRequest({ } = await execHostShared.createAndRegisterDefaultExecApprovalRequest({
warnings: params.warnings, ...requestArgs,
approvalRunningNoticeMs: params.approvalRunningNoticeMs, register: registerNodeApproval,
createApprovalSlug, });
const followupTarget = execHostShared.buildExecApprovalFollowupTarget({
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel, turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId, turnSourceAccountId: params.turnSourceAccountId,
register: async (approvalId) => turnSourceThreadId: params.turnSourceThreadId,
await registerExecApprovalRequestForHostOrThrow({
approvalId,
systemRunPlan: prepared.plan,
env: nodeEnv,
workdir: runCwd,
host: "node",
nodeId,
security: hostSecurity,
ask: hostAsk,
...buildExecApprovalRequesterContext({
agentId: runAgentId,
sessionKey: runSessionKey,
}),
...buildExecApprovalTurnSourceContext(params),
}),
}); });
void (async () => { void (async () => {
const decision = await resolveApprovalDecisionOrUndefined({ const decision = await execHostShared.resolveApprovalDecisionOrUndefined({
approvalId, approvalId,
preResolvedDecision, preResolvedDecision,
onFailure: () => onFailure: () =>
void sendExecApprovalFollowup({ void execHostShared.sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, ),
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${params.command}`,
}),
}); });
if (decision === undefined) { if (decision === undefined) {
return; return;
} }
const baseDecision = resolveBaseExecApprovalDecision({ const {
baseDecision,
approvedByAsk: initialApprovedByAsk,
deniedReason: initialDeniedReason,
} = execHostShared.createExecApprovalDecisionState({
decision, decision,
askFallback, askFallback,
obfuscationDetected: obfuscation.detected, obfuscationDetected: obfuscation.detected,
}); });
let approvedByAsk = baseDecision.approvedByAsk; let approvedByAsk = initialApprovedByAsk;
let approvalDecision: "allow-once" | "allow-always" | null = null; let approvalDecision: "allow-once" | "allow-always" | null = null;
let deniedReason = baseDecision.deniedReason; let deniedReason = initialDeniedReason;
if (baseDecision.timedOut && askFallback === "full" && approvedByAsk) { if (baseDecision.timedOut && askFallback === "full" && approvedByAsk) {
approvalDecision = "allow-once"; approvalDecision = "allow-once";
@ -288,15 +291,10 @@ export async function executeNodeHostCommand(
} }
if (deniedReason) { if (deniedReason) {
await sendExecApprovalFollowup({ await execHostShared.sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (node=${nodeId} id=${approvalId}, ${deniedReason}): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, );
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (node=${nodeId} id=${approvalId}, ${deniedReason}): ${params.command}`,
}).catch(() => {});
return; return;
} }
@ -330,76 +328,28 @@ export async function executeNodeHostCommand(
const summary = output const summary = output
? `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})\n${output}` ? `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})\n${output}`
: `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})`; : `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})`;
await sendExecApprovalFollowup({ await execHostShared.sendExecApprovalFollowupResult(followupTarget, summary);
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: summary,
}).catch(() => {});
} catch { } catch {
await sendExecApprovalFollowup({ await execHostShared.sendExecApprovalFollowupResult(
approvalId, followupTarget,
sessionKey: params.notifySessionKey, `Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${params.command}`,
turnSourceChannel: params.turnSourceChannel, );
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
resultText: `Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${params.command}`,
}).catch(() => {});
} }
})(); })();
return { return execHostShared.buildExecApprovalPendingToolResult({
content: [ host: "node",
{ command: params.command,
type: "text", cwd: params.workdir,
text: warningText,
unavailableReason !== null approvalId,
? (buildExecApprovalUnavailableReplyPayload({ approvalSlug,
warningText, expiresAtMs,
reason: unavailableReason, initiatingSurface,
channelLabel: initiatingSurface.channelLabel, sentApproverDms,
sentApproverDms, unavailableReason,
}).text ?? "") nodeId,
: buildApprovalPendingMessage({ });
warningText,
approvalSlug,
approvalId,
command: prepared.plan.commandText,
cwd: runCwd,
host: "node",
nodeId,
}),
},
],
details:
unavailableReason !== null
? ({
status: "approval-unavailable",
reason: unavailableReason,
channelLabel: initiatingSurface.channelLabel,
sentApproverDms,
host: "node",
command: params.command,
cwd: params.workdir,
nodeId,
warningText,
} satisfies ExecToolDetails)
: ({
status: "approval-pending",
approvalId,
approvalSlug,
expiresAtMs,
host: "node",
command: params.command,
cwd: params.workdir,
nodeId,
warningText,
} satisfies ExecToolDetails),
};
} }
const startedAt = Date.now(); const startedAt = Date.now();

View File

@ -1,5 +1,7 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
import { import {
hasConfiguredExecApprovalDmRoute, hasConfiguredExecApprovalDmRoute,
type ExecApprovalInitiatingSurfaceState, type ExecApprovalInitiatingSurfaceState,
@ -12,11 +14,14 @@ import {
type ExecAsk, type ExecAsk,
type ExecSecurity, type ExecSecurity,
} from "../infra/exec-approvals.js"; } from "../infra/exec-approvals.js";
import { sendExecApprovalFollowup } from "./bash-tools.exec-approval-followup.js";
import { import {
type ExecApprovalRegistration, type ExecApprovalRegistration,
resolveRegisteredExecApprovalDecision, resolveRegisteredExecApprovalDecision,
} from "./bash-tools.exec-approval-request.js"; } from "./bash-tools.exec-approval-request.js";
import { buildApprovalPendingMessage } from "./bash-tools.exec-runtime.js";
import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js"; import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js";
import type { ExecToolDetails } from "./bash-tools.exec-types.js";
type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>; type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>;
@ -53,6 +58,23 @@ export type RegisteredExecApprovalRequestContext = {
unavailableReason: ExecApprovalUnavailableReason | null; unavailableReason: ExecApprovalUnavailableReason | null;
}; };
export type ExecApprovalFollowupTarget = {
approvalId: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
};
export type DefaultExecApprovalRequestArgs = {
warnings: string[];
approvalRunningNoticeMs: number;
createApprovalSlug: (approvalId: string) => string;
turnSourceChannel?: string;
turnSourceAccountId?: string;
};
export function createExecApprovalPendingState(params: { export function createExecApprovalPendingState(params: {
warnings: string[]; warnings: string[];
timeoutMs: number; timeoutMs: number;
@ -257,3 +279,123 @@ export async function createAndRegisterDefaultExecApprovalRequest(params: {
unavailableReason, unavailableReason,
}; };
} }
export function buildDefaultExecApprovalRequestArgs(
params: DefaultExecApprovalRequestArgs,
): DefaultExecApprovalRequestArgs {
return {
warnings: params.warnings,
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
createApprovalSlug: params.createApprovalSlug,
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
};
}
export function buildExecApprovalFollowupTarget(
params: ExecApprovalFollowupTarget,
): ExecApprovalFollowupTarget {
return {
approvalId: params.approvalId,
sessionKey: params.sessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
};
}
export function createExecApprovalDecisionState(params: {
decision: string | null | undefined;
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
obfuscationDetected: boolean;
}) {
const baseDecision = resolveBaseExecApprovalDecision({
decision: params.decision ?? null,
askFallback: params.askFallback,
obfuscationDetected: params.obfuscationDetected,
});
return {
baseDecision,
approvedByAsk: baseDecision.approvedByAsk,
deniedReason: baseDecision.deniedReason,
};
}
export async function sendExecApprovalFollowupResult(
target: ExecApprovalFollowupTarget,
resultText: string,
): Promise<void> {
await sendExecApprovalFollowup({
approvalId: target.approvalId,
sessionKey: target.sessionKey,
turnSourceChannel: target.turnSourceChannel,
turnSourceTo: target.turnSourceTo,
turnSourceAccountId: target.turnSourceAccountId,
turnSourceThreadId: target.turnSourceThreadId,
resultText,
}).catch(() => {});
}
export function buildExecApprovalPendingToolResult(params: {
host: "gateway" | "node";
command: string;
cwd: string;
warningText: string;
approvalId: string;
approvalSlug: string;
expiresAtMs: number;
initiatingSurface: ExecApprovalInitiatingSurfaceState;
sentApproverDms: boolean;
unavailableReason: ExecApprovalUnavailableReason | null;
nodeId?: string;
}): AgentToolResult<ExecToolDetails> {
return {
content: [
{
type: "text",
text:
params.unavailableReason !== null
? (buildExecApprovalUnavailableReplyPayload({
warningText: params.warningText,
reason: params.unavailableReason,
channelLabel: params.initiatingSurface.channelLabel,
sentApproverDms: params.sentApproverDms,
}).text ?? "")
: buildApprovalPendingMessage({
warningText: params.warningText,
approvalSlug: params.approvalSlug,
approvalId: params.approvalId,
command: params.command,
cwd: params.cwd,
host: params.host,
nodeId: params.nodeId,
}),
},
],
details:
params.unavailableReason !== null
? ({
status: "approval-unavailable",
reason: params.unavailableReason,
channelLabel: params.initiatingSurface.channelLabel,
sentApproverDms: params.sentApproverDms,
host: params.host,
command: params.command,
cwd: params.cwd,
nodeId: params.nodeId,
warningText: params.warningText,
} satisfies ExecToolDetails)
: ({
status: "approval-pending",
approvalId: params.approvalId,
approvalSlug: params.approvalSlug,
expiresAtMs: params.expiresAtMs,
host: params.host,
command: params.command,
cwd: params.cwd,
nodeId: params.nodeId,
warningText: params.warningText,
} satisfies ExecToolDetails),
};
}