agent.libx.js 0.94.24 → 0.94.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,10 @@ type SpawnFn = (command: string, args: string[], options: {
26
26
  cwd?: string;
27
27
  env?: Record<string, string | undefined>;
28
28
  signal?: AbortSignal;
29
+ /** stdio layout. We force stdin to /dev/null so a child can't block on (or steal) the REPL's input. */
30
+ stdio?: ['ignore', 'pipe', 'pipe'];
31
+ /** Run in a new session/process group (setsid) — detaches from the controlling terminal. See DETACHED. */
32
+ detached?: boolean;
29
33
  }) => SpawnedProcess;
30
34
  /** Minimal `ChildProcess` surface this tool uses. */
31
35
  interface SpawnedProcess {
@@ -38,6 +42,8 @@ interface SpawnedProcess {
38
42
  on(ev: 'close', cb: (code: number | null) => void): void;
39
43
  on(ev: 'error', cb: (err: Error) => void): void;
40
44
  kill(signal?: string): void;
45
+ /** Child PID — present on the real node child; used to signal the whole process group on abort. */
46
+ pid?: number;
41
47
  }
42
48
  interface RealShellOptions {
43
49
  /** Working directory the shell is bound to (typically a NodeDiskFilesystem `baseDir`). Required. */
@@ -283,6 +283,16 @@ async function findSandboxWrapper(platform = process.platform) {
283
283
  // src/tools.shell.ts
284
284
  var log3 = forComponent("shell");
285
285
  var clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
286
+ var DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
287
+ function killGroup(proc, signal) {
288
+ if (!proc?.pid) return false;
289
+ try {
290
+ process.kill(-proc.pid, signal);
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
286
296
  async function spawnArgvFor(command, cwd, osSandbox) {
287
297
  if (!osSandbox) return { bin: "/bin/sh", args: ["-c", command] };
288
298
  const opts = osSandbox === true ? {} : osSandbox;
@@ -322,7 +332,7 @@ var ShellJobRegistry = class {
322
332
  try {
323
333
  const spawn = this.cfg.spawn ?? await nodeSpawn();
324
334
  const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
325
- const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
335
+ const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });
326
336
  job.proc = proc;
327
337
  proc.stdout?.on("data", append);
328
338
  proc.stderr?.on("data", append);
@@ -361,9 +371,11 @@ var ShellJobRegistry = class {
361
371
  const j = this.jobs.get(id);
362
372
  if (!j) return false;
363
373
  if (j.status === "running") {
364
- try {
365
- j.proc?.kill("SIGTERM");
366
- } catch {
374
+ if (!killGroup(j.proc, "SIGTERM")) {
375
+ try {
376
+ j.proc?.kill("SIGTERM");
377
+ } catch {
378
+ }
367
379
  }
368
380
  j.status = "killed";
369
381
  }
@@ -377,7 +389,7 @@ function makeRealShellTool(options) {
377
389
  const timeoutMs = options.timeoutMs ?? 12e4;
378
390
  return {
379
391
  name: "Shell",
380
- description: "Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. Set `background:true` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.",
392
+ description: "Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input fail fast rather than hang \u2014 for privileged actions use a non-interactive flag (e.g. `sudo -n`), or ask the user to run the command themselves. Set `background:true` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.",
381
393
  parameters: {
382
394
  type: "object",
383
395
  required: ["command"],
@@ -437,10 +449,12 @@ function makeRealShellTool(options) {
437
449
  };
438
450
  let proc;
439
451
  try {
440
- proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
452
+ proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });
441
453
  } catch (e) {
442
454
  return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
443
455
  }
456
+ if (ctl.signal.aborted) killGroup(proc, "SIGKILL");
457
+ else ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
444
458
  const collect = (chunk) => {
445
459
  const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
446
460
  out += s;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tools.ts","../src/redact.ts","../src/logging.ts","../src/tools.web.ts","../src/shell.sandbox.ts","../src/tools.shell.ts"],"sourcesContent":["import type { IFilesystem } from '@livx.cc/wcli/core';\nimport { CommandExecutor, registerHeadlessCommands } from '@livx.cc/wcli/core';\nimport type { Tool, ChatLike } from './llm';\nimport { grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool, fuzzyLineReplace } from './tools.structured';\nimport { todoWriteTool, type TodoItem } from './todo';\nimport { webFetchTool, webSearchTool } from './tools.web';\nimport { OverlayFilesystem } from './OverlayFilesystem';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\nimport type { SandboxJobRegistry } from './tools.jobs';\n\n/** A structured multiple-choice question the model can pose to a human. */\nexport interface UserQuestion {\n question: string;\n header?: string;\n options: { label: string; description?: string }[];\n multiSelect?: boolean;\n}\n\n/**\n * The host / human-in-the-loop seam (the \"third seam\" beyond LLM + filesystem).\n * Injected per host: a CLI reads stdin, a browser renders a dialog, edge/headless\n * omits it. Unifies user-questions, permission/plan approvals, and notifications.\n */\nexport type HostEvent =\n | { kind: 'text_delta'; message: string }\n | { kind: 'thinking_delta'; message: string }\n | { kind: 'tool_use'; id: string; name: string; input: unknown }\n | { kind: 'tool_result'; id: string; output: string; isError?: boolean }\n | { kind: 'tool_result_image'; id: string; dataUrl: string }\n | { kind: string; message: string; data?: unknown };\n\nexport interface HostBridge {\n /** Ask the user a structured question; resolve to the chosen label(s) / free text. */\n ask?(q: UserQuestion): Promise<string>;\n /** Request approval for a sensitive action (permission 'ask' / plan approval). */\n confirm?(prompt: string, meta?: { tool: string; input: unknown }): Promise<boolean>;\n /** Emit a progress / notification event to the host UI (non-blocking). */\n notify?(event: HostEvent): void;\n}\n\nexport interface ToolContext {\n fs: IFilesystem;\n exec: CommandExecutor;\n /** path -> content snapshot at last Read/Edit; powers the read-before-edit staleness guard. */\n readState: Map<string, string>;\n /** optional host interaction channel; absent => autonomous/headless. */\n host?: HostBridge;\n /** optional run-cancellation signal (mirrors AgentOptions.signal); lets abort-aware tools\n * (e.g. the real shell) kill in-flight work when the run is cancelled. */\n signal?: AbortSignal;\n /** the agent's working todo list (TodoWrite planning aid); replaced wholesale per call. */\n todos: TodoItem[];\n /** optional syntax guardrail: if set, write-class tools refuse to persist a broken result. */\n lint?: (path: string, content: string) => string | null;\n /** optional PDF text extraction (node hosts wire a pdftotext-backed impl); absent => Read explains. */\n pdfText?: (path: string) => Promise<string>;\n /** optional model handle for tools that run their own LLM pass (e.g. Review, a self-critique).\n * Populated by the Agent from its own ai/model; absent => such tools degrade to a no-op. */\n ai?: ChatLike;\n model?: string;\n /** optional sandbox background-job registry; enables `bash({background:true})`. Absent => no backgrounding. */\n jobs?: SandboxJobRegistry;\n /** optional incremental-output channel: long-running tools (e.g. the real Shell) stream chunks\n * here mid-run. Wired PER CALL by Agent.dispatch to Hooks.onToolOutput (cleared when the call\n * settles — a late emit is a silent no-op). Fire-and-forget: never awaited, never part of the result. */\n emit?: (chunk: string) => void;\n /** Wrap a HUMAN-blocking await (permission/plan confirm, an interactive question) so the time\n * spent parked on the user is excluded from the run's wall-clock kill-switches. Wired by the Agent;\n * absent => no accounting (the promise is awaited as-is). Idle prompt time must not count as work. */\n parkHuman?<T>(p: Promise<T>): Promise<T>;\n}\n\nexport interface AgentTool {\n name: string;\n description: string;\n parameters: object; // JSON Schema for the function's arguments\n run(args: any, ctx: ToolContext): Promise<string | { text: string; images?: { mimeType: string; data: string }[] }>;\n}\n\n/** Build a tool context bound to a filesystem backend (Mem / Disk / …) and an optional host. */\nexport function makeContext(fs: IFilesystem, host?: HostBridge): ToolContext {\n const exec = new CommandExecutor(fs);\n registerHeadlessCommands(exec);\n return { fs, exec, readState: new Map(), host, todos: [] };\n}\n\n/** Convert AgentTools into the ai.libx.js `tools` array for chat(). */\nexport function toWireTools(tools: AgentTool[]): Tool[] {\n return tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n}\n\nconst numberLines = (content: string, offset = 0, limit?: number): string => {\n const lines = content.split('\\n');\n const start = Math.max(0, offset);\n const end = limit != null ? start + limit : lines.length;\n return lines\n .slice(start, end)\n .map((l, i) => `${start + i + 1}\\t${l}`)\n .join('\\n');\n};\n\n/** Keep huge tool output high-signal: head+tail with an omission marker (cuts re-runs to \"parse the wall\"). */\nexport function truncateOutput(s: string, headLines = 80, tailLines = 20): string {\n const lines = s.split('\\n');\n if (lines.length <= headLines + tailLines + 1) return s;\n const omitted = lines.length - headLines - tailLines;\n return [...lines.slice(0, headLines), `… (${omitted} lines omitted — narrow the command to see more) …`, ...lines.slice(-tailLines)].join('\\n');\n}\n\n/** Run any shell command line over the VFS (ls/cat/grep/find/head/tail/echo/mkdir/rm/mv/wc, pipes, redirects, &&/||/;). */\nexport const bashTool: AgentTool = {\n name: 'bash',\n description:\n 'Run a shell command. Supports ls, cat, grep, find, head, tail, echo, mkdir, rm, mv, cp, wc, pipes (|), redirects (>, >>), and chaining (&&, ||, ;). Best for: running tests/builds, file operations (mkdir/mv/rm), and piped workflows. For searching file contents, prefer `Grep` (structured results, no re-parse). For finding files by name, prefer `Glob`.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the command line to execute' },\n background: { type: 'boolean', description: 'run detached over an isolated overlay (writes commit when it finishes); returns a job id to poll with JobOutput. Only worth it for slow work (remote VFS / long pipelines).' },\n },\n },\n async run({ command, background }, ctx) {\n if (background && ctx.jobs) return startBashJob(String(command ?? ''), ctx);\n const r = await ctx.exec.execute(String(command ?? ''));\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n if (r.exitCode !== 0) {\n const err = (r.error ?? '').trim();\n return `[exit ${r.exitCode}]${err ? ' ' + err : ''}${out ? '\\n' + out : ''}`;\n }\n return out || '(command succeeded, no output)'; // explicit sentinel: don't re-run to \"check\"\n },\n};\n\n/** Kick a bash command into the background over an isolated overlay; its writes commit only on success.\n * A kill (abort) before completion skips the commit — the parent VFS is never touched mid-flight. */\nfunction startBashJob(command: string, ctx: ToolContext): string {\n const baseFs = ctx.fs;\n const id = ctx.jobs!.start(\n async ({ signal }) => {\n const overlay = new OverlayFilesystem(baseFs);\n const exec = new CommandExecutor(overlay);\n registerHeadlessCommands(exec);\n const r = await exec.execute(command); // wcli is sync-to-completion; abort can only gate the commit below\n if (signal.aborted) return '[killed before commit]';\n await overlay.commit(); // atomically flush this job's writes down into the parent VFS\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n return r.exitCode !== 0 ? `[exit ${r.exitCode}] ${(r.error ?? '').trim()}\\n${out}`.trim() : out || '(command succeeded, no output)';\n },\n { kind: 'bash', label: command.slice(0, 60) },\n );\n return `Started background job ${id} — poll with JobOutput({id:\"${id}\"}) / JobStatus, stop with JobKill.`;\n}\n\n/** Image extensions the Read tool returns as a visual block (when the fs can read bytes). */\nconst IMG_MIME: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };\n\n/** Read a text file as 1-indexed numbered lines; arms the staleness guard for Edit. */\nexport const readTool: AgentTool = {\n name: 'Read',\n description:\n 'Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.',\n parameters: {\n type: 'object',\n required: ['path'],\n properties: {\n path: { type: 'string' },\n offset: { type: 'number' },\n limit: { type: 'number' },\n },\n },\n async run({ path, offset, limit }, ctx) {\n // Image file → return it as a visual block. The adapter turns a tool result whose JSON carries a\n // `dataUrl` into an image tool_result the model can see. Needs a binary-capable fs (disk default);\n // the utf8 VFS (sandbox/Mem) can't, so we say so instead of corrupting the bytes.\n const ext = String(path).toLowerCase().split('.').pop() ?? '';\n // PDF → extracted text when the host wired an extractor (CLI: pdftotext); else say how to proceed.\n if (ext === 'pdf') {\n if (!ctx.pdfText) return `[${path} is a PDF — text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`; // jail-aware: hidden paths read as absent\n const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();\n return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;\n }\n if (IMG_MIME[ext]) {\n const fs = ctx.fs as { readFileBytes?: (p: string) => Promise<Uint8Array> };\n if (typeof fs.readFileBytes !== 'function') {\n return `[${path} is an image, but this filesystem can't read binary — attach it as @${path} instead, or run on disk.]`;\n }\n const bytes = await fs.readFileBytes(path);\n const b64 = Buffer.from(bytes).toString('base64');\n return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });\n }\n const raw = await ctx.fs.readFile(path);\n ctx.readState.set(ctx.fs.resolvePath(path), raw); // staleness guard tracks the REAL content\n // Mask secret values in config files so keys can live there usably-but-hidden (line count is preserved).\n const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;\n const total = content === '' ? 0 : content.split('\\n').length;\n const start = Math.max(0, offset ?? 0);\n const body = numberLines(content, start, limit);\n // snippet-with-pointer: when the slice doesn't cover the whole file, tell the\n // model what it's missing + how to pull it — so it expands precisely instead of re-reading blind.\n const shownEnd = limit != null ? Math.min(start + limit, total) : total;\n const shownCount = Math.max(0, shownEnd - start);\n if (shownCount >= total) return body; // whole file shown — no footer\n if (shownCount === 0) return `[no lines in range (offset ${start}${limit != null ? `, limit ${limit}` : ''}) — file has ${total} line(s)]`;\n return `${body}\\n\\n[lines ${start + 1}–${shownEnd} of ${total} · re-Read with offset/limit for the rest]`;\n },\n};\n\n/** Replace an exact, unique substring; requires a prior Read and guards against stale edits. */\nexport const editTool: AgentTool = {\n name: 'Edit',\n description:\n 'Replace an exact substring in a file. Requires a prior Read of the same file. `old_string` must occur exactly once — include surrounding context to disambiguate.',\n parameters: {\n type: 'object',\n required: ['path', 'old_string', 'new_string'],\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n },\n },\n async run({ path, old_string, new_string }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n const current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const count = old_string === '' ? 0 : current.split(old_string).length - 1;\n if (count > 1) throw new Error(`old_string is not unique in ${path} (${count} matches). Provide more surrounding context.`);\n let next: string, note = '';\n if (count === 1) {\n next = current.replace(old_string, () => new_string); // exact: function replacer, no $-pattern expansion\n } else {\n // exact match failed — try a whitespace-tolerant unique match before giving up (cuts re-read churn)\n const fuzzy = fuzzyLineReplace(current, old_string, new_string);\n if (fuzzy == null) throw new Error(`old_string not found in ${path}.`);\n next = fuzzy;\n note = ' (whitespace-tolerant match)';\n }\n if (ctx.lint) { const err = ctx.lint(path, next); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, next);\n ctx.readState.set(key, next);\n return `Edited ${path}${note}`;\n },\n};\n\n/** Session-exit tool: the model calls this when the user wants to end the conversation.\n * The `onExit` callback is injected by the host (CLI sets it to flip a flag that breaks the REPL loop). */\nexport function exitSessionTool(onExit: () => void): AgentTool {\n return {\n name: 'ExitSession',\n description:\n 'End the current session and exit the CLI. Call this when the user says goodbye, asks to quit, ' +\n 'or clearly indicates they want to stop the conversation (e.g. \"ok bye\", \"that\\'s all\", \"exit\", \"goodnight\").',\n parameters: { type: 'object', properties: {} },\n async run() {\n onExit();\n return 'Session ending. Goodbye!';\n },\n };\n}\n\nexport function defaultTools(): AgentTool[] {\n return [bashTool, readTool, editTool];\n}\n\n/**\n * The full catalog of selectable tools, keyed by name. The evolve loop's mutation\n * surface picks from this registry; embedders can build a custom tool set by name.\n */\nexport function toolRegistry(): Record<string, AgentTool> {\n const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool];\n return Object.fromEntries(all.map((t) => [t.name, t]));\n}\n\n/** Resolve a list of tool names against the registry (unknown names throw). */\nexport function toolsByName(names: string[]): AgentTool[] {\n const reg = toolRegistry();\n return names.map((n) => {\n const t = reg[n];\n if (!t) throw new Error(`unknown tool '${n}'. Known: ${Object.keys(reg).join(', ')}`);\n return t;\n });\n}\n","/**\n * Mask secret-looking values in arbitrary text before it reaches the model.\n *\n * Two complementary seams use this: real-shell output (`cat .env`, `printenv`) and the\n * `Read` tool (so provider keys stored in `.agent/settings.json` are usable-but-masked).\n * The FS jail hides whole secret FILES by name; this hides secret VALUES wherever they\n * surface in otherwise-legitimate content.\n *\n * Both regexes are linear (no nested quantifiers) — safe against catastrophic backtracking\n * and cheap enough to run on every tool output (see tests/redact.bench).\n */\n\nexport const REDACTED = '‹redacted›';\n\n/** Config/control files that may carry provider keys — readers (Read/Grep) mask secret VALUES in\n * these while keeping the rest readable. (Whole secret FILES like .env are hidden by the FS jail.) */\nexport const CONFIG_FILE_RE = /(^|\\/)\\.(agent|claude)\\/(settings(\\.[\\w-]+)?\\.json|config\\.(json|js|mjs|cjs|ts))$/i;\n\n// (A) `NAME=value` / `\"name\": \"value\"` pairs where NAME looks like a secret. Masks the value only,\n// so the agent still sees WHICH key exists (useful config context) without the secret itself.\nconst SECRET_PAIR =\n /((?:^|[\\s,{[])(?:export\\s+)?[\"']?[\\w.\\-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_?KEY|AUTH(?:_?TOKEN)?|BEARER)[\\w.\\-]*[\"']?\\s*[:=]\\s*)([\"']?)([^\\s\"',{}\\]]+)/gi;\n\n// (B) Bare tokens by well-known shape — catches secrets that appear without an obvious key\n// (Authorization headers, URLs, JSON dumps). Conservative prefixes to avoid false positives.\nconst SECRET_TOKEN =\n /\\b(sk-ant-[\\w-]{12,}|sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[\\w]{20,}|xox[baprs]-[\\w-]{10,}|AKIA[0-9A-Z]{12,}|AIza[\\w-]{20,}|eyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,})\\b/g;\n\n/** Return `text` with secret values masked. Cheap no-op when nothing matches. */\nexport function redactSecrets(text: string): string {\n if (!text) return text;\n return text\n .replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`)\n .replace(SECRET_TOKEN, REDACTED);\n}\n","// Import the log module directly from libx.js source: libx.js's main bundle\n// doesn't re-export `log` as a named ESM export, and source-importing keeps\n// libx.js patches live (no rebuild) — matching the `bun link` workflow.\nimport { log } from 'libx.js/src/modules/log';\n\n/** Component-scoped logger (libx.js). debug/verbose gated via DEBUG env/localStorage. */\nexport const forComponent = (name: string) => log.forComponent(name);\nexport { log };\n","import type { AgentTool } from './tools';\nimport { forComponent } from './logging';\n\n/**\n * Web tools — `WebFetch` (retrieve a URL as readable text) and `WebSearch` (ranked\n * results via a configured provider). Opt-in (NOT in the default tool set): network\n * access is a deliberate capability. Factory-built with an injectable `fetch` so they\n * stay edge-portable and unit-testable without real network. `fetch` is read at call\n * time, so a no-network runtime simply has the tool return an error.\n */\nconst log = forComponent('web');\n\n/** Strip HTML to readable text — dependency-free: drop script/style/comments, block tags → newlines, decode common entities. */\nexport function htmlToText(html: string): string {\n let s = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<title[\\s\\S]*?<\\/title>/gi, ' ') // drop title text (don't leak it into context)\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ') // …same for noscript / textarea content\n .replace(/<textarea[\\s\\S]*?<\\/textarea>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<\\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, '\\n')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ');\n s = s\n .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#0?39;/g, \"'\").replace(/&#x27;/gi, \"'\");\n return s\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .split('\\n').map((l) => l.trim()).join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nexport interface WebFetchOptions {\n /** Override the global fetch (tests inject a mock; edge runtimes can supply their own). */\n fetch?: typeof globalThis.fetch;\n maxBytes?: number; // cap the downloaded body (default 2 MB)\n maxChars?: number; // cap the returned text (default 100k)\n timeoutMs?: number; // request timeout (default 15s)\n /** Allow fetching private/loopback/link-local hosts (default false — blocks basic SSRF). */\n allowPrivateHosts?: boolean;\n}\n\n/**\n * Block obvious SSRF targets by hostname/IP literal (loopback, private ranges, link-local incl.\n * cloud metadata 169.254.169.254, `.internal`). Pure/edge-safe — no DNS, so DNS-rebinding and\n * redirect-to-internal are NOT covered (an embedder needing that should supply a vetting `fetch`).\n */\nexport function isPrivateHost(host: string): boolean {\n const h = host.toLowerCase().replace(/^\\[|\\]$/g, ''); // strip IPv6 brackets\n if (h === '' || h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.internal')) return true;\n if (h === '::1' || h === '::' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; // IPv6 loopback/link-local/ULA\n const m = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (m) {\n const a = +m[1], b = +m[2];\n return a === 0 || a === 127 || a === 10 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n return false;\n}\n\n/** Lazily-loaded node DNS resolver (absent on edge/browser) — closes DNS-rebinding (a public\n * hostname resolving to an internal IP) on the real-network path. Resolves null where unavailable. */\nlet _dnsLookup: ((h: string, opts?: any) => Promise<{ address: string }[]>) | null | undefined;\nasync function resolveIps(host: string): Promise<string[] | null> {\n if (_dnsLookup === undefined) {\n try { _dnsLookup = (await import('node:dns/promises')).lookup as any; }\n catch { _dnsLookup = null; } // edge/browser: no DNS — rely on the literal isPrivateHost check\n }\n if (!_dnsLookup) return null;\n try { return (await _dnsLookup(host, { all: true } as any)).map((a) => a.address); } catch { return null; }\n}\n\n/** Read a response body but stop at `maxBytes` of ACTUAL bytes (cancel the stream) — no unbounded download. */\nasync function readCapped(res: Response, maxBytes: number): Promise<string> {\n const reader = (res.body as any)?.getReader?.();\n if (!reader) { const t = await res.text(); return t.length > maxBytes ? t.slice(0, maxBytes) : t; }\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) { chunks.push(value); total += value.length; }\n if (total >= maxBytes) { try { await reader.cancel(); } catch { /* already closed */ } break; }\n }\n const out = new Uint8Array(Math.min(total, maxBytes));\n let off = 0;\n for (const c of chunks) { if (off >= out.length) break; const take = Math.min(c.length, out.length - off); out.set(c.subarray(0, take), off); off += take; }\n return new TextDecoder().decode(out);\n}\n\n/** Build a WebFetch tool. */\nexport function makeWebFetchTool(options: WebFetchOptions = {}): AgentTool {\n const maxBytes = options.maxBytes ?? 2_000_000;\n const maxChars = options.maxChars ?? 100_000;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebFetch',\n description:\n 'Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.',\n parameters: { type: 'object', required: ['url'], properties: { url: { type: 'string', description: 'absolute http(s) URL' } } },\n async run({ url }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n const customFetch = !!options.fetch; // injected fetch (tests/edge) owns its own vetting → skip DNS\n const u = String(url ?? '');\n try { new URL(u); } catch { return `Error: invalid URL: ${u}`; }\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n // Reject a host that's a private/internal IP literal, or (on the real-network path) a name that\n // RESOLVES to one — re-checked on EVERY redirect hop so an external page can't bounce us internal.\n const hostBlock = async (hostname: string): Promise<string | null> => {\n if (options.allowPrivateHosts) return null;\n if (isPrivateHost(hostname)) return hostname;\n if (!customFetch) { const ips = await resolveIps(hostname); if (ips) for (const ip of ips) if (isPrivateHost(ip)) return `${hostname} → ${ip}`; }\n return null;\n };\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n let current = u;\n let res: Response;\n for (let hop = 0; ; hop++) {\n const pu = new URL(current);\n if (pu.protocol !== 'http:' && pu.protocol !== 'https:') return `Error: only http/https URLs are allowed (got \"${pu.protocol}\")`;\n const blocked = await hostBlock(pu.hostname);\n if (blocked) return `Error: refusing to fetch a private/internal address (${blocked}) — set allowPrivateHosts to override`;\n res = await doFetch(current, { signal: ctl.signal, redirect: 'manual', headers: { 'user-agent': 'agentx (+https://github.com/Livshitz/agent.libx.js)' } });\n if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n if (hop >= 5) return `Error fetching ${u}: too many redirects`;\n current = new URL(res.headers.get('location')!, current).toString(); // re-validated at loop top\n continue;\n }\n break;\n }\n const type = res.headers.get('content-type') ?? '';\n const body = await readCapped(res, maxBytes);\n const text = /html/i.test(type) || /^\\s*<(?:!doctype|html)/i.test(body) ? htmlToText(body) : body.trim();\n const capped = text.length > maxChars ? text.slice(0, maxChars) + `\\n… [truncated at ${maxChars} chars]` : text;\n return `${res.status} ${res.statusText} · ${new URL(current).host}\\n\\n${capped}`;\n } catch (e: any) {\n log.debug(`WebFetch ${u} failed`, e);\n return `Error fetching ${u}: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\nexport interface WebSearchOptions {\n fetch?: typeof globalThis.fetch;\n /** Provider: 'auto' (default) uses Tavily if an API key is present, else keyless DuckDuckGo. */\n provider?: 'auto' | 'tavily' | 'duckduckgo';\n /** API key for Tavily (default: process.env.TAVILY_API_KEY). */\n apiKey?: string;\n /** Tavily endpoint override. */\n endpoint?: string;\n maxResults?: number; // default 5\n timeoutMs?: number; // default 15s\n}\n\ninterface SearchHit { title: string; url: string; snippet: string }\n\n/** Decode a DuckDuckGo HTML result href: results are `//duckduckgo.com/l/?uddg=<encoded-target>` redirects. */\nexport function decodeDdgUrl(href: string): string {\n const m = href.match(/[?&]uddg=([^&]+)/);\n if (m) { try { return decodeURIComponent(m[1]); } catch { /* fall through */ } }\n return href.startsWith('//') ? 'https:' + href : href;\n}\n\n/** Parse DuckDuckGo's HTML results page into hits (title/url/snippet) — dependency-free, zips anchors to snippets in order. */\nexport function parseDdgHtml(html: string, max: number): SearchHit[] {\n const anchors = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__a[^\"]*\"[^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/g)];\n const snippets = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__snippet[^\"]*\"[^>]*>([\\s\\S]*?)<\\/a>/g)].map((m) => htmlToText(m[1]));\n const hits: SearchHit[] = [];\n for (let i = 0; i < anchors.length && hits.length < max; i++) {\n const url = decodeDdgUrl(anchors[i][1]);\n try { if (isPrivateHost(new URL(url).hostname)) continue; } catch { continue; } // skip junk/internal redirects\n hits.push({ title: htmlToText(anchors[i][2]) || '(untitled)', url, snippet: snippets[i] ?? '' });\n }\n return hits;\n}\n\nfunction formatHits(hits: SearchHit[]): string {\n if (!hits.length) return '(no results)';\n return hits.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet.replace(/\\s+/g, ' ').slice(0, 240)}`).join('\\n\\n');\n}\n\n/**\n * Build a WebSearch tool. Keyless by default (DuckDuckGo HTML) so it works in any deployment with\n * no setup; auto-upgrades to Tavily (better, agent-oriented results) when TAVILY_API_KEY is present.\n */\nexport function makeWebSearchTool(options: WebSearchOptions = {}): AgentTool {\n const tavilyEndpoint = options.endpoint ?? 'https://api.tavily.com/search';\n const maxResults = options.maxResults ?? 5;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebSearch',\n description:\n 'Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic — then WebFetch a result URL to read it in full.',\n parameters: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } },\n async run({ query }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n const q = String(query ?? '').trim();\n if (!q) return 'Error: empty query';\n const key = options.apiKey ?? process.env.TAVILY_API_KEY;\n const provider = options.provider ?? 'auto';\n const useTavily = provider === 'tavily' || (provider === 'auto' && !!key);\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n if (useTavily) {\n if (!key) return 'Error: Tavily provider selected but TAVILY_API_KEY is not set';\n const res = await doFetch(tavilyEndpoint, {\n method: 'POST',\n signal: ctl.signal,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ api_key: key, query: q, max_results: maxResults }),\n });\n if (!res.ok) return `Error: search provider returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.results) ? data.results.slice(0, maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.content ?? '') })));\n }\n // Keyless: DuckDuckGo HTML endpoint (no key, edge-portable).\n const res = await doFetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(q), {\n signal: ctl.signal,\n headers: { 'user-agent': 'Mozilla/5.0 (compatible; agentx/1.0; +https://github.com/Livshitz/agent.libx.js)' },\n });\n if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;\n return formatHits(parseDdgHtml(await res.text(), maxResults));\n } catch (e: any) {\n log.debug('WebSearch failed', e);\n return `Error searching: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/** Default instances (registered in the tool registry; opt-in by name). */\nexport const webFetchTool = makeWebFetchTool();\nexport const webSearchTool = makeWebSearchTool();\n","/**\n * Tier-1 OS sandbox for the real `Shell` tool (mind/03-roadmap.md \"capability tiers\").\n *\n * Wraps the spawned `/bin/sh` in the platform's process sandbox so a hostile/buggy command\n * can read the machine but can only WRITE inside an allowlist (cwd + tmp + extra `writePaths`),\n * and gets no network unless granted:\n * - macOS: `sandbox-exec` (seatbelt) with a generated profile\n * - Linux: `bwrap` (bubblewrap) with `--ro-bind / /` + writable binds\n *\n * Pure argv builders (unit-testable, no node imports) + an async wrapper-binary locator.\n * This complements — does not replace — env secret-scrubbing and the permission prompt:\n * the FS jail can't contain a real process; this makes the *process* itself contained.\n */\n\nexport class OsSandboxOptions {\n /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */\n network = false;\n /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */\n writePaths: string[] = [];\n}\n\nexport interface SandboxWrap {\n bin: string;\n args: string[]; // full argv: wrapper flags + /bin/sh -c <command>\n}\n\n/** Writable allowlist shared by both platforms: cwd, the tmp roots, /dev. */\nfunction writable(cwd: string, o: OsSandboxOptions, tmpDir?: string): string[] {\n const set = new Set<string>([cwd, '/tmp', '/private/tmp', '/private/var/folders', '/dev', ...(tmpDir ? [tmpDir] : []), ...o.writePaths]);\n return [...set];\n}\n\nconst sbQuote = (p: string) => `\"${p.replace(/([\"\\\\])/g, '\\\\$1')}\"`;\n\n/** macOS seatbelt profile: allow everything, then deny writes/network, then re-allow the allowlist\n * (seatbelt resolves conflicts by specificity, so subpath allows override the broad deny). */\nexport function seatbeltProfile(cwd: string, o: OsSandboxOptions, tmpDir?: string): string {\n const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(' ');\n return [\n '(version 1)',\n '(allow default)',\n ...(o.network ? [] : ['(deny network*)']),\n '(deny file-write*)',\n `(allow file-write* ${allows})`,\n ].join('\\n');\n}\n\n/** Build the wrapped argv for `sh -c <command>`, or null if `platform` has no supported wrapper. */\nexport function sandboxArgv(command: string, cwd: string, opts: Partial<OsSandboxOptions> = {}, platform: string = process.platform, tmpDir?: string): SandboxWrap | null {\n const o = { ...new OsSandboxOptions(), ...opts };\n if (platform === 'darwin') {\n return { bin: '/usr/bin/sandbox-exec', args: ['-p', seatbeltProfile(cwd, o, tmpDir), '/bin/sh', '-c', command] };\n }\n if (platform === 'linux') {\n const binds = writable(cwd, o, tmpDir).filter((p) => p !== '/dev' && !p.startsWith('/private')).flatMap((p) => ['--bind-try', p, p]);\n return {\n bin: 'bwrap',\n args: ['--ro-bind', '/', '/', ...binds, '--dev', '/dev', '--proc', '/proc', '--die-with-parent', ...(o.network ? [] : ['--unshare-net']), '/bin/sh', '-c', command],\n };\n }\n return null;\n}\n\n/** Locate the wrapper binary for this platform; null = sandboxing unavailable here. */\nexport async function findSandboxWrapper(platform: string = process.platform): Promise<string | null> {\n const { existsSync } = await import('node:fs');\n if (platform === 'darwin') return existsSync('/usr/bin/sandbox-exec') ? '/usr/bin/sandbox-exec' : null;\n if (platform === 'linux') {\n for (const dir of (process.env.PATH ?? '/usr/bin:/bin').split(':')) if (dir && existsSync(`${dir}/bwrap`)) return `${dir}/bwrap`;\n return null;\n }\n return null;\n}\n","import type { AgentTool } from './tools';\nimport { truncateOutput } from './tools';\nimport { redactSecrets } from './redact';\nimport { forComponent } from './logging';\nimport { sandboxArgv, findSandboxWrapper, type OsSandboxOptions } from './shell.sandbox';\n\n/**\n * Real shell tool — node-only, OPT-IN, and deliberately NOT edge-portable.\n *\n * ⚠️ Unlike the default VFS `bash` (a sandboxed JS interpreter over the virtual filesystem),\n * this spawns a REAL `/bin/sh` process. It can run `bun`, `git`, `ssh`, scripts, deploys —\n * and, by the same token, it is NOT sandboxed: only cwd-binding constrains it. It is a\n * deliberate host escalation, kept out of `defaultTools()`/`toolRegistry()` and out of the\n * edge-safe `src/index.ts` (same policy as `mcp.client.ts`). A host opts in explicitly:\n *\n * tools: [...defaultTools(), makeRealShellTool({ cwd: nodeDiskRoot })]\n *\n * Mirrors `tools.web.ts`: a factory with an injectable `spawn` (tests + edge never import\n * node:child_process), an options bag, abort + timeout honored, output capped. Safety beyond\n * cwd-binding is the host's to add (e.g. a PermissionPolicy `decision:'ask'` per command, or\n * an OS sandbox wrapper) — see mind/03-roadmap.md \"OS-level access — capability tiers\".\n */\n\nconst log = forComponent('shell');\n\n/** Normalize shell output for return: trim trailing newlines, mask secret values, then size-truncate.\n * Redaction runs BEFORE truncation so a masked tail can't smuggle a secret past the line cap. */\nconst clean = (s: string): string => truncateOutput(redactSecrets(s.replace(/\\n+$/, '')));\n\n/** The slice of node's `child_process.spawn` we depend on — injectable so tests supply a fake. */\nexport type SpawnFn = (\n command: string,\n args: string[],\n options: { cwd?: string; env?: Record<string, string | undefined>; signal?: AbortSignal },\n) => SpawnedProcess;\n\n/** Minimal `ChildProcess` surface this tool uses. */\nexport interface SpawnedProcess {\n stdout?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n stderr?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n on(ev: 'close', cb: (code: number | null) => void): void;\n on(ev: 'error', cb: (err: Error) => void): void;\n kill(signal?: string): void;\n}\n\nexport interface RealShellOptions {\n /** Working directory the shell is bound to (typically a NodeDiskFilesystem `baseDir`). Required. */\n cwd: string;\n /** Override the spawner (tests inject a fake; default lazily imports node:child_process). */\n spawn?: SpawnFn;\n /** Per-command wall-clock cap (kill on overrun). Default 120s. */\n timeoutMs?: number;\n /** Extra env merged over the (optionally scrubbed) base env for the child. */\n env?: Record<string, string>;\n /** Strip likely-secret vars (API keys, tokens, cloud creds) from the child's env. Default ON.\n * The FS jail does NOT contain a real process, so this is the seam that keeps `echo $ANTHROPIC_API_KEY`\n * from leaking the host's secrets to a spawned command. `false` passes `process.env` through verbatim. */\n redactEnv?: boolean;\n /** Job registry enabling `Shell({background:true})` (long-running processes). Pair with `makeShellJobTools`. */\n registry?: ShellJobRegistry;\n /** Tier-1 OS sandbox: wrap /bin/sh in sandbox-exec (macOS) / bwrap (Linux) — writes confined to\n * cwd+tmp, network blocked unless granted. `true` = defaults; commands FAIL (don't silently run\n * unsandboxed) if no wrapper exists on this platform. See src/shell.sandbox.ts. */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n}\n\n/** Resolve the (bin,args) to spawn for `command`, honoring the optional OS sandbox.\n * Throws when sandboxing was requested but this platform has no wrapper — fail closed. */\nasync function spawnArgvFor(command: string, cwd: string, osSandbox?: boolean | Partial<OsSandboxOptions>): Promise<{ bin: string; args: string[] }> {\n if (!osSandbox) return { bin: '/bin/sh', args: ['-c', command] };\n const opts = osSandbox === true ? {} : osSandbox;\n const wrapper = await findSandboxWrapper();\n const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;\n if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);\n return wrapped;\n}\n\n/** Env var names that look like secrets and are dropped before spawning (unless redactEnv:false). */\nconst SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;\n\n/** Build the child's env: `process.env` minus likely-secrets (when redacting), plus explicit `env`. */\nfunction childEnv(opts: { env?: Record<string, string>; redactEnv?: boolean }): Record<string, string | undefined> {\n const base: Record<string, string | undefined> = {};\n const redact = opts.redactEnv !== false; // default ON\n for (const [k, v] of Object.entries(process.env)) if (!(redact && SECRET_ENV_RE.test(k))) base[k] = v;\n return { ...base, ...opts.env };\n}\n\n/** Lazily resolve node's spawn (kept out of any eager edge import path). */\nlet _spawn: SpawnFn | undefined;\nasync function nodeSpawn(): Promise<SpawnFn> {\n if (!_spawn) _spawn = (await import('node:child_process')).spawn as unknown as SpawnFn;\n return _spawn;\n}\n\n// ---------------------------------------------------------------------------\n// Background jobs — long-running processes the agent starts, polls, and kills.\n// ---------------------------------------------------------------------------\nexport type JobStatus = 'running' | 'exited' | 'killed' | 'error';\n\nexport interface ShellJobConfig {\n cwd: string;\n spawn?: SpawnFn;\n env?: Record<string, string>;\n redactEnv?: boolean;\n /** Tail buffer cap per job (bytes); older output is dropped. Default 256 KB. */\n maxBuffer?: number;\n /** Kill all jobs on process exit (the CLI sets this; tests leave it off to avoid global handlers). */\n killOnExit?: boolean;\n /** Tier-1 OS sandbox for background jobs too (same semantics as RealShellOptions.osSandbox). */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n}\n\ninterface Job { command: string; buf: string; status: JobStatus; exitCode?: number; proc?: SpawnedProcess; }\n\n/**\n * Per-session registry of background `/bin/sh` jobs. Backs `Shell({background:true})` and the\n * `ShellOutput`/`ShellStatus`/`ShellKill` tools. Output accumulates into a tail-capped ring so a\n * chatty process can't OOM. Bounded + killable; the CLI wires `killOnExit` so children are reaped.\n */\nexport class ShellJobRegistry {\n private jobs = new Map<string, Job>();\n private seq = 0;\n constructor(private cfg: ShellJobConfig) {\n if (cfg.killOnExit && typeof process !== 'undefined') process.once('exit', () => this.killAll());\n }\n\n async start(command: string): Promise<string> {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: '', status: 'running' };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max); // ring: keep the tail\n };\n try {\n const spawn = this.cfg.spawn ?? (await nodeSpawn());\n const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: '/bin/sh', args: ['-c', command] };\n const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg) });\n job.proc = proc;\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; } });\n } catch (e: any) {\n job.status = 'error';\n job.buf = `failed to spawn: ${e?.message ?? e}`;\n }\n this.jobs.set(id, job);\n return id;\n }\n\n /** Current tail output for a job (null = no such job). */\n output(id: string): string | null { return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? '' : null); }\n\n status(id: string): { status: JobStatus; exitCode?: number; bytes: number } | null {\n const j = this.jobs.get(id);\n return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;\n }\n\n list(): Array<{ id: string; command: string; status: JobStatus }> {\n return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));\n }\n\n kill(id: string): boolean {\n const j = this.jobs.get(id);\n if (!j) return false;\n if (j.status === 'running') { try { j.proc?.kill('SIGTERM'); } catch { /* already gone */ } j.status = 'killed'; }\n return true;\n }\n\n killAll(): void { for (const id of this.jobs.keys()) this.kill(id); }\n}\n\n/** Build an opt-in real-shell tool bound to `options.cwd`. */\nexport function makeRealShellTool(options: RealShellOptions): AgentTool {\n const timeoutMs = options.timeoutMs ?? 120_000;\n return {\n name: 'Shell',\n description:\n 'Run a shell command via /bin/sh in the working directory. ' +\n 'Executes any installed binary — ls, cat, grep, git, bun, node, curl, scripts, etc. ' +\n 'Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. ' +\n 'Set `background:true` for long-running processes (servers, watchers) — returns a job id immediately; poll with ShellOutput, stop with ShellKill.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the shell command line to execute' },\n background: { type: 'boolean', description: 'run detached and return a job id immediately (for servers/watchers/long builds)' },\n },\n },\n async run({ command, background }, ctx) {\n const cmd = String(command ?? '');\n if (!cmd.trim()) return '[exit 1] empty command';\n if (background) {\n if (!options.registry) return 'Error: background execution is not enabled in this host (no job registry).';\n const id = await options.registry.start(cmd);\n return `Started background job ${id}. Poll output with ShellOutput({id:\"${id}\"}), check ShellStatus({id:\"${id}\"}), stop with ShellKill({id:\"${id}\"}).`;\n }\n const spawn = options.spawn ?? (await nodeSpawn());\n // Sandbox-off keeps this path await-free (after spawn resolution) so an abort racing the call\n // start still lands before listener registration, exactly as pre-sandbox semantics.\n let argv = { bin: '/bin/sh', args: ['-c', cmd] };\n if (options.osSandbox) {\n try {\n argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);\n } catch (e: any) {\n return `[exit 1] ${e?.message ?? e}`; // fail closed — never run unsandboxed when sandboxing was asked for\n }\n }\n // Compose abort: the run's signal (ctx.signal) OR our per-command timeout both kill the child.\n const ctl = new AbortController();\n const onAbort = () => ctl.abort();\n if (ctx.signal) { if (ctx.signal.aborted) ctl.abort(); else ctx.signal.addEventListener('abort', onAbort, { once: true }); }\n let timedOut = false;\n const timer = setTimeout(() => { timedOut = true; ctl.abort(); }, timeoutMs);\n // Incremental output → ctx.emit (when the host listens), coalesced to ≥250ms / ≥1KB batches\n // so a chatty child doesn't spam hooks. Redacted per batch (the final result is re-redacted\n // whole — a secret split across batch boundaries can slip the per-batch pass, hence the cap\n // on what consumers may do with chunks: display/digest, never persistence).\n let pend = '';\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n const flushEmit = (ctx: { emit?: (s: string) => void }) => {\n if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }\n if (pend) { ctx.emit?.(redactSecrets(pend)); pend = ''; }\n };\n try {\n return await new Promise<string>((resolve) => {\n let out = '';\n let settled = false;\n const finish = (s: string) => { if (settled) return; settled = true; resolve(s); };\n let proc: SpawnedProcess;\n try {\n proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });\n } catch (e: any) {\n return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);\n }\n const collect = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n out += s;\n if (ctx.emit && !settled) {\n pend += s;\n if (pend.length >= 1024) flushEmit(ctx);\n else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);\n }\n };\n proc.stdout?.on('data', collect);\n proc.stderr?.on('data', collect);\n proc.on('error', (err: any) => {\n // AbortError fires here when ctl.abort() kills the child — report timeout vs cancel.\n if (err?.name === 'AbortError' || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n log.debug('shell spawn error', err);\n finish(`[exit 1] ${err?.message ?? err}${out ? '\\n' + clean(out) : ''}`);\n });\n proc.on('close', (code: number | null) => {\n flushEmit(ctx); // drain the coalesce buffer before settling (still pre-resolve, so ctx.emit is live)\n if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n const body = clean(out);\n if (code && code !== 0) return finish(`[exit ${code}]${body ? '\\n' + body : ''}`);\n finish(body || '(command succeeded, no output)');\n });\n });\n } finally {\n clearTimeout(timer);\n if (flushTimer) clearTimeout(flushTimer); // no emits after the call settles\n ctx.signal?.removeEventListener('abort', onAbort);\n }\n },\n };\n}\n\n/** Abort message: timeout vs external cancel, preserving any partial output. */\nfunction reasonFor(timedOut: boolean, timeoutMs: number, body: string): string {\n const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : '[exit 130] cancelled (killed)';\n return body ? `${head}\\n${body}` : head;\n}\n\nconst NO_JOB = (id: string) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;\n\n/** Build the background-job companion tools (ShellOutput / ShellStatus / ShellKill) over a registry. */\nexport function makeShellJobTools(registry: ShellJobRegistry): AgentTool[] {\n const idParam = { type: 'object', properties: { id: { type: 'string', description: 'the job id from Shell({background:true})' } } };\n return [\n {\n name: 'ShellOutput',\n description: 'Read the accumulated output (tail) of a background Shell job by id.',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n const out = registry.output(String(id));\n if (out == null) return NO_JOB(String(id));\n const st = registry.status(String(id))!;\n return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ''}]\\n${clean(out) || '(no output yet)'}`;\n },\n },\n {\n name: 'ShellStatus',\n description: 'Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.',\n parameters: idParam,\n async run({ id }) {\n if (!id) {\n const jobs = registry.list();\n return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join('\\n') : '(no background jobs)';\n }\n const st = registry.status(String(id));\n return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ''} · ${st.bytes} byte(s) buffered` : NO_JOB(String(id));\n },\n },\n {\n name: 'ShellKill',\n description: 'Stop a running background Shell job by id (SIGTERM).',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));\n },\n },\n ];\n}\n"],"mappings":"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","names":["log","res","log","ctx"]}
1
+ {"version":3,"sources":["../src/tools.ts","../src/redact.ts","../src/logging.ts","../src/tools.web.ts","../src/shell.sandbox.ts","../src/tools.shell.ts"],"sourcesContent":["import type { IFilesystem } from '@livx.cc/wcli/core';\nimport { CommandExecutor, registerHeadlessCommands } from '@livx.cc/wcli/core';\nimport type { Tool, ChatLike } from './llm';\nimport { grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool, fuzzyLineReplace } from './tools.structured';\nimport { todoWriteTool, type TodoItem } from './todo';\nimport { webFetchTool, webSearchTool } from './tools.web';\nimport { OverlayFilesystem } from './OverlayFilesystem';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\nimport type { SandboxJobRegistry } from './tools.jobs';\n\n/** A structured multiple-choice question the model can pose to a human. */\nexport interface UserQuestion {\n question: string;\n header?: string;\n options: { label: string; description?: string }[];\n multiSelect?: boolean;\n}\n\n/**\n * The host / human-in-the-loop seam (the \"third seam\" beyond LLM + filesystem).\n * Injected per host: a CLI reads stdin, a browser renders a dialog, edge/headless\n * omits it. Unifies user-questions, permission/plan approvals, and notifications.\n */\nexport type HostEvent =\n | { kind: 'text_delta'; message: string }\n | { kind: 'thinking_delta'; message: string }\n | { kind: 'tool_use'; id: string; name: string; input: unknown }\n | { kind: 'tool_result'; id: string; output: string; isError?: boolean }\n | { kind: 'tool_result_image'; id: string; dataUrl: string }\n | { kind: string; message: string; data?: unknown };\n\nexport interface HostBridge {\n /** Ask the user a structured question; resolve to the chosen label(s) / free text. */\n ask?(q: UserQuestion): Promise<string>;\n /** Request approval for a sensitive action (permission 'ask' / plan approval). */\n confirm?(prompt: string, meta?: { tool: string; input: unknown }): Promise<boolean>;\n /** Emit a progress / notification event to the host UI (non-blocking). */\n notify?(event: HostEvent): void;\n}\n\nexport interface ToolContext {\n fs: IFilesystem;\n exec: CommandExecutor;\n /** path -> content snapshot at last Read/Edit; powers the read-before-edit staleness guard. */\n readState: Map<string, string>;\n /** optional host interaction channel; absent => autonomous/headless. */\n host?: HostBridge;\n /** optional run-cancellation signal (mirrors AgentOptions.signal); lets abort-aware tools\n * (e.g. the real shell) kill in-flight work when the run is cancelled. */\n signal?: AbortSignal;\n /** the agent's working todo list (TodoWrite planning aid); replaced wholesale per call. */\n todos: TodoItem[];\n /** optional syntax guardrail: if set, write-class tools refuse to persist a broken result. */\n lint?: (path: string, content: string) => string | null;\n /** optional PDF text extraction (node hosts wire a pdftotext-backed impl); absent => Read explains. */\n pdfText?: (path: string) => Promise<string>;\n /** optional model handle for tools that run their own LLM pass (e.g. Review, a self-critique).\n * Populated by the Agent from its own ai/model; absent => such tools degrade to a no-op. */\n ai?: ChatLike;\n model?: string;\n /** optional sandbox background-job registry; enables `bash({background:true})`. Absent => no backgrounding. */\n jobs?: SandboxJobRegistry;\n /** optional incremental-output channel: long-running tools (e.g. the real Shell) stream chunks\n * here mid-run. Wired PER CALL by Agent.dispatch to Hooks.onToolOutput (cleared when the call\n * settles — a late emit is a silent no-op). Fire-and-forget: never awaited, never part of the result. */\n emit?: (chunk: string) => void;\n /** Wrap a HUMAN-blocking await (permission/plan confirm, an interactive question) so the time\n * spent parked on the user is excluded from the run's wall-clock kill-switches. Wired by the Agent;\n * absent => no accounting (the promise is awaited as-is). Idle prompt time must not count as work. */\n parkHuman?<T>(p: Promise<T>): Promise<T>;\n}\n\nexport interface AgentTool {\n name: string;\n description: string;\n parameters: object; // JSON Schema for the function's arguments\n run(args: any, ctx: ToolContext): Promise<string | { text: string; images?: { mimeType: string; data: string }[] }>;\n}\n\n/** Build a tool context bound to a filesystem backend (Mem / Disk / …) and an optional host. */\nexport function makeContext(fs: IFilesystem, host?: HostBridge): ToolContext {\n const exec = new CommandExecutor(fs);\n registerHeadlessCommands(exec);\n return { fs, exec, readState: new Map(), host, todos: [] };\n}\n\n/** Convert AgentTools into the ai.libx.js `tools` array for chat(). */\nexport function toWireTools(tools: AgentTool[]): Tool[] {\n return tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n}\n\nconst numberLines = (content: string, offset = 0, limit?: number): string => {\n const lines = content.split('\\n');\n const start = Math.max(0, offset);\n const end = limit != null ? start + limit : lines.length;\n return lines\n .slice(start, end)\n .map((l, i) => `${start + i + 1}\\t${l}`)\n .join('\\n');\n};\n\n/** Keep huge tool output high-signal: head+tail with an omission marker (cuts re-runs to \"parse the wall\"). */\nexport function truncateOutput(s: string, headLines = 80, tailLines = 20): string {\n const lines = s.split('\\n');\n if (lines.length <= headLines + tailLines + 1) return s;\n const omitted = lines.length - headLines - tailLines;\n return [...lines.slice(0, headLines), `… (${omitted} lines omitted — narrow the command to see more) …`, ...lines.slice(-tailLines)].join('\\n');\n}\n\n/** Run any shell command line over the VFS (ls/cat/grep/find/head/tail/echo/mkdir/rm/mv/wc, pipes, redirects, &&/||/;). */\nexport const bashTool: AgentTool = {\n name: 'bash',\n description:\n 'Run a shell command. Supports ls, cat, grep, find, head, tail, echo, mkdir, rm, mv, cp, wc, pipes (|), redirects (>, >>), and chaining (&&, ||, ;). Best for: running tests/builds, file operations (mkdir/mv/rm), and piped workflows. For searching file contents, prefer `Grep` (structured results, no re-parse). For finding files by name, prefer `Glob`.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the command line to execute' },\n background: { type: 'boolean', description: 'run detached over an isolated overlay (writes commit when it finishes); returns a job id to poll with JobOutput. Only worth it for slow work (remote VFS / long pipelines).' },\n },\n },\n async run({ command, background }, ctx) {\n if (background && ctx.jobs) return startBashJob(String(command ?? ''), ctx);\n const r = await ctx.exec.execute(String(command ?? ''));\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n if (r.exitCode !== 0) {\n const err = (r.error ?? '').trim();\n return `[exit ${r.exitCode}]${err ? ' ' + err : ''}${out ? '\\n' + out : ''}`;\n }\n return out || '(command succeeded, no output)'; // explicit sentinel: don't re-run to \"check\"\n },\n};\n\n/** Kick a bash command into the background over an isolated overlay; its writes commit only on success.\n * A kill (abort) before completion skips the commit — the parent VFS is never touched mid-flight. */\nfunction startBashJob(command: string, ctx: ToolContext): string {\n const baseFs = ctx.fs;\n const id = ctx.jobs!.start(\n async ({ signal }) => {\n const overlay = new OverlayFilesystem(baseFs);\n const exec = new CommandExecutor(overlay);\n registerHeadlessCommands(exec);\n const r = await exec.execute(command); // wcli is sync-to-completion; abort can only gate the commit below\n if (signal.aborted) return '[killed before commit]';\n await overlay.commit(); // atomically flush this job's writes down into the parent VFS\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n return r.exitCode !== 0 ? `[exit ${r.exitCode}] ${(r.error ?? '').trim()}\\n${out}`.trim() : out || '(command succeeded, no output)';\n },\n { kind: 'bash', label: command.slice(0, 60) },\n );\n return `Started background job ${id} — poll with JobOutput({id:\"${id}\"}) / JobStatus, stop with JobKill.`;\n}\n\n/** Image extensions the Read tool returns as a visual block (when the fs can read bytes). */\nconst IMG_MIME: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };\n\n/** Read a text file as 1-indexed numbered lines; arms the staleness guard for Edit. */\nexport const readTool: AgentTool = {\n name: 'Read',\n description:\n 'Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.',\n parameters: {\n type: 'object',\n required: ['path'],\n properties: {\n path: { type: 'string' },\n offset: { type: 'number' },\n limit: { type: 'number' },\n },\n },\n async run({ path, offset, limit }, ctx) {\n // Image file → return it as a visual block. The adapter turns a tool result whose JSON carries a\n // `dataUrl` into an image tool_result the model can see. Needs a binary-capable fs (disk default);\n // the utf8 VFS (sandbox/Mem) can't, so we say so instead of corrupting the bytes.\n const ext = String(path).toLowerCase().split('.').pop() ?? '';\n // PDF → extracted text when the host wired an extractor (CLI: pdftotext); else say how to proceed.\n if (ext === 'pdf') {\n if (!ctx.pdfText) return `[${path} is a PDF — text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`; // jail-aware: hidden paths read as absent\n const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();\n return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;\n }\n if (IMG_MIME[ext]) {\n const fs = ctx.fs as { readFileBytes?: (p: string) => Promise<Uint8Array> };\n if (typeof fs.readFileBytes !== 'function') {\n return `[${path} is an image, but this filesystem can't read binary — attach it as @${path} instead, or run on disk.]`;\n }\n const bytes = await fs.readFileBytes(path);\n const b64 = Buffer.from(bytes).toString('base64');\n return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });\n }\n const raw = await ctx.fs.readFile(path);\n ctx.readState.set(ctx.fs.resolvePath(path), raw); // staleness guard tracks the REAL content\n // Mask secret values in config files so keys can live there usably-but-hidden (line count is preserved).\n const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;\n const total = content === '' ? 0 : content.split('\\n').length;\n const start = Math.max(0, offset ?? 0);\n const body = numberLines(content, start, limit);\n // snippet-with-pointer: when the slice doesn't cover the whole file, tell the\n // model what it's missing + how to pull it — so it expands precisely instead of re-reading blind.\n const shownEnd = limit != null ? Math.min(start + limit, total) : total;\n const shownCount = Math.max(0, shownEnd - start);\n if (shownCount >= total) return body; // whole file shown — no footer\n if (shownCount === 0) return `[no lines in range (offset ${start}${limit != null ? `, limit ${limit}` : ''}) — file has ${total} line(s)]`;\n return `${body}\\n\\n[lines ${start + 1}–${shownEnd} of ${total} · re-Read with offset/limit for the rest]`;\n },\n};\n\n/** Replace an exact, unique substring; requires a prior Read and guards against stale edits. */\nexport const editTool: AgentTool = {\n name: 'Edit',\n description:\n 'Replace an exact substring in a file. Requires a prior Read of the same file. `old_string` must occur exactly once — include surrounding context to disambiguate.',\n parameters: {\n type: 'object',\n required: ['path', 'old_string', 'new_string'],\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n },\n },\n async run({ path, old_string, new_string }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n const current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const count = old_string === '' ? 0 : current.split(old_string).length - 1;\n if (count > 1) throw new Error(`old_string is not unique in ${path} (${count} matches). Provide more surrounding context.`);\n let next: string, note = '';\n if (count === 1) {\n next = current.replace(old_string, () => new_string); // exact: function replacer, no $-pattern expansion\n } else {\n // exact match failed — try a whitespace-tolerant unique match before giving up (cuts re-read churn)\n const fuzzy = fuzzyLineReplace(current, old_string, new_string);\n if (fuzzy == null) throw new Error(`old_string not found in ${path}.`);\n next = fuzzy;\n note = ' (whitespace-tolerant match)';\n }\n if (ctx.lint) { const err = ctx.lint(path, next); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, next);\n ctx.readState.set(key, next);\n return `Edited ${path}${note}`;\n },\n};\n\n/** Session-exit tool: the model calls this when the user wants to end the conversation.\n * The `onExit` callback is injected by the host (CLI sets it to flip a flag that breaks the REPL loop). */\nexport function exitSessionTool(onExit: () => void): AgentTool {\n return {\n name: 'ExitSession',\n description:\n 'End the current session and exit the CLI. Call this when the user says goodbye, asks to quit, ' +\n 'or clearly indicates they want to stop the conversation (e.g. \"ok bye\", \"that\\'s all\", \"exit\", \"goodnight\").',\n parameters: { type: 'object', properties: {} },\n async run() {\n onExit();\n return 'Session ending. Goodbye!';\n },\n };\n}\n\nexport function defaultTools(): AgentTool[] {\n return [bashTool, readTool, editTool];\n}\n\n/**\n * The full catalog of selectable tools, keyed by name. The evolve loop's mutation\n * surface picks from this registry; embedders can build a custom tool set by name.\n */\nexport function toolRegistry(): Record<string, AgentTool> {\n const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool];\n return Object.fromEntries(all.map((t) => [t.name, t]));\n}\n\n/** Resolve a list of tool names against the registry (unknown names throw). */\nexport function toolsByName(names: string[]): AgentTool[] {\n const reg = toolRegistry();\n return names.map((n) => {\n const t = reg[n];\n if (!t) throw new Error(`unknown tool '${n}'. Known: ${Object.keys(reg).join(', ')}`);\n return t;\n });\n}\n","/**\n * Mask secret-looking values in arbitrary text before it reaches the model.\n *\n * Two complementary seams use this: real-shell output (`cat .env`, `printenv`) and the\n * `Read` tool (so provider keys stored in `.agent/settings.json` are usable-but-masked).\n * The FS jail hides whole secret FILES by name; this hides secret VALUES wherever they\n * surface in otherwise-legitimate content.\n *\n * Both regexes are linear (no nested quantifiers) — safe against catastrophic backtracking\n * and cheap enough to run on every tool output (see tests/redact.bench).\n */\n\nexport const REDACTED = '‹redacted›';\n\n/** Config/control files that may carry provider keys — readers (Read/Grep) mask secret VALUES in\n * these while keeping the rest readable. (Whole secret FILES like .env are hidden by the FS jail.) */\nexport const CONFIG_FILE_RE = /(^|\\/)\\.(agent|claude)\\/(settings(\\.[\\w-]+)?\\.json|config\\.(json|js|mjs|cjs|ts))$/i;\n\n// (A) `NAME=value` / `\"name\": \"value\"` pairs where NAME looks like a secret. Masks the value only,\n// so the agent still sees WHICH key exists (useful config context) without the secret itself.\nconst SECRET_PAIR =\n /((?:^|[\\s,{[])(?:export\\s+)?[\"']?[\\w.\\-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_?KEY|AUTH(?:_?TOKEN)?|BEARER)[\\w.\\-]*[\"']?\\s*[:=]\\s*)([\"']?)([^\\s\"',{}\\]]+)/gi;\n\n// (B) Bare tokens by well-known shape — catches secrets that appear without an obvious key\n// (Authorization headers, URLs, JSON dumps). Conservative prefixes to avoid false positives.\nconst SECRET_TOKEN =\n /\\b(sk-ant-[\\w-]{12,}|sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[\\w]{20,}|xox[baprs]-[\\w-]{10,}|AKIA[0-9A-Z]{12,}|AIza[\\w-]{20,}|eyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,})\\b/g;\n\n/** Return `text` with secret values masked. Cheap no-op when nothing matches. */\nexport function redactSecrets(text: string): string {\n if (!text) return text;\n return text\n .replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`)\n .replace(SECRET_TOKEN, REDACTED);\n}\n","// Import the log module directly from libx.js source: libx.js's main bundle\n// doesn't re-export `log` as a named ESM export, and source-importing keeps\n// libx.js patches live (no rebuild) — matching the `bun link` workflow.\nimport { log } from 'libx.js/src/modules/log';\n\n/** Component-scoped logger (libx.js). debug/verbose gated via DEBUG env/localStorage. */\nexport const forComponent = (name: string) => log.forComponent(name);\nexport { log };\n","import type { AgentTool } from './tools';\nimport { forComponent } from './logging';\n\n/**\n * Web tools — `WebFetch` (retrieve a URL as readable text) and `WebSearch` (ranked\n * results via a configured provider). Opt-in (NOT in the default tool set): network\n * access is a deliberate capability. Factory-built with an injectable `fetch` so they\n * stay edge-portable and unit-testable without real network. `fetch` is read at call\n * time, so a no-network runtime simply has the tool return an error.\n */\nconst log = forComponent('web');\n\n/** Strip HTML to readable text — dependency-free: drop script/style/comments, block tags → newlines, decode common entities. */\nexport function htmlToText(html: string): string {\n let s = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<title[\\s\\S]*?<\\/title>/gi, ' ') // drop title text (don't leak it into context)\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ') // …same for noscript / textarea content\n .replace(/<textarea[\\s\\S]*?<\\/textarea>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<\\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, '\\n')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ');\n s = s\n .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#0?39;/g, \"'\").replace(/&#x27;/gi, \"'\");\n return s\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .split('\\n').map((l) => l.trim()).join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nexport interface WebFetchOptions {\n /** Override the global fetch (tests inject a mock; edge runtimes can supply their own). */\n fetch?: typeof globalThis.fetch;\n maxBytes?: number; // cap the downloaded body (default 2 MB)\n maxChars?: number; // cap the returned text (default 100k)\n timeoutMs?: number; // request timeout (default 15s)\n /** Allow fetching private/loopback/link-local hosts (default false — blocks basic SSRF). */\n allowPrivateHosts?: boolean;\n}\n\n/**\n * Block obvious SSRF targets by hostname/IP literal (loopback, private ranges, link-local incl.\n * cloud metadata 169.254.169.254, `.internal`). Pure/edge-safe — no DNS, so DNS-rebinding and\n * redirect-to-internal are NOT covered (an embedder needing that should supply a vetting `fetch`).\n */\nexport function isPrivateHost(host: string): boolean {\n const h = host.toLowerCase().replace(/^\\[|\\]$/g, ''); // strip IPv6 brackets\n if (h === '' || h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.internal')) return true;\n if (h === '::1' || h === '::' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; // IPv6 loopback/link-local/ULA\n const m = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (m) {\n const a = +m[1], b = +m[2];\n return a === 0 || a === 127 || a === 10 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n return false;\n}\n\n/** Lazily-loaded node DNS resolver (absent on edge/browser) — closes DNS-rebinding (a public\n * hostname resolving to an internal IP) on the real-network path. Resolves null where unavailable. */\nlet _dnsLookup: ((h: string, opts?: any) => Promise<{ address: string }[]>) | null | undefined;\nasync function resolveIps(host: string): Promise<string[] | null> {\n if (_dnsLookup === undefined) {\n try { _dnsLookup = (await import('node:dns/promises')).lookup as any; }\n catch { _dnsLookup = null; } // edge/browser: no DNS — rely on the literal isPrivateHost check\n }\n if (!_dnsLookup) return null;\n try { return (await _dnsLookup(host, { all: true } as any)).map((a) => a.address); } catch { return null; }\n}\n\n/** Read a response body but stop at `maxBytes` of ACTUAL bytes (cancel the stream) — no unbounded download. */\nasync function readCapped(res: Response, maxBytes: number): Promise<string> {\n const reader = (res.body as any)?.getReader?.();\n if (!reader) { const t = await res.text(); return t.length > maxBytes ? t.slice(0, maxBytes) : t; }\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) { chunks.push(value); total += value.length; }\n if (total >= maxBytes) { try { await reader.cancel(); } catch { /* already closed */ } break; }\n }\n const out = new Uint8Array(Math.min(total, maxBytes));\n let off = 0;\n for (const c of chunks) { if (off >= out.length) break; const take = Math.min(c.length, out.length - off); out.set(c.subarray(0, take), off); off += take; }\n return new TextDecoder().decode(out);\n}\n\n/** Build a WebFetch tool. */\nexport function makeWebFetchTool(options: WebFetchOptions = {}): AgentTool {\n const maxBytes = options.maxBytes ?? 2_000_000;\n const maxChars = options.maxChars ?? 100_000;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebFetch',\n description:\n 'Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.',\n parameters: { type: 'object', required: ['url'], properties: { url: { type: 'string', description: 'absolute http(s) URL' } } },\n async run({ url }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n const customFetch = !!options.fetch; // injected fetch (tests/edge) owns its own vetting → skip DNS\n const u = String(url ?? '');\n try { new URL(u); } catch { return `Error: invalid URL: ${u}`; }\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n // Reject a host that's a private/internal IP literal, or (on the real-network path) a name that\n // RESOLVES to one — re-checked on EVERY redirect hop so an external page can't bounce us internal.\n const hostBlock = async (hostname: string): Promise<string | null> => {\n if (options.allowPrivateHosts) return null;\n if (isPrivateHost(hostname)) return hostname;\n if (!customFetch) { const ips = await resolveIps(hostname); if (ips) for (const ip of ips) if (isPrivateHost(ip)) return `${hostname} → ${ip}`; }\n return null;\n };\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n let current = u;\n let res: Response;\n for (let hop = 0; ; hop++) {\n const pu = new URL(current);\n if (pu.protocol !== 'http:' && pu.protocol !== 'https:') return `Error: only http/https URLs are allowed (got \"${pu.protocol}\")`;\n const blocked = await hostBlock(pu.hostname);\n if (blocked) return `Error: refusing to fetch a private/internal address (${blocked}) — set allowPrivateHosts to override`;\n res = await doFetch(current, { signal: ctl.signal, redirect: 'manual', headers: { 'user-agent': 'agentx (+https://github.com/Livshitz/agent.libx.js)' } });\n if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n if (hop >= 5) return `Error fetching ${u}: too many redirects`;\n current = new URL(res.headers.get('location')!, current).toString(); // re-validated at loop top\n continue;\n }\n break;\n }\n const type = res.headers.get('content-type') ?? '';\n const body = await readCapped(res, maxBytes);\n const text = /html/i.test(type) || /^\\s*<(?:!doctype|html)/i.test(body) ? htmlToText(body) : body.trim();\n const capped = text.length > maxChars ? text.slice(0, maxChars) + `\\n… [truncated at ${maxChars} chars]` : text;\n return `${res.status} ${res.statusText} · ${new URL(current).host}\\n\\n${capped}`;\n } catch (e: any) {\n log.debug(`WebFetch ${u} failed`, e);\n return `Error fetching ${u}: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\nexport interface WebSearchOptions {\n fetch?: typeof globalThis.fetch;\n /** Provider: 'auto' (default) uses Tavily if an API key is present, else keyless DuckDuckGo. */\n provider?: 'auto' | 'tavily' | 'duckduckgo';\n /** API key for Tavily (default: process.env.TAVILY_API_KEY). */\n apiKey?: string;\n /** Tavily endpoint override. */\n endpoint?: string;\n maxResults?: number; // default 5\n timeoutMs?: number; // default 15s\n}\n\ninterface SearchHit { title: string; url: string; snippet: string }\n\n/** Decode a DuckDuckGo HTML result href: results are `//duckduckgo.com/l/?uddg=<encoded-target>` redirects. */\nexport function decodeDdgUrl(href: string): string {\n const m = href.match(/[?&]uddg=([^&]+)/);\n if (m) { try { return decodeURIComponent(m[1]); } catch { /* fall through */ } }\n return href.startsWith('//') ? 'https:' + href : href;\n}\n\n/** Parse DuckDuckGo's HTML results page into hits (title/url/snippet) — dependency-free, zips anchors to snippets in order. */\nexport function parseDdgHtml(html: string, max: number): SearchHit[] {\n const anchors = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__a[^\"]*\"[^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/g)];\n const snippets = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__snippet[^\"]*\"[^>]*>([\\s\\S]*?)<\\/a>/g)].map((m) => htmlToText(m[1]));\n const hits: SearchHit[] = [];\n for (let i = 0; i < anchors.length && hits.length < max; i++) {\n const url = decodeDdgUrl(anchors[i][1]);\n try { if (isPrivateHost(new URL(url).hostname)) continue; } catch { continue; } // skip junk/internal redirects\n hits.push({ title: htmlToText(anchors[i][2]) || '(untitled)', url, snippet: snippets[i] ?? '' });\n }\n return hits;\n}\n\nfunction formatHits(hits: SearchHit[]): string {\n if (!hits.length) return '(no results)';\n return hits.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet.replace(/\\s+/g, ' ').slice(0, 240)}`).join('\\n\\n');\n}\n\n/**\n * Build a WebSearch tool. Keyless by default (DuckDuckGo HTML) so it works in any deployment with\n * no setup; auto-upgrades to Tavily (better, agent-oriented results) when TAVILY_API_KEY is present.\n */\nexport function makeWebSearchTool(options: WebSearchOptions = {}): AgentTool {\n const tavilyEndpoint = options.endpoint ?? 'https://api.tavily.com/search';\n const maxResults = options.maxResults ?? 5;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebSearch',\n description:\n 'Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic — then WebFetch a result URL to read it in full.',\n parameters: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } },\n async run({ query }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n const q = String(query ?? '').trim();\n if (!q) return 'Error: empty query';\n const key = options.apiKey ?? process.env.TAVILY_API_KEY;\n const provider = options.provider ?? 'auto';\n const useTavily = provider === 'tavily' || (provider === 'auto' && !!key);\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n if (useTavily) {\n if (!key) return 'Error: Tavily provider selected but TAVILY_API_KEY is not set';\n const res = await doFetch(tavilyEndpoint, {\n method: 'POST',\n signal: ctl.signal,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ api_key: key, query: q, max_results: maxResults }),\n });\n if (!res.ok) return `Error: search provider returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.results) ? data.results.slice(0, maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.content ?? '') })));\n }\n // Keyless: DuckDuckGo HTML endpoint (no key, edge-portable).\n const res = await doFetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(q), {\n signal: ctl.signal,\n headers: { 'user-agent': 'Mozilla/5.0 (compatible; agentx/1.0; +https://github.com/Livshitz/agent.libx.js)' },\n });\n if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;\n return formatHits(parseDdgHtml(await res.text(), maxResults));\n } catch (e: any) {\n log.debug('WebSearch failed', e);\n return `Error searching: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/** Default instances (registered in the tool registry; opt-in by name). */\nexport const webFetchTool = makeWebFetchTool();\nexport const webSearchTool = makeWebSearchTool();\n","/**\n * Tier-1 OS sandbox for the real `Shell` tool (mind/03-roadmap.md \"capability tiers\").\n *\n * Wraps the spawned `/bin/sh` in the platform's process sandbox so a hostile/buggy command\n * can read the machine but can only WRITE inside an allowlist (cwd + tmp + extra `writePaths`),\n * and gets no network unless granted:\n * - macOS: `sandbox-exec` (seatbelt) with a generated profile\n * - Linux: `bwrap` (bubblewrap) with `--ro-bind / /` + writable binds\n *\n * Pure argv builders (unit-testable, no node imports) + an async wrapper-binary locator.\n * This complements — does not replace — env secret-scrubbing and the permission prompt:\n * the FS jail can't contain a real process; this makes the *process* itself contained.\n */\n\nexport class OsSandboxOptions {\n /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */\n network = false;\n /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */\n writePaths: string[] = [];\n}\n\nexport interface SandboxWrap {\n bin: string;\n args: string[]; // full argv: wrapper flags + /bin/sh -c <command>\n}\n\n/** Writable allowlist shared by both platforms: cwd, the tmp roots, /dev. */\nfunction writable(cwd: string, o: OsSandboxOptions, tmpDir?: string): string[] {\n const set = new Set<string>([cwd, '/tmp', '/private/tmp', '/private/var/folders', '/dev', ...(tmpDir ? [tmpDir] : []), ...o.writePaths]);\n return [...set];\n}\n\nconst sbQuote = (p: string) => `\"${p.replace(/([\"\\\\])/g, '\\\\$1')}\"`;\n\n/** macOS seatbelt profile: allow everything, then deny writes/network, then re-allow the allowlist\n * (seatbelt resolves conflicts by specificity, so subpath allows override the broad deny). */\nexport function seatbeltProfile(cwd: string, o: OsSandboxOptions, tmpDir?: string): string {\n const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(' ');\n return [\n '(version 1)',\n '(allow default)',\n ...(o.network ? [] : ['(deny network*)']),\n '(deny file-write*)',\n `(allow file-write* ${allows})`,\n ].join('\\n');\n}\n\n/** Build the wrapped argv for `sh -c <command>`, or null if `platform` has no supported wrapper. */\nexport function sandboxArgv(command: string, cwd: string, opts: Partial<OsSandboxOptions> = {}, platform: string = process.platform, tmpDir?: string): SandboxWrap | null {\n const o = { ...new OsSandboxOptions(), ...opts };\n if (platform === 'darwin') {\n return { bin: '/usr/bin/sandbox-exec', args: ['-p', seatbeltProfile(cwd, o, tmpDir), '/bin/sh', '-c', command] };\n }\n if (platform === 'linux') {\n const binds = writable(cwd, o, tmpDir).filter((p) => p !== '/dev' && !p.startsWith('/private')).flatMap((p) => ['--bind-try', p, p]);\n return {\n bin: 'bwrap',\n args: ['--ro-bind', '/', '/', ...binds, '--dev', '/dev', '--proc', '/proc', '--die-with-parent', ...(o.network ? [] : ['--unshare-net']), '/bin/sh', '-c', command],\n };\n }\n return null;\n}\n\n/** Locate the wrapper binary for this platform; null = sandboxing unavailable here. */\nexport async function findSandboxWrapper(platform: string = process.platform): Promise<string | null> {\n const { existsSync } = await import('node:fs');\n if (platform === 'darwin') return existsSync('/usr/bin/sandbox-exec') ? '/usr/bin/sandbox-exec' : null;\n if (platform === 'linux') {\n for (const dir of (process.env.PATH ?? '/usr/bin:/bin').split(':')) if (dir && existsSync(`${dir}/bwrap`)) return `${dir}/bwrap`;\n return null;\n }\n return null;\n}\n","import type { AgentTool } from './tools';\nimport { truncateOutput } from './tools';\nimport { redactSecrets } from './redact';\nimport { forComponent } from './logging';\nimport { sandboxArgv, findSandboxWrapper, type OsSandboxOptions } from './shell.sandbox';\n\n/**\n * Real shell tool — node-only, OPT-IN, and deliberately NOT edge-portable.\n *\n * ⚠️ Unlike the default VFS `bash` (a sandboxed JS interpreter over the virtual filesystem),\n * this spawns a REAL `/bin/sh` process. It can run `bun`, `git`, `ssh`, scripts, deploys —\n * and, by the same token, it is NOT sandboxed: only cwd-binding constrains it. It is a\n * deliberate host escalation, kept out of `defaultTools()`/`toolRegistry()` and out of the\n * edge-safe `src/index.ts` (same policy as `mcp.client.ts`). A host opts in explicitly:\n *\n * tools: [...defaultTools(), makeRealShellTool({ cwd: nodeDiskRoot })]\n *\n * Mirrors `tools.web.ts`: a factory with an injectable `spawn` (tests + edge never import\n * node:child_process), an options bag, abort + timeout honored, output capped. Safety beyond\n * cwd-binding is the host's to add (e.g. a PermissionPolicy `decision:'ask'` per command, or\n * an OS sandbox wrapper) — see mind/03-roadmap.md \"OS-level access — capability tiers\".\n */\n\nconst log = forComponent('shell');\n\n/** Normalize shell output for return: trim trailing newlines, mask secret values, then size-truncate.\n * Redaction runs BEFORE truncation so a masked tail can't smuggle a secret past the line cap. */\nconst clean = (s: string): string => truncateOutput(redactSecrets(s.replace(/\\n+$/, '')));\n\n/** The slice of node's `child_process.spawn` we depend on — injectable so tests supply a fake. */\nexport type SpawnFn = (\n command: string,\n args: string[],\n options: {\n cwd?: string;\n env?: Record<string, string | undefined>;\n signal?: AbortSignal;\n /** stdio layout. We force stdin to /dev/null so a child can't block on (or steal) the REPL's input. */\n stdio?: ['ignore', 'pipe', 'pipe'];\n /** Run in a new session/process group (setsid) — detaches from the controlling terminal. See DETACHED. */\n detached?: boolean;\n },\n) => SpawnedProcess;\n\n/** Minimal `ChildProcess` surface this tool uses. */\nexport interface SpawnedProcess {\n stdout?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n stderr?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n on(ev: 'close', cb: (code: number | null) => void): void;\n on(ev: 'error', cb: (err: Error) => void): void;\n kill(signal?: string): void;\n /** Child PID — present on the real node child; used to signal the whole process group on abort. */\n pid?: number;\n}\n\n/**\n * Detach every spawned child from the REPL's controlling terminal.\n *\n * `stdio: ['ignore', 'pipe', 'pipe']` — stdin is /dev/null (clean EOF; nothing to block on).\n * `detached: true` — setsid() puts the child in its OWN session with NO controlling tty.\n *\n * Without this a child inherits the agent's tty: an interactive prompt (`sudo`, `ssh`, a git\n * credential helper) opens `/dev/tty` directly and then RACES the REPL's raw-mode input reader for\n * the user's keystrokes — a deadlock that also captures whatever the user types (e.g. a password)\n * into the agent instead of the program. Detached, those programs find no tty and fail FAST with a\n * legible error (\"sudo: a terminal is required …\") which the model can act on (tell the user to run\n * it via `!`), instead of hanging until the 120s timeout. Side benefit: the child is a process-group\n * leader, so abort/timeout can reap the whole subtree, not just /bin/sh. */\nconst DETACHED = { stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'], detached: true };\n\n/** Signal a child's WHOLE process group (`-pid`). Children are group leaders (DETACHED), so `proc.kill`\n * hits only /bin/sh and orphans descendants; this reaps the subtree. Best-effort — the group may\n * already be gone, and the fake spawn in tests has no real pid. Returns false when nothing was signaled. */\nfunction killGroup(proc: SpawnedProcess | undefined, signal: 'SIGTERM' | 'SIGKILL'): boolean {\n if (!proc?.pid) return false;\n try { process.kill(-proc.pid, signal); return true; } catch { return false; /* already exited / no such group */ }\n}\n\nexport interface RealShellOptions {\n /** Working directory the shell is bound to (typically a NodeDiskFilesystem `baseDir`). Required. */\n cwd: string;\n /** Override the spawner (tests inject a fake; default lazily imports node:child_process). */\n spawn?: SpawnFn;\n /** Per-command wall-clock cap (kill on overrun). Default 120s. */\n timeoutMs?: number;\n /** Extra env merged over the (optionally scrubbed) base env for the child. */\n env?: Record<string, string>;\n /** Strip likely-secret vars (API keys, tokens, cloud creds) from the child's env. Default ON.\n * The FS jail does NOT contain a real process, so this is the seam that keeps `echo $ANTHROPIC_API_KEY`\n * from leaking the host's secrets to a spawned command. `false` passes `process.env` through verbatim. */\n redactEnv?: boolean;\n /** Job registry enabling `Shell({background:true})` (long-running processes). Pair with `makeShellJobTools`. */\n registry?: ShellJobRegistry;\n /** Tier-1 OS sandbox: wrap /bin/sh in sandbox-exec (macOS) / bwrap (Linux) — writes confined to\n * cwd+tmp, network blocked unless granted. `true` = defaults; commands FAIL (don't silently run\n * unsandboxed) if no wrapper exists on this platform. See src/shell.sandbox.ts. */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n}\n\n/** Resolve the (bin,args) to spawn for `command`, honoring the optional OS sandbox.\n * Throws when sandboxing was requested but this platform has no wrapper — fail closed. */\nasync function spawnArgvFor(command: string, cwd: string, osSandbox?: boolean | Partial<OsSandboxOptions>): Promise<{ bin: string; args: string[] }> {\n if (!osSandbox) return { bin: '/bin/sh', args: ['-c', command] };\n const opts = osSandbox === true ? {} : osSandbox;\n const wrapper = await findSandboxWrapper();\n const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;\n if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);\n return wrapped;\n}\n\n/** Env var names that look like secrets and are dropped before spawning (unless redactEnv:false). */\nconst SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;\n\n/** Build the child's env: `process.env` minus likely-secrets (when redacting), plus explicit `env`. */\nfunction childEnv(opts: { env?: Record<string, string>; redactEnv?: boolean }): Record<string, string | undefined> {\n const base: Record<string, string | undefined> = {};\n const redact = opts.redactEnv !== false; // default ON\n for (const [k, v] of Object.entries(process.env)) if (!(redact && SECRET_ENV_RE.test(k))) base[k] = v;\n return { ...base, ...opts.env };\n}\n\n/** Lazily resolve node's spawn (kept out of any eager edge import path). */\nlet _spawn: SpawnFn | undefined;\nasync function nodeSpawn(): Promise<SpawnFn> {\n if (!_spawn) _spawn = (await import('node:child_process')).spawn as unknown as SpawnFn;\n return _spawn;\n}\n\n// ---------------------------------------------------------------------------\n// Background jobs — long-running processes the agent starts, polls, and kills.\n// ---------------------------------------------------------------------------\nexport type JobStatus = 'running' | 'exited' | 'killed' | 'error';\n\nexport interface ShellJobConfig {\n cwd: string;\n spawn?: SpawnFn;\n env?: Record<string, string>;\n redactEnv?: boolean;\n /** Tail buffer cap per job (bytes); older output is dropped. Default 256 KB. */\n maxBuffer?: number;\n /** Kill all jobs on process exit (the CLI sets this; tests leave it off to avoid global handlers). */\n killOnExit?: boolean;\n /** Tier-1 OS sandbox for background jobs too (same semantics as RealShellOptions.osSandbox). */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n}\n\ninterface Job { command: string; buf: string; status: JobStatus; exitCode?: number; proc?: SpawnedProcess; }\n\n/**\n * Per-session registry of background `/bin/sh` jobs. Backs `Shell({background:true})` and the\n * `ShellOutput`/`ShellStatus`/`ShellKill` tools. Output accumulates into a tail-capped ring so a\n * chatty process can't OOM. Bounded + killable; the CLI wires `killOnExit` so children are reaped.\n */\nexport class ShellJobRegistry {\n private jobs = new Map<string, Job>();\n private seq = 0;\n constructor(private cfg: ShellJobConfig) {\n if (cfg.killOnExit && typeof process !== 'undefined') process.once('exit', () => this.killAll());\n }\n\n async start(command: string): Promise<string> {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: '', status: 'running' };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max); // ring: keep the tail\n };\n try {\n const spawn = this.cfg.spawn ?? (await nodeSpawn());\n const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: '/bin/sh', args: ['-c', command] };\n const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });\n job.proc = proc;\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; } });\n } catch (e: any) {\n job.status = 'error';\n job.buf = `failed to spawn: ${e?.message ?? e}`;\n }\n this.jobs.set(id, job);\n return id;\n }\n\n /** Current tail output for a job (null = no such job). */\n output(id: string): string | null { return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? '' : null); }\n\n status(id: string): { status: JobStatus; exitCode?: number; bytes: number } | null {\n const j = this.jobs.get(id);\n return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;\n }\n\n list(): Array<{ id: string; command: string; status: JobStatus }> {\n return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));\n }\n\n kill(id: string): boolean {\n const j = this.jobs.get(id);\n if (!j) return false;\n // Group-kill: bg children are detached (own group), so SIGTERM the whole subtree — not just /bin/sh —\n // else a forked server survives the kill and the agent's exit teardown. Fall back to the pid for fakes.\n if (j.status === 'running') { if (!killGroup(j.proc, 'SIGTERM')) { try { j.proc?.kill('SIGTERM'); } catch { /* already gone */ } } j.status = 'killed'; }\n return true;\n }\n\n killAll(): void { for (const id of this.jobs.keys()) this.kill(id); }\n}\n\n/** Build an opt-in real-shell tool bound to `options.cwd`. */\nexport function makeRealShellTool(options: RealShellOptions): AgentTool {\n const timeoutMs = options.timeoutMs ?? 120_000;\n return {\n name: 'Shell',\n description:\n 'Run a shell command via /bin/sh in the working directory. ' +\n 'Executes any installed binary — ls, cat, grep, git, bun, node, curl, scripts, etc. ' +\n 'Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. ' +\n 'Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input ' +\n 'fail fast rather than hang — for privileged actions use a non-interactive flag (e.g. `sudo -n`), ' +\n 'or ask the user to run the command themselves. ' +\n 'Set `background:true` for long-running processes (servers, watchers) — returns a job id immediately; poll with ShellOutput, stop with ShellKill.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the shell command line to execute' },\n background: { type: 'boolean', description: 'run detached and return a job id immediately (for servers/watchers/long builds)' },\n },\n },\n async run({ command, background }, ctx) {\n const cmd = String(command ?? '');\n if (!cmd.trim()) return '[exit 1] empty command';\n if (background) {\n if (!options.registry) return 'Error: background execution is not enabled in this host (no job registry).';\n const id = await options.registry.start(cmd);\n return `Started background job ${id}. Poll output with ShellOutput({id:\"${id}\"}), check ShellStatus({id:\"${id}\"}), stop with ShellKill({id:\"${id}\"}).`;\n }\n const spawn = options.spawn ?? (await nodeSpawn());\n // Sandbox-off keeps this path await-free (after spawn resolution) so an abort racing the call\n // start still lands before listener registration, exactly as pre-sandbox semantics.\n let argv = { bin: '/bin/sh', args: ['-c', cmd] };\n if (options.osSandbox) {\n try {\n argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);\n } catch (e: any) {\n return `[exit 1] ${e?.message ?? e}`; // fail closed — never run unsandboxed when sandboxing was asked for\n }\n }\n // Compose abort: the run's signal (ctx.signal) OR our per-command timeout both kill the child.\n const ctl = new AbortController();\n const onAbort = () => ctl.abort();\n if (ctx.signal) { if (ctx.signal.aborted) ctl.abort(); else ctx.signal.addEventListener('abort', onAbort, { once: true }); }\n let timedOut = false;\n const timer = setTimeout(() => { timedOut = true; ctl.abort(); }, timeoutMs);\n // The child is its own process-group leader (DETACHED): node's `signal` kills only /bin/sh, so on\n // abort also SIGKILL the whole group to reap any descendants (see killGroup).\n // Incremental output → ctx.emit (when the host listens), coalesced to ≥250ms / ≥1KB batches\n // so a chatty child doesn't spam hooks. Redacted per batch (the final result is re-redacted\n // whole — a secret split across batch boundaries can slip the per-batch pass, hence the cap\n // on what consumers may do with chunks: display/digest, never persistence).\n let pend = '';\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n const flushEmit = (ctx: { emit?: (s: string) => void }) => {\n if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }\n if (pend) { ctx.emit?.(redactSecrets(pend)); pend = ''; }\n };\n try {\n return await new Promise<string>((resolve) => {\n let out = '';\n let settled = false;\n const finish = (s: string) => { if (settled) return; settled = true; resolve(s); };\n let proc: SpawnedProcess;\n try {\n proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });\n } catch (e: any) {\n return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);\n }\n // Reap the detached group when the timeout/cancel fires (node's `signal` only hits /bin/sh).\n if (ctl.signal.aborted) killGroup(proc, 'SIGKILL');\n else ctl.signal.addEventListener('abort', () => killGroup(proc, 'SIGKILL'), { once: true });\n const collect = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n out += s;\n if (ctx.emit && !settled) {\n pend += s;\n if (pend.length >= 1024) flushEmit(ctx);\n else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);\n }\n };\n proc.stdout?.on('data', collect);\n proc.stderr?.on('data', collect);\n proc.on('error', (err: any) => {\n // AbortError fires here when ctl.abort() kills the child — report timeout vs cancel.\n if (err?.name === 'AbortError' || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n log.debug('shell spawn error', err);\n finish(`[exit 1] ${err?.message ?? err}${out ? '\\n' + clean(out) : ''}`);\n });\n proc.on('close', (code: number | null) => {\n flushEmit(ctx); // drain the coalesce buffer before settling (still pre-resolve, so ctx.emit is live)\n if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n const body = clean(out);\n if (code && code !== 0) return finish(`[exit ${code}]${body ? '\\n' + body : ''}`);\n finish(body || '(command succeeded, no output)');\n });\n });\n } finally {\n clearTimeout(timer);\n if (flushTimer) clearTimeout(flushTimer); // no emits after the call settles\n ctx.signal?.removeEventListener('abort', onAbort);\n }\n },\n };\n}\n\n/** Abort message: timeout vs external cancel, preserving any partial output. */\nfunction reasonFor(timedOut: boolean, timeoutMs: number, body: string): string {\n const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : '[exit 130] cancelled (killed)';\n return body ? `${head}\\n${body}` : head;\n}\n\nconst NO_JOB = (id: string) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;\n\n/** Build the background-job companion tools (ShellOutput / ShellStatus / ShellKill) over a registry. */\nexport function makeShellJobTools(registry: ShellJobRegistry): AgentTool[] {\n const idParam = { type: 'object', properties: { id: { type: 'string', description: 'the job id from Shell({background:true})' } } };\n return [\n {\n name: 'ShellOutput',\n description: 'Read the accumulated output (tail) of a background Shell job by id.',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n const out = registry.output(String(id));\n if (out == null) return NO_JOB(String(id));\n const st = registry.status(String(id))!;\n return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ''}]\\n${clean(out) || '(no output yet)'}`;\n },\n },\n {\n name: 'ShellStatus',\n description: 'Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.',\n parameters: idParam,\n async run({ id }) {\n if (!id) {\n const jobs = registry.list();\n return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join('\\n') : '(no background jobs)';\n }\n const st = registry.status(String(id));\n return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ''} · ${st.bytes} byte(s) buffered` : NO_JOB(String(id));\n },\n },\n {\n name: 'ShellKill',\n description: 'Stop a running background Shell job by id (SIGTERM).',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));\n },\n },\n ];\n}\n"],"mappings":"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","names":["log","res","log","ctx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent.libx.js",
3
- "version": "0.94.24",
3
+ "version": "0.94.25",
4
4
  "description": "Edge-native AI agent runtime — drives a virtual filesystem via any LLM (ai.libx.js). Same bytes run in node, browser, or edge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",