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 +1 -0
- package/dist/loop.js +7 -1
- package/dist/permissions.js +24 -1
- package/package.json +1 -1
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.";
|
package/dist/permissions.js
CHANGED
|
@@ -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".
|