agent-from-zero 0.39.1 → 0.39.2

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/README.md CHANGED
@@ -97,6 +97,18 @@ auto 模式下,改当前项目、其他 git 仓库或临时目录里的文件
97
97
 
98
98
  也可以在确认弹窗里选 "Yes, and trust …/ for this session",只对本次会话有效。家目录本身和 `/` 不能被信任,`settings.json` 本身永远要你确认。
99
99
 
100
+ ### prod 保护(permissions.ask)
101
+
102
+ `permissions.ask` 里列的文字,只要出现在命令或文件路径里,就一定要你确认(不区分大小写,按子串匹配)。auto 审核和 `--dangerously-skip-permissions` 都不能替你放行:
103
+
104
+ ```json
105
+ {
106
+ "permissions": { "ask": ["--context prod", "cipherowl-aws-prod", "terraform apply"] }
107
+ }
108
+ ```
109
+
110
+ 排查 prod 问题时,往往一连串命令都带着同一个标记(比如每条查询都用 `AWS_PROFILE=cipherowl-aws-prod`)。auto 模式下,确认弹窗里可以选 "Yes, and let auto review handle … commands this session":本次会话里,命中这些规则的命令不再强制找你,改为交给 auto 审核逐条判断。只读的直接执行,删除、部署这类做了就回不去的仍然会问你。这个放行只在 auto 模式下有效,关掉 auto 或用 `--dangerously-skip-permissions` 运行时都会恢复每次确认。
111
+
100
112
  ### 换别的模型
101
113
 
102
114
  和 API key 放在同一个地方,设置这三个变量就行,各家的 base URL 写在 [.env.example](.env.example) 里:
