Files
UHD_Media_Tool/packages/agent/src/redact.ts
T
rootandClaude Fable 5.1 9c881e737f feat(agent): capability registry, Pi AgentRuntimePort, fake model provider and redaction guard (W1-5)
- CapabilityRegistry: descriptor(id/version/schema/scope/risk/idempotent/preview/replay),
  invoke = scope limit -> input schema -> server-side authorize -> idempotency key -> handler
  -> output schema -> audit; capability id <-> tool name mapping (download.submit <-> download__submit)
- PiAgentRuntime (pi-agent-core AgentHarness 0.87.1): whitelist tools + before_tool re-check,
  redacted event stream, JSONL session at sessionDir/<runId>.jsonl, resume/abort/compact,
  tool-call/duration/token/cost budgets -> abort + run_failed(budget)
- AgentSettings -> pi-ai createProvider (anthropic / openai-compatible), compaction mapping
- FakeModelProvider + createFakeRuntime (scripted, transcript-derived, survives crash/resume)
- buildSystemPrompt, redactForModel; 27 tests incl. SIGKILL + resume

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 17:55:28 +08:00

84 lines
3.5 KiB
TypeScript

/**
* 敏感信息守卫:工具输入输出进入模型上下文、事件流、审计前统一经过这里。
* 覆盖 cookie / passkey / token / apiKey / authorization / 含 passkey 等私密参数的 URL / URL 内嵌账号密码。
* 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送给外部模型。
*/
export const REDACTED = "[REDACTED]";
const SENSITIVE_KEY_PARTS = [
"cookie",
"passkey",
"authkey",
"torrentpass",
"rsskey",
"apikey",
"authorization",
"secret",
"password",
"passwd",
"credential",
"privatekey",
];
/** 规范化键名(去掉 - _ 并小写)后判断。以 token 结尾算敏感,tokens(用量统计)不算。 */
export function isSensitiveKey(key: string): boolean {
const k = key.toLowerCase().replace(/[-_\s]/g, "");
if (k.endsWith("token")) return true;
if (k === "sign" || k === "signature") return true;
return SENSITIVE_KEY_PARTS.some((part) => k.includes(part));
}
/** URL 查询参数/片段中视为凭证的参数名 */
const SENSITIVE_PARAM =
"passkey|authkey|torrent_pass|rsskey|api[_-]?key|access[_-]?token|refresh[_-]?token|token|auth|sign|signature|credential|secret|password|key|uid_key|downhash";
const PARAM_RE = new RegExp(`([?&;#]|\\b)(${SENSITIVE_PARAM})=([^&;#\\s"'<>]+)`, "gi");
const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
const BEARER_RE = /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{6,}/g;
const HEADER_RE = /\b(cookie|set-cookie|authorization|x-api-key)\s*:\s*[^\r\n]+/gi;
const KNOWN_KEY_RE = /\b(sk-[A-Za-z0-9_-]{8,}|sk-ant-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{16,})\b/g;
export function redactString(input: string): string {
return input
.replace(HEADER_RE, (_m, name: string) => `${name}: ${REDACTED}`)
.replace(URL_USERINFO_RE, (_m, scheme: string) => `${scheme}${REDACTED}@`)
.replace(PARAM_RE, (_m, lead: string, name: string) => `${lead}${name}=${REDACTED}`)
.replace(BEARER_RE, (_m, scheme: string) => `${scheme} ${REDACTED}`)
.replace(KNOWN_KEY_RE, REDACTED);
}
/** 深度脱敏,返回新对象;不修改入参。循环引用替换为 "[Circular]"。 */
export function redactForModel<T>(value: T): T {
return redactValue(value, new WeakSet()) as T;
}
function redactValue(value: unknown, seen: WeakSet<object>): unknown {
if (typeof value === "string") return redactString(value);
if (value === null || typeof value !== "object") return value;
if (value instanceof Date) return value;
if (value instanceof URL) return redactString(value.toString());
if (seen.has(value)) return "[Circular]";
seen.add(value);
if (Array.isArray(value)) return value.map((item) => redactValue(item, seen));
if (value instanceof Map) {
return Object.fromEntries(
[...value.entries()].map(([k, v]) => [String(k), isSensitiveKey(String(k)) ? REDACTED : redactValue(v, seen)]),
);
}
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
out[k] = isSensitiveKey(k) && v !== null && v !== undefined && v !== "" ? REDACTED : redactValue(v, seen);
}
return out;
}
/** 事件/审计用的短摘要:先脱敏再截断 */
export function summarizeForLog(value: unknown, maxLength = 500): string {
let text: string;
try {
text = typeof value === "string" ? redactString(value) : (JSON.stringify(redactForModel(value)) ?? "undefined");
} catch {
text = "[unserializable]";
}
return text.length > maxLength ? `${text.slice(0, maxLength)}…(${text.length - maxLength} more)` : text;
}