disclaw/src/runtime/sanitize.ts
Nick Tabeling 21052bc646
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
feat(DIS-152): mapped project paths (host:virtual) for agents
Adds optional path_mappings to agent.yaml so host directories are
transparently mapped to virtual paths in agent responses and CLAUDE.md.
Sanitizer replaces host paths (longest first, both slash styles) before
repoRoot/home substitutions. /new-agent accepts a host:virtual path
option with Windows-safe splitting and fs.existsSync validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 10:41:10 +02:00

72 lines
2.3 KiB
TypeScript

export interface SanitizeOptions {
repoRoot?: string; // e.g. /home/user/.disclaw
home?: string; // os.homedir()
pathMappings?: Array<{ host: string; virtual: string }>;
}
/**
* Sanitizes agent output before it is sent to a Discord channel.
* Replaces absolute paths and sensitive tokens so they are never leaked.
*
* Replacement order is intentional — longest/most-specific patterns first:
* 1. repoRoot → <disclaw>
* 2. home → ~
* 3. Discord bot token pattern → <redacted>
* 4. Anthropic API key pattern → <redacted>
*/
/** Returns both backslash and forward-slash variants of a path (for Windows). */
function pathVariants(p: string): string[] {
const fwd = p.replace(/\\/g, "/");
const bwd = p.replace(/\//g, "\\");
return fwd === bwd ? [p] : [p, fwd, bwd];
}
function replaceAll(text: string, search: string, replacement: string): string {
return text.split(search).join(replacement);
}
export function sanitizeForDiscord(
text: string,
opts: SanitizeOptions = {}
): string {
let result = text;
// 0. Replace path mappings first (most-specific, longest host paths first)
// so they take precedence over generic repoRoot/home replacements.
if (opts.pathMappings && opts.pathMappings.length > 0) {
const sorted = [...opts.pathMappings].sort(
(a, b) => b.host.length - a.host.length
);
for (const mapping of sorted) {
for (const variant of pathVariants(mapping.host)) {
result = replaceAll(result, variant, mapping.virtual);
}
}
}
// 1. Replace repo root before home so that sub-paths are caught by the
// more specific substitution first (repoRoot is usually inside home).
if (opts.repoRoot) {
for (const variant of pathVariants(opts.repoRoot)) {
result = replaceAll(result, variant, "<disclaw>");
}
}
// 2. Replace home directory with ~ (both slash styles for Windows)
if (opts.home) {
for (const variant of pathVariants(opts.home)) {
result = replaceAll(result, variant, "~");
}
}
// 3. Discord bot token — format: [MN]<23 base64url>.<6 base64url>.<27 base64url>
result = result.replace(
/[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}/g,
"<redacted>"
);
// 4. Anthropic API key — format: sk-ant-<20+ base64url chars>
result = result.replace(/sk-ant-[A-Za-z0-9\-_]{20,}/g, "<redacted>");
return result;
}