package/dist/agent.js CHANGED
@@ -442,7 +442,7 @@ async function main() {
442
442
  // Fail closed: in a non-interactive session nobody can say yes, so the
443
443
  // answer is no. MINI_AGENT_AUTO_APPROVE=1 bypasses the QUESTION, but hard
444
444
  // denies were already enforced in permissions.ts before we get here.
445
- const confirm = async (question, toolName, trustDir) => {
445
+ const confirm = async (question, toolName, offer) => {
446
446
  console.log(chalk.yellow(`\n⚠ approval needed — ${question}`)); // always show what is being asked
447
447
  if (!autoMode.enabled && process.env.MINI_AGENT_AUTO_APPROVE === "1") {
448
448
  console.log(chalk.dim(" auto-approved (MINI_AGENT_AUTO_APPROVE=1)")); // bypass mode — say so out loud
@@ -454,8 +454,8 @@ async function main() {
454
454
  }
455
455
  // Selecting beats typing: ↑/↓ + Enter instead of "type y/N". The middle
456
456
  // option lets the user stop being asked about this tool (or, in auto mode,
457
- // this outside directory) for the session.
458
- const menu = approvalMenu(autoMode.enabled, toolName, trustDir);
457
+ // this outside directory or these prod-guard rules) for the session.
458
+ const menu = approvalMenu(autoMode.enabled, toolName, offer);
459
459
  const { approved, granted } = menu.choose(await select(menu.options));
460
460
  if (granted)
461
461
  console.log(chalk.dim(` ${granted}`));
package/dist/ink/app.js CHANGED
@@ -279,8 +279,8 @@ export function App({ session, runTurn, clipboard }) {
279
279
  const hooks = {
280
280
  images: attachments,
281
281
  output: sink,
282
- confirm: (question, toolName, trustDir) => new Promise((resolve) => {
283
- const menu = approvalMenu(autoMode.enabled, toolName, trustDir); // same choices and session grants as the readline REPL
282
+ confirm: (question, toolName, offer) => new Promise((resolve) => {
283
+ const menu = approvalMenu(autoMode.enabled, toolName, offer); // same choices and session grants as the readline REPL
284
284
  openSelect(`⚠ approval needed — ${question}`, menu.options, (i) => {
285
285
  const { approved, granted } = menu.choose(i);
286
286
  note(`${chalk.yellow("⚠ approval")} — ${question.split("\n")[0]} → ${approved ? chalk.green("✓ approved") : chalk.yellow("✗ declined")}${granted ? chalk.dim(` (${granted})`) : ""}`);
package/dist/loop.js CHANGED
@@ -772,7 +772,7 @@ async function authorizeCall(call, opts) {
772
772
  if (preview)
773
773
  sink(opts).note(preview.replace(/^/gm, agentIndent(opts)));
774
774
  }
775
- const ok = autoAllowed || (!(auto && opts.canPrompt === false) && await opts.confirm(`${call.name} (${v.reason}):\n ${v.summary}${reviewReason ? `\n ${reviewReason}` : ""}`, call.name, v.trustDir));
775
+ const ok = autoAllowed || (!(auto && opts.canPrompt === false) && await opts.confirm(`${call.name} (${v.reason}):\n ${v.summary}${reviewReason ? `\n ${reviewReason}` : ""}`, call.name, { trustDir: v.trustDir, guardRules: v.guardRules }));
776
776
  // Every human prompt in auto mode is an interruption worth counting, with
777
777
  // where it came from: the reviewer asked ("review", joinable by reviewId),
778
778
  // a doubt outlived the agent's retries ("escalated"), or a rule required a
@@ -468,14 +468,27 @@ export function recoverableShell(command) {
468
468
  // Text that always needs a human: a prod context, a deployment verb, a prod
469
469
  // path. Checked BEFORE auto review and BEFORE the bypass rewrite, so neither
470
470
  // can approve it. Case-insensitive substring, on the whole command or the path.
471
- function guardHit(text) {
471
+ //
472
+ // Investigating prod means many commands carrying the same marker (every query
473
+ // under AWS_PROFILE=cipherowl-aws-prod), so in auto mode the human may release
474
+ // the rules a command matched, for the rest of the session. A released rule no
475
+ // longer forces a human: its commands go to auto review one by one, which
476
+ // still asks for anything irreversible. Only auto mode honours a release: the
477
+ // default mode has no reviewer behind it, and bypass has nothing at all.
478
+ const releasedGuards = new Set();
479
+ export function releaseGuards(rules) {
480
+ for (const rule of rules)
481
+ releasedGuards.add(rule.toLowerCase());
482
+ emit("agent_guard_released", { rules: rules.join(",") });
483
+ }
484
+ // Every rule the text matches that still needs a human (all of them, so one
485
+ // approval can release them together rather than one prompt per rule).
486
+ function guardHits(text, auto) {
472
487
  const haystack = text.toLowerCase();
473
- for (const rule of CONFIG.permissions.ask ?? []) {
474
- if (rule && haystack.includes(rule.toLowerCase()))
475
- return rule;
476
- }
477
- return null;
488
+ const honourReleases = auto && !CONFIG.bypassPermissions;
489
+ return (CONFIG.permissions.ask ?? []).filter((rule) => rule && haystack.includes(rule.toLowerCase()) && !(honourReleases && releasedGuards.has(rule.toLowerCase())));
478
490
  }
491
+ const guardVerdict = (rules, summary) => ({ decision: "ask", requiresHuman: true, reason: `prod guard: ${rules.map((r) => `"${r}"`).join(", ")}`, summary, guardRules: rules });
479
492
  // Decide what a bash command deserves: deny, ask, or allow.
480
493
  function checkBash(command, auto = false) {
481
494
  const summary = command.trim(); // what the user will see in the prompt
@@ -492,9 +505,9 @@ function checkBash(command, auto = false) {
492
505
  for (const d of user.deny)
493
506
  if (command.toLowerCase().includes(d.toLowerCase()))
494
507
  return { decision: "deny", reason: `denied by your settings ("${d}")`, summary };
495
- const guard = guardHit(command);
496
- if (guard)
497
- return { decision: "ask", requiresHuman: true, reason: `prod guard: "${guard}"`, summary };
508
+ const guards = guardHits(command, auto);
509
+ if (guards.length)
510
+ return guardVerdict(guards, summary);
498
511
  // node/npm/npx, git aliases and tool-wide grants can run arbitrary code.
499
512
  // Auto mode must inspect the actual command, including background commands.
500
513
  if (auto) {
@@ -690,9 +703,9 @@ function basePermission(toolName, argsJson, auto) {
690
703
  const hit = noFlyHit(p); // is it in the no-fly zone?
691
704
  if (hit)
692
705
  return { decision: "deny", reason: hit, summary: p }; // hard stop — deny always wins
693
- const guard = guardHit(p); // the user's prod guard, applied to the path
694
- if (guard)
695
- return { decision: "ask", requiresHuman: true, reason: `prod guard: "${guard}"`, summary: p };
706
+ const guards = guardHits(p, auto); // the user's prod guard, applied to the path
707
+ if (guards.length)
708
+ return guardVerdict(guards, p);
696
709
  // The user may pre-approve write tools ("tool:edit_file") to skip the
697
710
  // prompt — note this runs AFTER the no-fly check, so it widens "ask",
698
711
  // never "deny".
@@ -717,7 +730,8 @@ function basePermission(toolName, argsJson, auto) {
717
730
  // upgrades an ASK to ALLOW — but a hard DENY (rm -rf /, .git, .env…) always
718
731
  // stands. Convenience never overrides the no-fly rules. A grant for either
719
732
  // shell tool covers the other: the command is what was vouched for.
720
- if (!auto && verdict.decision === "ask" && (CONFIG.permissions.allow.includes(`tool:${toolName}`) || CONFIG.permissions.allow.includes("tool:run_bash"))) {
733
+ // Nor a prod-guard ask: that one needs a human every time, grant or not.
734
+ if (!auto && verdict.decision === "ask" && !verdict.requiresHuman && (CONFIG.permissions.allow.includes(`tool:${toolName}`) || CONFIG.permissions.allow.includes("tool:run_bash"))) {
721
735
  return { decision: "allow", reason: "bash pre-approved for this session", summary: verdict.summary };
722
736
  }
723
737
  return verdict;
@@ -772,15 +786,11 @@ function resolveFileTarget(target) {
772
786
  }
773
787
  return path.join(fs.realpathSync(current), ...missing);
774
788
  }
775
- // ---- The approval menu ----------------------------------------------------------
776
- // Both frontends (readline and Ink) offer the same choices and apply the same
777
- // session grant, so the menu lives next to the rules it widens. Outside auto
778
- // mode the grant is "don't ask again for this tool"; in auto mode it is "trust
779
- // this directory", offered only for an outside write. The last option is always
780
- // "No", and so is -1 (Esc). `granted` is the note to show after a grant.
781
- export function approvalMenu(auto, toolName, trustDir) {
789
+ export function approvalMenu(auto, toolName, offer = {}) {
790
+ const { trustDir, guardRules } = offer;
791
+ const rules = guardRules?.map((r) => `"${r}"`).join(", ");
782
792
  const grant = auto
783
- ? (trustDir ? `Yes, and trust ${trustDir}/ for this session` : null)
793
+ ? (trustDir ? `Yes, and trust ${trustDir}/ for this session` : rules ? `Yes, and let auto review handle ${rules} commands this session` : null)
784
794
  : (toolName ? `Yes, and don't ask again for ${toolName} this session` : "Yes, and don't ask again this session");
785
795
  const options = [auto ? "Yes, once" : "Yes", ...(grant ? [grant] : []), "No — let me tell the agent what to do instead"];
786
796
  const choose = (i) => {
@@ -792,6 +802,10 @@ export function approvalMenu(auto, toolName, trustDir) {
792
802
  CONFIG.autoMode.trustedDirs.push(trustDir);
793
803
  return { approved: true, granted: `trusting ${trustDir}/ for this session` };
794
804
  }
805
+ if (auto && guardRules?.length) {
806
+ releaseGuards(guardRules);
807
+ return { approved: true, granted: `${rules} commands go to auto review for the rest of this session` };
808
+ }
795
809
  if (!toolName)
796
810
  return { approved: true };
797
811
  CONFIG.permissions.allow.push(`tool:${toolName}`); // "don't ask again" → session allowlist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-from-zero",
3
- "version": "0.39.1",
3
+ "version": "0.39.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mini-agent": "dist/agent.js"