UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

288 lines (287 loc) • 12.5 kB
import { writtenAssignmentPrefix } from "../tools/fs/bash-program-position.js"; const ACTIVITY_ARG_MAX = 80; const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i; export function summarizeRedactions(findings) { if (findings.length === 0) return "no redactions"; const counts = new Map(); for (const f of findings) { const label = `${f.kind} (${f.confidence})`; counts.set(label, (counts.get(label) ?? 0) + 1); } const parts = [...counts].map(([label, n]) => `${label} x${n}`); return `${findings.length} redaction${findings.length === 1 ? "" : "s"}: ${parts.join(", ")}`; } function mapBackOnePass(edits, pos) { let delta = 0; for (const e of edits) { const outStart = e.at + delta; if (pos < outStart) return pos - delta; const outEnd = outStart + e.insertedLen; if (pos < outEnd) return e.at; delta += e.insertedLen - e.removedLen; } return pos - delta; } const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g; const FORMAT_CHAR_RE = /\p{Cf}/u; const MAX_FORMAT_SLOT_SCANS = 64; const REDACTED_WHOLE = "[redacted]"; function applyPasses(input, passes, report, toOriginal) { const batches = []; let cur = input; for (const pass of passes) { const edits = []; cur = cur.replace(pass.re, (...args) => { const match = args[0]; const offset = args[args.length - 2]; const groups = args.slice(1, -2); const inserted = typeof pass.replace === "string" ? pass.replace.replace(/\$(\d)/g, (_m, d) => groups[Number(d) - 1] ?? "") : pass.replace(match, ...groups); if (inserted !== match) { if (report !== undefined) { let s0 = offset; let e0 = offset + match.length; for (let i = batches.length - 1; i >= 0; i--) { s0 = mapBackOnePass(batches[i], s0); e0 = mapBackOnePass(batches[i], e0); } report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [toOriginal(s0), toOriginal(e0)], marker: pass.marker, source: pass.source, }); } edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted }); } return inserted; }); batches.push(edits); } return { out: cur, batches }; } function replacedSpans(batches) { const spans = []; for (let j = 0; j < batches.length; j++) { for (const e of batches[j]) { let s0 = e.at; let e0 = e.at + e.removedLen; for (let i = j - 1; i >= 0; i--) { s0 = mapBackOnePass(batches[i], s0); e0 = mapBackOnePass(batches[i], e0); } if (e0 - s0 !== e.removedLen) return undefined; spans.push({ start: s0, end: e0, inserted: e.inserted, pass: j }); } } spans.sort((a, b) => a.start - b.start); for (let i = 1; i < spans.length; i++) if (spans[i].start < spans[i - 1].end) return undefined; return spans; } export function runRedactionPasses(input, passes, report) { if (report !== undefined && report.preexistingMarkers === undefined) { report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0; } if (!FORMAT_CHAR_RE.test(input)) return applyPasses(input, passes, report, (pos) => pos).out; const viewChars = []; const viewToOriginal = []; const formatAtSlot = []; const charStartOffsets = []; { let orig = 0; let viewOff = 0; let slotBuf = ""; for (const ch of input) { if (FORMAT_CHAR_RE.test(ch)) { slotBuf += ch; } else { formatAtSlot.push(slotBuf); slotBuf = ""; viewToOriginal.push(orig); charStartOffsets.push(viewOff); viewChars.push(ch); viewOff += ch.length; } orig += ch.length; } formatAtSlot.push(slotBuf); viewToOriginal.push(input.length); charStartOffsets.push(viewOff); } const view = viewChars.join(""); const offsetToCharIndex = new Map(); charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx)); const toOriginal = (pos) => { const idx = offsetToCharIndex.get(pos); return idx === undefined ? input.length : viewToOriginal[idx]; }; const { out, batches } = applyPasses(view, passes, report, toOriginal); const viewSpans = replacedSpans(batches); if (viewSpans === undefined) return out; const spans = viewSpans.map((sp) => { const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length; const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length; const lastIdx = endIdx - 1; const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx]; return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted }; }); const viewOffsetToOriginal = (viewStart, viewEnd) => { const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length; const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length; const lastIdx = endIdx - 1; const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx]; return { start: viewToOriginal[startIdx], end: endOrig }; }; const cutOffsets = []; for (let k = 1; k < viewChars.length; k++) if (formatAtSlot[k] !== "") cutOffsets.push(charStartOffsets[k]); if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS) return REDACTED_WHOLE; const extra = []; const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos).batches); if (rawSpans !== undefined) extra.push(...rawSpans); for (const cut of cutOffsets) { const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos).batches); if (cutSpans === undefined) continue; for (const c of cutSpans) { const o = viewOffsetToOriginal(c.start + cut, c.end + cut); extra.push({ start: o.start, end: o.end, inserted: c.inserted, pass: c.pass }); } } extra.sort((a, b) => a.start - b.start); for (const r of extra) { const overlapping = spans.find((v) => r.start < v.end && v.start < r.end); if (overlapping !== undefined) { if (r.start < overlapping.start) overlapping.start = r.start; if (r.end > overlapping.end) overlapping.end = r.end; continue; } spans.push({ start: r.start, end: r.end, inserted: r.inserted }); if (report !== undefined) { const pass = passes[r.pass]; report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [r.start, r.end], marker: pass.marker, source: pass.source }); } spans.sort((a, b) => a.start - b.start); } for (let i = 1; i < spans.length;) { if (spans[i].start < spans[i - 1].end) { spans[i - 1].end = Math.max(spans[i - 1].end, spans[i].end); spans.splice(i, 1); } else i++; } let result = ""; let cursor = 0; for (const sp of spans) { result += input.slice(cursor, sp.start) + sp.inserted; cursor = sp.end; } result += input.slice(cursor); return result; } export const SECRET_PASSES = [ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur]|github_pat)[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" }, { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" }, { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" }, { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" }, { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" }, { kind: "jwt", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" }, { kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----|$)/g, replace: "[redacted]" }, { kind: "bearer-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" }, { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(AccountKey=)[A-Za-z0-9+/]{40,}={0,2}/g, replace: "$1[redacted]" }, { kind: "keyword-value", confidence: "medium", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])((?:token|secret|key|password|passwd|credential|authorization)["']?\s*[=:]\s*["']?)[^\s"';|&]{4,}/gi, replace: "$1[redacted]", }, ]; export function scrubSecrets(s, report) { return runRedactionPasses(s, SECRET_PASSES, report); } function truncCodePoints(s) { const cp = Array.from(s); return cp.length > ACTIVITY_ARG_MAX ? cp.slice(0, ACTIVITY_ARG_MAX).join("") + "…" : s; } const basename = (p) => p.split(/[/\\]/).filter((x) => x.length > 0).pop() ?? p; export function primaryActivityArg(args) { if (!args || typeof args !== "object") return undefined; const a = args; const str = (k) => typeof a[k] === "string" && a[k].length > 0 ? a[k] : undefined; let val; const command = str("command"); const path = str("file_path") ?? str("filePath") ?? str("path") ?? str("notebook_path"); const url = str("url"); if (command !== undefined) { const tokens = command.trim().split(/\s+/); let i = 0; while (i < tokens.length && writtenAssignmentPrefix(tokens[i]) !== undefined) { if (/["'\\]/.test(tokens[i])) return undefined; i++; } if (i >= tokens.length) return undefined; const name = basename(tokens[i]); val = tokens.length > i + 1 ? `${name} …` : name; } else if (path !== undefined) { val = basename(path); } else if (url !== undefined) { const parsed = (() => { try { return new URL(url); } catch { return undefined; } })(); if (parsed !== undefined && parsed.host !== "") { val = `${parsed.origin}${parsed.pathname}`; } else { const head = url.split(/[?#]/, 1)[0] ?? url; const sep = head.indexOf("://"); const scheme = sep > 0 && /^[a-z][a-z0-9+.-]*$/i.test(head.slice(0, sep)) ? head.slice(0, sep + 3) : ""; const rest = head.slice(scheme.length); const at = rest.lastIndexOf("@"); val = scheme + (at === -1 ? rest : rest.slice(at + 1)); } } else { val = str("pattern") ?? str("query") ?? str("description"); if (val === undefined) { for (const k of Object.keys(a)) { if (SENSITIVE_KEY.test(k)) continue; const v = a[k]; if (typeof v === "string" && v.length > 0) { val = v; break; } } } } if (val === undefined) return undefined; return truncCodePoints(scrubSecrets(val)); }