fix(v2r1): bind OpenClaw trust only to authoritative runId
This commit is contained in:
+32
-129
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user