agent-from-zero 0.36.0 → 0.37.0

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/config.js CHANGED
@@ -111,6 +111,7 @@ export const CONFIG = {
111
111
  // the test suite injects rules here instead of writing temp files.
112
112
  allow: [...(globalSettings.permissions?.allow ?? []), ...(projectSettings.permissions?.allow ?? [])],
113
113
  deny: [...(globalSettings.permissions?.deny ?? []), ...(projectSettings.permissions?.deny ?? [])],
114
+ ask: [...(globalSettings.permissions?.ask ?? []), ...(projectSettings.permissions?.ask ?? [])],
114
115
  },
115
116
  // Hooks for every event, merged across layers (global first, then project) by
116
117
  // a generic union so adding a new event needs no edit here.
package/dist/loop.js CHANGED
@@ -744,8 +744,14 @@ async function authorizeCall(call, opts) {
744
744
  emit("agent_tool_declined", { tool: call.name, ...(reviewId ? { reviewId } : {}) }); // the human said no — that is signal
745
745
  if (!ok && !opts.quiet)
746
746
  sink(opts).note(mark.declined); // make the refusal visible
747
- if (!ok)
747
+ if (!ok) {
748
+ // Say what actually happened. An unattended worker was never declined by
749
+ // anyone — saying so teaches the model to rephrase and retry against a
750
+ // refusal that never existed.
751
+ if (opts.canPrompt === false)
752
+ return `[permission] Nobody is reachable to approve this here${reviewReason ? `: ${reviewReason}` : "."} Not executed. Report it in your result and continue with what the rules allow; do not retry the same effect through another tool, wrapper or worker.`;
748
753
  return `[permission] Action not approved${reviewReason ? `: ${reviewReason}` : ". The user declined this action"}. Ask the user how to proceed, or choose a safer alternative.`;
754
+ }
749
755
  }
750
756
  if (opts.reviewBudget?.exhausted)
751
757
  return "[permission] Review limit reached; not executed. Wait for human direction.";
@@ -3,6 +3,7 @@ import fs from "node:fs"; // resolve symlinks before auto mode grants file acces
3
3
  import os from "node:os"; // `cd ~` in the recoverable-shell analyzer
4
4
  import { execFileSync } from "node:child_process"; // ask git whether a deletion target is disposable
5
5
  import { CONFIG } from "./config.js"; // user-configured allow/deny rules from settings files
6
+ import { emit } from "./telemetry.js"; // a bypassed approval leaves an audit row, not silence
6
7
  // ---- The no-fly zone --------------------------------------------------------
7
8
  // Writes here are never OK, in any mode. .git can destroy history, shell rc
8
9
  // files inject code into every future terminal, .ssh/.env leak credentials.
@@ -442,6 +443,18 @@ export function recoverableShell(command) {
442
443
  }
443
444
  return readOnly ? { readOnly, reason: "read-only command" } : { readOnly, reason: "recoverable: git can undo it, or it only removes gitignored or temp files" };
444
445
  }
446
+ // ---- The user's prod guard (permissions.ask) ----------------------------------
447
+ // Text that always needs a human: a prod context, a deployment verb, a prod
448
+ // path. Checked BEFORE auto review and BEFORE the bypass rewrite, so neither
449
+ // can approve it. Case-insensitive substring, on the whole command or the path.
450
+ function guardHit(text) {
451
+ const haystack = text.toLowerCase();
452
+ for (const rule of CONFIG.permissions.ask ?? []) {
453
+ if (rule && haystack.includes(rule.toLowerCase()))
454
+ return rule;
455
+ }
456
+ return null;
457
+ }
445
458
  // Decide what a bash command deserves: deny, ask, or allow.
446
459
  function checkBash(command, auto = false) {
447
460
  const summary = command.trim(); // what the user will see in the prompt
@@ -458,6 +471,9 @@ function checkBash(command, auto = false) {
458
471
  for (const d of user.deny)
459
472
  if (command.toLowerCase().includes(d.toLowerCase()))
460
473
  return { decision: "deny", reason: `denied by your settings ("${d}")`, summary };
474
+ const guard = guardHit(command);
475
+ if (guard)
476
+ return { decision: "ask", requiresHuman: true, reason: `prod guard: "${guard}"`, summary };
461
477
  // node/npm/npx, git aliases and tool-wide grants can run arbitrary code.
462
478
  // Auto mode must inspect the actual command, including background commands.
463
479
  if (auto) {
@@ -539,7 +555,11 @@ export function checkPermission(toolName, argsJson, auto = false) {
539
555
  summary: base.summary,
540
556
  };
541
557
  }
542
- if (CONFIG.bypassPermissions && base.decision === "ask" && toolName !== "exit_plan_mode" && toolName !== "ask_user") {
558
+ if (CONFIG.bypassPermissions && base.decision === "ask" && !base.requiresHuman && toolName !== "exit_plan_mode" && toolName !== "ask_user") {
559
+ // Bypass may skip an ordinary approval, never one the user's own guard list
560
+ // marked for a human. Every skip is recorded: a bypass run with no audit row
561
+ // is indistinguishable from a run that never checked.
562
+ emit("agent_permission_bypassed", { tool: toolName, reason: base.reason });
543
563
  return { decision: "allow", reason: "permission approvals bypassed for this run", summary: base.summary };
544
564
  }
545
565
  return base;
@@ -645,6 +665,9 @@ function basePermission(toolName, argsJson, auto) {
645
665
  const hit = noFlyHit(p); // is it in the no-fly zone?
646
666
  if (hit)
647
667
  return { decision: "deny", reason: hit, summary: p }; // hard stop — deny always wins
668
+ const guard = guardHit(p); // the user's prod guard, applied to the path
669
+ if (guard)
670
+ return { decision: "ask", requiresHuman: true, reason: `prod guard: "${guard}"`, summary: p };
648
671
  // The user may pre-approve write tools ("tool:edit_file") to skip the
649
672
  // prompt — note this runs AFTER the no-fly check, so it widens "ask",
650
673
  // never "deny".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-from-zero",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mini-agent": "dist/agent.js"