fix(v2r1): bind OpenClaw trust only to authoritative runId

This commit is contained in:
openclaw
2026-07-12 23:37:33 +08:00
parent d2de7f23d1
commit 729c7bb23b
+32 -129
View File
@@ -1,41 +1,21 @@
export const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
"read",
"grep",
"glob",
"find",
"ls",
"web_search",
"web_fetch",
"memory_search",
"sessions_list",
"sessions_history",
"session_status",
const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
"read", "grep", "glob", "find", "ls", "web_search", "web_fetch",
"memory_search", "sessions_list", "sessions_history", "session_status",
]);
function nonEmpty(value) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function strings(value) {
return Array.isArray(value)
? value.map(nonEmpty).filter(Boolean)
? value.filter((item) => typeof item === "string" && item.trim())
.map((item) => item.trim())
: [];
}
export function pluginConfig(event = {}, ctx = {}) {
const value = ctx?.pluginConfig ?? event?.context?.pluginConfig ?? {};
return value && typeof value === "object" ? value : {};
}
export function hookContext(event = {}, ctx = {}) {
const eventContext = event?.context && typeof event.context === "object"
? event.context
: {};
return { ...eventContext, ...(ctx && typeof ctx === "object" ? ctx : {}) };
}
export function resolveSenderId(ctx = {}) {
return nonEmpty(ctx.senderId) ?? nonEmpty(ctx.channelContext?.sender?.id);
const direct = typeof ctx.senderId === "string" ? ctx.senderId.trim() : "";
const nested = typeof ctx.channelContext?.sender?.id === "string"
? ctx.channelContext.sender.id.trim()
: "";
return direct || nested || undefined;
}
export function classifyRun(ctx = {}, config = {}) {
@@ -43,41 +23,18 @@ export function classifyRun(ctx = {}, config = {}) {
const trusted = new Set(strings(config.trustedSenderIds));
const blocked = new Set(strings(config.blockedSenderIds));
if (nonEmpty(ctx.jobId)) {
return {
trusted: false,
origin: "automation",
reason: "job_id_present",
senderId,
};
if (typeof ctx.jobId === "string" && ctx.jobId) {
return { trusted: false, origin: "automation", reason: "job_id_present" };
}
if (!senderId) {
return {
trusted: false,
origin: "unknown",
reason: "sender_id_missing",
};
return { trusted: false, origin: "system_or_unknown", reason: "sender_id_missing" };
}
if (blocked.has(senderId)) {
return {
trusted: false,
origin: "blocked_sender",
reason: "sender_blocked",
senderId,
};
return { trusted: false, origin: "blocked_sender", reason: "sender_blocked", senderId };
}
if (config.requireExplicitTrustedSenders === true && !trusted.has(senderId)) {
return {
trusted: false,
origin: "untrusted_sender",
reason: "sender_not_allowlisted",
senderId,
};
if (config.requireExplicitTrustedSenders !== false && !trusted.has(senderId)) {
return { trusted: false, origin: "untrusted_sender", reason: "sender_not_allowlisted", senderId };
}
return {
trusted: true,
origin: "authenticated_channel_user",
@@ -86,83 +43,29 @@ export function classifyRun(ctx = {}, config = {}) {
};
}
export function readOnlyTools(config = {}) {
const configured = strings(config.readOnlyTools).map((value) => value.toLowerCase());
return new Set(configured.length ? configured : DEFAULT_READ_ONLY_TOOLS);
}
export function decideToolCall(toolName, trust, config = {}) {
const normalized = nonEmpty(toolName)?.toLowerCase();
if (!normalized) {
return { action: "block", code: "invalid_tool", reason: "tool_name_missing" };
export function decideToolCall(toolName, runTrust, config = {}) {
const name = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
const configured = strings(config.readOnlyTools).map((item) => item.toLowerCase());
const readOnly = new Set(configured.length ? configured : DEFAULT_READ_ONLY_TOOLS);
if (!name) {
return { action: "block", code: "tool_name_missing" };
}
if (readOnlyTools(config).has(normalized)) {
return { action: "allow", code: "read_only", reason: "read_only_tool_allowed" };
if (readOnly.has(name)) {
return { action: "allow", code: "read_only" };
}
if (trust?.trusted !== true) {
if (!runTrust || runTrust.trusted !== true) {
return {
action: "block",
code: "untrusted_side_effect",
reason: trust?.reason ?? "trusted_origin_not_proven",
code: runTrust?.reason || "run_provenance_missing",
};
}
if (config.requireApprovalForHumanSideEffects !== false) {
return {
action: "approval",
code: "approval_required",
reason: "authenticated_human_side_effect",
};
if (config.requireApprovalForHumanSideEffects === false) {
return { action: "allow", code: "trusted_side_effect" };
}
return { action: "allow", code: "trusted_side_effect", reason: "trusted_human" };
return { action: "approval", code: "human_side_effect" };
}
function candidateKeys(event = {}, ctx = {}) {
return [
nonEmpty(event.runId),
nonEmpty(ctx.runId),
nonEmpty(ctx.sessionKey),
nonEmpty(ctx.sessionId),
].filter(Boolean);
}
export function createRunTrustStore({ ttlMs = 30 * 60_000, maxEntries = 10_000 } = {}) {
const values = new Map();
function prune(now = Date.now()) {
for (const [key, value] of values) {
if (value.expiresAt <= now) values.delete(key);
}
while (values.size > maxEntries) {
const oldest = values.keys().next().value;
if (oldest === undefined) break;
values.delete(oldest);
}
}
return {
remember(event, ctx, trust) {
const now = Date.now();
prune(now);
for (const key of candidateKeys(event, ctx)) {
values.set(key, { trust, expiresAt: now + ttlMs });
}
},
recall(event, ctx) {
const now = Date.now();
prune(now);
for (const key of candidateKeys(event, ctx)) {
const value = values.get(key);
if (value && value.expiresAt > now) return value.trust;
}
return undefined;
},
size() {
prune();
return values.size;
},
};
export function runKey(event = {}, ctx = {}) {
const key = event.runId || ctx.runId;
return typeof key === "string" && key.trim() ? key.trim() : undefined;
}