refactor: share exec approval registration context

This commit is contained in:
Peter Steinberger 2026-03-13 18:22:49 +00:00
parent e003038261
commit 565dc0d17b
3 changed files with 149 additions and 92 deletions

View File

@ -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,21 +144,19 @@ 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,
});
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({
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
register: async (approvalId) =>
await registerExecApprovalRequestForHostOrThrow({
approvalId,
command: params.command,
workdir: params.workdir,
@ -174,27 +167,13 @@ export async function processGatewayAllowlist(
agentId: params.agentId,
sessionKey: params.sessionKey,
}),
resolvedPath,
resolvedPath: allowlistEval.segments[0]?.resolution?.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;
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
const effectiveTimeout =
typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec;
void (async () => {
const decision = await resolveApprovalDecisionOrUndefined({

View File

@ -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,18 +220,19 @@ 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,
});
let expiresAtMs = defaultExpiresAtMs;
let preResolvedDecision = defaultPreResolvedDecision;
// Register first so the returned approval ID is actionable immediately.
const registration = await registerExecApprovalRequestForHostOrThrow({
turnSourceChannel: params.turnSourceChannel,
turnSourceAccountId: params.turnSourceAccountId,
register: async (approvalId) =>
await registerExecApprovalRequestForHostOrThrow({
approvalId,
systemRunPlan: prepared.plan,
env: nodeEnv,
@ -250,25 +246,8 @@ export async function executeNodeHostCommand(
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({

View File

@ -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<typeof resolveExecApprovals>;
@ -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<ExecApprovalRegistration>;
}): Promise<RegisteredExecApprovalRequestContext> {
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,
};
}