@armadra/agent 0.10.0 → 0.11.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 +18 -0
- package/CHANGELOG.zh-CN.md +18 -0
- package/README.md +5 -0
- package/README.zh-CN.md +4 -0
- package/dist/bundle/ama.cjs +1606 -423
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +1 -0
- 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.js +2 -2
- 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/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/rules.d.ts +1 -0
- package/dist/permissions/rules.js +5 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/presets.js +2 -2
- package/dist/tools/registry.d.ts +3 -1
- package/dist/tools/registry.js +4 -2
- 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 +23 -1
- package/docs/en/reference/host-api.md +61 -19
- package/docs/en/reference/rpc.md +7 -7
- package/docs/guides/agents.md +14 -0
- package/docs/guides/hooks.md +86 -8
- package/docs/guides/permissions.md +23 -1
- package/docs/guides/sandbox.md +6 -0
- package/docs/reference/host-api.md +61 -19
- package/docs/reference/rpc.md +7 -7
- package/package.json +1 -1
|
@@ -58,8 +58,15 @@ export interface CommandInfo {
|
|
|
58
58
|
name: string;
|
|
59
59
|
args?: string;
|
|
60
60
|
description: string;
|
|
61
|
+
/** 只在该能力可用时列出(`/help`、补全、RPC `get_commands`);输入时照常分派。 */
|
|
62
|
+
requires?: "memory" | "task";
|
|
61
63
|
}
|
|
62
64
|
export declare const BUILTIN_COMMANDS: readonly CommandInfo[];
|
|
65
|
+
/**
|
|
66
|
+
* 能力关闭的命令不列出(capability-off.test.ts 锁定):memory = 记忆工具已注册(`memory.enabled`);
|
|
67
|
+
* task = 模型能直接调 `task`,或能经 codemode 脚本调(`task` 已注册且 codemode 在活动集)。
|
|
68
|
+
*/
|
|
69
|
+
export declare function availableCommands(runtime: Pick<Runtime, "tools">, session: Pick<AgentSession, "getTools">, list?: readonly CommandInfo[]): CommandInfo[];
|
|
63
70
|
export declare function parseSlash(line: string): {
|
|
64
71
|
name: string;
|
|
65
72
|
args: string;
|
|
@@ -66,7 +66,9 @@ export const BUILTIN_COMMANDS = [
|
|
|
66
66
|
command("cache", "cache", "[warm off|streaming|idle | fingerprint]"),
|
|
67
67
|
command("statusline", "statusline", "[full|compact]"),
|
|
68
68
|
command("plan", "plan", { key: "planArgs" }),
|
|
69
|
-
command("tasks", "tasks", "[id] | stop <id> | bg [id]"),
|
|
69
|
+
Object.assign(command("tasks", "tasks", "[id] | stop <id> | bg [id]"), {
|
|
70
|
+
requires: "task",
|
|
71
|
+
}),
|
|
70
72
|
command("agents", "agents"),
|
|
71
73
|
command("paste", "paste"),
|
|
72
74
|
command("interrupt", "interrupt", { key: "interruptArgs" }),
|
|
@@ -74,8 +76,24 @@ export const BUILTIN_COMMANDS = [
|
|
|
74
76
|
// [W6-C0] 第六波(W6-S / W6-T1 / W6-M 实现)
|
|
75
77
|
command("config", "config", "[key=value]"),
|
|
76
78
|
command("trace", "trace", { key: "traceArgs" }),
|
|
77
|
-
command("memory", "memory", { key: "memoryArgs" }),
|
|
79
|
+
Object.assign(command("memory", "memory", { key: "memoryArgs" }), {
|
|
80
|
+
requires: "memory",
|
|
81
|
+
}),
|
|
78
82
|
];
|
|
83
|
+
/**
|
|
84
|
+
* 能力关闭的命令不列出(capability-off.test.ts 锁定):memory = 记忆工具已注册(`memory.enabled`);
|
|
85
|
+
* task = 模型能直接调 `task`,或能经 codemode 脚本调(`task` 已注册且 codemode 在活动集)。
|
|
86
|
+
*/
|
|
87
|
+
export function availableCommands(runtime, session, list = BUILTIN_COMMANDS) {
|
|
88
|
+
const active = new Set(session.getTools().map((t) => t.name));
|
|
89
|
+
const registered = new Set(runtime.tools.list());
|
|
90
|
+
const has = (requires) => requires === "memory"
|
|
91
|
+
? registered.has("memory")
|
|
92
|
+
: requires === "task"
|
|
93
|
+
? active.has("task") || (active.has("codemode") && registered.has("task"))
|
|
94
|
+
: true;
|
|
95
|
+
return list.filter((c) => has(c.requires));
|
|
96
|
+
}
|
|
79
97
|
const ALIASES = { quit: "exit", q: "exit", "?": "help" };
|
|
80
98
|
export function parseSlash(line) {
|
|
81
99
|
const m = /^\/([A-Za-z?][\w-]*)(?:\s+([\s\S]*))?$/.exec(line.trim());
|
|
@@ -84,8 +102,8 @@ export function parseSlash(line) {
|
|
|
84
102
|
const raw = m[1].toLowerCase();
|
|
85
103
|
return { name: ALIASES[raw] ?? raw, args: (m[2] ?? "").trim() };
|
|
86
104
|
}
|
|
87
|
-
function helpText() {
|
|
88
|
-
const lines =
|
|
105
|
+
function helpText(ctx) {
|
|
106
|
+
const lines = availableCommands(ctx.runtime, ctx.session()).map((c) => `/${c.name}${c.args !== undefined ? ` ${c.args}` : ""} ${c.description}`);
|
|
89
107
|
const m = msg().report.commands;
|
|
90
108
|
return [...lines, m.skillLine, m.templateLine].join("\n");
|
|
91
109
|
}
|
|
@@ -142,7 +160,7 @@ export async function runSlashCommand(line, ctx) {
|
|
|
142
160
|
const session = ctx.session();
|
|
143
161
|
switch (name) {
|
|
144
162
|
case "help":
|
|
145
|
-
return { kind: "handled", message: helpText() };
|
|
163
|
+
return { kind: "handled", message: helpText(ctx) };
|
|
146
164
|
case "exit":
|
|
147
165
|
return { kind: "exit" };
|
|
148
166
|
case "new": {
|
|
@@ -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;
|
|
@@ -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";
|
|
@@ -114,7 +114,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
114
114
|
? undefined
|
|
115
115
|
: (options.historyFile ?? join(runtime.paths.dataDir, "history"));
|
|
116
116
|
const completion = new InteractiveCompletion({
|
|
117
|
-
commands: () =>
|
|
117
|
+
commands: () => completionCommands(runtime, session),
|
|
118
118
|
prompts: () => runtime.resources.prompts,
|
|
119
119
|
skills: () => runtime.resources.skills,
|
|
120
120
|
cwd: () => session.state.cwd,
|
|
@@ -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) => {
|
|
@@ -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)
|
|
@@ -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 + 宿主注册的工具 |
|
package/dist/tools/presets.js
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 + 宿主注册的工具 |
|
|
@@ -35,7 +35,7 @@ export const CODEMODE_TOOL = "codemode";
|
|
|
35
35
|
/** 各预设下模型直接看到的内置工具(codemode-only 预设见 `resolvePreset`)。 */
|
|
36
36
|
export const PRESET_TOOLS = Object.freeze({
|
|
37
37
|
// D20:todo 不在 default(W5-Z 复测未过门,见文件头)
|
|
38
|
-
default: ["bash", "edit", "glob", "grep", "read", "write"],
|
|
38
|
+
default: ["bash", "edit", "glob", "grep", "read", "web_fetch", "write"],
|
|
39
39
|
minimal: ["bash", "edit", "read", "write"],
|
|
40
40
|
"codemode-only": [CODEMODE_TOOL],
|
|
41
41
|
coordinator: ["read"],
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { ToolDefinition, ToolExecutionMode, ToolRegistryApi, ToolSource } f
|
|
|
15
15
|
import { type ReadToolOptions } from "./read.js";
|
|
16
16
|
import { type BashToolOptions } from "./bash.js";
|
|
17
17
|
import { type TaskToolOptions } from "./task.js";
|
|
18
|
+
import { type WebFetchOptions } from "./web/fetch.js";
|
|
18
19
|
/** [W5-C0] 主工具 → 随它出现 / 消失的伴随工具。 */
|
|
19
20
|
export declare const TOOL_COMPANIONS: Readonly<Record<string, readonly string[]>>;
|
|
20
21
|
/** 按 `TOOL_COMPANIONS` 配对:有主工具就带上(已注册的)伴随工具,没有就去掉。 */
|
|
@@ -40,8 +41,9 @@ export interface BuiltinToolOptions {
|
|
|
40
41
|
read?: ReadToolOptions;
|
|
41
42
|
bash?: BashToolOptions;
|
|
42
43
|
task?: TaskToolOptions;
|
|
44
|
+
webFetch?: WebFetchOptions;
|
|
43
45
|
}
|
|
44
|
-
/** 全部内置工具(read / write / edit / bash / grep / glob / ls / todo / task / task_ctl)。 */
|
|
46
|
+
/** 全部内置工具(read / write / edit / bash / grep / glob / ls / todo / task / task_ctl / web_fetch)。 */
|
|
45
47
|
export declare function builtinTools(options?: BuiltinToolOptions): ToolDefinition[];
|
|
46
48
|
/** 新建注册表并登记全部内置工具;`disabled` 来自 config.tools.disabled。 */
|
|
47
49
|
export declare function createToolRegistry(options?: BuiltinToolOptions & {
|
package/dist/tools/registry.js
CHANGED
|
@@ -23,6 +23,7 @@ import { createLsTool } from "./ls.js";
|
|
|
23
23
|
import { createTodoTool } from "./todo.js";
|
|
24
24
|
import { createTaskTool } from "./task.js";
|
|
25
25
|
import { TASK_CTL_TOOL, createTaskCtlTool } from "./task-ctl.js";
|
|
26
|
+
import { createWebFetchTool } from "./web/fetch.js";
|
|
26
27
|
/** [W5-C0] 主工具 → 随它出现 / 消失的伴随工具。 */
|
|
27
28
|
export const TOOL_COMPANIONS = Object.freeze({
|
|
28
29
|
task: [TASK_CTL_TOOL],
|
|
@@ -49,7 +50,7 @@ export function validateToolDefinition(tool) {
|
|
|
49
50
|
if (typeof tool.name !== "string" || !TOOL_NAME_RE.test(tool.name)) {
|
|
50
51
|
throw new AmaError("invalid_arguments", `Invalid tool name "${String(tool.name)}"`);
|
|
51
52
|
}
|
|
52
|
-
if (!["read", "write", "execute", "memory"].includes(tool.permission)) {
|
|
53
|
+
if (!["read", "write", "execute", "memory", "network"].includes(tool.permission)) {
|
|
53
54
|
throw new AmaError("invalid_arguments", `Tool ${tool.name}: invalid permission`);
|
|
54
55
|
}
|
|
55
56
|
if (typeof tool.execute !== "function") {
|
|
@@ -102,7 +103,7 @@ export class ToolRegistry {
|
|
|
102
103
|
this.activeNames = new Set(pairCompanions(names, (n) => this.entries.has(n) && !this.disabled.has(n)));
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
|
-
/** 全部内置工具(read / write / edit / bash / grep / glob / ls / todo / task / task_ctl)。 */
|
|
106
|
+
/** 全部内置工具(read / write / edit / bash / grep / glob / ls / todo / task / task_ctl / web_fetch)。 */
|
|
106
107
|
export function builtinTools(options = {}) {
|
|
107
108
|
const tools = [
|
|
108
109
|
createReadTool(options.read),
|
|
@@ -115,6 +116,7 @@ export function builtinTools(options = {}) {
|
|
|
115
116
|
createTodoTool(),
|
|
116
117
|
createTaskTool(options.task),
|
|
117
118
|
createTaskCtlTool(),
|
|
119
|
+
createWebFetchTool(options.webFetch),
|
|
118
120
|
];
|
|
119
121
|
return tools;
|
|
120
122
|
}
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -27,7 +27,10 @@ export type { JsonSchema } from "../ai/types.js";
|
|
|
27
27
|
/**
|
|
28
28
|
* [W6-C0] `memory`:记忆工具专用(`permissions/memory-class.ts`)——`view` 按 read、写命令按 execute 判定。
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `network`:联网读取(`web_fetch`,`permissions/network-class.ts`)——按 execute 判定,plan 下询问。
|
|
32
|
+
*/
|
|
33
|
+
export type ToolPermission = "read" | "write" | "execute" | "memory" | "network";
|
|
31
34
|
export type ToolExecutionMode = "sequential" | "parallel";
|
|
32
35
|
export interface ToolAnnotations {
|
|
33
36
|
readOnly?: boolean;
|