@gaberrb/polypus 0.4.3 → 0.4.5

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.
package/dist/index.js CHANGED
@@ -109,6 +109,7 @@ var en = {
109
109
  "cli.opt.agent": "which configured agent to use",
110
110
  "cli.opt.mode": "plan | review | bypass (overrides config)",
111
111
  "cli.opt.maxSteps": "maximum agent steps",
112
+ "cli.opt.json": "headless mode: emit a single JSON object (steps, tool calls, files changed, usage) instead of the TUI \u2014 use with --mode bypass",
112
113
  "cli.arg.swarmTask": "high-level task to split across agents",
113
114
  "cli.opt.agents": "comma-separated agent names (default: all configured)",
114
115
  "cli.opt.maxSubtasks": "maximum number of parallel subtasks",
@@ -135,6 +136,7 @@ var en = {
135
136
  "run.reprompt": "\u21BB no tool call \u2014 reinforcing instructions (attempt {attempt})",
136
137
  "run.autocorrect": "\u21BB tool failed \u2014 auto-correcting with extra context",
137
138
  "run.cancelled": "\u25A0 cancelled",
139
+ "run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
138
140
  // repl
139
141
  "repl.welcome": "Polypus interactive session.",
140
142
  "repl.welcomeHint": " Type /help for commands, /exit to quit.",
@@ -289,7 +291,10 @@ var en = {
289
291
  // @-mentions
290
292
  "mentions.injectedHeader": "Referenced files (@-mentions)",
291
293
  "mentions.dirHeader": "@{path} (directory listing)",
292
- "mentions.notFound": "(could not resolve @{path}: not found or outside the allow-list)"
294
+ "mentions.notFound": "(could not resolve @{path}: not found or outside the allow-list)",
295
+ // safety policy
296
+ "policy.blockedCommand": "blocked by safety policy ({reason}) \u2014 refusing in all modes",
297
+ "policy.secretFound": "write blocked: a possible secret ({kind}) was found on line {line}. Remove it or load it from an environment variable instead of hard-coding it."
293
298
  };
294
299
  var ptBR = {
295
300
  "common.default": "padr\xE3o",
@@ -334,6 +339,7 @@ var ptBR = {
334
339
  "cli.opt.agent": "qual agente configurado usar",
335
340
  "cli.opt.mode": "plan | review | bypass (sobrescreve a config)",
336
341
  "cli.opt.maxSteps": "n\xFAmero m\xE1ximo de passos do agente",
342
+ "cli.opt.json": "modo headless: emite um \xFAnico objeto JSON (passos, tool calls, arquivos alterados, uso) em vez da TUI \u2014 use com --mode bypass",
337
343
  "cli.arg.swarmTask": "tarefa de alto n\xEDvel para dividir entre os agentes",
338
344
  "cli.opt.agents": "nomes de agentes separados por v\xEDrgula (padr\xE3o: todos)",
339
345
  "cli.opt.maxSubtasks": "n\xFAmero m\xE1ximo de subtarefas paralelas",
@@ -358,6 +364,7 @@ var ptBR = {
358
364
  "run.reprompt": "\u21BB nenhuma chamada de tool \u2014 refor\xE7ando instru\xE7\xF5es (tentativa {attempt})",
359
365
  "run.autocorrect": "\u21BB tool falhou \u2014 autocorrigindo com contexto extra",
360
366
  "run.cancelled": "\u25A0 cancelado",
367
+ "run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
361
368
  "repl.welcome": "Sess\xE3o interativa do Polypus.",
362
369
  "repl.welcomeHint": " Digite /help para comandos, /exit para sair.",
363
370
  "repl.modeChanged": "modo \u2192 {mode}",
@@ -486,6 +493,9 @@ var ptBR = {
486
493
  "mentions.injectedHeader": "Arquivos referenciados (@-mentions)",
487
494
  "mentions.dirHeader": "@{path} (conte\xFAdo do diret\xF3rio)",
488
495
  "mentions.notFound": "(n\xE3o foi poss\xEDvel resolver @{path}: n\xE3o encontrado ou fora da allow-list)",
496
+ // safety policy
497
+ "policy.blockedCommand": "bloqueado pela pol\xEDtica de seguran\xE7a ({reason}) \u2014 recusado em todos os modos",
498
+ "policy.secretFound": "escrita bloqueada: poss\xEDvel segredo ({kind}) encontrado na linha {line}. Remova-o ou carregue de uma vari\xE1vel de ambiente em vez de fix\xE1-lo no c\xF3digo.",
489
499
  "models.fetching": "Buscando modelos do OpenRouter\u2026",
490
500
  "models.fetchError": "N\xE3o foi poss\xEDvel buscar modelos: {msg}",
491
501
  "models.none": "Nenhum modelo corresponde aos filtros.",
@@ -1001,6 +1011,53 @@ function isCommandPreApproved(allowedCommands, command) {
1001
1011
  return allowedCommands.some((prefix) => c === prefix || c.startsWith(prefix.trim() + " "));
1002
1012
  }
1003
1013
 
1014
+ // src/core/permissions/policy.ts
1015
+ var DANGEROUS_COMMANDS = [
1016
+ { re: /--no-preserve-root/i, reason: "rm --no-preserve-root" },
1017
+ { re: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: "fork bomb" },
1018
+ { re: /\bmkfs(\.\w+)?\b/i, reason: "filesystem format (mkfs)" },
1019
+ { re: /\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|hd|disk)/i, reason: "dd writing to a raw disk device" },
1020
+ { re: />\s*\/dev\/(sd|nvme|hd|disk)/i, reason: "redirect to a raw disk device" },
1021
+ { re: /\bchmod\s+(-[a-z]*\s+)*-?R?\s*777\s+\//i, reason: "chmod 777 on /" },
1022
+ { re: /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/i, reason: "piping a downloaded script straight into a shell" }
1023
+ ];
1024
+ function isDangerousRm(command) {
1025
+ if (!/\brm\b/i.test(command)) return false;
1026
+ const recursive = /(?:^|\s)-[a-z]*r[a-z]*f/i.test(command) || /(?:^|\s)-[a-z]*f[a-z]*r/i.test(command) || /(?:^|\s)-[a-z]*r\b/i.test(command) && /(?:^|\s)-[a-z]*f\b/i.test(command);
1027
+ if (!recursive) return false;
1028
+ return /(?:\s|^)(?:\/\*|\/|~|\$HOME|\*)(?:\s|$)/.test(command);
1029
+ }
1030
+ function screenCommand(command) {
1031
+ if (isDangerousRm(command)) {
1032
+ return { blocked: true, reason: "recursive force-delete of / ~ or *" };
1033
+ }
1034
+ for (const { re, reason } of DANGEROUS_COMMANDS) {
1035
+ if (re.test(command)) return { blocked: true, reason };
1036
+ }
1037
+ return { blocked: false };
1038
+ }
1039
+ var SECRET_PATTERNS = [
1040
+ { re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/, kind: "private key block" },
1041
+ { re: /\bAKIA[0-9A-Z]{16}\b/, kind: "AWS access key id" },
1042
+ { re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, kind: "GitHub token" },
1043
+ { re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, kind: "Slack token" },
1044
+ { re: /\bsk-[A-Za-z0-9]{32,}\b/, kind: "OpenAI-style secret key" },
1045
+ { re: /\bAIza[0-9A-Za-z_-]{35}\b/, kind: "Google API key" }
1046
+ ];
1047
+ function scanSecrets(text2) {
1048
+ const findings = [];
1049
+ const lines = text2.split("\n");
1050
+ for (let i = 0; i < lines.length; i++) {
1051
+ for (const { re, kind } of SECRET_PATTERNS) {
1052
+ if (re.test(lines[i])) {
1053
+ findings.push({ line: i + 1, kind });
1054
+ break;
1055
+ }
1056
+ }
1057
+ }
1058
+ return findings;
1059
+ }
1060
+
1004
1061
  // src/core/permissions/modes.ts
1005
1062
  var PermissionEngine = class {
1006
1063
  constructor(opts) {
@@ -1015,9 +1072,17 @@ var PermissionEngine = class {
1015
1072
  const d = checkPath(this.opts.policy, target);
1016
1073
  return d.allowed ? { allowed: true } : { allowed: false, reason: d.reason };
1017
1074
  }
1018
- async authorizeWrite(target, preview) {
1075
+ async authorizeWrite(target, preview, content) {
1019
1076
  const d = checkPath(this.opts.policy, target);
1020
1077
  if (!d.allowed) return { allowed: false, reason: d.reason };
1078
+ const findings = scanSecrets(content ?? preview ?? "");
1079
+ if (findings.length > 0) {
1080
+ const first = findings[0];
1081
+ return {
1082
+ allowed: false,
1083
+ reason: t("policy.secretFound", { kind: first.kind, line: first.line })
1084
+ };
1085
+ }
1021
1086
  if (this.opts.mode === "plan") {
1022
1087
  return { allowed: false, reason: "plan mode: file modifications are disabled" };
1023
1088
  }
@@ -1026,6 +1091,10 @@ var PermissionEngine = class {
1026
1091
  return ok ? { allowed: true } : { allowed: false, reason: "rejected by user" };
1027
1092
  }
1028
1093
  async authorizeCommand(command) {
1094
+ const screen = screenCommand(command);
1095
+ if (screen.blocked) {
1096
+ return { allowed: false, reason: t("policy.blockedCommand", { reason: screen.reason ?? "" }) };
1097
+ }
1029
1098
  if (this.opts.mode === "plan") {
1030
1099
  return { allowed: false, reason: "plan mode: running commands is disabled" };
1031
1100
  }
@@ -1313,7 +1382,8 @@ var editFileTool = {
1313
1382
  const decision = await ctx.permissions.authorizeWrite(
1314
1383
  args.data.path,
1315
1384
  `- ${firstLine(args.data.search)}
1316
- + ${firstLine(args.data.replace)}`
1385
+ + ${firstLine(args.data.replace)}`,
1386
+ args.data.replace
1317
1387
  );
1318
1388
  if (!decision.allowed) return { ok: false, output: `Edit denied: ${decision.reason}` };
1319
1389
  try {
@@ -1595,7 +1665,7 @@ var writeFileTool = {
1595
1665
  };
1596
1666
  }
1597
1667
  const preview = previewContent(args.data.content);
1598
- const decision = await ctx.permissions.authorizeWrite(args.data.path, preview);
1668
+ const decision = await ctx.permissions.authorizeWrite(args.data.path, preview, args.data.content);
1599
1669
  if (!decision.allowed) return { ok: false, output: `Write denied: ${decision.reason}` };
1600
1670
  try {
1601
1671
  const abs = resolve6(ctx.workspace, args.data.path);
@@ -2032,6 +2102,60 @@ ${blocks.join("\n\n")}`;
2032
2102
  return { task: augmented, injected };
2033
2103
  }
2034
2104
 
2105
+ // src/cli/commands/json-output.ts
2106
+ var OUTPUT_PREVIEW = 500;
2107
+ function createJsonCollector() {
2108
+ const log = [];
2109
+ const filesChanged = /* @__PURE__ */ new Set();
2110
+ const events = {
2111
+ onStep(step) {
2112
+ log.push({ type: "step", step });
2113
+ },
2114
+ onAssistantText(text2) {
2115
+ if (text2.trim()) log.push({ type: "assistant", text: text2 });
2116
+ },
2117
+ onToolCall(call) {
2118
+ log.push({ type: "tool_call", name: call.name, arguments: call.arguments });
2119
+ },
2120
+ onToolResult(call, result) {
2121
+ log.push({
2122
+ type: "tool_result",
2123
+ name: call.name,
2124
+ ok: result.ok,
2125
+ output: result.output.slice(0, OUTPUT_PREVIEW)
2126
+ });
2127
+ if (result.ok && (call.name === "write_file" || call.name === "edit_file")) {
2128
+ const path = call.arguments.path;
2129
+ if (typeof path === "string") filesChanged.add(path);
2130
+ }
2131
+ },
2132
+ onCorrection(call) {
2133
+ log.push({ type: "correction", name: call.name });
2134
+ },
2135
+ onReprompt(attempt) {
2136
+ log.push({ type: "reprompt", attempt });
2137
+ },
2138
+ onUsage() {
2139
+ }
2140
+ };
2141
+ return {
2142
+ events,
2143
+ build(result) {
2144
+ return {
2145
+ result: {
2146
+ reason: result.reason,
2147
+ finished: result.finished,
2148
+ steps: result.steps,
2149
+ summary: result.summary,
2150
+ filesChanged: [...filesChanged],
2151
+ usage: result.usage
2152
+ },
2153
+ events: log
2154
+ };
2155
+ }
2156
+ };
2157
+ }
2158
+
2035
2159
  // src/ui/repl.ts
2036
2160
  import pc6 from "picocolors";
2037
2161
 
@@ -3285,20 +3409,23 @@ async function run(task, opts) {
3285
3409
  const resolved2 = createProvider(active);
3286
3410
  await executeTask(taskText, resolved2, workspace, session);
3287
3411
  };
3412
+ if (opts.json && !task) throw new Error(t("run.jsonNeedsTask"));
3288
3413
  if (task) {
3289
3414
  const resolved2 = createProvider(agentConfig);
3290
- console.log(
3291
- pc8.dim(
3292
- t("run.status", {
3293
- name: resolved2.config.name,
3294
- provider: resolved2.config.provider,
3295
- model: resolved2.config.model,
3296
- toolMode: resolved2.toolMode,
3297
- mode: session.mode
3298
- })
3299
- )
3300
- );
3301
- await executeTask(task, resolved2, workspace, session);
3415
+ if (!opts.json) {
3416
+ console.log(
3417
+ pc8.dim(
3418
+ t("run.status", {
3419
+ name: resolved2.config.name,
3420
+ provider: resolved2.config.provider,
3421
+ model: resolved2.config.model,
3422
+ toolMode: resolved2.toolMode,
3423
+ mode: session.mode
3424
+ })
3425
+ )
3426
+ );
3427
+ }
3428
+ await executeTask(task, resolved2, workspace, session, opts.json ?? false);
3302
3429
  return;
3303
3430
  }
3304
3431
  const resolved = createProvider(agentConfig);
@@ -3321,7 +3448,7 @@ async function run(task, opts) {
3321
3448
  };
3322
3449
  await startRepl(ctx);
3323
3450
  }
3324
- async function executeTask(task, resolved, workspace, session) {
3451
+ async function executeTask(task, resolved, workspace, session, json = false) {
3325
3452
  const mention = await resolveMentions(task, {
3326
3453
  workspace,
3327
3454
  allow: session.allow,
@@ -3329,16 +3456,18 @@ async function executeTask(task, resolved, workspace, session) {
3329
3456
  });
3330
3457
  if (mention.injected.length > 0) {
3331
3458
  task = mention.task;
3332
- console.log(pc8.dim(`\u21B3 @ ${mention.injected.join(", ")}`));
3459
+ if (!json) console.log(pc8.dim(`\u21B3 @ ${mention.injected.join(", ")}`));
3333
3460
  }
3334
3461
  const spinner3 = new Spinner();
3335
3462
  const controller = new AbortController();
3336
3463
  const cancel2 = listenForCancel(controller);
3464
+ const collector = json ? createJsonCollector() : void 0;
3337
3465
  const permissions = new PermissionEngine({
3338
3466
  mode: session.mode,
3339
3467
  policy: { workspace, allow: session.allow, deny: session.deny },
3340
3468
  allowedCommands: session.allowedCommands,
3341
- confirm: async (req) => {
3469
+ // Headless runs have no TTY for confirmations — use --mode bypass instead.
3470
+ confirm: json ? async () => false : async (req) => {
3342
3471
  spinner3.stop();
3343
3472
  cancel2.pause();
3344
3473
  const ok = await confirmAction(req);
@@ -3346,7 +3475,7 @@ async function executeTask(task, resolved, workspace, session) {
3346
3475
  return ok;
3347
3476
  }
3348
3477
  });
3349
- spinner3.start(t("ui.thinking"));
3478
+ if (!json) spinner3.start(t("ui.thinking"));
3350
3479
  let result;
3351
3480
  try {
3352
3481
  result = await runAgent({
@@ -3358,13 +3487,17 @@ async function executeTask(task, resolved, workspace, session) {
3358
3487
  history: session.history,
3359
3488
  maxSteps: session.maxSteps,
3360
3489
  signal: controller.signal,
3361
- events: renderEvents(spinner3)
3490
+ events: collector ? collector.events : renderEvents(spinner3)
3362
3491
  });
3363
3492
  } finally {
3364
3493
  spinner3.stop();
3365
3494
  cancel2.dispose();
3366
3495
  }
3367
3496
  session.history = result.messages;
3497
+ if (collector) {
3498
+ process.stdout.write(JSON.stringify(collector.build(result)) + "\n");
3499
+ return;
3500
+ }
3368
3501
  if (result.reason === "finished") {
3369
3502
  console.log(pc8.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
3370
3503
  } else if (result.reason === "cancelled") {
@@ -4115,7 +4248,7 @@ function buildProgram() {
4115
4248
  program.command("add-agent").argument("<name>", t("cli.arg.addAgentName")).requiredOption("--provider <provider>", t("cli.opt.provider")).requiredOption("--model <model>", t("cli.opt.model")).option("--api-key <key>", t("cli.opt.apiKey")).option("--base-url <url>", t("cli.opt.baseUrl")).option("--tool-mode <mode>", t("cli.opt.toolMode"), "auto").option("--set-default", t("cli.opt.setDefault")).description(t("cli.cmd.addAgent")).action((name, opts) => addAgent(name, opts));
4116
4249
  program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
4117
4250
  program.command("list-agents").alias("agents").description(t("cli.cmd.listAgents")).action(() => listAgents());
4118
- program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
4251
+ program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).option("--json", t("cli.opt.json")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
4119
4252
  program.command("swarm").argument("<task>", t("cli.arg.swarmTask")).option("--agents <names>", t("cli.opt.agents")).option("--max-subtasks <n>", t("cli.opt.maxSubtasks")).description(t("cli.cmd.swarm")).action((task, opts) => swarm(task, opts));
4120
4253
  program.command("models").option("--search <text>", t("cli.opt.search")).option("--tools", t("cli.opt.toolsOnly")).option("--free", t("cli.opt.free")).option("--max-price <usd>", t("cli.opt.maxPrice")).option("--sort <order>", t("cli.opt.sort")).option("--limit <n>", t("cli.opt.limit")).description(t("cli.cmd.models")).action((opts) => models(opts));
4121
4254
  program.command("prd").argument("<issue>", t("cli.arg.prdIssue")).option("--out <file>", t("cli.opt.out")).option("--model <model>", t("cli.opt.model")).option("--input <file>", t("cli.opt.input")).description(t("cli.cmd.prd")).action((issue, opts) => prd(issue, opts));