openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
318 lines (317 loc) • 13.5 kB
JavaScript
import { s as normalizeNullableString } from "./string-coerce-1VuiLaaR.mjs";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-K7ftY-g4.mjs";
import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs";
import { a as mintMcpToolGrantLocked } from "./exec-approvals-sqlite-BRXIFW7Z.mjs";
import { r as mintCronStandingGrantLocked } from "./operator-approval-standing-grants-pk0FVWgH.mjs";
import { _ as requireDecodedRecord, a as clampAuditTimestamp, c as denyCorruptPendingRow, g as requireApprovalId, l as expirePendingRow, m as matchesExpectedApprovalOwner, p as isValidTimestamp, s as decodeOperatorApprovalRow, v as requireString, y as selectOperatorApprovalRow } from "./operator-approval-store.rows-CUyuQiIg.mjs";
//#region src/gateway/operator-approval-store.transitions.ts
function resolveOperatorApprovalInDatabase(params) {
const id = requireApprovalId(params.id);
const resolverId = normalizeNullableString(params.resolver.id);
const runtimeEpoch = params.runtimeEpoch === void 0 ? void 0 : requireString(params.runtimeEpoch, "operator approval runtime epoch");
return runOpenClawStateWriteTransaction((database) => {
const nowMs = params.nowMs ?? Date.now();
let row = selectOperatorApprovalRow(database, id);
if (!row) return { outcome: "not-found" };
if (!matchesExpectedApprovalOwner({
row,
expectedKind: params.expectedKind,
runtimeEpoch
})) return { outcome: "not-found" };
let record = decodeOperatorApprovalRow(row);
if (!record) {
denyCorruptPendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
return { outcome: "corrupt" };
}
if (record.status !== "pending") return {
outcome: "already-resolved",
retry: record.decision === params.decision ? "same" : "conflict",
record
};
if (record.expiresAtMs <= nowMs) {
row = expirePendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
if (!row) return { outcome: "not-found" };
record = requireDecodedRecord(row);
return {
outcome: "expired",
record
};
}
if (!Array.prototype.includes.call(record.presentation.allowedDecisions, params.decision)) return {
outcome: "decision-not-allowed",
record
};
const auditTimestampMs = clampAuditTimestamp(nowMs, record.createdAtMs);
let resolveQuery = getNodeSqliteKysely(database.db).updateTable("operator_approvals").set({
status: params.decision === "deny" ? "denied" : "allowed",
decision: params.decision,
terminal_reason: "user",
resolved_at_ms: auditTimestampMs,
resolver_kind: params.resolver.kind,
resolver_id: resolverId,
updated_at_ms: auditTimestampMs
}).where("approval_id", "=", id).where("status", "=", "pending").where("expires_at_ms", ">", nowMs);
if (params.expectedKind !== void 0) resolveQuery = resolveQuery.where("kind", "=", params.expectedKind);
if (runtimeEpoch !== void 0) resolveQuery = resolveQuery.where("runtime_epoch", "=", runtimeEpoch);
const result = executeSqliteQuerySync(database.db, resolveQuery);
row = selectOperatorApprovalRow(database, id);
if (!row) return { outcome: "not-found" };
record = requireDecodedRecord(row);
if (result.numAffectedRows === 1n) {
if (params.decision === "allow-always" && params.mcpToolGrant && record.kind === "plugin" && record.source.agentId === params.mcpToolGrant.agentId) mintMcpToolGrantLocked(database.db, params.mcpToolGrant, auditTimestampMs);
if (params.decision === "allow-always" && params.standingGrant) mintCronStandingGrantLocked(database, {
...params.standingGrant,
approvalId: id,
nowMs: auditTimestampMs
});
return {
outcome: "resolved",
record
};
}
if (record.status === "pending" && record.expiresAtMs <= nowMs) {
const expiredRow = expirePendingRow({
database,
id,
nowMs,
createdAtMs: record.createdAtMs
});
if (!expiredRow) return { outcome: "not-found" };
return {
outcome: "expired",
record: requireDecodedRecord(expiredRow)
};
}
return {
outcome: "already-resolved",
retry: record.decision === params.decision ? "same" : "conflict",
record
};
}, params.databaseOptions);
}
function forceDenyOperatorApprovalInDatabase(params) {
const id = requireApprovalId(params.id);
const runtimeEpoch = params.runtimeEpoch === void 0 ? void 0 : requireString(params.runtimeEpoch, "operator approval runtime epoch");
return runOpenClawStateWriteTransaction((database) => {
const nowMs = params.nowMs ?? Date.now();
const row = selectOperatorApprovalRow(database, id);
if (!row) return { outcome: "not-found" };
if (!matchesExpectedApprovalOwner({
row,
expectedKind: params.expectedKind,
runtimeEpoch
})) return { outcome: "not-found" };
if (row.status === "pending" && row.expires_at_ms <= nowMs) {
const expiredRow = expirePendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
if (!expiredRow) return { outcome: "not-found" };
const expiredRecord = decodeOperatorApprovalRow(expiredRow);
return expiredRecord ? {
outcome: "expired",
record: expiredRecord
} : { outcome: "corrupt" };
}
const record = decodeOperatorApprovalRow(row);
if (!record) {
denyCorruptPendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
return { outcome: "corrupt" };
}
if (record.status !== "pending") return {
outcome: "already-terminal",
record
};
if (params.status === "expired" && params.requireDue === true && record.expiresAtMs > nowMs) return {
outcome: "not-due",
record
};
const auditTimestampMs = clampAuditTimestamp(nowMs, record.createdAtMs);
let denyQuery = getNodeSqliteKysely(database.db).updateTable("operator_approvals").set({
status: params.status ?? "denied",
decision: "deny",
terminal_reason: params.reason,
resolved_at_ms: auditTimestampMs,
resolver_kind: params.resolver.kind,
resolver_id: normalizeNullableString(params.resolver.id),
updated_at_ms: auditTimestampMs
}).where("approval_id", "=", id).where("status", "=", "pending");
if (params.expectedKind !== void 0) denyQuery = denyQuery.where("kind", "=", params.expectedKind);
if (runtimeEpoch !== void 0) denyQuery = denyQuery.where("runtime_epoch", "=", runtimeEpoch);
executeSqliteQuerySync(database.db, denyQuery);
const terminalRow = selectOperatorApprovalRow(database, id);
if (!terminalRow) return { outcome: "not-found" };
return {
outcome: "denied",
record: requireDecodedRecord(terminalRow)
};
}, params.databaseOptions);
}
function expireDueOperatorApprovalsInDatabase(params) {
return runOpenClawStateWriteTransaction((database) => {
const nowMs = params.nowMs ?? Date.now();
const stateDb = getNodeSqliteKysely(database.db);
const dueRows = executeSqliteQuerySync(database.db, stateDb.selectFrom("operator_approvals").selectAll().where("status", "=", "pending").where("expires_at_ms", "<=", nowMs).orderBy("expires_at_ms", "asc").orderBy("approval_id", "asc")).rows;
if (dueRows.length === 0) return {
affected: 0,
records: []
};
const terminalFields = {
status: "expired",
decision: "deny",
terminal_reason: "timeout",
resolved_at_ms: nowMs,
resolver_kind: "system",
resolver_id: null,
updated_at_ms: nowMs
};
const result = executeSqliteQuerySync(database.db, stateDb.updateTable("operator_approvals").set(terminalFields).where("status", "=", "pending").where("expires_at_ms", "<=", nowMs));
return {
affected: Number(result.numAffectedRows ?? 0n),
records: dueRows.map((row) => decodeOperatorApprovalRow({
...row,
...terminalFields
})).filter((record) => record !== null)
};
}, params.databaseOptions);
}
function closeOrphanedOperatorApprovals(params) {
const runtimeEpoch = requireString(params.runtimeEpoch, "operator approval runtime epoch");
return runOpenClawStateWriteTransaction((database) => {
const nowMs = params.nowMs ?? Date.now();
const stateDb = getNodeSqliteKysely(database.db);
const orphanRows = executeSqliteQuerySync(database.db, stateDb.selectFrom("operator_approvals").selectAll().where("status", "=", "pending").where("runtime_epoch", "!=", runtimeEpoch).orderBy("created_at_ms", "asc").orderBy("approval_id", "asc")).rows;
if (orphanRows.length === 0) return {
affected: 0,
records: []
};
let affected = 0;
const terminalRows = [];
for (const row of orphanRows) {
const auditTimestampMs = clampAuditTimestamp(nowMs, row.created_at_ms);
const terminalFields = {
status: "cancelled",
decision: "deny",
terminal_reason: "gateway-restart",
resolved_at_ms: auditTimestampMs,
resolver_kind: "system",
resolver_id: null,
updated_at_ms: auditTimestampMs
};
const result = executeSqliteQuerySync(database.db, stateDb.updateTable("operator_approvals").set(terminalFields).where("approval_id", "=", row.approval_id).where("status", "=", "pending"));
const rowAffected = Number(result.numAffectedRows ?? 0n);
affected += rowAffected;
if (rowAffected === 1) terminalRows.push({
...row,
...terminalFields
});
}
return {
affected,
records: terminalRows.map((row) => decodeOperatorApprovalRow(row)).filter((record) => record !== null)
};
}, params.databaseOptions);
}
function consumeOperatorApprovalAllowOnceInDatabase(params) {
const id = requireApprovalId(params.id);
const consumerId = requireString(params.consumerId, "operator approval consumer id");
const runtimeEpoch = params.runtimeEpoch === void 0 ? void 0 : requireString(params.runtimeEpoch, "operator approval runtime epoch");
if (params.redemptionWindowMs !== void 0 && !isValidTimestamp(params.redemptionWindowMs)) throw new Error("operator approval redemption window must be a non-negative safe integer");
return runOpenClawStateWriteTransaction((database) => {
const nowMs = params.nowMs ?? Date.now();
const redemptionThresholdMs = params.redemptionWindowMs === void 0 ? void 0 : nowMs - params.redemptionWindowMs;
let row = selectOperatorApprovalRow(database, id);
if (!row) return { outcome: "not-found" };
if (!matchesExpectedApprovalOwner({
row,
expectedKind: params.expectedKind,
runtimeEpoch
})) return { outcome: "not-found" };
if (row.status === "pending" && row.expires_at_ms <= nowMs) {
row = expirePendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
if (!row) return { outcome: "not-found" };
}
let record = decodeOperatorApprovalRow(row);
if (!record) {
denyCorruptPendingRow({
database,
id,
nowMs,
createdAtMs: row.created_at_ms
});
return { outcome: "corrupt" };
}
if (record.status !== "allowed" || record.decision !== "allow-once") return {
outcome: "not-allow-once",
record
};
if (record.consumedAtMs !== null) return {
outcome: "already-consumed",
record
};
if (record.resolvedAtMs === null) return { outcome: "corrupt" };
if (redemptionThresholdMs !== void 0 && record.resolvedAtMs <= redemptionThresholdMs) return {
outcome: "redemption-expired",
record
};
const auditTimestampMs = clampAuditTimestamp(nowMs, record.createdAtMs, record.resolvedAtMs, record.updatedAtMs);
let consumeQuery = getNodeSqliteKysely(database.db).updateTable("operator_approvals").set({
consumed_at_ms: auditTimestampMs,
consumed_by: consumerId,
updated_at_ms: auditTimestampMs
}).where("approval_id", "=", id).where("status", "=", "allowed").where("decision", "=", "allow-once").where("consumed_at_ms", "is", null);
if (redemptionThresholdMs !== void 0) consumeQuery = consumeQuery.where("resolved_at_ms", ">", redemptionThresholdMs);
if (params.expectedKind !== void 0) consumeQuery = consumeQuery.where("kind", "=", params.expectedKind);
if (runtimeEpoch !== void 0) consumeQuery = consumeQuery.where("runtime_epoch", "=", runtimeEpoch);
const result = executeSqliteQuerySync(database.db, consumeQuery);
row = selectOperatorApprovalRow(database, id);
if (!row) return { outcome: "not-found" };
record = requireDecodedRecord(row);
if (result.numAffectedRows === 1n) return {
outcome: "consumed",
record
};
if (redemptionThresholdMs !== void 0 && record.resolvedAtMs !== null && record.resolvedAtMs <= redemptionThresholdMs) return {
outcome: "redemption-expired",
record
};
return {
outcome: "already-consumed",
record
};
}, params.databaseOptions);
}
function pruneTerminalOperatorApprovals(params) {
const retentionMs = params.retentionMs ?? 2592e6;
if (!Number.isSafeInteger(retentionMs) || retentionMs < 0) throw new Error("operator approval retention must be a non-negative safe integer");
return runOpenClawStateWriteTransaction((database) => {
const cutoffMs = (params.nowMs ?? Date.now()) - retentionMs;
const stateDb = getNodeSqliteKysely(database.db);
const result = executeSqliteQuerySync(database.db, stateDb.deleteFrom("operator_approvals").where("status", "!=", "pending").where("resolved_at_ms", "is not", null).where("resolved_at_ms", "<=", cutoffMs));
return Number(result.numAffectedRows ?? 0n);
}, params.databaseOptions);
}
//#endregion
export { pruneTerminalOperatorApprovals as a, forceDenyOperatorApprovalInDatabase as i, consumeOperatorApprovalAllowOnceInDatabase as n, resolveOperatorApprovalInDatabase as o, expireDueOperatorApprovalsInDatabase as r, closeOrphanedOperatorApprovals as t };