UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

230 lines (229 loc) • 8.72 kB
import { delimitUntrusted, inlineUntrusted, neutralizeForFence } from "./untrusted-text.js"; import { egressPasses, runRedactionPasses } from "./redaction-passes.js"; import { CREDENTIAL_VOCABULARY, egressPassGroups, isSensitiveRecordKey, URI_SCHEME_SOURCE } from "./credential-vocabulary.js"; import { isErrorLike, readMember, thrownText } from "./thrown-value.js"; import { sliceHeadSafe } from "./surrogate-safe-slice.js"; export { summarizeRedactions } from "./redaction-passes.js"; const URL_OF_ANY_PROTOCOL = new RegExp(String.raw `${URI_SCHEME_SOURCE}:\/\/[^\s"'\`]+`, "gi"); const BARE_USERNAME_MAX_LEN = 20; function redactUserinfo(scheme, userinfoWithAt) { const userinfo = userinfoWithAt.slice(0, -1); if (CREDENTIAL_VOCABULARY.policy.urlUserinfo === "whole" || userinfo.includes("@")) return `${scheme}[redacted-credentials]@`; const colonIdx = userinfo.indexOf(":"); if (colonIdx === -1) { return userinfo.length >= BARE_USERNAME_MAX_LEN ? `${scheme}[redacted-credentials]@` : `${scheme}${userinfo}@`; } return `${scheme}${userinfo.slice(0, colonIdx)}:[redacted-credentials]@`; } const SECRET_TIER_PASSES = egressPasses(egressPassGroups(CREDENTIAL_VOCABULARY), { "url-credentials": (_m, scheme, userinfo) => redactUserinfo(scheme, userinfo), }); const HOST_LEAK_PASSES = [ ...SECRET_TIER_PASSES, { kind: "url", confidence: "medium", marker: "[redacted-url]", source: "untrusted-egress", re: URL_OF_ANY_PROTOCOL, replace: "[redacted-url]" }, { kind: "fs-path", confidence: "low", marker: "[redacted-path]", source: "untrusted-egress", re: /(?:\/[A-Za-z0-9_.-]+){2,}/g, replace: "[redacted-path]" }, ]; const TIER_PASSES = { "same-principal": SECRET_TIER_PASSES, human: HOST_LEAK_PASSES }; export function egressText(tier, text, report) { return runRedactionPasses(text, TIER_PASSES[tier], report); } export function egressThrownText(tier, err) { return egressText(tier, thrownText(err)); } export function redactSecrets(s, report) { return egressText("same-principal", s, report); } export function extensionWords(s, max) { return inlineUntrusted(egressText("same-principal", s), max); } export function redactHostLeaks(s, report) { return egressText("human", s, report); } const VALUE_DEPTH_MAX = 32; const VALUE_MEMBERS_MAX = 2_000; const WHOLE = "[redacted]"; export function egressValue(tier, value) { const snapshots = new Map(); let budget = VALUE_MEMBERS_MAX; const ownKeys = (o, all) => { try { return all ? Reflect.ownKeys(o) : Object.keys(o); } catch { return undefined; } }; const prototypeOf = (o) => { try { return Object.getPrototypeOf(o); } catch { return Object.prototype; } }; const readOwn = (o, key) => { try { const d = Reflect.getOwnPropertyDescriptor(o, key); if (d === undefined) return { value: undefined, enumerable: true }; return { value: "value" in d ? d.value : d.get?.call(o), enumerable: d.enumerable ?? true }; } catch { return { value: undefined, enumerable: true }; } }; const member = (read, depth, name) => { const out = pass(read, depth + 1, name); return typeof out === "string" && isSensitiveRecordKey(name) ? WHOLE : out; }; const fill = (o, depth, keys, snapshot) => { snapshots.set(o, snapshot); for (const key of keys ?? []) { if (budget-- <= 0) break; const read = readOwn(o, key); const name = typeof key === "string" ? egressText(tier, key) : key; Object.defineProperty(snapshot, name, { value: member(read.value, depth, typeof key === "string" ? key : ""), enumerable: read.enumerable, writable: true, configurable: true }); } return snapshot; }; const errorSnapshot = (o, depth) => { const snapshot = new Error(); for (const key of Reflect.ownKeys(snapshot)) Reflect.deleteProperty(snapshot, key); fill(o, depth, ownKeys(o, true), snapshot); for (const key of ["name", "message", "code", "stack"]) { if (Object.prototype.hasOwnProperty.call(snapshot, key)) continue; const read = readMember(o, key); if (read !== undefined && typeof read !== "function") Object.defineProperty(snapshot, key, { value: member(read, depth, key), enumerable: false, writable: true, configurable: true }); } return snapshot; }; function pass(v, depth, key) { if (typeof v === "string") return egressText(tier, v); if (typeof v === "function") return undefined; if (typeof v !== "object" || v === null) return v; const known = snapshots.get(v); if (known !== undefined) return known; if (depth >= VALUE_DEPTH_MAX) return WHOLE; if (isErrorLike(v)) return errorSnapshot(v, depth); const toJSON = readMember(v, "toJSON"); if (typeof toJSON === "function") { let reading; try { reading = toJSON.call(v, key); } catch { return WHOLE; } if (reading !== v) { const out = pass(reading, depth + 1, key); snapshots.set(v, out); return out; } } const array = (() => { try { return Array.isArray(v); } catch { return false; } })(); return fill(v, depth, ownKeys(v, false), array ? [] : prototypeOf(v) === null ? Object.create(null) : {}); } return pass(value, 0, ""); } const REDACTION_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g; function countRedactionMarkers(s) { return (s.match(REDACTION_MARKER_RE) ?? []).length; } function safeEllipsisTruncate(s, max) { if (max < 1) return ""; const budget = max - 1; let out = ""; for (const codePoint of s) { if (out.length + codePoint.length > budget) break; out += codePoint; } return out + "…"; } function boundedString(value, max, redact) { let s; try { s = typeof value === "string" ? value : JSON.stringify(value) ?? String(value); } catch { s = "[unserializable result]"; } s = redact(s); if (s.length <= max) return s; const totalMarkers = countRedactionMarkers(s); const plainSuffix = (kept) => `…[+${s.length - kept.length} chars]`; const enrichedSuffix = (kept) => { const lost = totalMarkers - countRedactionMarkers(kept); return lost > 0 ? `…[+${s.length - kept.length} chars, ${lost} redaction marker(s) among them]` : plainSuffix(kept); }; const converge = (suffixFor) => { let keep = max; let out = ""; for (let i = 0; i < 4; i++) { const kept = sliceHeadSafe(s, keep); out = `${kept}${suffixFor(kept)}`; if (out.length <= max) return out; if (keep === 0) return undefined; keep = Math.max(0, keep - (out.length - max)); } return out.length <= max ? out : undefined; }; const out = (totalMarkers > 0 ? converge(enrichedSuffix) : undefined) ?? converge(plainSuffix); if (out === undefined) { return safeEllipsisTruncate(s, max); } if (out.length >= s.length) return s; return out; } export function boundedRedactedSummary(value, max) { return boundedString(value, max, (s) => egressText("same-principal", s)); } export function untrustedEgressForHuman(value, opts) { const source = boundedString(value, Number.POSITIVE_INFINITY, (s) => egressText("human", s)); const probe = (budget) => { const bounded = boundedString(source, budget, (s) => s); return { bounded, overflow: [...neutralizeForFence(bounded)].length - opts.max }; }; let chosen = probe(opts.max); if (chosen.overflow > 0) { let fits; let lo = 0; let hi = opts.max; while (hi - lo > 1) { const mid = lo + Math.floor((hi - lo) / 2); const p = probe(mid); if (p.overflow <= 0) { lo = mid; fits = p; } else { hi = mid; } } chosen = fits ?? probe(lo); } return delimitUntrusted(opts.label, chosen.bounded, opts.max); }