openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
554 lines (553 loc) • 24.3 kB
JavaScript
import { D as resolveExpiresAtMsFromDurationMs, F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord, i as asOptionalObjectRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { i as sanitizeExecApprovalWarningText, n as sanitizeExecApprovalDisplayText, t as exceedsApprovalTextLimit } from "./exec-approval-text-sanitize-C1BzZQH8.js";
import { t as sanitizeApprovalScope } from "./approval-scope-BL54HpUv.js";
import { p as truncatePluginApprovalDetail } from "./plugin-approvals-BUrr4V_c.js";
import { t as resolveExecApprovalCommandDisplay } from "./exec-approval-command-display-03IqfHT_.js";
import { a as forceDenyOperatorApproval, c as insertOperatorApproval, p as resolveOperatorApproval, r as consumeOperatorApprovalAllowOnce } from "./operator-approval-store-BxnBnd-1.js";
import { n as EXEC_APPROVAL_RESOLVED_ENTRY_GRACE_MS, r as ExecApprovalLifecycle } from "./exec-approval-lifecycle-CRyRxBLA.js";
import { randomUUID } from "node:crypto";
//#region src/infra/approval-presentation.ts
const PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH = 80;
function normalizeDecisionList(decisions) {
const result = [];
for (const decision of decisions) if (!result.includes(decision)) result.push(decision);
if (!result.includes("deny")) result.push("deny");
return result;
}
function sanitizeOptionalSingleLine(value) {
const normalized = normalizeOptionalString(value);
return normalized ? sanitizeExecApprovalDisplayText(normalized) : null;
}
function normalizePluginExternalResolution(value) {
if (!value) return null;
const rawLabel = value.label?.trim();
const label = rawLabel ? sanitizeExecApprovalDisplayText(rawLabel) : "";
if (!label || exceedsApprovalTextLimit(label, PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH)) throw new Error("invalid external approval label");
const decisions = value.decisions ?? ["allow-once"];
if (decisions.length < 1 || decisions.length > 2 || decisions.some((decision) => decision !== "allow-once" && decision !== "allow-always") || new Set(decisions).size !== decisions.length) throw new Error("invalid external approval decisions");
return {
label,
decisions: [...decisions]
};
}
function buildExecApprovalPresentation(params) {
if (!isRecord(params.request)) return null;
const request = params.request;
const { commandText, commandPreview } = resolveExecApprovalCommandDisplay(request);
if (!commandText.trim()) return null;
const warningText = typeof request.warningText === "string" && request.warningText.trim() ? sanitizeExecApprovalWarningText(request.warningText) : null;
const scope = request.scope ? sanitizeApprovalScope(request.scope) : null;
return {
kind: "exec",
commandText,
commandPreview,
warningText,
host: sanitizeOptionalSingleLine(request.host),
nodeId: sanitizeOptionalSingleLine(request.nodeId),
agentId: sanitizeOptionalSingleLine(request.agentId),
...scope ? { scope } : {},
allowedDecisions: normalizeDecisionList(params.allowedDecisions)
};
}
function buildPluginApprovalPresentation(params) {
if (!isRecord(params.request)) return null;
const request = params.request;
const rawTitle = normalizeOptionalString(request.title);
const rawDescription = normalizeOptionalString(request.description);
if (!rawTitle || !rawDescription) return null;
const title = sanitizeExecApprovalDisplayText(rawTitle);
const description = sanitizeExecApprovalWarningText(rawDescription);
if (exceedsApprovalTextLimit(title, 80) || exceedsApprovalTextLimit(description, 512)) return null;
const severity = request.severity === "info" || request.severity === "warning" || request.severity === "critical" ? request.severity : "warning";
const rawDetail = normalizeOptionalString(request.detail);
const detail = rawDetail ? truncatePluginApprovalDetail(sanitizeExecApprovalWarningText(rawDetail)) : null;
const scope = request.scope ? sanitizeApprovalScope(request.scope) : null;
let externalResolution;
try {
externalResolution = normalizePluginExternalResolution(request.externalResolution);
} catch {
return null;
}
return {
kind: "plugin",
title,
description,
...detail ? { detail } : {},
severity,
pluginId: sanitizeOptionalSingleLine(request.pluginId),
toolName: sanitizeOptionalSingleLine(request.toolName),
agentId: sanitizeOptionalSingleLine(request.agentId),
...scope ? { scope } : {},
allowedDecisions: normalizeDecisionList(params.allowedDecisions),
...externalResolution ? { externalResolution: {
label: externalResolution.label,
decisions: [...externalResolution.decisions ?? ["allow-once"]]
} } : {}
};
}
function buildSystemAgentApprovalPresentation(params) {
if (!isRecord(params.request)) return null;
const request = params.request;
const title = normalizeOptionalString(request.title);
const description = normalizeOptionalString(request.description);
if (!title || !description || !/^[a-f0-9]{64}$/.test(request.proposalHash)) return null;
return {
kind: "system-agent",
title: truncateUtf16Safe(sanitizeExecApprovalDisplayText(title), 80),
description: truncateUtf16Safe(sanitizeExecApprovalWarningText(description), 512),
proposalHash: request.proposalHash,
agentId: sanitizeOptionalSingleLine(request.agentId),
allowedDecisions: ["allow-once", "deny"]
};
}
/** Returns the safe cross-surface presentation, or null when no prompt can be rendered. */
function buildApprovalPresentation(params) {
if (params.kind === "exec") return buildExecApprovalPresentation(params);
return params.kind === "plugin" ? buildPluginApprovalPresentation(params) : buildSystemAgentApprovalPresentation(params);
}
//#endregion
//#region src/gateway/exec-approval-manager.ts
const EXPLICIT_APPROVAL_ID_INVALID_CHAR_PATTERN = /[^A-Za-z0-9._:-]/;
/** Typed creation failure for an explicit approval id outside the shared safe format. */
var InvalidApprovalIdError = class extends Error {
constructor() {
super("approval id must be 1-128 characters using only letters, numbers, '.', '_', ':', or '-', and cannot be '.' or '..'");
this.code = "EXEC_APPROVAL_ID_INVALID";
this.reason = "INVALID_APPROVAL_ID";
this.name = "InvalidApprovalIdError";
}
};
function readRequestString(request, key) {
const value = asOptionalObjectRecord(request)?.[key];
return typeof value === "string" && value.trim() ? value.trim() : null;
}
/** Approval creation and persistence precede every local wait or delivery handoff. */
var ExecApprovalManager = class extends ExecApprovalLifecycle {
constructor(options) {
super();
this.options = options;
}
get approvalKind() {
return this.options.approvalKind ?? "exec";
}
get runtimeEpoch() {
return this.options.persistence.runtimeEpoch;
}
resolveApprovalSource(request) {
return {
agentId: readRequestString(request, "agentId"),
sessionKey: readRequestString(request, "sessionKey"),
sessionId: readRequestString(request, "sessionId"),
runId: readRequestString(request, "runId"),
toolCallId: readRequestString(request, "toolCallId"),
toolName: readRequestString(request, "toolName")
};
}
allowedDecisionsForRequest(request) {
const decisions = this.options.resolveAllowedDecisions?.(request);
const normalized = [];
for (const decision of decisions ?? [
"allow-once",
"allow-always",
"deny"
]) if ((decision === "allow-once" || decision === "allow-always" || decision === "deny") && !normalized.includes(decision)) normalized.push(decision);
if (!normalized.includes("deny")) normalized.push("deny");
return normalized;
}
create(request, timeoutMs, id) {
this.assertNotRetired();
const now = Date.now();
const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, 1);
const expiresAtMs = resolveExpiresAtMsFromDurationMs(resolvedTimeoutMs, { nowMs: now });
if (expiresAtMs === void 0) throw new Error("approval expiry is unavailable");
const hasExplicitId = id !== null && id !== void 0 && id.length > 0;
if (hasExplicitId && (id.length > 128 || id === "." || id === ".." || EXPLICIT_APPROVAL_ID_INVALID_CHAR_PATTERN.test(id))) throw new InvalidApprovalIdError();
return {
id: hasExplicitId ? id : randomUUID(),
request,
createdAtMs: now,
expiresAtMs
};
}
/** Synchronously persists/registers the request before returning its authority promise. */
register(record, _timeoutMs) {
this.assertNotRetired();
if (record.agentRuntimeDelegatedAuthority && this.options.validateAgentRuntimeDelegatedAuthority?.(record.agentRuntimeDelegatedAuthority) !== true) throw new Error("agent runtime approval authority is no longer active");
if (record.approvalAuthority && record.approvalAuthority() === false) throw new Error("approval authority is no longer active");
const persistence = this.options.persistence;
const presentation = buildApprovalPresentation({
kind: this.approvalKind,
request: record.request,
allowedDecisions: this.allowedDecisionsForRequest(record.request)
});
if (!presentation) throw new Error("approval cannot be persisted without a valid reviewer presentation");
const existing = this.pending.get(record.id);
if (existing) {
if (existing.record.resolvedAtMs === void 0) return existing.promise;
throw new Error(`approval id '${record.id}' already resolved`);
}
const source = this.resolveApprovalSource(record.request);
let audienceSessionKeys = [];
if (source.sessionKey) audienceSessionKeys = this.options.resolveAudienceSessionKeys?.(source.sessionKey, source.agentId) ?? [source.sessionKey];
const inserted = insertOperatorApproval({
approval: {
id: record.id,
kind: this.approvalKind,
presentation,
requester: {
deviceId: record.requestedByDeviceId,
clientId: record.requestedByClientId,
deviceTokenAuth: record.requestedByDeviceTokenAuth === true
},
reviewerDeviceIds: record.approvalReviewerDeviceIds,
source,
audienceSessionKeys,
runtimeEpoch: persistence.runtimeEpoch,
createdAtMs: record.createdAtMs,
expiresAtMs: record.expiresAtMs,
...record.executionIdentityToken ? { executionIdentityToken: record.executionIdentityToken } : {}
},
databaseOptions: persistence.databaseOptions
});
if (inserted.outcome === "conflict") throw new Error(`approval id '${record.id}' conflicts with persisted state`);
const promise = this.registerEntry(record);
for (const signal of record.approvalSignals ?? []) {
if (signal.aborted) {
this.forceDenyIfRuntimeAuthorityClosed(record.id);
continue;
}
signal.addEventListener("abort", () => {
const closed = this.forceDenyIfRuntimeAuthorityClosed(record.id);
if (closed?.outcome === "denied" && closed.liveRecord) this.options.onExpired?.(closed.record, closed.liveRecord);
}, { once: true });
}
if (inserted.outcome === "inserted") this.emitLifecycle({
phase: "pending",
record: inserted.record
});
return promise;
}
isRuntimeAuthorityActive(record) {
const delegated = record.agentRuntimeDelegatedAuthority;
if (delegated && this.options.validateAgentRuntimeDelegatedAuthority?.(delegated) !== true) return false;
if (record.approvalSignals?.some((signal) => signal.aborted)) return false;
try {
return record.approvalAuthority?.() !== false;
} catch {
return false;
}
}
emitLifecycle(event) {
try {
this.options.onLifecycle?.(event);
} catch {}
}
/** Persist the first verdict, then release the process-local waiter. */
resolveDetailed(recordId, decision, resolver, localResolvedBy = null, localResolutionSource = "operator", options = {}) {
if (this.retired) return { outcome: "not-found" };
if (decision !== "deny") {
const closed = this.forceDenyIfRuntimeAuthorityClosed(recordId);
if (closed) {
if (closed.outcome === "not-found" || closed.outcome === "corrupt") return closed;
return {
outcome: "already-resolved",
retry: "conflict",
record: closed.record,
...closed.liveRecord ? { liveRecord: closed.liveRecord } : {}
};
}
}
const persistence = this.options.persistence;
const localEntry = this.pending.get(recordId);
if (localEntry?.record.terminalReason === "storage-corrupt") {
const repaired = this.persistStorageCorruptDeny(recordId);
if (repaired.outcome === "expired") return repaired;
if (repaired.outcome === "not-found" || repaired.outcome === "corrupt") return repaired;
if (repaired.outcome === "denied" && decision === "deny") return {
outcome: "resolved",
record: repaired.record,
...repaired.liveRecord ? { liveRecord: repaired.liveRecord } : {}
};
return {
outcome: "already-resolved",
retry: repaired.record.decision === decision ? "same" : "conflict",
record: repaired.record,
...repaired.liveRecord ? { liveRecord: repaired.liveRecord } : {}
};
}
if (decision !== "deny" && !localEntry) return { outcome: "not-found" };
let standingGrantSpec = decision === "allow-always" && localEntry ? this.options.resolveStandingGrantMint?.(localEntry.record.request) ?? void 0 : void 0;
if (standingGrantSpec?.kind === "mcp-tool" && localEntry?.record.mcpToolApprovalActive?.() !== true) standingGrantSpec = void 0;
const standingGrant = standingGrantSpec ? {
...standingGrantSpec,
expiresAtMs: options.grantExpiresAtMs !== void 0 ? options.grantExpiresAtMs : this.options.resolveStandingGrantExpiresAtMs?.(Date.now()) ?? null
} : void 0;
let result;
try {
result = resolveOperatorApproval({
id: recordId,
decision,
resolver,
expectedKind: this.approvalKind,
runtimeEpoch: persistence.runtimeEpoch,
databaseOptions: persistence.databaseOptions,
...standingGrant?.kind === "cron" ? { standingGrant } : {},
...standingGrant?.kind === "mcp-tool" ? { mcpToolGrant: standingGrant } : {}
});
} catch (error) {
this.settleLocalStorageFailure(recordId);
throw error;
}
if (result.outcome === "resolved" && standingGrant?.kind === "placement") this.options.retainPlacementStandingGrant?.({
...standingGrant,
approvalId: recordId,
nowMs: result.record.resolvedAtMs ?? Date.now()
});
if (result.outcome === "resolved" || result.outcome === "expired" || result.outcome === "already-resolved") this.settleLocalFromStore(result.record, void 0, localResolvedBy, result.outcome === "resolved" ? localResolutionSource : "operator");
else if (result.outcome === "not-found" || result.outcome === "corrupt") this.settleLocalStorageFailure(recordId);
return "record" in result && localEntry ? {
...result,
liveRecord: localEntry.record
} : result;
}
/** Persist a fail-closed terminal state, then release the local waiter. */
forceDenyDetailed(recordId, reason, resolver, status = "denied", localDecision, requireDue = false, localResolvedBy = null) {
if (this.retired) return { outcome: "not-found" };
const persistence = this.options.persistence;
const localRecord = this.pending.get(recordId)?.record;
if (localRecord?.terminalReason === "storage-corrupt") return this.persistStorageCorruptDeny(recordId);
let result;
try {
result = forceDenyOperatorApproval({
id: recordId,
status,
requireDue,
reason,
resolver,
expectedKind: this.approvalKind,
runtimeEpoch: persistence.runtimeEpoch,
databaseOptions: persistence.databaseOptions
});
} catch (error) {
this.settleLocalStorageFailure(recordId);
throw error;
}
if (result.outcome === "denied") this.settleLocalFromStore(result.record, localDecision, localResolvedBy);
else if (result.outcome === "expired" || result.outcome === "already-terminal") this.settleLocalFromStore(result.record, void 0, localResolvedBy);
else if (result.outcome === "not-found" || result.outcome === "corrupt") this.settleLocalStorageFailure(recordId);
return "record" in result && localRecord ? {
...result,
liveRecord: localRecord
} : result;
}
settleLocalFromStore(record, localDecision, localResolvedBy = null, localResolutionSource = "operator") {
const persistence = this.options.persistence;
const liveRecord = this.pending.get(record.id)?.record;
if (record.kind !== this.approvalKind || record.runtimeEpoch !== persistence.runtimeEpoch || record.status === "pending" || record.resolvedAtMs === null) return false;
const decision = localDecision === void 0 ? record.status === "allowed" || record.status === "denied" ? record.decision : null : localDecision;
const settled = this.settleLocalEntry({
recordId: record.id,
decision,
resolvedAtMs: record.resolvedAtMs,
resolvedBy: localResolvedBy,
resolverKind: record.resolver?.kind ?? null,
status: record.status,
terminalReason: record.terminalReason,
consumedAtMs: record.consumedAtMs,
consumedBy: record.consumedBy,
resolutionSource: localResolutionSource
});
if (settled) {
this.emitLifecycle({
phase: "terminal",
record
});
if (record.status === "expired" && liveRecord) try {
this.options.onExpired?.(record, liveRecord);
} catch (error) {
this.reportError(error, {
approvalId: record.id,
operation: "expire"
});
}
}
return settled;
}
/** Settle one durable terminal transition and report whether this manager published it. */
reconcileDurableTerminal(record) {
return this.settleLocalFromStore(record);
}
/** Reconciles durable truth with an existing waiter without rehydrating its request. */
reconcileDurableLookup(lookup, localResolvedBy = null) {
if (this.retired) return null;
const recordId = lookup.outcome === "found" ? lookup.record.id : lookup.id;
const entry = this.pending.get(recordId);
if (lookup.outcome !== "found") {
if (entry) this.settleLocalStorageFailure(recordId);
return null;
}
const persistence = this.options.persistence;
if (!entry || lookup.record.kind !== this.approvalKind || lookup.record.runtimeEpoch !== persistence.runtimeEpoch) return lookup.record;
if (lookup.record.status === "pending" && entry.record.terminalReason === "storage-corrupt") {
const repaired = this.persistStorageCorruptDeny(recordId);
return "record" in repaired ? repaired.record : null;
}
if (lookup.record.status !== "pending") this.settleLocalFromStore(lookup.record, void 0, localResolvedBy);
return lookup.record;
}
settleLocalStorageFailure(recordId) {
this.settleLocalEntry({
recordId,
decision: "deny",
resolvedAtMs: Date.now(),
resolvedBy: "storage-error",
resolverKind: "system",
status: "denied",
terminalReason: "storage-corrupt",
retainForManagerLifetime: true
});
}
persistStorageCorruptDeny(recordId) {
const localEntry = this.pending.get(recordId);
const persistence = this.options.persistence;
if (!localEntry) return { outcome: "not-found" };
const result = forceDenyOperatorApproval({
id: recordId,
status: "denied",
reason: "storage-corrupt",
resolver: {
kind: "system",
id: "storage-error"
},
expectedKind: this.approvalKind,
runtimeEpoch: persistence.runtimeEpoch,
databaseOptions: persistence.databaseOptions
});
if (result.outcome === "denied" || result.outcome === "expired") this.emitLifecycle({
phase: "terminal",
record: result.record
});
return "record" in result ? {
...result,
liveRecord: localEntry.record
} : result;
}
reportError(error, context) {
const onError = this.options.onError;
if (!onError) return;
try {
onError(error instanceof Error ? error : new Error(String(error)), {
...context,
approvalKind: this.approvalKind
});
} catch {}
}
expireDue(recordId) {
if (this.retired) return false;
const entry = this.pending.get(recordId);
if (!entry || entry.record.resolvedAtMs !== void 0) return false;
const result = this.forceDenyDetailed(recordId, "timeout", {
kind: "system",
id: null
}, "expired", void 0, true);
if (result.outcome === "not-due") {
this.scheduleExpiryTimer(entry);
return false;
}
return result.outcome === "denied" || result.outcome === "expired";
}
resolve(recordId, decision, resolvedBy, options = {}) {
return this.resolveDetailed(recordId, decision, {
kind: "runtime",
id: resolvedBy ?? null
}, resolvedBy ?? null, "operator", options).outcome === "resolved";
}
/**
* Trusted auto-review resolution (identity-matched approval runtime).
* Always allow-once; system.run replay validation treats the resulting
* record more strictly than an operator decision (see #103515).
*/
resolveAutoReview(recordId, resolvedBy) {
return this.resolveDetailed(recordId, "allow-once", {
kind: "runtime",
id: resolvedBy ?? null
}, resolvedBy ?? null, "auto-review").outcome === "resolved";
}
/**
* One-shot ask-fallback re-admission for a timed-out approval. This is
* pre-gate policy on the process-local record only: the durable row stays
* `expired` and no execution authority is minted here. The shipped askFallback
* policy (docs/tools/exec-approvals.md) still applies; system.run replay
* uses this flag to keep re-admission single-use.
*/
consumeAskFallback(recordId) {
const entry = this.pending.get(recordId);
if (!entry) return false;
const record = entry.record;
if (record.resolvedAtMs === void 0 || record.decision !== void 0 || record.consumedDecision !== void 0 || record.askFallbackConsumed === true || record.status !== "expired" && record.terminalReason !== "no-route") return false;
record.askFallbackConsumed = true;
return true;
}
expire(recordId, resolvedBy) {
const noRoute = resolvedBy === "no-approval-route";
return this.forceDenyDetailed(recordId, noRoute ? "no-route" : "timeout", {
kind: "system",
id: resolvedBy ?? null
}, noRoute ? "denied" : "expired", noRoute ? null : void 0, false, resolvedBy ?? null).outcome === "denied";
}
consumeAllowOnce(recordId, consumerId = recordId) {
if (!this.canUseRetainedBinding() || this.forceDenyIfRuntimeAuthorityClosed(recordId)) return false;
const entry = this.pending.get(recordId);
if (!entry) return false;
const nowMs = Date.now();
const resolvedAtMs = entry.record.resolvedAtMs;
const graceAnchorMs = this.resolvedGraceAnchorMs(entry, nowMs);
if (resolvedAtMs === void 0 || graceAnchorMs === null || nowMs - graceAnchorMs >= 15e3 || entry.record.decision !== "allow-once" || entry.record.consumedDecision) return false;
const persistence = this.options.persistence;
const result = consumeOperatorApprovalAllowOnce({
id: recordId,
consumerId,
expectedKind: this.approvalKind,
runtimeEpoch: persistence.runtimeEpoch,
redemptionWindowMs: EXEC_APPROVAL_RESOLVED_ENTRY_GRACE_MS + Math.max(0, graceAnchorMs - resolvedAtMs),
databaseOptions: persistence.databaseOptions
});
if (result.outcome !== "consumed") return false;
entry.record.consumedDecision = "allow-once";
entry.record.consumedAtMs = result.record.consumedAtMs;
entry.record.consumedBy = result.record.consumedBy;
return true;
}
/** Observes a registered decision; Gateway closure rejects the wait, not the approval. */
awaitDecision(recordId) {
this.assertNotRetired();
this.forceDenyIfRuntimeAuthorityClosed(recordId);
if (!this.getSnapshot(recordId)) return null;
const entry = this.pending.get(recordId);
return entry ? this.observeEntry(entry, entry.promise) : null;
}
/** Projects an allowed decision only while its exact runtime authority is live. */
projectDecisionIfActive(recordId, decision) {
if (decision !== "allow-once" && decision !== "allow-always") return decision;
if (!this.canUseRetainedBinding()) return null;
const record = this.pending.get(recordId)?.record;
if (!record) return null;
if (this.isRuntimeAuthorityActive(record)) return decision;
this.forceDenyIfRuntimeAuthorityClosed(recordId);
return null;
}
/** Atomically closes a live approval whose exact runtime owner is gone. */
forceDenyIfRuntimeAuthorityClosed(recordId) {
const record = this.pending.get(recordId)?.record;
if (!record || this.isRuntimeAuthorityActive(record)) return null;
return this.forceDenyDetailed(recordId, "run-aborted", {
kind: "system",
id: null
}, "cancelled");
}
};
//#endregion
export { InvalidApprovalIdError as n, ExecApprovalManager as t };