fix(approvals): preserve lifecycle refs on reopen

This commit is contained in:
Dallin Romney
2026-08-21 01:44:51 -07:00
parent abbb43d5cc
commit d03dc4ceac
5 changed files with 28 additions and 2 deletions
@@ -90,6 +90,7 @@ const SystemAgentApprovalAllowedDecisionsSchema = Type.Tuple([
export const ExecApprovalPresentationSchema = Type.Object(
{
kind: Type.Literal("exec"),
instanceId: Type.Optional(NonEmptyString),
commandText: NonEmptyString,
commandPreview: Type.Optional(Type.Union([Type.String(), Type.Null()])),
warningText: Type.Optional(Type.Union([Type.String(), Type.Null()])),
@@ -108,6 +109,7 @@ export const ExecApprovalPresentationSchema = Type.Object(
/** Plugin-supplied reviewer text safe to persist and render across surfaces. */
export const PluginApprovalPresentationSchema = closedObject({
kind: Type.Literal("plugin"),
instanceId: Type.Optional(NonEmptyString),
title: Type.String({ minLength: 1, maxLength: 80 }),
description: Type.String({ minLength: 1, maxLength: 512 }),
detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16_384 })),
@@ -121,6 +123,7 @@ export const PluginApprovalPresentationSchema = closedObject({
/** Reviewer-safe OpenClaw system change. Exact operation stays host-local. */
export const SystemAgentApprovalPresentationSchema = closedObject({
kind: Type.Literal("system-agent"),
instanceId: Type.Optional(NonEmptyString),
title: Type.String({ minLength: 1, maxLength: 80 }),
description: Type.String({ minLength: 1, maxLength: 512 }),
proposalHash: Type.String({ pattern: "^[a-f0-9]{64}$" }),
+2
View File
@@ -296,6 +296,7 @@ export class ExecApprovalManager<TPayload = ExecApprovalRequestPayload> {
? buildApprovalPresentation({
kind: this.approvalKind,
request: record.request,
instanceId: record.instanceId,
allowedDecisions: allowedDecisions ?? [],
})
: null;
@@ -398,6 +399,7 @@ export class ExecApprovalManager<TPayload = ExecApprovalRequestPayload> {
const presentation = buildApprovalPresentation({
kind: this.approvalKind,
request: record.request,
instanceId: record.instanceId,
allowedDecisions: normalizeAllowedDecisions(
this.options.resolveAllowedDecisions?.(record.request),
),
+5 -1
View File
@@ -444,7 +444,11 @@ function decodeOperatorApprovalRow(row: OperatorApprovalRow): OperatorApprovalRe
if (
presentation.kind !== kind ||
row.resolution_ref !==
buildApprovalResolutionRef({ approvalId: row.approval_id, approvalKind: kind }) ||
buildApprovalResolutionRef({
approvalId: row.approval_id,
approvalKind: kind,
instanceId: presentation.instanceId,
}) ||
!hasValidLifecycleTuple({ row, status, decision, terminalReason, resolverKind }) ||
(status === "allowed" &&
(!decision || !Array.prototype.includes.call(presentation.allowedDecisions, decision)))
+7
View File
@@ -45,6 +45,7 @@ function sanitizeOptionalSingleLine(value: unknown): string | null {
function buildExecApprovalPresentation(params: {
request: unknown;
instanceId?: string;
allowedDecisions: readonly ApprovalDecision[];
}): ApprovalPresentation | null {
if (!isRecord(params.request)) {
@@ -61,6 +62,7 @@ function buildExecApprovalPresentation(params: {
: null;
return {
kind: "exec",
...(params.instanceId ? { instanceId: params.instanceId } : {}),
commandText,
commandPreview,
warningText,
@@ -73,6 +75,7 @@ function buildExecApprovalPresentation(params: {
function buildPluginApprovalPresentation(params: {
request: unknown;
instanceId?: string;
allowedDecisions: readonly ApprovalDecision[];
}): ApprovalPresentation | null {
if (!isRecord(params.request)) {
@@ -104,6 +107,7 @@ function buildPluginApprovalPresentation(params: {
: null;
return {
kind: "plugin",
...(params.instanceId ? { instanceId: params.instanceId } : {}),
title,
description,
...(detail ? { detail } : {}),
@@ -117,6 +121,7 @@ function buildPluginApprovalPresentation(params: {
function buildSystemAgentApprovalPresentation(params: {
request: unknown;
instanceId?: string;
allowedDecisions: readonly ApprovalDecision[];
}): ApprovalPresentation | null {
if (!isRecord(params.request)) {
@@ -130,6 +135,7 @@ function buildSystemAgentApprovalPresentation(params: {
}
return {
kind: "system-agent",
...(params.instanceId ? { instanceId: params.instanceId } : {}),
title: truncateUtf16Safe(sanitizeExecApprovalDisplayText(title), 80),
description: truncateUtf16Safe(sanitizeExecApprovalWarningText(description), 512),
proposalHash: request.proposalHash,
@@ -142,6 +148,7 @@ function buildSystemAgentApprovalPresentation(params: {
export function buildApprovalPresentation(params: {
kind: ApprovalKind;
request: unknown;
instanceId?: string;
allowedDecisions: readonly ApprovalDecision[];
}): ApprovalPresentation | null {
if (params.kind === "exec") {
@@ -17,11 +17,14 @@ export function ensureOperatorApprovalResolutionRefs(db: DatabaseSync): void {
runSqliteImmediateTransactionSync(db, () => {
ensureColumn(db, "operator_approvals", "resolution_ref TEXT");
const rows = db
.prepare("SELECT approval_id, kind, resolution_ref FROM operator_approvals")
.prepare(
"SELECT approval_id, kind, resolution_ref, presentation_json FROM operator_approvals",
)
.all() as Array<{
approval_id?: unknown;
kind?: unknown;
resolution_ref?: unknown;
presentation_json?: unknown;
}>;
const update = db.prepare(
"UPDATE operator_approvals SET resolution_ref = ? WHERE approval_id = ?",
@@ -33,9 +36,16 @@ export function ensureOperatorApprovalResolutionRefs(db: DatabaseSync): void {
) {
throw new Error("operator approval row cannot be assigned a transport reference");
}
const presentation =
typeof row.presentation_json === "string"
? safeParseJsonRecord(row.presentation_json)
: undefined;
const instanceId =
typeof presentation?.instanceId === "string" ? presentation.instanceId : undefined;
const resolutionRef = buildApprovalResolutionRef({
approvalId: row.approval_id,
approvalKind: row.kind,
instanceId,
});
if (row.resolution_ref !== resolutionRef) {
update.run(resolutionRef, row.approval_id);