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 +12 -0
- package/dist/agent.js +3 -3
- package/dist/ink/app.js +2 -2
- package/dist/loop.js +1 -1
- package/dist/permissions.js +35 -21
- package/package.json +1 -1
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,
|
|
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,
|
|
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,
|
|
283
|
-
const menu = approvalMenu(autoMode.enabled, toolName,
|
|
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
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
|
496
|
-
if (
|
|
497
|
-
return
|
|
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
|
|
694
|
-
if (
|
|
695
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|