import { mkdir, rename, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { randomUUID } from "node:crypto"; import { hmacId, sanitizeText } from "../security/privacy.js"; import type { BaseEvent, PriceCatalog, UltraConfig } from "../types.js"; import { aggregateWeeklyMetrics, buildWeeklyCohorts, type WeeklyCohort, type WeeklyMetrics } from "./metrics.js"; import { ROUTING_REGRET_POLICY } from "./routing-regret.js"; import { isSafeTelemetryMetadata } from "./safe-metadata.js"; const OMIT = /(?:raw|payload|tool(?:args?|output)?|stdout|stderr|output|reasoning|chain.?of.?thought|path|scope|command|environment|secret|token|password)/i; const SANITIZED_TEXT = /(?:request|summary|claim|error|message|command|url|description|comment|objective|name)/i; const SENSITIVE_ERROR_FIELDS = new Set(["error", "errormessage", "stack", "stacktrace", "partialtext", "partialresult"]); const IDENTIFIER_FIELDS = new Set(["eventid", "sessionid", "taskid", "runid", "spanid", "parentspanid"]); const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi; const REDACTION = /<(?:REDACTED|SECRET(?:_TASK)?|PRIVATE_KEY|CONNECTION_STRING|EMAIL|PHONE|IP|PATH_[A-F0-9]+|URL(?:_HOST)?(?:_[A-F0-9]+)?|ID_[A-F0-9]+|TASK_[A-F0-9]+|CODE_BLOCK\b)[^>]*>/g; const BUNDLE_FILES = ["README.md", "manifest.json", "events.jsonl", "tasks.jsonl", "agents.jsonl", "model-calls.jsonl", "tools.jsonl", "interactions.jsonl", "verifications.jsonl", "metrics.json", "cohorts.json", "config.json", "price-catalog.json", "experiments.json", "privacy-report.json"] as const; function keyId(key: string): string { return key.replace(/[^a-z0-9]/gi, "").toLowerCase(); } function countRedactions(value: unknown): number { return JSON.stringify(value).match(REDACTION)?.length ?? 0; } function sanitizeValue(value: unknown, secret: Buffer, privacyClass: "public" | "internal" | "restricted" | "secret", key = ""): unknown { if (SENSITIVE_ERROR_FIELDS.has(keyId(key)) || (OMIT.test(key) && !isSafeTelemetryMetadata(key, value))) return ""; if (isSafeTelemetryMetadata(key, value)) return value; if (typeof value === "string") { if (IDENTIFIER_FIELDS.has(keyId(key))) return hmacId(secret, value, "ID"); const sanitized = value.replace(UUID, (id) => hmacId(secret, id, "ID")); return SANITIZED_TEXT.test(key) ? sanitizeText(sanitized, privacyClass, secret) : sanitized; } if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry, secret, privacyClass, key)); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record).map(([childKey, childValue]) => [childKey, sanitizeValue(childValue, secret, privacyClass, childKey)])); return value; } /** * Every other file in the bundle passes through sanitizeValue; config.json was written * verbatim. That was not a theoretical gap: a project overlay fills `projectScopePaths`, * `projectExcludedPaths` and `pinnedAcceptanceCommand` with absolute paths and a real shell * command, so the bundle shipped precisely what its own manifest promised it did not -- * `pathsIncluded: false` sat two files away from the paths themselves. * * This reuses the same key-based OMIT convention rather than a second list that would drift * from it, and needs no HMAC secret because config values are structured; the only free text * in there is exactly what OMIT already names. */ function redactConfig(value: unknown, key = ""): unknown { if (OMIT.test(key) && !isSafeTelemetryMetadata(key, value)) return ""; if (Array.isArray(value)) return value.map((entry) => redactConfig(entry, key)); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record).map(([childKey, childValue]) => [childKey, redactConfig(childValue, childKey)])); return value; } export interface ExportPreview { files: Record; privacyReport: { events: number; redactedFields: number }; metrics: WeeklyMetrics; cohorts: WeeklyCohort[]; experiments: BaseEvent[]; } export interface WeeklyExportBundle { week: string; files: Record<(typeof BUNDLE_FILES)[number], string>; } function exportedEvents(files: Record): BaseEvent[] { return Object.values(files).flat().filter((value): value is BaseEvent => Boolean(value) && typeof value === "object" && typeof (value as BaseEvent).eventType === "string" && typeof (value as BaseEvent).taskId === "string"); } function assemblePreview(files: Record): Omit { const events = exportedEvents(files); return { files, metrics: aggregateWeeklyMetrics(events), cohorts: buildWeeklyCohorts(events), experiments: events.filter((event) => event.eventType.startsWith("experiment.")) }; } export function previewExport(events: BaseEvent[], secret: Buffer): ExportPreview { const files: Record = { "tasks.jsonl": [], "agents.jsonl": [], "model-calls.jsonl": [], "tools.jsonl": [], "interactions.jsonl": [], "verifications.jsonl": [] }; let redactedFields = 0; for (const [index, event] of events.entries()) { const privacyClass = (event.privacyClass as "public" | "internal" | "restricted" | "secret" | undefined) ?? "restricted"; const sanitized = { ...(sanitizeValue(event, secret, privacyClass) as Record), eventSequence: index + 1 }; redactedFields += countRedactions(sanitized); const file = event.eventType.startsWith("agent.") || event.eventType === "attribution.created" ? "agents.jsonl" : event.eventType.startsWith("model.call") ? "model-calls.jsonl" : event.eventType.startsWith("tool.") ? "tools.jsonl" : event.eventType.startsWith("handoff") || event.eventType === "blackboard.event" ? "interactions.jsonl" : event.eventType.startsWith("verification") ? "verifications.jsonl" : "tasks.jsonl"; files[file]!.push(sanitized); } return { ...assemblePreview(files), privacyReport: { events: events.length, redactedFields } }; } function weekStart(week: string): number { const match = /^(\d{4})-W(\d{2})$/.exec(week); if (!match) throw new Error("Week must be YYYY-Www"); const year = Number(match[1]); const number = Number(match[2]); if (number < 1 || number > 53) throw new Error("Week must be YYYY-Www"); const januaryFourth = Date.UTC(year, 0, 4); const monday = januaryFourth - (((new Date(januaryFourth).getUTCDay() || 7) - 1) * 86_400_000); const start = monday + ((number - 1) * 7 * 86_400_000); if (new Date(start + 3 * 86_400_000).getUTCFullYear() !== year) throw new Error("Week does not exist in the requested ISO year"); return start; } function forWeek(preview: ExportPreview, week: string): ExportPreview { const start = weekStart(week); const end = start + 7 * 86_400_000; const files = Object.fromEntries(Object.entries(preview.files).map(([file, rows]) => [file, rows.filter((row) => { const timestamp = typeof row === "object" && row && typeof (row as Record).timestamp === "string" ? Date.parse((row as Record).timestamp as string) : NaN; return Number.isFinite(timestamp) && timestamp >= start && timestamp < end; })])); const selected = assemblePreview(files); return { ...selected, privacyReport: { events: exportedEvents(files).length, redactedFields: countRedactions(files) } }; } function json(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n`; } function jsonl(rows: readonly unknown[]): string { return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length ? "\n" : ""); } export function buildWeeklyExportBundle(week: string, preview: ExportPreview, config: UltraConfig, catalog: PriceCatalog): WeeklyExportBundle { const weekly = forWeek(preview, week); const events = exportedEvents(weekly.files).sort((left, right) => Number(left.eventSequence) - Number(right.eventSequence)); const profiles = Object.entries(weekly.metrics.profileDistribution).map(([profile, count]) => `${profile}=${count}`).join(", ") || "none"; const topologies = Object.entries(weekly.metrics.topologyDistribution).map(([topology, count]) => `${topology}=${count}`).join(", ") || "none"; const models = Object.entries(weekly.metrics.modelDistribution).map(([model, count]) => `${model}=${count}`).join(", ") || "none"; const regret = weekly.metrics.routingRegret; const readme = `# UltraPi weekly export ${week}\n\n- Period: ${week}\n- Tasks: ${weekly.metrics.tasks}\n- Events: ${events.length}\n- Profiles: ${profiles}\n- Total credits: ${weekly.metrics.totalCredits}\n- Total tokens: ${weekly.metrics.totalTokens}\n- Verified success: ${weekly.metrics.verifiedSuccessfulTasks}/${weekly.metrics.tasks} (${weekly.metrics.verifiedSuccessRate})\n- Human rework: ${weekly.metrics.feedback.fixed}/${weekly.metrics.tasks} (${weekly.metrics.humanReworkRate})\n- Routing regret candidates: over-spawn=${regret.overSpawnCandidates}, under-spawn=${regret.underSpawnCandidates}, model-overkill=${regret.modelOverkillCandidates}, model-underpower=${regret.modelUnderpowerCandidates}, misscoped-role=${regret.misscopedRoleCandidates}\n- Topologies: ${topologies}\n- Models: ${models}\n- Sanitizer redactions: ${weekly.privacyReport.redactedFields}\n- Data quality: ${weekly.metrics.dataQualityLimitations.join(" ")}\n- Canonical log: events.jsonl. Join tasks by taskId/runId and agents by taskId/nodeId.\n- Privacy: sanitized local telemetry only; no source, paths, tool arguments/output, provider payload, or reasoning.\n`; const manifest = { schemaVersion: 1, week, canonicalLog: "events.jsonl", orderKey: "eventSequence", eventCount: events.length, routingRegretPolicy: ROUTING_REGRET_POLICY, joinKeys: { task: ["taskId", "runId"], agent: ["taskId", "nodeId"], delegation: ["taskId", "fromNodeId", "toNodeId"], modelCall: ["taskId", "nodeId", "callIndex"], toolCall: ["taskId", "toolCallHash"] }, views: { tasks: "tasks.jsonl", agents: "agents.jsonl", modelCalls: "model-calls.jsonl", tools: "tools.jsonl", interactions: "interactions.jsonl", verifications: "verifications.jsonl" }, privacy: { sanitized: true, rawVaultIncluded: false, taskTextIncluded: false, sourceCodeIncluded: false, pathsIncluded: false, toolArgumentsIncluded: false, toolOutputIncluded: false, providerPayloadIncluded: false, reasoningIncluded: false }, }; return { week, files: { "README.md": readme, "manifest.json": json(manifest), "events.jsonl": jsonl(events), "tasks.jsonl": jsonl(weekly.files["tasks.jsonl"] ?? []), "agents.jsonl": jsonl(weekly.files["agents.jsonl"] ?? []), "model-calls.jsonl": jsonl(weekly.files["model-calls.jsonl"] ?? []), "tools.jsonl": jsonl(weekly.files["tools.jsonl"] ?? []), "interactions.jsonl": jsonl(weekly.files["interactions.jsonl"] ?? []), "verifications.jsonl": jsonl(weekly.files["verifications.jsonl"] ?? []), "metrics.json": json(weekly.metrics), "cohorts.json": json(weekly.cohorts), "config.json": json(redactConfig(config)), "price-catalog.json": json(catalog), "experiments.json": json(weekly.experiments), "privacy-report.json": json(weekly.privacyReport), }, }; } export async function writeWeeklyExport(root: string, bundle: WeeklyExportBundle, approved: boolean): Promise { if (!approved) throw new Error("Weekly export requires explicit approval"); weekStart(bundle.week); const directory = join(root, bundle.week); const staging = `${directory}.staging-${randomUUID()}`; await mkdir(staging, { recursive: true, mode: 0o700 }); try { await Promise.all(BUNDLE_FILES.map((file) => writeFile(join(staging, file), bundle.files[file], { encoding: "utf8", mode: 0o600 }))); await rename(staging, directory); return directory; } catch (error) { await rm(staging, { recursive: true, force: true }); throw error; } }