@armadra/agent 0.10.0 → 0.12.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 +31 -0
- package/CHANGELOG.zh-CN.md +31 -0
- package/README.md +5 -0
- package/README.zh-CN.md +4 -0
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +2 -2
- package/dist/bundle/ama.cjs +1730 -446
- package/dist/cli/compose-agents.js +8 -0
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +1 -0
- package/dist/drivers/acp/client.d.ts +54 -5
- package/dist/drivers/acp/client.js +96 -9
- package/dist/drivers/acp/types.d.ts +69 -3
- package/dist/drivers/acp/types.js +30 -1
- package/dist/drivers/agents.js +1 -1
- package/dist/drivers/host-runners.d.ts +26 -1
- package/dist/drivers/host-runners.js +147 -0
- package/dist/hooks/matcher.d.ts +2 -2
- package/dist/hooks/matcher.js +3 -3
- package/dist/host/api-impl.d.ts +3 -0
- package/dist/host/api-impl.js +7 -2
- package/dist/host/types.d.ts +37 -1
- package/dist/host/types.js +5 -0
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/i18n/catalog.d.ts +5 -0
- package/dist/i18n/messages/approval.d.ts +5 -0
- package/dist/i18n/messages/approval.js +5 -0
- package/dist/i18n/messages/drivers.d.ts +2 -0
- package/dist/i18n/messages/drivers.js +2 -0
- package/dist/i18n/messages/interactive-view.d.ts +5 -0
- package/dist/i18n/messages/interactive-view.js +5 -0
- package/dist/i18n/messages/interactive.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modes/commands-core.d.ts +7 -0
- package/dist/modes/commands-core.js +23 -5
- package/dist/modes/interactive/approval-dialog.js +17 -4
- package/dist/modes/interactive/commands.d.ts +2 -0
- package/dist/modes/interactive/commands.js +5 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.js +14 -6
- package/dist/modes/interactive/tool-summary.d.ts +2 -0
- package/dist/modes/interactive/tool-summary.js +27 -0
- package/dist/modes/rpc/commands.js +4 -4
- package/dist/permissions/auto-safe.d.ts +3 -0
- package/dist/permissions/auto-safe.js +10 -3
- package/dist/permissions/classifier.js +1 -1
- package/dist/permissions/network-class.d.ts +28 -0
- package/dist/permissions/network-class.js +91 -0
- package/dist/permissions/pipeline.d.ts +1 -1
- package/dist/permissions/pipeline.js +24 -7
- package/dist/permissions/preview.js +3 -0
- package/dist/permissions/readonly-bash.d.ts +2 -2
- package/dist/permissions/readonly-bash.js +2 -2
- package/dist/permissions/rules.d.ts +1 -0
- package/dist/permissions/rules.js +5 -1
- package/dist/tools/presets.d.ts +8 -1
- package/dist/tools/presets.js +12 -3
- package/dist/tools/registry.d.ts +3 -1
- package/dist/tools/registry.js +4 -2
- package/dist/tools/task-ctl.d.ts +9 -0
- package/dist/tools/task-ctl.js +22 -3
- package/dist/tools/types.d.ts +4 -1
- package/dist/tools/web/fetch.d.ts +48 -0
- package/dist/tools/web/fetch.js +307 -0
- package/dist/tools/web/html-to-markdown.d.ts +31 -0
- package/dist/tools/web/html-to-markdown.js +522 -0
- package/dist/tools/web/url-guard.d.ts +59 -0
- package/dist/tools/web/url-guard.js +221 -0
- package/docs/en/guides/permissions.md +33 -11
- package/docs/en/reference/acp.md +7 -3
- package/docs/en/reference/host-api.md +61 -19
- package/docs/en/reference/rpc.md +7 -7
- package/docs/guides/agents.md +18 -1
- package/docs/guides/hooks.md +86 -8
- package/docs/guides/permissions.md +33 -11
- package/docs/guides/sandbox.md +6 -0
- package/docs/reference/acp.md +7 -3
- package/docs/reference/host-api.md +61 -19
- package/docs/reference/rpc.md +7 -7
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@ import { msg } from "../../i18n/index.js";
|
|
|
25
25
|
import { autoLayerText, isPermissionMode, permissionModeLabel } from "../../permissions/modes.js";
|
|
26
26
|
import { previewDisplayLines } from "../../permissions/preview.js";
|
|
27
27
|
import { Box, defaultKeybindings, matchesKey, padToWidth, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "../../tui.js";
|
|
28
|
-
import { displayPath, flat } from "./tool-summary.js";
|
|
28
|
+
import { displayPath, fetchHost, flat } from "./tool-summary.js";
|
|
29
29
|
import { toolSummary } from "./tool-view.js";
|
|
30
30
|
const COMMAND_LINES = 8;
|
|
31
31
|
const EDIT_LINES = 3;
|
|
@@ -130,6 +130,13 @@ export function describeRequest(request, theme, options = {}) {
|
|
|
130
130
|
if (hidden > 0)
|
|
131
131
|
out.push(more(hidden));
|
|
132
132
|
}
|
|
133
|
+
else if (request.toolName === "web_fetch" && typeof input["url"] === "string") {
|
|
134
|
+
// 完整 URL(含协议与查询串),按 72 列折行,不截断
|
|
135
|
+
const { shown, hidden } = lines(input["url"].replace(/(.{72})/g, "$1\n"), COMMAND_LINES);
|
|
136
|
+
out.push(...shown.map((l) => theme.fg("code", l)));
|
|
137
|
+
if (hidden > 0)
|
|
138
|
+
out.push(more(hidden));
|
|
139
|
+
}
|
|
133
140
|
else if (request.toolName === "write" && typeof input["content"] === "string") {
|
|
134
141
|
const count = lines(input["content"], Number.MAX_SAFE_INTEGER).shown.length;
|
|
135
142
|
out.push(`${toolSummary("write", input, options.cwd)} ${theme.fg("dim", m.writeLines(count))}`);
|
|
@@ -293,10 +300,14 @@ class ApprovalDialog {
|
|
|
293
300
|
const { theme } = this.host;
|
|
294
301
|
const g = theme.glyphs;
|
|
295
302
|
const keyWidth = compact ? 1 : Math.max(...APPROVAL_OPTIONS.map((o) => o.keys.length));
|
|
303
|
+
const host = fetchHost(this.request.toolName, this.request.input);
|
|
296
304
|
return APPROVAL_OPTIONS.map((option, i) => {
|
|
297
305
|
const selected = i === this.selected;
|
|
298
306
|
const keys = compact ? option.keys.slice(0, 1) : option.keys;
|
|
299
|
-
const
|
|
307
|
+
const text = option.decision === "allow_session" && host !== undefined
|
|
308
|
+
? msg().approval.option.allowSessionHost(host)
|
|
309
|
+
: option.label;
|
|
310
|
+
const left = `${selected ? g.prompt : " "} ${i + 1}. ${text}`;
|
|
300
311
|
const right = keys.padEnd(keyWidth);
|
|
301
312
|
const room = Math.max(1, width - visibleWidth(right) - 1);
|
|
302
313
|
const label = selected ? theme.fg("accent", theme.bold(left)) : left;
|
|
@@ -361,8 +372,10 @@ export function approvalOutcomeText(request, outcome, taskAgent) {
|
|
|
361
372
|
switch (outcome) {
|
|
362
373
|
case "allow":
|
|
363
374
|
return m.allow(what);
|
|
364
|
-
case "allow_session":
|
|
365
|
-
|
|
375
|
+
case "allow_session": {
|
|
376
|
+
const host = fetchHost(request.toolName, request.input);
|
|
377
|
+
return host === undefined ? m.allowSession(what) : m.allowSessionHost(what, host);
|
|
378
|
+
}
|
|
366
379
|
case "deny":
|
|
367
380
|
return m.deny(what);
|
|
368
381
|
case "cancelled":
|
|
@@ -27,6 +27,8 @@ import { type PickerSpec } from "./pickers.js";
|
|
|
27
27
|
export declare const INTERACTIVE_COMMANDS: readonly CommandInfo[];
|
|
28
28
|
/** 补全与 /help 用的完整命令表。 */
|
|
29
29
|
export declare const ALL_COMMANDS: readonly CommandInfo[];
|
|
30
|
+
/** 补全用:完整命令表去掉能力关闭的(`availableCommands`)。 */
|
|
31
|
+
export declare function completionCommands(runtime: Pick<Runtime, "tools">, session: Pick<AgentSession, "getTools">): CommandInfo[];
|
|
30
32
|
/** `/help` 末尾的按键说明(多行)。 */
|
|
31
33
|
export declare function keyHints(): string;
|
|
32
34
|
export interface CommandUi {
|
|
@@ -22,7 +22,7 @@ import { THINKING_LEVELS } from "../../ai/thinking.js";
|
|
|
22
22
|
import { listSessions } from "../../cli/compose-store.js";
|
|
23
23
|
import { hideFakeProvider } from "../../cli/fake-visibility.js";
|
|
24
24
|
import { autoLayerText, permissionModeLabel } from "../../permissions/modes.js";
|
|
25
|
-
import { BUILTIN_COMMANDS, parseSlash, runSlashCommand, } from "../commands-core.js";
|
|
25
|
+
import { BUILTIN_COMMANDS, availableCommands, parseSlash, runSlashCommand, } from "../commands-core.js";
|
|
26
26
|
import { contextPanel } from "../context-report.js";
|
|
27
27
|
import { contentText } from "./message-view.js";
|
|
28
28
|
import { cachePanel, permissionsPanel, sessionPanel } from "./panels.js";
|
|
@@ -42,6 +42,10 @@ export const INTERACTIVE_COMMANDS = [
|
|
|
42
42
|
];
|
|
43
43
|
/** 补全与 /help 用的完整命令表。 */
|
|
44
44
|
export const ALL_COMMANDS = [...BUILTIN_COMMANDS, ...INTERACTIVE_COMMANDS];
|
|
45
|
+
/** 补全用:完整命令表去掉能力关闭的(`availableCommands`)。 */
|
|
46
|
+
export function completionCommands(runtime, session) {
|
|
47
|
+
return availableCommands(runtime, session, ALL_COMMANDS);
|
|
48
|
+
}
|
|
45
49
|
/** `/help` 末尾的按键说明(多行)。 */
|
|
46
50
|
export function keyHints() {
|
|
47
51
|
return msg().interactive.commands.keyHints;
|
|
@@ -32,6 +32,11 @@ import { StatusArea } from "./status-area.js";
|
|
|
32
32
|
import type { StatusBar } from "./status-bar.js";
|
|
33
33
|
import { type ProgramStatusOptions } from "./program-status-ui.js";
|
|
34
34
|
import { ToolTracker } from "./tool-view.js";
|
|
35
|
+
/** 底部提示行的清除计时器(测试注入手动时钟,帧黄金不再依赖墙钟)。 */
|
|
36
|
+
export interface HintTimers {
|
|
37
|
+
set(fn: () => void, ms: number): unknown;
|
|
38
|
+
clear(handle: unknown): void;
|
|
39
|
+
}
|
|
35
40
|
export interface InteractiveModeOptions {
|
|
36
41
|
/** 缺省 `ProcessTerminal`(测试注入 MemoryTerminal)。 */
|
|
37
42
|
terminal?: Terminal;
|
|
@@ -49,6 +54,8 @@ export interface InteractiveModeOptions {
|
|
|
49
54
|
onReady?(handle: InteractiveHandle): void;
|
|
50
55
|
/** 剪贴板读取的注入(测试;缺省调系统命令)。 */
|
|
51
56
|
clipboard?: ClipboardDeps;
|
|
57
|
+
/** 提示行清除计时器的注入(测试;缺省真实 `setTimeout`)。 */
|
|
58
|
+
hintTimers?: HintTimers;
|
|
52
59
|
/** 终端程序状态 OSC 7501(缺省只在真实终端启用;测试注入终端时给了才启用)。 */
|
|
53
60
|
programStatus?: ProgramStatusOptions;
|
|
54
61
|
}
|
|
@@ -33,7 +33,7 @@ import { BYPASS_MODE, createBypassGate } from "../../permissions/bypass.js";
|
|
|
33
33
|
import { onTerminationSignals } from "../shared.js";
|
|
34
34
|
import { AgentUi, agentCommandHooks } from "./agent-ui.js";
|
|
35
35
|
import { approvalOverlayHooks, createApprovalUi } from "./approval-ui.js";
|
|
36
|
-
import {
|
|
36
|
+
import { completionCommands, runInteractiveCommand } from "./commands.js";
|
|
37
37
|
import { memoryPanelFor } from "./memory-panel.js";
|
|
38
38
|
import { InteractiveCompletion } from "./completion.js";
|
|
39
39
|
import { confirmBypass } from "./confirm-dialog.js";
|
|
@@ -56,6 +56,14 @@ import { loadKeys, processTerminal } from "./terminal-setup.js";
|
|
|
56
56
|
import { ToolTracker } from "./tool-view.js";
|
|
57
57
|
import { backgroundTasks, blockingTasks, keyLabel, runningBackground } from "./task-background.js";
|
|
58
58
|
const HINT_MS = 2500;
|
|
59
|
+
const realHintTimers = {
|
|
60
|
+
set: (fn, ms) => {
|
|
61
|
+
const handle = setTimeout(fn, ms);
|
|
62
|
+
handle.unref?.();
|
|
63
|
+
return handle;
|
|
64
|
+
},
|
|
65
|
+
clear: (handle) => clearTimeout(handle),
|
|
66
|
+
};
|
|
59
67
|
export function runInteractiveMode(runtime, context, options = {}) {
|
|
60
68
|
const terminal = options.terminal ?? processTerminal();
|
|
61
69
|
const ui = runtime.config.ui ?? {};
|
|
@@ -114,7 +122,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
114
122
|
? undefined
|
|
115
123
|
: (options.historyFile ?? join(runtime.paths.dataDir, "history"));
|
|
116
124
|
const completion = new InteractiveCompletion({
|
|
117
|
-
commands: () =>
|
|
125
|
+
commands: () => completionCommands(runtime, session),
|
|
118
126
|
prompts: () => runtime.resources.prompts,
|
|
119
127
|
skills: () => runtime.resources.skills,
|
|
120
128
|
cwd: () => session.state.cwd,
|
|
@@ -142,6 +150,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
142
150
|
tui.addChild(area.quota);
|
|
143
151
|
// ---- 小工具 ---------------------------------------------------------------
|
|
144
152
|
let finished = false;
|
|
153
|
+
const hintTimers = options.hintTimers ?? realHintTimers;
|
|
145
154
|
let hintTimer;
|
|
146
155
|
let resolveExit = () => undefined;
|
|
147
156
|
const render = () => tui.requestRender();
|
|
@@ -152,14 +161,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
152
161
|
const showHint = (text, ms = HINT_MS) => {
|
|
153
162
|
hint.setText(text === "" ? "" : theme.fg("dim", text));
|
|
154
163
|
if (hintTimer !== undefined)
|
|
155
|
-
|
|
164
|
+
hintTimers.clear(hintTimer);
|
|
156
165
|
hintTimer = undefined;
|
|
157
166
|
if (text !== "") {
|
|
158
|
-
hintTimer =
|
|
167
|
+
hintTimer = hintTimers.set(() => {
|
|
159
168
|
hint.setText("");
|
|
160
169
|
render();
|
|
161
170
|
}, ms);
|
|
162
|
-
hintTimer.unref?.();
|
|
163
171
|
}
|
|
164
172
|
render();
|
|
165
173
|
};
|
|
@@ -423,7 +431,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
423
431
|
finished = true;
|
|
424
432
|
logoAnimation?.finish();
|
|
425
433
|
if (hintTimer !== undefined)
|
|
426
|
-
|
|
434
|
+
hintTimers.clear(hintTimer);
|
|
427
435
|
loader.stop();
|
|
428
436
|
area.dispose();
|
|
429
437
|
osc7501.exit();
|
|
@@ -44,3 +44,5 @@ export interface SummaryInput {
|
|
|
44
44
|
}
|
|
45
45
|
/** 完成后的一行结果摘要(已着色,不含 `⎿`)。 */
|
|
46
46
|
export declare function resultSummary(input: SummaryInput, theme: Theme): string;
|
|
47
|
+
/** web_fetch 请求的主机名(审批「本会话允许该主机」用);不是 web_fetch 或 URL 不合法返回 undefined。 */
|
|
48
|
+
export declare function fetchHost(toolName: string, input: unknown): string | undefined;
|
|
@@ -12,6 +12,7 @@ import { msg } from "../../i18n/index.js";
|
|
|
12
12
|
import { isAbsolute, relative } from "node:path";
|
|
13
13
|
import { stripAnsi } from "../../tui.js";
|
|
14
14
|
import { formatTokens } from "./status-bar.js";
|
|
15
|
+
import { formatSize } from "../../tools/truncate.js";
|
|
15
16
|
/** 工具输出 → 可安全显示的行。 */
|
|
16
17
|
export function cleanLines(text) {
|
|
17
18
|
const clean = stripAnsi(text)
|
|
@@ -60,6 +61,8 @@ export function toolSummary(name, args, cwd) {
|
|
|
60
61
|
}
|
|
61
62
|
case "task":
|
|
62
63
|
return flat(str("description") ?? str("prompt") ?? "");
|
|
64
|
+
case "web_fetch":
|
|
65
|
+
return flat((str("url") ?? "").replace(/^https?:\/\//i, ""));
|
|
63
66
|
default:
|
|
64
67
|
if (path !== undefined)
|
|
65
68
|
return displayPath(path, cwd);
|
|
@@ -216,7 +219,31 @@ export function resultSummary(input, theme) {
|
|
|
216
219
|
: [];
|
|
217
220
|
return muted([m.done, duration, ...tokens].join(" · "));
|
|
218
221
|
}
|
|
222
|
+
case "web_fetch": {
|
|
223
|
+
const finalUrl = details["finalUrl"];
|
|
224
|
+
if (num(details["bytes"]) === 0 &&
|
|
225
|
+
typeof finalUrl === "string" &&
|
|
226
|
+
lines[0]?.startsWith("Redirected"))
|
|
227
|
+
return muted(m.fetchRedirect(finalUrl));
|
|
228
|
+
const title = typeof details["title"] === "string" ? flat(details["title"], 60) : undefined;
|
|
229
|
+
const format = typeof details["format"] === "string" ? details["format"] : "";
|
|
230
|
+
return muted(m.fetched(title, formatSize(num(details["bytes"]) ?? 0), format));
|
|
231
|
+
}
|
|
219
232
|
default:
|
|
220
233
|
return muted(count === 0 ? m.done : m.outputLines(count));
|
|
221
234
|
}
|
|
222
235
|
}
|
|
236
|
+
/** web_fetch 请求的主机名(审批「本会话允许该主机」用);不是 web_fetch 或 URL 不合法返回 undefined。 */
|
|
237
|
+
export function fetchHost(toolName, input) {
|
|
238
|
+
if (toolName !== "web_fetch")
|
|
239
|
+
return undefined;
|
|
240
|
+
const raw = record(input)["url"];
|
|
241
|
+
if (typeof raw !== "string")
|
|
242
|
+
return undefined;
|
|
243
|
+
try {
|
|
244
|
+
return new URL(raw.trim()).hostname.replace(/^\[|\]$/g, "").toLowerCase() || undefined;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -24,7 +24,7 @@ import { switchSession } from "../../cli/compose-session.js";
|
|
|
24
24
|
import { AmaError } from "../../errors.js";
|
|
25
25
|
import { isPermissionMode } from "../../permissions/modes.js";
|
|
26
26
|
import { PERMISSION_MODES_STRICT_FIRST } from "../../permissions/types.js";
|
|
27
|
-
import {
|
|
27
|
+
import { availableCommands } from "../commands-core.js";
|
|
28
28
|
import { msg } from "../../i18n/index.js";
|
|
29
29
|
import { sessionTrace } from "../../trace/query-session.js";
|
|
30
30
|
/** 审批 broker:等客户端 `permission_response`。 */
|
|
@@ -144,9 +144,9 @@ async function availableModels(runtime) {
|
|
|
144
144
|
}
|
|
145
145
|
return out;
|
|
146
146
|
}
|
|
147
|
-
function commands(runtime) {
|
|
147
|
+
function commands(runtime, session) {
|
|
148
148
|
return [
|
|
149
|
-
...
|
|
149
|
+
...availableCommands(runtime, session).map((c) => ({
|
|
150
150
|
name: c.name,
|
|
151
151
|
description: c.description,
|
|
152
152
|
source: "builtin",
|
|
@@ -308,7 +308,7 @@ export const handlers = {
|
|
|
308
308
|
ctx.session().setPermissionMode(p.mode);
|
|
309
309
|
return { mode: p.mode };
|
|
310
310
|
},
|
|
311
|
-
get_commands: async (_p, ctx) => ({ commands: commands(ctx.runtime) }),
|
|
311
|
+
get_commands: async (_p, ctx) => ({ commands: commands(ctx.runtime, ctx.session()) }),
|
|
312
312
|
get_skills: async (_p, ctx) => ({ skills: ctx.runtime.resources.skills }),
|
|
313
313
|
// [W5-F] 计划 / 任务命令(docs/history/wave5-plan.md §6.5、§7.5)
|
|
314
314
|
plan_response: async (p, ctx) => {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 写入目标:重定向(`>`、`>>`、`&>`)、tee、cp / mv / mkdir / touch / ln / chmod 等的目标在项目外或受保护;
|
|
9
9
|
* - 参数里出现机密路径(`cat .env`、`< ~/.ssh/id_rsa`、`--env-file=.env`)。
|
|
10
10
|
*
|
|
11
|
+
* `env` / `printenv` 不在安全名单里:子进程继承整个进程环境(供应商 API key 等),打印出来就会进
|
|
12
|
+
* tool_result 与会话文件。段里只剩环境赋值或包装命令(`env`、`env FOO=1`)同样不放行。
|
|
13
|
+
*
|
|
11
14
|
* 静态判定(全部满足即放行):每一段都命中安全名单({@link AUTO_SAFE_COMMANDS} + 用户追加),整条命令
|
|
12
15
|
* 没有命令替换、参数没有变量展开 `$X`、没有可能匹配点文件的通配、没有超深嵌套。
|
|
13
16
|
*
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 写入目标:重定向(`>`、`>>`、`&>`)、tee、cp / mv / mkdir / touch / ln / chmod 等的目标在项目外或受保护;
|
|
9
9
|
* - 参数里出现机密路径(`cat .env`、`< ~/.ssh/id_rsa`、`--env-file=.env`)。
|
|
10
10
|
*
|
|
11
|
+
* `env` / `printenv` 不在安全名单里:子进程继承整个进程环境(供应商 API key 等),打印出来就会进
|
|
12
|
+
* tool_result 与会话文件。段里只剩环境赋值或包装命令(`env`、`env FOO=1`)同样不放行。
|
|
13
|
+
*
|
|
11
14
|
* 静态判定(全部满足即放行):每一段都命中安全名单({@link AUTO_SAFE_COMMANDS} + 用户追加),整条命令
|
|
12
15
|
* 没有命令替换、参数没有变量展开 `$X`、没有可能匹配点文件的通配、没有超深嵌套。
|
|
13
16
|
*
|
|
@@ -38,7 +41,6 @@ export const AUTO_SAFE_COMMANDS = [
|
|
|
38
41
|
"echo",
|
|
39
42
|
"printf",
|
|
40
43
|
"which",
|
|
41
|
-
"printenv",
|
|
42
44
|
"true",
|
|
43
45
|
"false",
|
|
44
46
|
"cut",
|
|
@@ -451,6 +453,7 @@ function cdTarget(argv, cwd) {
|
|
|
451
453
|
function parseSegment(segment) {
|
|
452
454
|
const redirects = extractRedirects(segment);
|
|
453
455
|
return {
|
|
456
|
+
first: shellWords(redirects.rest.replace(/^[({\s]+/, ""))[0],
|
|
454
457
|
argv: commandWords(redirects.rest),
|
|
455
458
|
withSudo: commandWords(redirects.rest, true),
|
|
456
459
|
redirects,
|
|
@@ -478,8 +481,12 @@ function ruleAsk(info, options) {
|
|
|
478
481
|
}
|
|
479
482
|
function safeSegment(info, extra) {
|
|
480
483
|
const argv = info.withSudo;
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
// 只剩环境赋值或包装命令:`env` / `env FOO=1` 会打印整个环境(含密钥),不放行。
|
|
485
|
+
if (argv.length === 0) {
|
|
486
|
+
return info.first === undefined
|
|
487
|
+
? "empty command"
|
|
488
|
+
: `${base(info.first)} is not in the auto safe list`;
|
|
489
|
+
}
|
|
483
490
|
for (const word of argv) {
|
|
484
491
|
if (word.includes("$"))
|
|
485
492
|
return `variable expansion in ${word}`;
|
|
@@ -25,7 +25,7 @@ export const CLASSIFIER_SYSTEM_PROMPT = [
|
|
|
25
25
|
"- it stays inside the project directory;",
|
|
26
26
|
"- it is read-only, or a routine and easily reversible development step (build, test, lint, format, generate code, run a project script);",
|
|
27
27
|
"- it does not read or change secrets, credentials, keys or VCS internals;",
|
|
28
|
-
"- it does not use the network, install software, publish, deploy, push, or delete data the user may need;",
|
|
28
|
+
"- it does not use the network, install software, publish, deploy, push, or delete data the user may need (a web_fetch of a public documentation or package-registry page is routine; answer ask when its query string looks like a key or a long random token);",
|
|
29
29
|
"- it plausibly serves the user's latest request.",
|
|
30
30
|
'Otherwise answer "ask". When unsure, answer "ask".',
|
|
31
31
|
"",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限类 `network`(`web_fetch` 用;docs/guides/permissions.md「网络类」)。照 `memory-class.ts` 的写法
|
|
3
|
+
* 折成已有的类走真值表:
|
|
4
|
+
*
|
|
5
|
+
* - 按 `execute` 判:default / auto-edit 询问(审批第三项「本会话允许该主机」)、allowlist 只放行 allow 规则
|
|
6
|
+
* 命中的、full-auto 放行、auto 交分类器;
|
|
7
|
+
* - plan 例外:**询问**(只读但可外泄;plan 的调研常要看文档),无人值守照常 ask → deny;
|
|
8
|
+
* - auto 规则层:URL 静态校验不过(非 http(s)、userinfo、本机名、私网 / 元数据 IP 字面量)直接 deny;
|
|
9
|
+
* 解析后的地址检查在执行层(url-guard.ts),这里不查 DNS。
|
|
10
|
+
*
|
|
11
|
+
* 规则 `web_fetch(<pattern>)` 对 `host/path`(不含协议、端口与查询串,主机名小写)匹配:
|
|
12
|
+
* - 不含 `/` 的 pattern 只比主机:裸域名 `example.com` 命中它自己与全部子域;带 `*` 时按 glob
|
|
13
|
+
* (`*.example.com`),任意路径;
|
|
14
|
+
* - 含 `/` 的 pattern 对整个 `host/path` 做 glob:`*` 不跨 `/`,`**` 跨(`docs.example.com/api/**`)。
|
|
15
|
+
*/
|
|
16
|
+
import type { ToolPermission } from "../tools/types.js";
|
|
17
|
+
/** `network` 类在真值表里按哪一类判定(plan 例外由管线处理)。 */
|
|
18
|
+
export declare function networkEffectivePermission(): Exclude<ToolPermission, "memory" | "network">;
|
|
19
|
+
/** 输入里的 URL(`url` 字段,能解析的 http(s));没有返回 undefined。 */
|
|
20
|
+
export declare function inputUrl(input: unknown): URL | undefined;
|
|
21
|
+
/** 规则匹配的对象:`host/path`(主机名小写、无端口与查询串)。 */
|
|
22
|
+
export declare function urlSubject(url: URL): string;
|
|
23
|
+
/** `web_fetch(<pattern>)` 是否命中这次调用。 */
|
|
24
|
+
export declare function networkRuleMatches(pattern: string, input: unknown): boolean;
|
|
25
|
+
/** 会话记忆「本会话允许该主机」的归一化前缀:小写主机名。 */
|
|
26
|
+
export declare function networkGrantHost(input: unknown): string | undefined;
|
|
27
|
+
/** auto 规则层:URL 静态校验不过时的拒绝原因(不查 DNS)。 */
|
|
28
|
+
export declare function networkBlockReason(input: unknown): string | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限类 `network`(`web_fetch` 用;docs/guides/permissions.md「网络类」)。照 `memory-class.ts` 的写法
|
|
3
|
+
* 折成已有的类走真值表:
|
|
4
|
+
*
|
|
5
|
+
* - 按 `execute` 判:default / auto-edit 询问(审批第三项「本会话允许该主机」)、allowlist 只放行 allow 规则
|
|
6
|
+
* 命中的、full-auto 放行、auto 交分类器;
|
|
7
|
+
* - plan 例外:**询问**(只读但可外泄;plan 的调研常要看文档),无人值守照常 ask → deny;
|
|
8
|
+
* - auto 规则层:URL 静态校验不过(非 http(s)、userinfo、本机名、私网 / 元数据 IP 字面量)直接 deny;
|
|
9
|
+
* 解析后的地址检查在执行层(url-guard.ts),这里不查 DNS。
|
|
10
|
+
*
|
|
11
|
+
* 规则 `web_fetch(<pattern>)` 对 `host/path`(不含协议、端口与查询串,主机名小写)匹配:
|
|
12
|
+
* - 不含 `/` 的 pattern 只比主机:裸域名 `example.com` 命中它自己与全部子域;带 `*` 时按 glob
|
|
13
|
+
* (`*.example.com`),任意路径;
|
|
14
|
+
* - 含 `/` 的 pattern 对整个 `host/path` 做 glob:`*` 不跨 `/`,`**` 跨(`docs.example.com/api/**`)。
|
|
15
|
+
*/
|
|
16
|
+
import { bareHostname, checkFetchUrl } from "../tools/web/url-guard.js";
|
|
17
|
+
/** `network` 类在真值表里按哪一类判定(plan 例外由管线处理)。 */
|
|
18
|
+
export function networkEffectivePermission() {
|
|
19
|
+
return "execute";
|
|
20
|
+
}
|
|
21
|
+
/** 输入里的 URL(`url` 字段,能解析的 http(s));没有返回 undefined。 */
|
|
22
|
+
export function inputUrl(input) {
|
|
23
|
+
const raw = typeof input === "object" && input !== null
|
|
24
|
+
? input["url"]
|
|
25
|
+
: undefined;
|
|
26
|
+
if (typeof raw !== "string")
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(raw.trim());
|
|
30
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 规则匹配的对象:`host/path`(主机名小写、无端口与查询串)。 */
|
|
37
|
+
export function urlSubject(url) {
|
|
38
|
+
return `${bareHostname(url)}${url.pathname === "" ? "/" : url.pathname}`;
|
|
39
|
+
}
|
|
40
|
+
function globRegExp(pattern) {
|
|
41
|
+
let body = "";
|
|
42
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
43
|
+
const ch = pattern[i];
|
|
44
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
45
|
+
body += ".*";
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (ch === "*")
|
|
49
|
+
body += "[^/]*";
|
|
50
|
+
else if (ch === "?")
|
|
51
|
+
body += "[^/]";
|
|
52
|
+
else
|
|
53
|
+
body += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
}
|
|
55
|
+
return new RegExp(`^${body}$`, "i");
|
|
56
|
+
}
|
|
57
|
+
/** `web_fetch(<pattern>)` 是否命中这次调用。 */
|
|
58
|
+
export function networkRuleMatches(pattern, input) {
|
|
59
|
+
const url = inputUrl(input);
|
|
60
|
+
if (url === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
const p = pattern
|
|
63
|
+
.trim()
|
|
64
|
+
.replace(/^https?:\/\//i, "")
|
|
65
|
+
.toLowerCase();
|
|
66
|
+
if (p === "*" || p === "**")
|
|
67
|
+
return true;
|
|
68
|
+
const host = bareHostname(url);
|
|
69
|
+
if (!p.includes("/")) {
|
|
70
|
+
if (p.includes("*") || p.includes("?"))
|
|
71
|
+
return globRegExp(p).test(host);
|
|
72
|
+
const bare = p.replace(/\.$/, "");
|
|
73
|
+
return host === bare || host.endsWith(`.${bare}`);
|
|
74
|
+
}
|
|
75
|
+
return globRegExp(p).test(urlSubject(url));
|
|
76
|
+
}
|
|
77
|
+
/** 会话记忆「本会话允许该主机」的归一化前缀:小写主机名。 */
|
|
78
|
+
export function networkGrantHost(input) {
|
|
79
|
+
const url = inputUrl(input);
|
|
80
|
+
return url === undefined ? undefined : bareHostname(url);
|
|
81
|
+
}
|
|
82
|
+
/** auto 规则层:URL 静态校验不过时的拒绝原因(不查 DNS)。 */
|
|
83
|
+
export function networkBlockReason(input) {
|
|
84
|
+
const raw = typeof input === "object" && input !== null
|
|
85
|
+
? input["url"]
|
|
86
|
+
: undefined;
|
|
87
|
+
if (typeof raw !== "string")
|
|
88
|
+
return "url is missing";
|
|
89
|
+
const checked = checkFetchUrl(raw);
|
|
90
|
+
return checked.ok ? undefined : checked.reason;
|
|
91
|
+
}
|
|
@@ -60,7 +60,7 @@ export declare const UNATTENDED_MESSAGE = "This tool call requires approval, but
|
|
|
60
60
|
export declare const ALLOWLIST_MESSAGE = "Not in the allowlist: permission mode \"allowlist\" only runs read-only tools and calls matched by an allow rule, and never asks.";
|
|
61
61
|
interface SessionGrant {
|
|
62
62
|
toolName: string;
|
|
63
|
-
kind: "command" | "dir" | "any";
|
|
63
|
+
kind: "command" | "dir" | "host" | "any";
|
|
64
64
|
prefix: string;
|
|
65
65
|
}
|
|
66
66
|
export declare function sessionGrantFor(toolName: string, input: unknown, cwd: string): SessionGrant;
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { dirname, sep } from "node:path";
|
|
37
37
|
import { isBackgroundJobQuery } from "../tools/background-jobs.js";
|
|
38
38
|
import { memoryEffectivePermission } from "./memory-class.js";
|
|
39
|
+
import { networkBlockReason, networkEffectivePermission, networkGrantHost, } from "./network-class.js";
|
|
39
40
|
import { findAllowRule, findDenyRule, inputCommand, inputPath, normalizeCommand, splitShellSegments, } from "./rules.js";
|
|
40
41
|
import { collectNestedCommands, matchDangerous } from "./dangerous.js";
|
|
41
42
|
import { analyzeBashForAuto, secretReferenceReason } from "./auto-safe.js";
|
|
@@ -110,7 +111,7 @@ function auditSummary(toolName, input) {
|
|
|
110
111
|
const command = inputCommand(input);
|
|
111
112
|
const raw = command ??
|
|
112
113
|
(typeof input === "object" && input !== null
|
|
113
|
-
? input["path"]
|
|
114
|
+
? (input["path"] ?? input["url"])
|
|
114
115
|
: undefined);
|
|
115
116
|
const text = typeof raw === "string" ? raw.replace(/\s+/g, " ").trim() : toolName;
|
|
116
117
|
return text.length > 80 ? `${text.slice(0, 79)}…` : text;
|
|
@@ -118,6 +119,9 @@ function auditSummary(toolName, input) {
|
|
|
118
119
|
export function sessionGrantFor(toolName, input, cwd) {
|
|
119
120
|
if (toolName === "memory")
|
|
120
121
|
return { toolName, kind: "any", prefix: "" }; // [W6-M] 本会话允许全部写命令
|
|
122
|
+
const host = networkGrantHost(input); // web_fetch:本会话允许该主机
|
|
123
|
+
if (host !== undefined)
|
|
124
|
+
return { toolName, kind: "host", prefix: host };
|
|
121
125
|
const command = inputCommand(input);
|
|
122
126
|
if (command !== undefined) {
|
|
123
127
|
const words = normalizeCommand(command).split(" ").slice(0, 2);
|
|
@@ -133,6 +137,8 @@ function grantCovers(grant, toolName, input, cwd) {
|
|
|
133
137
|
return false;
|
|
134
138
|
if (grant.kind === "any")
|
|
135
139
|
return true;
|
|
140
|
+
if (grant.kind === "host")
|
|
141
|
+
return networkGrantHost(input) === grant.prefix;
|
|
136
142
|
if (grant.kind === "command") {
|
|
137
143
|
const command = inputCommand(input);
|
|
138
144
|
if (command === undefined || /\$\(|`/.test(command))
|
|
@@ -218,9 +224,13 @@ export class PermissionPipeline {
|
|
|
218
224
|
}
|
|
219
225
|
evaluate(raw) {
|
|
220
226
|
// [W6-C0] 权限类 memory 按命令折成 read / execute(memory-class.ts)
|
|
227
|
+
// network(web_fetch)按 execute 判,plan 例外为询问、auto 规则层先做 URL 静态校验(network-class.ts)
|
|
228
|
+
const network = raw.permission === "network";
|
|
221
229
|
const input = raw.permission === "memory"
|
|
222
230
|
? { ...raw, permission: memoryEffectivePermission(raw.input) }
|
|
223
|
-
:
|
|
231
|
+
: network
|
|
232
|
+
? { ...raw, permission: networkEffectivePermission() }
|
|
233
|
+
: raw;
|
|
224
234
|
const { toolName, permission } = input;
|
|
225
235
|
// ① deny
|
|
226
236
|
const layers = layeredInputs(toolName, input.input);
|
|
@@ -265,13 +275,15 @@ export class PermissionPipeline {
|
|
|
265
275
|
return verdict;
|
|
266
276
|
}
|
|
267
277
|
if (this.currentMode === "auto")
|
|
268
|
-
return this.evaluateAuto(input, layers, command);
|
|
278
|
+
return this.evaluateAuto(input, layers, command, network);
|
|
269
279
|
if (this.currentMode === "allowlist")
|
|
270
280
|
return this.evaluateAllowlist(input, layers);
|
|
271
281
|
// ③ 模式(plan 细化见 planDecision)
|
|
272
|
-
const plan = this.currentMode
|
|
273
|
-
?
|
|
274
|
-
:
|
|
282
|
+
const plan = this.currentMode !== "plan"
|
|
283
|
+
? undefined
|
|
284
|
+
: network
|
|
285
|
+
? { decision: "ask" }
|
|
286
|
+
: planDecision(input, this.planBashMode, this.cwd, this.projectRoot);
|
|
275
287
|
const byMode = plan?.decision ?? modeDecision(this.currentMode, permission);
|
|
276
288
|
if (byMode === "deny") {
|
|
277
289
|
return {
|
|
@@ -387,12 +399,17 @@ export class PermissionPipeline {
|
|
|
387
399
|
}
|
|
388
400
|
return {};
|
|
389
401
|
}
|
|
390
|
-
evaluateAuto(input, layers, command) {
|
|
402
|
+
evaluateAuto(input, layers, command, network = false) {
|
|
391
403
|
const rule = (decision, reason) => ({
|
|
392
404
|
layer: "rule",
|
|
393
405
|
decision,
|
|
394
406
|
reason,
|
|
395
407
|
});
|
|
408
|
+
const blocked = network ? networkBlockReason(input.input) : undefined;
|
|
409
|
+
if (blocked !== undefined) {
|
|
410
|
+
const message = `Auto mode refuses this fetch: ${blocked}`;
|
|
411
|
+
return { decision: "deny", step: "auto-rule", message, auto: rule("deny", blocked) };
|
|
412
|
+
}
|
|
396
413
|
const checked = this.autoRuleReason(input, command);
|
|
397
414
|
if (checked.ask !== undefined) {
|
|
398
415
|
return {
|
|
@@ -444,6 +444,9 @@ export function previewAction(request, options) {
|
|
|
444
444
|
else if (kind === "edit" && typeof input["path"] === "string") {
|
|
445
445
|
previewEdit(input, request, c);
|
|
446
446
|
}
|
|
447
|
+
else if (request.toolName === "web_fetch" && typeof input["url"] === "string") {
|
|
448
|
+
c.lines.push(`GET ${input["url"].replace(/\s+/g, "").slice(0, 2048)}`);
|
|
449
|
+
}
|
|
447
450
|
else if (kind === "other") {
|
|
448
451
|
const key = SUMMARY_KEYS.find((k) => typeof input[k] === "string" && input[k] !== "");
|
|
449
452
|
if (key !== undefined)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 判定 = auto 的静态判定(`analyzeBashForAuto`:分词、嵌套展开、规则层的网络 / 删除 / 写入目标 /
|
|
5
5
|
* 机密路径、命令替换、变量展开、点文件通配)**全部通过**,再加四条更窄的限制:
|
|
6
|
-
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 /
|
|
7
|
-
*
|
|
6
|
+
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 / 构建运行器;`env` /
|
|
7
|
+
* `printenv` 这类会把环境变量里的密钥打进上下文的命令两边都没有);
|
|
8
8
|
* - 没有输出重定向(`/dev/null` 等设备除外);
|
|
9
9
|
* - 没有嵌套 shell(`sh -c`、`eval`、`xargs`、`find -exec`)与进程替换;
|
|
10
10
|
* - 段首没有环境赋值或包装命令(`GIT_EXTERNAL_DIFF=… git diff`、`env …`、`nohup …`)。
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 判定 = auto 的静态判定(`analyzeBashForAuto`:分词、嵌套展开、规则层的网络 / 删除 / 写入目标 /
|
|
5
5
|
* 机密路径、命令替换、变量展开、点文件通配)**全部通过**,再加四条更窄的限制:
|
|
6
|
-
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 /
|
|
7
|
-
*
|
|
6
|
+
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 / 构建运行器;`env` /
|
|
7
|
+
* `printenv` 这类会把环境变量里的密钥打进上下文的命令两边都没有);
|
|
8
8
|
* - 没有输出重定向(`/dev/null` 等设备除外);
|
|
9
9
|
* - 没有嵌套 shell(`sh -c`、`eval`、`xargs`、`find -exec`)与进程替换;
|
|
10
10
|
* - 段首没有环境赋值或包装命令(`GIT_EXTERNAL_DIFF=… git diff`、`env …`、`nohup …`)。
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - 带 `path` 的工具(read / write / edit / ls / grep / glob 及宿主工具):对路径做 glob(`**`)。
|
|
7
7
|
* 相对模式匹配 cwd 内的相对路径;以 `**` 开头的模式也匹配绝对路径(例如 .ssh 目录规则命中 ~/.ssh 下的文件);
|
|
8
8
|
* 绝对模式(`/…`、`~/…`)匹配绝对路径。缺 path 的调用以 cwd 为路径。
|
|
9
|
+
* - 带 `url` 的工具(web_fetch):对 `host/path` 匹配,裸域名含子域(network-class.ts);
|
|
9
10
|
* - 其它工具带 pattern 的规则不匹配(无可比对的对象)。
|
|
10
11
|
*
|
|
11
12
|
* bash 复合命令:deny 规则命中整条或**任一**段即生效;allow 规则要求**每一**段都被某条 allow 覆盖,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - 带 `path` 的工具(read / write / edit / ls / grep / glob 及宿主工具):对路径做 glob(`**`)。
|
|
7
7
|
* 相对模式匹配 cwd 内的相对路径;以 `**` 开头的模式也匹配绝对路径(例如 .ssh 目录规则命中 ~/.ssh 下的文件);
|
|
8
8
|
* 绝对模式(`/…`、`~/…`)匹配绝对路径。缺 path 的调用以 cwd 为路径。
|
|
9
|
+
* - 带 `url` 的工具(web_fetch):对 `host/path` 匹配,裸域名含子域(network-class.ts);
|
|
9
10
|
* - 其它工具带 pattern 的规则不匹配(无可比对的对象)。
|
|
10
11
|
*
|
|
11
12
|
* bash 复合命令:deny 规则命中整条或**任一**段即生效;allow 规则要求**每一**段都被某条 allow 覆盖,
|
|
@@ -19,6 +20,7 @@ import { PERMISSION_MODES_STRICT_FIRST } from "./types.js";
|
|
|
19
20
|
import { globBody } from "../tools/glob.js";
|
|
20
21
|
import { expandHome, resolvePath, toPosix } from "../tools/paths.js";
|
|
21
22
|
import { memoryRuleMatches } from "./memory-class.js";
|
|
23
|
+
import { networkRuleMatches } from "./network-class.js";
|
|
22
24
|
/** 内置缺省 deny 表(用户可经 `builtinDeny` 选项移除)。 */
|
|
23
25
|
export const BUILTIN_DENY_RULES = [
|
|
24
26
|
"write(**/.git/**)",
|
|
@@ -153,7 +155,9 @@ function matchesSubject(rule, toolName, input, cwd, command) {
|
|
|
153
155
|
if (command !== undefined)
|
|
154
156
|
return wildcardToRegExp(rule.pattern).test(command);
|
|
155
157
|
const abs = inputPath(toolName, input, cwd);
|
|
156
|
-
|
|
158
|
+
if (abs !== undefined)
|
|
159
|
+
return pathMatches(rule.pattern, abs, cwd);
|
|
160
|
+
return networkRuleMatches(rule.pattern, input);
|
|
157
161
|
}
|
|
158
162
|
/** 第一条命中的 deny 规则。 */
|
|
159
163
|
export function findDenyRule(rules, toolName, input, cwd) {
|
package/dist/tools/presets.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* | 预设 | 模型直接看到 |
|
|
5
5
|
* | ------------- | ----------------------------------------- |
|
|
6
|
-
* | `default` | read、edit、write、bash、grep、glob
|
|
6
|
+
* | `default` | read、edit、write、bash、grep、glob、web_fetch |
|
|
7
7
|
* | `minimal` | read、edit、write、bash |
|
|
8
8
|
* | `codemode-only` | 只有 codemode(其余工具在脚本里调用) |
|
|
9
9
|
* | `coordinator` | read + 宿主注册的工具 |
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
|
|
24
24
|
* - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
|
|
25
25
|
* `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
|
|
26
|
+
* - (#242)`coordinator` 预设不阻塞:`task` 缺省后台(无视 `subagents.background`,调用参数 `background`
|
|
27
|
+
* 与类型定义的 `background:` 仍优先)、`task_ctl wait` 的超时封顶 5 秒;其它预设不变。
|
|
26
28
|
* - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
|
|
27
29
|
* (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
|
|
28
30
|
* warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
|
|
@@ -56,6 +58,11 @@ export declare function effectiveCodemodeMode(config: Pick<AmaConfig, "tools" |
|
|
|
56
58
|
* 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
|
|
57
59
|
*/
|
|
58
60
|
export declare function codemodeCallableFilter(config: Pick<AmaConfig, "tools">): "active" | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* [#242] 是否 `coordinator` 预设:协调者只分派、不阻塞——`task` 缺省后台、`task_ctl wait` 超时封顶
|
|
63
|
+
* (组装根据此绑定,见 cli/compose-agents.ts)。其它预设不受影响。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isCoordinatorPreset(config: Pick<AmaConfig, "tools">): boolean;
|
|
59
66
|
/**
|
|
60
67
|
* `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
|
|
61
68
|
* 返回去重后的名字(保持首次出现顺序)。
|