diff --git a/packages/gateway-protocol/src/schema/approvals.ts b/packages/gateway-protocol/src/schema/approvals.ts index 8b45ae7413de..304852d0998b 100644 --- a/packages/gateway-protocol/src/schema/approvals.ts +++ b/packages/gateway-protocol/src/schema/approvals.ts @@ -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}$" }), diff --git a/src/gateway/exec-approval-manager.ts b/src/gateway/exec-approval-manager.ts index efca40f017cf..6b70b269c50d 100644 --- a/src/gateway/exec-approval-manager.ts +++ b/src/gateway/exec-approval-manager.ts @@ -296,6 +296,7 @@ export class ExecApprovalManager { ? buildApprovalPresentation({ kind: this.approvalKind, request: record.request, + instanceId: record.instanceId, allowedDecisions: allowedDecisions ?? [], }) : null; @@ -398,6 +399,7 @@ export class ExecApprovalManager { const presentation = buildApprovalPresentation({ kind: this.approvalKind, request: record.request, + instanceId: record.instanceId, allowedDecisions: normalizeAllowedDecisions( this.options.resolveAllowedDecisions?.(record.request), ), diff --git a/src/gateway/operator-approval-store.ts b/src/gateway/operator-approval-store.ts index 1a95ff0fbd16..1077c8c9d9a8 100644 --- a/src/gateway/operator-approval-store.ts +++ b/src/gateway/operator-approval-store.ts @@ -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))) diff --git a/src/infra/approval-presentation.ts b/src/infra/approval-presentation.ts index d13fb8025470..8ba9b2805be2 100644 --- a/src/infra/approval-presentation.ts +++ b/src/infra/approval-presentation.ts @@ -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") { diff --git a/src/state/openclaw-state-db-legacy-backfills.ts b/src/state/openclaw-state-db-legacy-backfills.ts index 9250c734b9b0..6ffc3802c112 100644 --- a/src/state/openclaw-state-db-legacy-backfills.ts +++ b/src/state/openclaw-state-db-legacy-backfills.ts @@ -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);