Something went wrong. Try again.
Experimental TypeScript workflow DSL for the Letta Agent SDK
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254/* * Security sweep: audit files for real weaknesses, then aggregate through a monitor. * * Usage: * bun examples/security-sweep.ts <directory> [--model HANDLE] [--max 8] [--concurrency 3] [--out FILE] [--print] [--receipt FILE] * * Requires LETTA_AGENT_ID or AGENT_ID in the environment. Hunters are NOT told * to expect a vulnerability — a clean file is a valid result, so the sweep does * not manufacture findings. The security monitor aggregates reports into a * single hardening plan; a machine-readable receipt (counts and outcomes only, * no source or credentials) is written next to the report. Fresh conversations * without an explicit model run stateless; --model uses SDK conversation * overrides and never changes the root agent default. */
import { chmod, mkdir, writeFile } from "node:fs/promises";import { basename, dirname, relative, resolve } from "node:path";
import { flow, type FlowEvent } from "../src/index.js";import { DEFAULT_EXTENSIONS, countSummary, discover, hunterPrompt, huntReportParser, modelSummary, monitorPrompt, sweepReceipt, uniqueModels, type HuntReport, type SweepRecord, type SweepReceipt,} from "./security-sweep-lib.js";
// Hunters and the monitor only analyze embedded source; they get no client// tools, which also means they can never block on permission approvals.const readonlySession = { toolset: { base: "none" as const }, allowedTools: [],};
const useColor = process.stderr.isTTY === true && process.env.NO_COLOR === undefined;const color = { green: (text: string) => paint("32", text), red: (text: string) => paint("31", text), yellow: (text: string) => paint("33", text), cyan: (text: string) => paint("36", text), dim: (text: string) => paint("2", text),};
const target = resolve(process.argv[2] ?? ".");const maxFiles = numberFlag("max", 8);const concurrency = numberFlag("concurrency", 3);const model = stringFlag("model");const outPath = stringFlag("out");const receiptPath = stringFlag("receipt");const printReport = process.argv.includes("--print");const extensions = new Set(DEFAULT_EXTENSIONS);
const candidates = (await discover(target, extensions)).slice(0, maxFiles);if (candidates.length === 0) { throw new Error(`No supported source files found under ${target}`);}banner("Security sweep");kv("target", target);kv("files", `${candidates.length}: ${candidates.map((file) => relative(target, file)).join(", ")}`);kv("model", model ?? "agent default");
const final = await flow({ concurrency, onEvent: renderEvent }, async ($) => { const hunts = await $.phase("Hunt", () => $.map(candidates, async (path) => $.fresh({ label: `hunt:${relative(target, path)}`, ...(model ? { model } : { stateless: true }), session: readonlySession, output: huntReportParser(path), prompt: await hunterPrompt(target, path), }), ), );
for (const result of hunts.failures()) { const conversation = result.run.conversationId ?? "unknown conversation"; line( color.red("failed"), color.dim(conversation), color.red(`(${result.error.code}: ${result.error.message})`), ); }
const reports = hunts.values(); if (reports.length === 0) { const codes = hunts .failures() .map((failure) => `${failure.error.code}: ${failure.error.message}`) .join("; "); if (/Agent not found|404/.test(codes)) { console.error( color.yellow( `\nhint: hunter conversations could not find this agent. An inherited LETTA_API_KEY may\nscope to a workspace without it. Run ./examples/security-sweep.sh instead (it unsets\nthe key so device credentials are used), or: unset LETTA_API_KEY && bun examples/security-sweep.ts <dir>`, ), ); } throw new Error("Every hunter conversation failed"); } line( color.dim("hunt complete"), `${reports.length}/${candidates.length} reports · ${countSummary(reports)}`, );
const monitor = await $.phase("Monitor", () => $.fresh({ label: "security-monitor", ...(model ? { model } : { stateless: true }), session: readonlySession, prompt: monitorPrompt(target, candidates, reports, hunts.failures().length), }), );
if (monitor.ok) { const hunterModels = uniqueModels(hunts.all().map((result) => result.run.model)); const record: SweepRecord = { generatedAt: new Date().toISOString(), target, files: candidates, reports, failures: hunts.failures().map((failure) => ({ code: failure.error.code, message: failure.error.message, ...(failure.run.conversationId ? { conversationId: failure.run.conversationId } : {}), })), models: { hunters: hunterModels, ...(monitor.run.model ? { monitor: monitor.run.model } : {}), }, ...(monitor.run.conversationId ? { monitorConversationId: monitor.run.conversationId } : {}), }; const paths = await persist(monitor.value, record, outPath, receiptPath); banner("Results"); kv("findings", countSummary(reports)); kv("models", modelSummary(hunterModels, monitor.run.model)); kv("report", paths.report); kv("data", paths.data); kv("receipt", paths.receipt); kv("monitor", monitor.run.conversationId ?? "unknown"); }
return monitor;});
if (!final.ok) { console.error(`Flow failed: ${final.error.message}`); process.exit(1);}
if (printReport) console.log(`\n${final.value}`);
async function persist( reportMarkdown: string, record: SweepRecord, explicitOut?: string, explicitReceipt?: string,): Promise<{ report: string; data: string; receipt: string }> { const stamp = record.generatedAt.replace(/[:.]/g, "-"); const targetName = basename(record.target).replace(/[^a-zA-Z0-9._-]+/g, "-") || "sweep"; const requested = explicitOut ?? `security-sweeps/${stamp}-${targetName}.md`; const reportPath = resolve(requested.endsWith(".md") ? requested : `${requested}.md`); const dataPath = reportPath.replace(/\.md$/, ".json"); const receiptPath = resolve( explicitReceipt ?? reportPath.replace(/\.md$/, ".receipt.json"), ); const metadata = [ "<!-- Generated by flows security-sweep", `timestamp: ${record.generatedAt}`, `target: ${record.target}`, `monitor-conversation: ${record.monitorConversationId ?? "unknown"}`, "-->", "", ].join("\n");
const receipt: SweepReceipt = sweepReceipt(record); await mkdir(dirname(reportPath), { recursive: true }); await writeFile(reportPath, `${metadata}${reportMarkdown.trim()}\n`, "utf8"); await writeFile(dataPath, `${JSON.stringify(record, null, 2)}\n`, "utf8"); await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, "utf8"); await Promise.all([ chmod(reportPath, 0o600), chmod(dataPath, 0o600), chmod(receiptPath, 0o600), ]); return { report: reportPath, data: dataPath, receipt: receiptPath };}
function numberFlag(name: string, fallback: number): number { const index = process.argv.indexOf(`--${name}`); const value = index >= 0 ? Number(process.argv[index + 1]) : NaN; return Number.isFinite(value) && value > 0 ? value : fallback;}
function stringFlag(name: string): string | undefined { const index = process.argv.indexOf(`--${name}`); if (index < 0) return undefined; const value = process.argv[index + 1]; if (!value || value.startsWith("--")) { throw new Error(`--${name} requires a value`); } return value;}
function paint(code: string, text: string): string { return useColor ? `\u001b[${code}m${text}\u001b[0m` : text;}
function banner(title: string): void { const width = Math.min(Math.max(process.stderr.columns ?? 72, 48), 100); const rule = "─".repeat(Math.max(2, width - title.length - 4)); console.error(`\n${color.cyan(`── ${title} `)}${color.dim(rule)}`);}
function line(...parts: string[]): void { console.error(` ${parts.filter(Boolean).join(" ")}`);}
function kv(key: string, value: string): void { line(color.dim(key.padEnd(9)), value);}
function formatDuration(durationMs: number): number | string { return durationMs >= 1_000 ? `${(durationMs / 1_000).toFixed(1)}s` : `${durationMs}ms`;}
function renderEvent(event: FlowEvent): void { if (event.type === "phase.started") { banner(event.name); } if (event.type === "operation.completed") { const result = event.result; const label = result.run.label ?? result.run.operation; line( result.ok ? color.green("✓") : color.red("×"), label, color.dim( [formatDuration(result.run.durationMs), result.run.model] .filter(Boolean) .join(" · "), ), ); }}