Files
clawdbot/src/auto-reply/reply/reply-elevated.ts
2026-01-15 02:42:44 +00:00

194 lines
6.3 KiB
TypeScript

import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js";
import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import type { MsgContext } from "../templating.js";
function normalizeAllowToken(value?: string) {
if (!value) return "";
return value.trim().toLowerCase();
}
function slugAllowToken(value?: string) {
if (!value) return "";
let text = value.trim().toLowerCase();
if (!text) return "";
text = text.replace(/^[@#]+/, "");
text = text.replace(/[\s_]+/g, "-");
text = text.replace(/[^a-z0-9-]+/g, "-");
return text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
}
const SENDER_PREFIXES = [
...CHAT_CHANNEL_ORDER,
INTERNAL_MESSAGE_CHANNEL,
"user",
"group",
"channel",
];
const SENDER_PREFIX_RE = new RegExp(`^(${SENDER_PREFIXES.join("|")}):`, "i");
function stripSenderPrefix(value?: string) {
if (!value) return "";
const trimmed = value.trim();
return trimmed.replace(SENDER_PREFIX_RE, "");
}
function resolveElevatedAllowList(
allowFrom: AgentElevatedAllowFromConfig | undefined,
provider: string,
fallbackAllowFrom?: Array<string | number>,
): Array<string | number> | undefined {
if (!allowFrom) return fallbackAllowFrom;
const value = allowFrom[provider];
return Array.isArray(value) ? value : fallbackAllowFrom;
}
function isApprovedElevatedSender(params: {
provider: string;
ctx: MsgContext;
allowFrom?: AgentElevatedAllowFromConfig;
fallbackAllowFrom?: Array<string | number>;
}): boolean {
const rawAllow = resolveElevatedAllowList(
params.allowFrom,
params.provider,
params.fallbackAllowFrom,
);
if (!rawAllow || rawAllow.length === 0) return false;
const allowTokens = rawAllow.map((entry) => String(entry).trim()).filter(Boolean);
if (allowTokens.length === 0) return false;
if (allowTokens.some((entry) => entry === "*")) return true;
const tokens = new Set<string>();
const addToken = (value?: string) => {
if (!value) return;
const trimmed = value.trim();
if (!trimmed) return;
tokens.add(trimmed);
const normalized = normalizeAllowToken(trimmed);
if (normalized) tokens.add(normalized);
const slugged = slugAllowToken(trimmed);
if (slugged) tokens.add(slugged);
};
addToken(params.ctx.SenderName);
addToken(params.ctx.SenderUsername);
addToken(params.ctx.SenderTag);
addToken(params.ctx.SenderE164);
addToken(params.ctx.From);
addToken(stripSenderPrefix(params.ctx.From));
addToken(params.ctx.To);
addToken(stripSenderPrefix(params.ctx.To));
for (const rawEntry of allowTokens) {
const entry = rawEntry.trim();
if (!entry) continue;
const stripped = stripSenderPrefix(entry);
if (tokens.has(entry) || tokens.has(stripped)) return true;
const normalized = normalizeAllowToken(stripped);
if (normalized && tokens.has(normalized)) return true;
const slugged = slugAllowToken(stripped);
if (slugged && tokens.has(slugged)) return true;
}
return false;
}
export function resolveElevatedPermissions(params: {
cfg: ClawdbotConfig;
agentId: string;
ctx: MsgContext;
provider: string;
}): {
enabled: boolean;
allowed: boolean;
failures: Array<{ gate: string; key: string }>;
} {
const globalConfig = params.cfg.tools?.elevated;
const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools?.elevated;
const globalEnabled = globalConfig?.enabled !== false;
const agentEnabled = agentConfig?.enabled !== false;
const enabled = globalEnabled && agentEnabled;
const failures: Array<{ gate: string; key: string }> = [];
if (!globalEnabled) failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
if (!agentEnabled)
failures.push({
gate: "enabled",
key: "agents.list[].tools.elevated.enabled",
});
if (!enabled) return { enabled, allowed: false, failures };
if (!params.provider) {
failures.push({ gate: "provider", key: "ctx.Provider" });
return { enabled, allowed: false, failures };
}
const normalizedProvider = normalizeChannelId(params.provider);
const dockFallbackAllowFrom = normalizedProvider
? getChannelDock(normalizedProvider)?.elevated?.allowFromFallback?.({
cfg: params.cfg,
accountId: params.ctx.AccountId,
})
: undefined;
const fallbackAllowFrom = dockFallbackAllowFrom;
const globalAllowed = isApprovedElevatedSender({
provider: params.provider,
ctx: params.ctx,
allowFrom: globalConfig?.allowFrom,
fallbackAllowFrom,
});
if (!globalAllowed) {
failures.push({
gate: "allowFrom",
key: `tools.elevated.allowFrom.${params.provider}`,
});
return { enabled, allowed: false, failures };
}
const agentAllowed = agentConfig?.allowFrom
? isApprovedElevatedSender({
provider: params.provider,
ctx: params.ctx,
allowFrom: agentConfig.allowFrom,
fallbackAllowFrom,
})
: true;
if (!agentAllowed) {
failures.push({
gate: "allowFrom",
key: `agents.list[].tools.elevated.allowFrom.${params.provider}`,
});
}
return { enabled, allowed: globalAllowed && agentAllowed, failures };
}
export function formatElevatedUnavailableMessage(params: {
runtimeSandboxed: boolean;
failures: Array<{ gate: string; key: string }>;
sessionKey?: string;
}): string {
const lines: string[] = [];
lines.push(
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
);
if (params.failures.length > 0) {
lines.push(`Failing gates: ${params.failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`);
} else {
lines.push(
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
);
}
lines.push("Fix-it keys:");
lines.push("- tools.elevated.enabled");
lines.push("- tools.elevated.allowFrom.<provider>");
lines.push("- agents.list[].tools.elevated.enabled");
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
if (params.sessionKey) {
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
}
return lines.join("\n");
}