Files
openclaw/src/agents/bash-tools.exec-approval-request.ts
pashpashpash 6ce1058296 Wire diagnostics through the core chat command (#72936)
* feat: wire codex diagnostics feedback

* fix: harden codex diagnostics hints

* fix: neutralize codex diagnostics output

* fix: tighten codex diagnostics safeguards

* fix: bound codex diagnostics feedback output

* fix: tighten codex diagnostics throttling

* fix: confirm codex diagnostics uploads

* docs: clarify codex diagnostics add-on

* fix: route diagnostics through core command

* fix: tighten diagnostics authorization

* fix: pin diagnostics to bundled codex command

* fix: limit owner status in plugin commands

* fix: scope diagnostics confirmations

* fix: scope codex diagnostics cooldowns

* fix: harden codex diagnostics ownership scopes

* fix: harden diagnostics command trust and display

* fix: keep diagnostics command trust internal

* fix: clarify diagnostics exec boundary

* fix: consume codex diagnostics confirmations atomically

* test: include codex diagnostics binding metadata

* test: use string codex binding timestamps

* fix: keep reserved command trust host-only

* fix: harden diagnostics trust and resume hints

* wire diagnostics through exec approval

* fix: keep diagnostics tests aligned with bundled root trust

* fix telegram diagnostics owner auth

* route trajectory exports through exec approval

* fix trajectory exec command encoding

* fix telegram group owner auth

* fix export trajectory approval hardening

* fix pairing command owner bootstrap

* fix telegram owner exec approvals

* fix: make diagnostics approval flow pasteable

* fix: route native sensitive command followups

* fix: invoke diagnostics exports with current cli

* fix: refresh exec approval protocol models

* fix: list codex diagnostics from thread bindings

* fix: fold codex diagnostics into exec approval

* fix: preserve diagnostics approval line breaks

* docs: clarify diagnostics codex workflow
2026-04-29 07:40:37 +09:00

249 lines
7.6 KiB
TypeScript

import type { ExecAsk, ExecSecurity, SystemRunApprovalPlan } from "../infra/exec-approvals.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
DEFAULT_APPROVAL_TIMEOUT_MS,
} from "./bash-tools.exec-runtime.js";
import { callGatewayTool } from "./tools/gateway.js";
export type RequestExecApprovalDecisionParams = {
id: string;
command?: string;
commandArgv?: string[];
systemRunPlan?: SystemRunApprovalPlan;
env?: Record<string, string>;
cwd: string | undefined;
nodeId?: string;
host: "gateway" | "node";
security: ExecSecurity;
ask: ExecAsk;
warningText?: string;
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
};
type ExecApprovalRequestToolParams = RequestExecApprovalDecisionParams & {
timeoutMs: number;
twoPhase: true;
};
function buildExecApprovalRequestToolParams(
params: RequestExecApprovalDecisionParams,
): ExecApprovalRequestToolParams {
return {
id: params.id,
...(params.command ? { command: params.command } : {}),
...(params.commandArgv ? { commandArgv: params.commandArgv } : {}),
systemRunPlan: params.systemRunPlan,
env: params.env,
cwd: params.cwd,
nodeId: params.nodeId,
host: params.host,
security: params.security,
ask: params.ask,
warningText: params.warningText,
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,
};
}
type ParsedDecision = { present: boolean; value: string | null };
function parseDecision(value: unknown): ParsedDecision {
if (!value || typeof value !== "object") {
return { present: false, value: null };
}
// Distinguish "field missing" from "field present but null/invalid".
// Registration responses intentionally omit `decision`; decision waits can include it.
if (!Object.hasOwn(value, "decision")) {
return { present: false, value: null };
}
const decision = (value as { decision?: unknown }).decision;
return { present: true, value: typeof decision === "string" ? decision : null };
}
function parseString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
function parseExpiresAtMs(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
export type ExecApprovalRegistration = {
id: string;
expiresAtMs: number;
finalDecision?: string | null;
};
export async function registerExecApprovalRequest(
params: RequestExecApprovalDecisionParams,
): Promise<ExecApprovalRegistration> {
// Two-phase registration is critical: the ID must be registered server-side
// before exec returns `approval-pending`, otherwise `/approve` can race and orphan.
const registrationResult = await callGatewayTool(
"exec.approval.request",
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
buildExecApprovalRequestToolParams(params),
{ expectFinal: false },
);
const decision = parseDecision(registrationResult);
const id = parseString(registrationResult?.id) ?? params.id;
const expiresAtMs =
parseExpiresAtMs(registrationResult?.expiresAtMs) ?? Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
if (decision.present) {
return { id, expiresAtMs, finalDecision: decision.value };
}
return { id, expiresAtMs };
}
export async function waitForExecApprovalDecision(id: string): Promise<string | null> {
try {
const decisionResult = await callGatewayTool<{ decision: string }>(
"exec.approval.waitDecision",
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
{ id },
);
return parseDecision(decisionResult).value;
} catch (err) {
// Timeout/cleanup path: treat missing/expired as no decision so askFallback applies.
const message = normalizeLowercaseStringOrEmpty(String(err));
if (message.includes("approval expired or not found")) {
return null;
}
throw err;
}
}
export async function resolveRegisteredExecApprovalDecision(params: {
approvalId: string;
preResolvedDecision: string | null | undefined;
}): Promise<string | null> {
if (params.preResolvedDecision !== undefined) {
return params.preResolvedDecision ?? null;
}
return await waitForExecApprovalDecision(params.approvalId);
}
export async function requestExecApprovalDecision(
params: RequestExecApprovalDecisionParams,
): Promise<string | null> {
const registration = await registerExecApprovalRequest(params);
if (Object.hasOwn(registration, "finalDecision")) {
return registration.finalDecision ?? null;
}
return await waitForExecApprovalDecision(registration.id);
}
type HostExecApprovalParams = {
approvalId: string;
command?: string;
commandArgv?: string[];
systemRunPlan?: SystemRunApprovalPlan;
env?: Record<string, string>;
workdir: string | undefined;
host: "gateway" | "node";
nodeId?: string;
security: ExecSecurity;
ask: ExecAsk;
warningText?: string;
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
};
type ExecApprovalRequesterContext = {
agentId?: string;
sessionKey?: string;
};
export function buildExecApprovalRequesterContext(params: ExecApprovalRequesterContext): {
agentId?: string;
sessionKey?: string;
} {
return {
agentId: params.agentId,
sessionKey: params.sessionKey,
};
}
type ExecApprovalTurnSourceContext = {
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
};
export function buildExecApprovalTurnSourceContext(
params: ExecApprovalTurnSourceContext,
): ExecApprovalTurnSourceContext {
return {
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
};
}
function buildHostApprovalDecisionParams(
params: HostExecApprovalParams,
): RequestExecApprovalDecisionParams {
return {
id: params.approvalId,
command: params.command,
commandArgv: params.commandArgv,
systemRunPlan: params.systemRunPlan,
env: params.env,
cwd: params.workdir,
nodeId: params.nodeId,
host: params.host,
security: params.security,
ask: params.ask,
warningText: params.warningText,
...buildExecApprovalRequesterContext({
agentId: params.agentId,
sessionKey: params.sessionKey,
}),
resolvedPath: params.resolvedPath,
...buildExecApprovalTurnSourceContext(params),
};
}
export async function requestExecApprovalDecisionForHost(
params: HostExecApprovalParams,
): Promise<string | null> {
return await requestExecApprovalDecision(buildHostApprovalDecisionParams(params));
}
export async function registerExecApprovalRequestForHost(
params: HostExecApprovalParams,
): Promise<ExecApprovalRegistration> {
return await registerExecApprovalRequest(buildHostApprovalDecisionParams(params));
}
export async function registerExecApprovalRequestForHostOrThrow(
params: HostExecApprovalParams,
): Promise<ExecApprovalRegistration> {
try {
return await registerExecApprovalRequestForHost(params);
} catch (err) {
throw new Error(`Exec approval registration failed: ${String(err)}`, { cause: err });
}
}