194 lines
6.3 KiB
TypeScript
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");
|
|
}
|