@armadra/agent 0.3.0 → 0.4.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/CHANGELOG.md +84 -0
- package/README.md +93 -44
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +1 -1
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +12 -3
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +3 -2
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/registry.d.ts +8 -1
- package/dist/ai/providers/registry.js +30 -22
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +15279 -10151
- package/dist/cli/args.d.ts +19 -4
- package/dist/cli/args.js +83 -10
- package/dist/cli/bootstrap.js +27 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-session.d.ts +5 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +14 -4
- package/dist/cli/compose.js +29 -10
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +36 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +9 -2
- package/dist/cli/main.js +77 -3
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +14 -12
- package/dist/cli/subcommands/config.d.ts +19 -3
- package/dist/cli/subcommands/config.js +108 -21
- package/dist/cli/subcommands/context.js +4 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/models-discover.d.ts +15 -9
- package/dist/cli/subcommands/models-discover.js +53 -46
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +3 -2
- package/dist/cli/subcommands/providers.js +48 -45
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +6 -2
- package/dist/config/init.js +12 -5
- package/dist/config/json-schema.d.ts +1 -0
- package/dist/config/json-schema.js +36 -3
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +24 -5
- package/dist/config/types.d.ts +34 -6
- package/dist/config/types.js +17 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +56 -16
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +65 -81
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +6 -4
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +31 -4
- package/dist/modes/print/print-mode.js +116 -7
- package/dist/modes/rpc/commands.js +5 -0
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +33 -10
- package/docs/rpc.md +36 -36
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +134 -0
- package/docs/tui.md +139 -62
- package/package.json +3 -1
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto 模式下 bash 的规则层与静态判定(§7.4,docs/permissions.md)。
|
|
3
|
+
*
|
|
4
|
+
* 规则层(命中即询问,不调模型)——外层与每层嵌套命令(`sh -c`、`eval`、`xargs`、`find -exec`,
|
|
5
|
+
* 见 dangerous.ts)的每一段都查:
|
|
6
|
+
* - 网络命令({@link networkReason}):curl / wget / ssh / git push|pull|fetch|clone / npm install / npx …
|
|
7
|
+
* - 删除与回退({@link destructiveReason}):rm -r|-f、find -delete、git clean / restore / checkout -- …
|
|
8
|
+
* - 写入目标:重定向(`>`、`>>`、`&>`)、tee、cp / mv / mkdir / touch / ln / chmod 等的目标在项目外或受保护;
|
|
9
|
+
* - 参数里出现机密路径(`cat .env`、`< ~/.ssh/id_rsa`、`--env-file=.env`)。
|
|
10
|
+
*
|
|
11
|
+
* 静态判定(全部满足即放行):每一段都命中安全名单({@link AUTO_SAFE_COMMANDS} + 用户追加),整条命令
|
|
12
|
+
* 没有命令替换、参数没有变量展开 `$X`、没有可能匹配点文件的通配、没有超深嵌套。
|
|
13
|
+
*
|
|
14
|
+
* 分段与分词复用 rules.ts / dangerous.ts(引号内不切、剥环境赋值与 env / nohup / time 等前缀)。
|
|
15
|
+
*/
|
|
16
|
+
import { splitShellSegments, hasCommandSubstitution, wildcardToRegExp } from "./rules.js";
|
|
17
|
+
import { collectNestedCommands, commandWords, gitArgv, shellWords } from "./dangerous.js";
|
|
18
|
+
import { secretPathReason, writeProtectionReason } from "./protected.js";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { resolvePath } from "../tools/paths.js";
|
|
21
|
+
const s = (text, extra = {}) => ({
|
|
22
|
+
words: text.split(" "),
|
|
23
|
+
...extra,
|
|
24
|
+
});
|
|
25
|
+
const pm = (tools, scripts) => tools.flatMap((tool) => scripts.map((script) => s(`${tool} ${script}`)));
|
|
26
|
+
/** 初始安全名单(每条在 auto-safe.test.ts 有正反例)。 */
|
|
27
|
+
export const AUTO_SAFE_COMMANDS = [
|
|
28
|
+
...[
|
|
29
|
+
"ls",
|
|
30
|
+
"cat",
|
|
31
|
+
"head",
|
|
32
|
+
"tail",
|
|
33
|
+
"wc",
|
|
34
|
+
"grep",
|
|
35
|
+
"egrep",
|
|
36
|
+
"fgrep",
|
|
37
|
+
"pwd",
|
|
38
|
+
"echo",
|
|
39
|
+
"printf",
|
|
40
|
+
"which",
|
|
41
|
+
"printenv",
|
|
42
|
+
"true",
|
|
43
|
+
"false",
|
|
44
|
+
"cut",
|
|
45
|
+
"tr",
|
|
46
|
+
"diff",
|
|
47
|
+
"basename",
|
|
48
|
+
"dirname",
|
|
49
|
+
"realpath",
|
|
50
|
+
"stat",
|
|
51
|
+
"file",
|
|
52
|
+
"du",
|
|
53
|
+
"df",
|
|
54
|
+
"whoami",
|
|
55
|
+
"uname",
|
|
56
|
+
"cd",
|
|
57
|
+
"pushd",
|
|
58
|
+
"popd",
|
|
59
|
+
].map((name) => s(name)),
|
|
60
|
+
s("rg", { forbid: /^--pre(=.*)?$/ }),
|
|
61
|
+
s("sort", { forbid: /^(-o.*|--output.*)$/ }),
|
|
62
|
+
s("date", { forbid: /^(-s.*|--set.*)$/ }),
|
|
63
|
+
s("find", { forbid: /^-(exec|execdir|ok|okdir|delete|fprint0?|fprintf|fls)$/ }),
|
|
64
|
+
...["status", "rev-parse", "blame", "ls-files"].map((sub) => s(`git ${sub}`)),
|
|
65
|
+
...["diff", "log", "show"].map((sub) => s(`git ${sub}`, { forbid: /^(--output(=.*)?|--ext-diff|--textconv)$/ })),
|
|
66
|
+
s("git branch", {
|
|
67
|
+
forbid: /^(-[A-Za-z]*[dDmMcCfuet][A-Za-z]*|--(delete|move|copy|force|set-upstream-to.*|unset-upstream|edit-description|track.*|no-track|create-reflog))$/,
|
|
68
|
+
}),
|
|
69
|
+
...pm(["npm", "pnpm", "yarn"], ["test", "run test", "run lint", "run typecheck", "run build"]),
|
|
70
|
+
...pm(["pnpm", "yarn"], ["lint", "typecheck", "build"]),
|
|
71
|
+
s("npm t"),
|
|
72
|
+
s("node --test"),
|
|
73
|
+
s("tsc", { require: "--noEmit" }),
|
|
74
|
+
s("pnpm tsc", { require: "--noEmit" }),
|
|
75
|
+
s("vitest run"),
|
|
76
|
+
s("pnpm vitest run"),
|
|
77
|
+
s("pnpm exec vitest run"),
|
|
78
|
+
s("yarn vitest run"),
|
|
79
|
+
s("pytest"),
|
|
80
|
+
s("python -m pytest"),
|
|
81
|
+
s("python3 -m pytest"),
|
|
82
|
+
...["test", "check", "build", "clippy"].map((sub) => s(`cargo ${sub}`)),
|
|
83
|
+
...["test", "build", "vet"].map((sub) => s(`go ${sub}`)),
|
|
84
|
+
...["test", "check", "lint", "build"].map((sub) => s(`make ${sub}`)),
|
|
85
|
+
];
|
|
86
|
+
/** git 子命令之前允许出现的全局选项(`-c` 可改 pager / diff 程序,不允许)。 */
|
|
87
|
+
function gitPrefixSafe(argv) {
|
|
88
|
+
for (let i = 1; i < argv.length; i++) {
|
|
89
|
+
const a = argv[i];
|
|
90
|
+
if (a === "-C")
|
|
91
|
+
i++;
|
|
92
|
+
else if (a === "--no-pager")
|
|
93
|
+
continue;
|
|
94
|
+
else
|
|
95
|
+
return !a.startsWith("-");
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function matchesSpec(argv, spec) {
|
|
100
|
+
let words = argv;
|
|
101
|
+
if (spec.words[0] === "git") {
|
|
102
|
+
if (!gitPrefixSafe(argv))
|
|
103
|
+
return false;
|
|
104
|
+
words = gitArgv(argv) ?? argv;
|
|
105
|
+
}
|
|
106
|
+
if (words.length < spec.words.length)
|
|
107
|
+
return false;
|
|
108
|
+
if (!spec.words.every((w, i) => words[i] === w))
|
|
109
|
+
return false;
|
|
110
|
+
const rest = words.slice(spec.words.length);
|
|
111
|
+
if (spec.forbid !== undefined && rest.some((a) => spec.forbid.test(a)))
|
|
112
|
+
return false;
|
|
113
|
+
return spec.require === undefined || rest.includes(spec.require);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 用户追加的安全命令(`permission.autoSafeCommands`):不含 `*` 按词前缀;含 `*` 按通配匹配整段
|
|
117
|
+
* (剥过前缀、重定向之后的命令文本)。
|
|
118
|
+
*/
|
|
119
|
+
export function extraSafeMatches(argv, entry) {
|
|
120
|
+
const text = argv.join(" ");
|
|
121
|
+
if (entry.includes("*"))
|
|
122
|
+
return wildcardToRegExp(entry.trim().replace(/\s+/g, " ")).test(text);
|
|
123
|
+
const words = shellWords(entry);
|
|
124
|
+
return words.length > 0 && words.every((w, i) => argv[i] === w);
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// 规则层:网络、删除类
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
function base(word) {
|
|
130
|
+
return (word ?? "").split("/").pop() ?? "";
|
|
131
|
+
}
|
|
132
|
+
const NETWORK_TOOLS = new Set([
|
|
133
|
+
"curl",
|
|
134
|
+
"wget",
|
|
135
|
+
"ssh",
|
|
136
|
+
"scp",
|
|
137
|
+
"sftp",
|
|
138
|
+
"rsync",
|
|
139
|
+
"nc",
|
|
140
|
+
"ncat",
|
|
141
|
+
"netcat",
|
|
142
|
+
"telnet",
|
|
143
|
+
"ftp",
|
|
144
|
+
"socat",
|
|
145
|
+
"aria2c",
|
|
146
|
+
"http",
|
|
147
|
+
"https",
|
|
148
|
+
"gh",
|
|
149
|
+
"npx",
|
|
150
|
+
"bunx",
|
|
151
|
+
"pnpx",
|
|
152
|
+
]);
|
|
153
|
+
const GIT_NETWORK = new Set(["push", "pull", "fetch", "clone", "ls-remote"]);
|
|
154
|
+
const JS_NETWORK = new Set([
|
|
155
|
+
"install",
|
|
156
|
+
"i",
|
|
157
|
+
"add",
|
|
158
|
+
"ci",
|
|
159
|
+
"update",
|
|
160
|
+
"up",
|
|
161
|
+
"upgrade",
|
|
162
|
+
"publish",
|
|
163
|
+
"dlx",
|
|
164
|
+
"create",
|
|
165
|
+
"login",
|
|
166
|
+
"adduser",
|
|
167
|
+
]);
|
|
168
|
+
const PKG_INSTALL = {
|
|
169
|
+
pip: new Set(["install", "download"]),
|
|
170
|
+
pip3: new Set(["install", "download"]),
|
|
171
|
+
pipx: new Set(["install", "run"]),
|
|
172
|
+
poetry: new Set(["install", "add", "update"]),
|
|
173
|
+
uv: new Set(["add", "sync", "pip"]),
|
|
174
|
+
cargo: new Set(["install", "publish", "update", "add"]),
|
|
175
|
+
go: new Set(["get", "install"]),
|
|
176
|
+
gem: new Set(["install", "push"]),
|
|
177
|
+
brew: new Set(["install", "upgrade", "update", "tap"]),
|
|
178
|
+
apt: new Set(["install", "update", "upgrade"]),
|
|
179
|
+
"apt-get": new Set(["install", "update", "upgrade"]),
|
|
180
|
+
yum: new Set(["install", "update"]),
|
|
181
|
+
dnf: new Set(["install", "update"]),
|
|
182
|
+
apk: new Set(["add", "update"]),
|
|
183
|
+
pacman: new Set(["-S", "-Sy", "-Syu"]),
|
|
184
|
+
docker: new Set(["pull", "push", "login"]),
|
|
185
|
+
podman: new Set(["pull", "push", "login"]),
|
|
186
|
+
};
|
|
187
|
+
/** 网络命令:返回说明;不是返回 undefined。 */
|
|
188
|
+
export function networkReason(argv) {
|
|
189
|
+
const name = base(argv[0]);
|
|
190
|
+
if (NETWORK_TOOLS.has(name))
|
|
191
|
+
return `network command ${name}`;
|
|
192
|
+
if (name === "git") {
|
|
193
|
+
const g = gitArgv(argv) ?? argv;
|
|
194
|
+
const sub = g[1] ?? "";
|
|
195
|
+
if (GIT_NETWORK.has(sub))
|
|
196
|
+
return `network command git ${sub}`;
|
|
197
|
+
if (sub === "submodule" && g.includes("update"))
|
|
198
|
+
return "network command git submodule update";
|
|
199
|
+
if (sub === "remote" && g[2] === "update")
|
|
200
|
+
return "network command git remote update";
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(name)) {
|
|
204
|
+
if (name === "yarn" && argv.length === 1)
|
|
205
|
+
return "package install (yarn)";
|
|
206
|
+
const sub = argv[1] ?? "";
|
|
207
|
+
if (JS_NETWORK.has(sub))
|
|
208
|
+
return `package manager network command ${name} ${sub}`;
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (/^python3?$/.test(name) && argv[1] === "-m" && /^pip3?$/.test(argv[2] ?? "")) {
|
|
212
|
+
return ["install", "download"].includes(argv[3] ?? "") ? "package install (pip)" : undefined;
|
|
213
|
+
}
|
|
214
|
+
const subs = PKG_INSTALL[name];
|
|
215
|
+
if (subs !== undefined) {
|
|
216
|
+
if (name === "go" && argv[1] === "mod" && argv[2] === "download")
|
|
217
|
+
return "network command go mod download";
|
|
218
|
+
if (subs.has(argv[1] ?? ""))
|
|
219
|
+
return `network command ${name} ${argv[1]}`;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
/** 删除与丢弃改动的命令:返回说明;不是返回 undefined。 */
|
|
224
|
+
export function destructiveReason(argv) {
|
|
225
|
+
const name = base(argv[0]);
|
|
226
|
+
const flags = argv.filter((a) => /^-[A-Za-z]+$/.test(a)).join("");
|
|
227
|
+
if (name === "rm") {
|
|
228
|
+
if (/[rRf]/.test(flags) || argv.includes("--recursive") || argv.includes("--force")) {
|
|
229
|
+
return "recursive or forced rm";
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
if (name === "find" && argv.includes("-delete"))
|
|
234
|
+
return "find -delete";
|
|
235
|
+
if (name === "shred" || name === "truncate" || name === "srm")
|
|
236
|
+
return `${name} destroys file data`;
|
|
237
|
+
if (name === "git") {
|
|
238
|
+
const g = gitArgv(argv) ?? argv;
|
|
239
|
+
const sub = g[1] ?? "";
|
|
240
|
+
if (sub === "clean")
|
|
241
|
+
return "git clean";
|
|
242
|
+
if (sub === "restore")
|
|
243
|
+
return "git restore discards changes";
|
|
244
|
+
if (sub === "checkout" &&
|
|
245
|
+
g.slice(2).some((a) => a === "--" || a === "." || a === "-f" || a === "--force")) {
|
|
246
|
+
return "git checkout discarding changes";
|
|
247
|
+
}
|
|
248
|
+
if (sub === "stash" && (g[2] === "drop" || g[2] === "clear"))
|
|
249
|
+
return `git stash ${g[2]}`;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
/** 从一段命令里取出重定向(引号内不算);`2>&1`、`>&2` 是复制描述符,没有目标。 */
|
|
254
|
+
export function extractRedirects(segment) {
|
|
255
|
+
const out = { outputs: [], inputs: [], rest: "" };
|
|
256
|
+
let rest = "";
|
|
257
|
+
let quote;
|
|
258
|
+
const readWord = (from) => {
|
|
259
|
+
let i = from;
|
|
260
|
+
while (i < segment.length && /\s/.test(segment[i]))
|
|
261
|
+
i++;
|
|
262
|
+
let word = "";
|
|
263
|
+
let q;
|
|
264
|
+
for (; i < segment.length; i++) {
|
|
265
|
+
const ch = segment[i];
|
|
266
|
+
if (q !== undefined) {
|
|
267
|
+
word += ch;
|
|
268
|
+
if (ch === q)
|
|
269
|
+
q = undefined;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "'" || ch === '"') {
|
|
273
|
+
q = ch;
|
|
274
|
+
word += ch;
|
|
275
|
+
}
|
|
276
|
+
else if (/\s/.test(ch) || ch === "<" || ch === ">")
|
|
277
|
+
break;
|
|
278
|
+
else
|
|
279
|
+
word += ch;
|
|
280
|
+
}
|
|
281
|
+
return [shellWords(word)[0] ?? "", i];
|
|
282
|
+
};
|
|
283
|
+
for (let i = 0; i < segment.length; i++) {
|
|
284
|
+
const ch = segment[i];
|
|
285
|
+
if (quote !== undefined) {
|
|
286
|
+
rest += ch;
|
|
287
|
+
if (ch === "\\" && quote === '"' && i + 1 < segment.length)
|
|
288
|
+
rest += segment[++i];
|
|
289
|
+
else if (ch === quote)
|
|
290
|
+
quote = undefined;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (ch === "'" || ch === '"') {
|
|
294
|
+
quote = ch;
|
|
295
|
+
rest += ch;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (ch === "\\" && i + 1 < segment.length) {
|
|
299
|
+
rest += ch + segment[++i];
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (ch === ">") {
|
|
303
|
+
if (/\d$/.test(rest) && /(^|\s)\d$/.test(rest))
|
|
304
|
+
rest = rest.slice(0, -1);
|
|
305
|
+
if (rest.endsWith("&"))
|
|
306
|
+
rest = rest.slice(0, -1);
|
|
307
|
+
let j = i + 1;
|
|
308
|
+
if (segment[j] === ">" || segment[j] === "|")
|
|
309
|
+
j++;
|
|
310
|
+
if (segment[j] === "&") {
|
|
311
|
+
// `>&2`、`>&-`:描述符复制或关闭
|
|
312
|
+
j++;
|
|
313
|
+
while (j < segment.length && /[\d-]/.test(segment[j]))
|
|
314
|
+
j++;
|
|
315
|
+
i = j - 1;
|
|
316
|
+
rest += " ";
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const [target, end] = readWord(j);
|
|
320
|
+
if (target !== "")
|
|
321
|
+
out.outputs.push(target);
|
|
322
|
+
i = end - 1;
|
|
323
|
+
rest += " ";
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (ch === "<") {
|
|
327
|
+
if (segment[i + 1] === "(") {
|
|
328
|
+
rest += ch;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (/(^|\s)\d$/.test(rest))
|
|
332
|
+
rest = rest.slice(0, -1);
|
|
333
|
+
let j = i + 1;
|
|
334
|
+
let heredoc = false;
|
|
335
|
+
if (segment[j] === "<") {
|
|
336
|
+
heredoc = true;
|
|
337
|
+
j++;
|
|
338
|
+
if (segment[j] === "<" || segment[j] === "-")
|
|
339
|
+
j++;
|
|
340
|
+
}
|
|
341
|
+
if (segment[j] === "&") {
|
|
342
|
+
j++;
|
|
343
|
+
while (j < segment.length && /[\d-]/.test(segment[j]))
|
|
344
|
+
j++;
|
|
345
|
+
i = j - 1;
|
|
346
|
+
rest += " ";
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const [source, end] = readWord(j);
|
|
350
|
+
if (!heredoc && source !== "")
|
|
351
|
+
out.inputs.push(source);
|
|
352
|
+
i = end - 1;
|
|
353
|
+
rest += " ";
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
rest += ch;
|
|
357
|
+
}
|
|
358
|
+
out.rest = rest.trim().replace(/\s+/g, " ");
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
const WRITE_ALL_ARGS = new Set([
|
|
362
|
+
"mv",
|
|
363
|
+
"mkdir",
|
|
364
|
+
"touch",
|
|
365
|
+
"rmdir",
|
|
366
|
+
"rm",
|
|
367
|
+
"chmod",
|
|
368
|
+
"chown",
|
|
369
|
+
"chgrp",
|
|
370
|
+
"tee",
|
|
371
|
+
"unlink",
|
|
372
|
+
]);
|
|
373
|
+
const WRITE_LAST_ARG = new Set(["cp", "ln", "install", "rsync"]);
|
|
374
|
+
/** 命令会写的路径(词法;flag 不算)。 */
|
|
375
|
+
function writeTargets(argv) {
|
|
376
|
+
const name = base(argv[0]);
|
|
377
|
+
const args = argv.slice(1).filter((a) => !a.startsWith("-"));
|
|
378
|
+
if (WRITE_ALL_ARGS.has(name)) {
|
|
379
|
+
return name === "chmod" || name === "chown" || name === "chgrp" ? args.slice(1) : args;
|
|
380
|
+
}
|
|
381
|
+
if (WRITE_LAST_ARG.has(name)) {
|
|
382
|
+
const t = argv.indexOf("-t");
|
|
383
|
+
if (t !== -1 && argv[t + 1] !== undefined)
|
|
384
|
+
return [argv[t + 1]];
|
|
385
|
+
return args.length > 1 ? [args[args.length - 1]] : [];
|
|
386
|
+
}
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const GLOB = /[*?[]/;
|
|
390
|
+
/** 词里可能出现的路径(`--file=.env` 取等号后)。 */
|
|
391
|
+
function pathCandidates(word) {
|
|
392
|
+
const out = [word];
|
|
393
|
+
const eq = word.indexOf("=");
|
|
394
|
+
if (word.startsWith("-") && eq !== -1)
|
|
395
|
+
out.push(word.slice(eq + 1));
|
|
396
|
+
return out.filter((w) => w !== "" && !w.startsWith("-"));
|
|
397
|
+
}
|
|
398
|
+
function secretInWord(word, cwd) {
|
|
399
|
+
for (const candidate of pathCandidates(word)) {
|
|
400
|
+
if (GLOB.test(candidate)) {
|
|
401
|
+
const name = candidate.split("/").pop() ?? "";
|
|
402
|
+
if (/^\.env/.test(name) ||
|
|
403
|
+
/(^|\/)\.(ssh|aws|gnupg)(\/|$)/.test(candidate) ||
|
|
404
|
+
/^id_/.test(name)) {
|
|
405
|
+
return `glob may match secret files (${candidate})`;
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
let abs;
|
|
410
|
+
try {
|
|
411
|
+
abs = resolvePath(candidate, cwd);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const reason = secretPathReason(abs);
|
|
417
|
+
if (reason !== undefined)
|
|
418
|
+
return reason;
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
/** 输出设备(写它们不算写文件);按原文判断,Windows 上 `/dev/null` 解析后带盘符。 */
|
|
423
|
+
const DEVICE_TARGETS = /^(\/dev\/(null|stdout|stderr|tty)|NUL)$/i;
|
|
424
|
+
function writeReason(target, cwd, projectRoot) {
|
|
425
|
+
if (DEVICE_TARGETS.test(target))
|
|
426
|
+
return undefined;
|
|
427
|
+
if (target.includes("$"))
|
|
428
|
+
return `write target with variable expansion (${target})`;
|
|
429
|
+
let abs;
|
|
430
|
+
try {
|
|
431
|
+
abs = resolvePath(target, cwd);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return `invalid write target ${target}`;
|
|
435
|
+
}
|
|
436
|
+
return writeProtectionReason(abs, projectRoot);
|
|
437
|
+
}
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// 总入口
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
const CD = new Set(["cd", "pushd"]);
|
|
442
|
+
function cdTarget(argv, cwd) {
|
|
443
|
+
const target = argv.slice(1).find((a) => !a.startsWith("-"));
|
|
444
|
+
try {
|
|
445
|
+
return resolvePath(target ?? homedir(), cwd);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return cwd;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function parseSegment(segment) {
|
|
452
|
+
const redirects = extractRedirects(segment);
|
|
453
|
+
return {
|
|
454
|
+
argv: commandWords(redirects.rest),
|
|
455
|
+
withSudo: commandWords(redirects.rest, true),
|
|
456
|
+
redirects,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function ruleAsk(info, options) {
|
|
460
|
+
const { argv, redirects } = info;
|
|
461
|
+
const network = networkReason(argv);
|
|
462
|
+
if (network !== undefined)
|
|
463
|
+
return network;
|
|
464
|
+
const destructive = destructiveReason(argv);
|
|
465
|
+
if (destructive !== undefined)
|
|
466
|
+
return destructive;
|
|
467
|
+
for (const target of [...redirects.outputs, ...writeTargets(argv)]) {
|
|
468
|
+
const reason = writeReason(target, options.cwd, options.projectRoot);
|
|
469
|
+
if (reason !== undefined)
|
|
470
|
+
return `${reason} (${target})`;
|
|
471
|
+
}
|
|
472
|
+
for (const word of [...argv.slice(1), ...redirects.inputs]) {
|
|
473
|
+
const reason = secretInWord(word, options.cwd);
|
|
474
|
+
if (reason !== undefined)
|
|
475
|
+
return reason;
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function safeSegment(info, extra) {
|
|
480
|
+
const argv = info.withSudo;
|
|
481
|
+
if (argv.length === 0)
|
|
482
|
+
return undefined;
|
|
483
|
+
for (const word of argv) {
|
|
484
|
+
if (word.includes("$"))
|
|
485
|
+
return `variable expansion in ${word}`;
|
|
486
|
+
if (GLOB.test(word) && /(^|\/)\.[^./]/.test(word))
|
|
487
|
+
return `glob over dotfiles (${word})`;
|
|
488
|
+
}
|
|
489
|
+
if (AUTO_SAFE_COMMANDS.some((spec) => matchesSpec(argv, spec)))
|
|
490
|
+
return undefined;
|
|
491
|
+
if (extra.some((entry) => extraSafeMatches(argv, entry)))
|
|
492
|
+
return undefined;
|
|
493
|
+
return `${base(argv[0])} is not in the auto safe list`;
|
|
494
|
+
}
|
|
495
|
+
/** auto 模式下一条 bash 命令的规则层与静态判定结论。 */
|
|
496
|
+
export function analyzeBashForAuto(command, options) {
|
|
497
|
+
const nested = collectNestedCommands(command);
|
|
498
|
+
// 外层按顺序跟踪 `cd`:之后的相对路径按新的目录解析。嵌套命令按会话 cwd 解析。
|
|
499
|
+
let cwd = options.cwd;
|
|
500
|
+
for (const segment of splitShellSegments(command)) {
|
|
501
|
+
const info = parseSegment(segment);
|
|
502
|
+
const ask = ruleAsk(info, { ...options, cwd });
|
|
503
|
+
if (ask !== undefined)
|
|
504
|
+
return { ask, safe: false, reason: ask };
|
|
505
|
+
if (CD.has(base(info.argv[0])))
|
|
506
|
+
cwd = cdTarget(info.argv, cwd);
|
|
507
|
+
}
|
|
508
|
+
for (const text of nested.commands) {
|
|
509
|
+
for (const segment of splitShellSegments(text)) {
|
|
510
|
+
const ask = ruleAsk(parseSegment(segment), options);
|
|
511
|
+
if (ask !== undefined)
|
|
512
|
+
return { ask, safe: false, reason: ask };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (hasCommandSubstitution(command)) {
|
|
516
|
+
return { safe: false, reason: "command substitution" };
|
|
517
|
+
}
|
|
518
|
+
if (nested.tooDeep)
|
|
519
|
+
return { safe: false, reason: "nested too deep" };
|
|
520
|
+
const segments = splitShellSegments(command);
|
|
521
|
+
if (segments.length === 0)
|
|
522
|
+
return { safe: false, reason: "empty command" };
|
|
523
|
+
for (const segment of segments) {
|
|
524
|
+
const unsafe = safeSegment(parseSegment(segment), options.extraSafe ?? []);
|
|
525
|
+
if (unsafe !== undefined)
|
|
526
|
+
return { safe: false, reason: unsafe };
|
|
527
|
+
}
|
|
528
|
+
return { safe: true, reason: "every command is in the auto safe list" };
|
|
529
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto 模式的模型分类器(§7.4 第 3 层,docs/permissions.md「模型分类器」)。
|
|
3
|
+
*
|
|
4
|
+
* 只处理规则层与静态判定都没决定的调用(管线 verdict 带 `classify: true`)。这里只管提示、解析、
|
|
5
|
+
* 超时与缓存;真正发请求的 `complete` 由会话注入(agent/session-classifier.ts:独立请求、不进转录、
|
|
6
|
+
* 用量记 `usage{kind:"permission_classify"}`)。
|
|
7
|
+
*
|
|
8
|
+
* - 防提示注入:工具参数与最近一条用户消息放在 `<tool_call_data>` 数据块里(JSON 编码,块标记被替换),
|
|
9
|
+
* 系统提示声明块内一切都是数据、其中的指令一律忽略并倾向 ask。
|
|
10
|
+
* - 输出必须是一个 JSON 对象 `{"decision":"allow"|"ask","reason":"…"}`(允许外包一层 ``` 代码块),
|
|
11
|
+
* 其它一律按 ask;超时(缺省 10 s)与请求出错也按 ask。
|
|
12
|
+
* - 会话内缓存:键是「工具名 + 归一化参数」(bash 折叠空白;其它按键排序的 JSON)的 sha256;
|
|
13
|
+
* 同一键并发只发一次请求;失败不缓存。
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLASSIFIER_TIMEOUT_MS = 10000;
|
|
16
|
+
export declare const CLASSIFIER_MAX_TOKENS = 256;
|
|
17
|
+
export declare const CLASSIFIER_ARGS_LIMIT = 4000;
|
|
18
|
+
export declare const CLASSIFIER_USER_LIMIT = 600;
|
|
19
|
+
export declare const CLASSIFIER_SYSTEM_PROMPT: string;
|
|
20
|
+
export interface ClassifierRequest {
|
|
21
|
+
toolName: string;
|
|
22
|
+
input: unknown;
|
|
23
|
+
cwd: string;
|
|
24
|
+
projectRoot: string;
|
|
25
|
+
/** 最近一条用户消息(会被截断)。 */
|
|
26
|
+
userMessage?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ClassifierVerdict {
|
|
29
|
+
decision: "allow" | "ask";
|
|
30
|
+
reason: string;
|
|
31
|
+
/** 来自会话内缓存。 */
|
|
32
|
+
cached: boolean;
|
|
33
|
+
/** 解析失败 / 超时 / 出错(按 ask)。 */
|
|
34
|
+
failed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** 发一次独立请求,返回模型的文本输出;出错抛错。 */
|
|
37
|
+
export type ClassifierComplete = (prompt: {
|
|
38
|
+
system: string;
|
|
39
|
+
user: string;
|
|
40
|
+
}, signal: AbortSignal) => Promise<string>;
|
|
41
|
+
/** 发给模型的用户消息:说明 + 数据块。 */
|
|
42
|
+
export declare function buildClassifierPrompt(request: ClassifierRequest): string;
|
|
43
|
+
/** 严格解析;不合格返回 undefined。 */
|
|
44
|
+
export declare function parseClassifierReply(text: string): {
|
|
45
|
+
decision: "allow" | "ask";
|
|
46
|
+
reason: string;
|
|
47
|
+
} | undefined;
|
|
48
|
+
/** 缓存键:工具名 + 归一化参数。 */
|
|
49
|
+
export declare function classifierCacheKey(toolName: string, input: unknown): string;
|
|
50
|
+
export interface PermissionClassifierOptions {
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
log?(level: "debug" | "warn", message: string): void;
|
|
53
|
+
}
|
|
54
|
+
export declare class PermissionClassifier {
|
|
55
|
+
private readonly complete;
|
|
56
|
+
private readonly options;
|
|
57
|
+
private readonly cache;
|
|
58
|
+
private readonly settled;
|
|
59
|
+
constructor(complete: ClassifierComplete, options?: PermissionClassifierOptions);
|
|
60
|
+
/** 缓存里的条目数(测试与诊断)。 */
|
|
61
|
+
get size(): number;
|
|
62
|
+
classify(request: ClassifierRequest, signal: AbortSignal): Promise<ClassifierVerdict>;
|
|
63
|
+
private run;
|
|
64
|
+
}
|