From 565dc0d17b672347b4a0e5021da1e3a745953b65 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 18:22:49 +0000 Subject: [PATCH] refactor: share exec approval registration context --- src/agents/bash-tools.exec-host-gateway.ts | 69 +++++--------- src/agents/bash-tools.exec-host-node.ts | 71 +++++---------- src/agents/bash-tools.exec-host-shared.ts | 101 ++++++++++++++++++++- 3 files changed, 149 insertions(+), 92 deletions(-) diff --git a/src/agents/bash-tools.exec-host-gateway.ts b/src/agents/bash-tools.exec-host-gateway.ts index 6b43fbe8663..ac6ed57aa72 100644 --- a/src/agents/bash-tools.exec-host-gateway.ts +++ b/src/agents/bash-tools.exec-host-gateway.ts @@ -1,10 +1,5 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { loadConfig } from "../config/config.js"; import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js"; -import { - hasConfiguredExecApprovalDmRoute, - resolveExecApprovalInitiatingSurfaceState, -} from "../infra/exec-approval-surface.js"; import { addAllowlistEntry, type ExecAsk, @@ -26,7 +21,7 @@ import { registerExecApprovalRequestForHostOrThrow, } from "./bash-tools.exec-approval-request.js"; import { - createDefaultExecApprovalRequestContext, + createAndRegisterDefaultExecApprovalRequest, resolveBaseExecApprovalDecision, resolveApprovalDecisionOrUndefined, resolveExecHostApprovalContext, @@ -149,52 +144,36 @@ export async function processGatewayAllowlist( approvalId, approvalSlug, warningText, - expiresAtMs: defaultExpiresAtMs, - preResolvedDecision: defaultPreResolvedDecision, - } = createDefaultExecApprovalRequestContext({ + expiresAtMs, + preResolvedDecision, + initiatingSurface, + sentApproverDms, + unavailableReason, + } = await createAndRegisterDefaultExecApprovalRequest({ warnings: params.warnings, approvalRunningNoticeMs: params.approvalRunningNoticeMs, 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 effectiveTimeout = typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec; - let expiresAtMs = defaultExpiresAtMs; - let preResolvedDecision = defaultPreResolvedDecision; - - // Register first so the returned approval ID is actionable immediately. - const registration = await registerExecApprovalRequestForHostOrThrow({ - approvalId, - command: params.command, - workdir: params.workdir, - host: "gateway", - security: hostSecurity, - ask: hostAsk, - ...buildExecApprovalRequesterContext({ - agentId: params.agentId, - sessionKey: params.sessionKey, - }), - resolvedPath, - ...buildExecApprovalTurnSourceContext(params), - }); - expiresAtMs = registration.expiresAtMs; - preResolvedDecision = registration.finalDecision; - const initiatingSurface = resolveExecApprovalInitiatingSurfaceState({ - channel: params.turnSourceChannel, - accountId: params.turnSourceAccountId, - }); - const cfg = loadConfig(); - const sentApproverDms = - (initiatingSurface.kind === "disabled" || initiatingSurface.kind === "unsupported") && - hasConfiguredExecApprovalDmRoute(cfg); - const unavailableReason = - preResolvedDecision === null - ? "no-approval-route" - : initiatingSurface.kind === "disabled" - ? "initiating-platform-disabled" - : initiatingSurface.kind === "unsupported" - ? "initiating-platform-unsupported" - : null; void (async () => { const decision = await resolveApprovalDecisionOrUndefined({ diff --git a/src/agents/bash-tools.exec-host-node.ts b/src/agents/bash-tools.exec-host-node.ts index c3a23197f0a..6f5fc25f966 100644 --- a/src/agents/bash-tools.exec-host-node.ts +++ b/src/agents/bash-tools.exec-host-node.ts @@ -1,11 +1,6 @@ import crypto from "node:crypto"; import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { loadConfig } from "../config/config.js"; import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js"; -import { - hasConfiguredExecApprovalDmRoute, - resolveExecApprovalInitiatingSurfaceState, -} from "../infra/exec-approval-surface.js"; import { type ExecApprovalsFile, type ExecAsk, @@ -25,7 +20,7 @@ import { registerExecApprovalRequestForHostOrThrow, } from "./bash-tools.exec-approval-request.js"; import { - createDefaultExecApprovalRequestContext, + createAndRegisterDefaultExecApprovalRequest, resolveBaseExecApprovalDecision, resolveApprovalDecisionOrUndefined, resolveExecHostApprovalContext, @@ -225,50 +220,34 @@ export async function executeNodeHostCommand( approvalId, approvalSlug, warningText, - expiresAtMs: defaultExpiresAtMs, - preResolvedDecision: defaultPreResolvedDecision, - } = createDefaultExecApprovalRequestContext({ + expiresAtMs, + preResolvedDecision, + initiatingSurface, + sentApproverDms, + unavailableReason, + } = await createAndRegisterDefaultExecApprovalRequest({ warnings: params.warnings, approvalRunningNoticeMs: params.approvalRunningNoticeMs, createApprovalSlug, + turnSourceChannel: params.turnSourceChannel, + turnSourceAccountId: params.turnSourceAccountId, + register: async (approvalId) => + await registerExecApprovalRequestForHostOrThrow({ + approvalId, + systemRunPlan: prepared.plan, + env: nodeEnv, + workdir: runCwd, + host: "node", + nodeId, + security: hostSecurity, + ask: hostAsk, + ...buildExecApprovalRequesterContext({ + agentId: runAgentId, + sessionKey: runSessionKey, + }), + ...buildExecApprovalTurnSourceContext(params), + }), }); - let expiresAtMs = defaultExpiresAtMs; - let preResolvedDecision = defaultPreResolvedDecision; - - // Register first so the returned approval ID is actionable immediately. - const registration = await registerExecApprovalRequestForHostOrThrow({ - approvalId, - systemRunPlan: prepared.plan, - env: nodeEnv, - workdir: runCwd, - host: "node", - nodeId, - security: hostSecurity, - ask: hostAsk, - ...buildExecApprovalRequesterContext({ - agentId: runAgentId, - sessionKey: runSessionKey, - }), - ...buildExecApprovalTurnSourceContext(params), - }); - expiresAtMs = registration.expiresAtMs; - preResolvedDecision = registration.finalDecision; - const initiatingSurface = resolveExecApprovalInitiatingSurfaceState({ - channel: params.turnSourceChannel, - accountId: params.turnSourceAccountId, - }); - const cfg = loadConfig(); - const sentApproverDms = - (initiatingSurface.kind === "disabled" || initiatingSurface.kind === "unsupported") && - hasConfiguredExecApprovalDmRoute(cfg); - const unavailableReason = - preResolvedDecision === null - ? "no-approval-route" - : initiatingSurface.kind === "disabled" - ? "initiating-platform-disabled" - : initiatingSurface.kind === "unsupported" - ? "initiating-platform-unsupported" - : null; void (async () => { const decision = await resolveApprovalDecisionOrUndefined({ diff --git a/src/agents/bash-tools.exec-host-shared.ts b/src/agents/bash-tools.exec-host-shared.ts index c24e0a2f1fa..e62bc8d484a 100644 --- a/src/agents/bash-tools.exec-host-shared.ts +++ b/src/agents/bash-tools.exec-host-shared.ts @@ -1,4 +1,10 @@ import crypto from "node:crypto"; +import { loadConfig } from "../config/config.js"; +import { + hasConfiguredExecApprovalDmRoute, + type ExecApprovalInitiatingSurfaceState, + resolveExecApprovalInitiatingSurfaceState, +} from "../infra/exec-approval-surface.js"; import { maxAsk, minSecurity, @@ -6,7 +12,10 @@ import { type ExecAsk, type ExecSecurity, } from "../infra/exec-approvals.js"; -import { resolveRegisteredExecApprovalDecision } from "./bash-tools.exec-approval-request.js"; +import { + type ExecApprovalRegistration, + resolveRegisteredExecApprovalDecision, +} from "./bash-tools.exec-approval-request.js"; import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js"; type ResolvedExecApprovals = ReturnType; @@ -28,6 +37,22 @@ export type ExecApprovalRequestState = ExecApprovalPendingState & { noticeSeconds: number; }; +export type ExecApprovalUnavailableReason = + | "no-approval-route" + | "initiating-platform-disabled" + | "initiating-platform-unsupported"; + +export type RegisteredExecApprovalRequestContext = { + approvalId: string; + approvalSlug: string; + warningText: string; + expiresAtMs: number; + preResolvedDecision: string | null | undefined; + initiatingSurface: ExecApprovalInitiatingSurfaceState; + sentApproverDms: boolean; + unavailableReason: ExecApprovalUnavailableReason | null; +}; + export function createExecApprovalPendingState(params: { warnings: string[]; timeoutMs: number; @@ -158,3 +183,77 @@ export async function resolveApprovalDecisionOrUndefined(params: { return undefined; } } + +export function resolveExecApprovalUnavailableState(params: { + turnSourceChannel?: string; + turnSourceAccountId?: string; + preResolvedDecision: string | null | undefined; +}): { + initiatingSurface: ExecApprovalInitiatingSurfaceState; + sentApproverDms: boolean; + unavailableReason: ExecApprovalUnavailableReason | null; +} { + const initiatingSurface = resolveExecApprovalInitiatingSurfaceState({ + channel: params.turnSourceChannel, + accountId: params.turnSourceAccountId, + }); + const sentApproverDms = + (initiatingSurface.kind === "disabled" || initiatingSurface.kind === "unsupported") && + hasConfiguredExecApprovalDmRoute(loadConfig()); + const unavailableReason = + params.preResolvedDecision === null + ? "no-approval-route" + : initiatingSurface.kind === "disabled" + ? "initiating-platform-disabled" + : initiatingSurface.kind === "unsupported" + ? "initiating-platform-unsupported" + : null; + return { + initiatingSurface, + sentApproverDms, + unavailableReason, + }; +} + +export async function createAndRegisterDefaultExecApprovalRequest(params: { + warnings: string[]; + approvalRunningNoticeMs: number; + createApprovalSlug: (approvalId: string) => string; + turnSourceChannel?: string; + turnSourceAccountId?: string; + register: (approvalId: string) => Promise; +}): Promise { + const { + approvalId, + approvalSlug, + warningText, + expiresAtMs: defaultExpiresAtMs, + preResolvedDecision: defaultPreResolvedDecision, + } = createDefaultExecApprovalRequestContext({ + warnings: params.warnings, + approvalRunningNoticeMs: params.approvalRunningNoticeMs, + createApprovalSlug: params.createApprovalSlug, + }); + const registration = await params.register(approvalId); + const preResolvedDecision = registration.finalDecision; + const { initiatingSurface, sentApproverDms, unavailableReason } = + resolveExecApprovalUnavailableState({ + turnSourceChannel: params.turnSourceChannel, + turnSourceAccountId: params.turnSourceAccountId, + preResolvedDecision, + }); + + return { + approvalId, + approvalSlug, + warningText, + expiresAtMs: registration.expiresAtMs ?? defaultExpiresAtMs, + preResolvedDecision: + registration.finalDecision === undefined + ? defaultPreResolvedDecision + : registration.finalDecision, + initiatingSurface, + sentApproverDms, + unavailableReason, + }; +}