refactor(auto-reply): split reply pipeline
This commit is contained in:
206
src/auto-reply/reply/reply-elevated.ts
Normal file
206
src/auto-reply/reply/reply-elevated.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import {
|
||||
CHAT_CHANNEL_ORDER,
|
||||
normalizeChannelId,
|
||||
} 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");
|
||||
}
|
||||
Reference in New Issue
Block a user