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 type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { loadConfig } from "../config/config.js";
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js"; import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
import {
hasConfiguredExecApprovalDmRoute,
resolveExecApprovalInitiatingSurfaceState,
} from "../infra/exec-approval-surface.js";
import { import {
addAllowlistEntry, addAllowlistEntry,
type ExecAsk, type ExecAsk,
@ -26,7 +21,7 @@ import {
registerExecApprovalRequestForHostOrThrow, registerExecApprovalRequestForHostOrThrow,
} from "./bash-tools.exec-approval-request.js"; } from "./bash-tools.exec-approval-request.js";
import { import {
createDefaultExecApprovalRequestContext, createAndRegisterDefaultExecApprovalRequest,
resolveBaseExecApprovalDecision, resolveBaseExecApprovalDecision,
resolveApprovalDecisionOrUndefined, resolveApprovalDecisionOrUndefined,
resolveExecHostApprovalContext, resolveExecHostApprovalContext,
@ -149,21 +144,19 @@ export async function processGatewayAllowlist(
approvalId, approvalId,
approvalSlug, approvalSlug,
warningText, warningText,
expiresAtMs: defaultExpiresAtMs, expiresAtMs,
preResolvedDecision: defaultPreResolvedDecision, preResolvedDecision,
} = createDefaultExecApprovalRequestContext({ initiatingSurface,
sentApproverDms,
unavailableReason,
} = await createAndRegisterDefaultExecApprovalRequest({
warnings: params.warnings, warnings: params.warnings,
approvalRunningNoticeMs: params.approvalRunningNoticeMs, approvalRunningNoticeMs: params.approvalRunningNoticeMs,
createApprovalSlug, createApprovalSlug,
}); turnSourceChannel: params.turnSourceChannel,
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath; turnSourceAccountId: params.turnSourceAccountId,
const effectiveTimeout = register: async (approvalId) =>
typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec; await registerExecApprovalRequestForHostOrThrow({
let expiresAtMs = defaultExpiresAtMs;
let preResolvedDecision = defaultPreResolvedDecision;
// Register first so the returned approval ID is actionable immediately.
const registration = await registerExecApprovalRequestForHostOrThrow({
approvalId, approvalId,
command: params.command, command: params.command,
workdir: params.workdir, workdir: params.workdir,
@ -174,27 +167,13 @@ export async function processGatewayAllowlist(
agentId: params.agentId, agentId: params.agentId,
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
}), }),
resolvedPath, resolvedPath: allowlistEval.segments[0]?.resolution?.resolvedPath,
...buildExecApprovalTurnSourceContext(params), ...buildExecApprovalTurnSourceContext(params),
}),
}); });
expiresAtMs = registration.expiresAtMs; const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
preResolvedDecision = registration.finalDecision; const effectiveTimeout =
const initiatingSurface = resolveExecApprovalInitiatingSurfaceState({ typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec;
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 () => { void (async () => {
const decision = await resolveApprovalDecisionOrUndefined({ const decision = await resolveApprovalDecisionOrUndefined({

View File

@ -1,11 +1,6 @@
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 { loadConfig } from "../config/config.js";
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js"; import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
import {
hasConfiguredExecApprovalDmRoute,
resolveExecApprovalInitiatingSurfaceState,
} from "../infra/exec-approval-surface.js";
import { import {
type ExecApprovalsFile, type ExecApprovalsFile,
type ExecAsk, type ExecAsk,
@ -25,7 +20,7 @@ import {
registerExecApprovalRequestForHostOrThrow, registerExecApprovalRequestForHostOrThrow,
} from "./bash-tools.exec-approval-request.js"; } from "./bash-tools.exec-approval-request.js";
import { import {
createDefaultExecApprovalRequestContext, createAndRegisterDefaultExecApprovalRequest,
resolveBaseExecApprovalDecision, resolveBaseExecApprovalDecision,
resolveApprovalDecisionOrUndefined, resolveApprovalDecisionOrUndefined,
resolveExecHostApprovalContext, resolveExecHostApprovalContext,
@ -225,18 +220,19 @@ export async function executeNodeHostCommand(
approvalId, approvalId,
approvalSlug, approvalSlug,
warningText, warningText,
expiresAtMs: defaultExpiresAtMs, expiresAtMs,
preResolvedDecision: defaultPreResolvedDecision, preResolvedDecision,
} = createDefaultExecApprovalRequestContext({ initiatingSurface,
sentApproverDms,
unavailableReason,
} = await createAndRegisterDefaultExecApprovalRequest({
warnings: params.warnings, warnings: params.warnings,
approvalRunningNoticeMs: params.approvalRunningNoticeMs, approvalRunningNoticeMs: params.approvalRunningNoticeMs,
createApprovalSlug, createApprovalSlug,
}); turnSourceChannel: params.turnSourceChannel,
let expiresAtMs = defaultExpiresAtMs; turnSourceAccountId: params.turnSourceAccountId,
let preResolvedDecision = defaultPreResolvedDecision; register: async (approvalId) =>
await registerExecApprovalRequestForHostOrThrow({
// Register first so the returned approval ID is actionable immediately.
const registration = await registerExecApprovalRequestForHostOrThrow({
approvalId, approvalId,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
env: nodeEnv, env: nodeEnv,
@ -250,25 +246,8 @@ export async function executeNodeHostCommand(
sessionKey: runSessionKey, sessionKey: runSessionKey,
}), }),
...buildExecApprovalTurnSourceContext(params), ...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 () => { void (async () => {
const decision = await resolveApprovalDecisionOrUndefined({ const decision = await resolveApprovalDecisionOrUndefined({

View File

@ -1,4 +1,10 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { loadConfig } from "../config/config.js";
import {
hasConfiguredExecApprovalDmRoute,
type ExecApprovalInitiatingSurfaceState,
resolveExecApprovalInitiatingSurfaceState,
} from "../infra/exec-approval-surface.js";
import { import {
maxAsk, maxAsk,
minSecurity, minSecurity,
@ -6,7 +12,10 @@ import {
type ExecAsk, type ExecAsk,
type ExecSecurity, type ExecSecurity,
} from "../infra/exec-approvals.js"; } 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"; import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js";
type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>; type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>;
@ -28,6 +37,22 @@ export type ExecApprovalRequestState = ExecApprovalPendingState & {
noticeSeconds: number; 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: { export function createExecApprovalPendingState(params: {
warnings: string[]; warnings: string[];
timeoutMs: number; timeoutMs: number;
@ -158,3 +183,77 @@ export async function resolveApprovalDecisionOrUndefined(params: {
return undefined; 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,
};
}