@cjhyy/code-shell-core 0.6.0-rc.16 → 0.6.0-rc.18
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cli/agent-server-stdio.js +2 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.js +5 -5
- package/dist/credentials/use-credential-tool.d.ts +8 -1
- package/dist/credentials/use-credential-tool.js +55 -45
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +40 -13
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +4 -0
- package/dist/engine/turn-loop.js +106 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/preset/index.js +10 -9
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +18 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +12 -0
- package/dist/protocol/server.js +116 -38
- package/dist/protocol/types.d.ts +37 -0
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/tool-system/builtin/drive-claude-code.d.ts +1 -0
- package/dist/tool-system/builtin/drive-claude-code.js +137 -18
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +15 -13
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +114 -5
- package/dist/tool-system/builtin/view-image.js +9 -0
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +2 -0
- package/dist/tool-system/mcp-manager.js +23 -8
- package/dist/tool-system/path-policy.js +13 -0
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/types.d.ts +23 -4
- package/package.json +4 -3
|
@@ -4,6 +4,8 @@ import { backgroundJobRegistry } from "./background-jobs.js";
|
|
|
4
4
|
import { readExternalChangedFiles } from "../../cc-orchestrator/external-agent-changes.js";
|
|
5
5
|
import { isExistingDirectory, normalizeCwdPath } from "../../cc-orchestrator/cwd-normalize.js";
|
|
6
6
|
import { notificationQueue } from "./agent-notifications.js";
|
|
7
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
9
|
import { externalAgentSessionStore, } from "../../cc-orchestrator/external-agent-session-store.js";
|
|
8
10
|
import { logger } from "../../logging/logger.js";
|
|
9
11
|
export const DRIVE_AGENT_FOREGROUND_HANDOFF_MS = 110_000;
|
|
@@ -40,19 +42,48 @@ export const driveAgentToolDef = {
|
|
|
40
42
|
inputSchema: {
|
|
41
43
|
type: "object",
|
|
42
44
|
properties: {
|
|
43
|
-
prompt: {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
prompt: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "The task for the agent. Make it COMPLETE and self-contained: state the goal, a concrete definition of done, and (for open-ended work) an explicit scope bound so it finishes end-to-end and verifies its own work rather than sprawling or stopping half-way.",
|
|
48
|
+
},
|
|
49
|
+
cli: {
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: ["claude", "codex"],
|
|
52
|
+
description: "Which external CLI to drive. 'claude' = Claude Code (default), 'codex' = OpenAI Codex. resumeSessionId is only valid against the same cli that produced it.",
|
|
53
|
+
},
|
|
54
|
+
resumeSessionId: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Existing session id to resume (keeps context). Must come from a prior run of the SAME cli. Omit for a fresh session.",
|
|
57
|
+
},
|
|
46
58
|
cwd: { type: "string", description: "Working directory the run operates in." },
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
attachmentPaths: {
|
|
60
|
+
type: "array",
|
|
61
|
+
items: { type: "string" },
|
|
62
|
+
description: "Optional local file paths to hand to the driven agent. Paths must resolve inside cwd. Images are also passed to Codex with -i when the installed Codex CLI supports it; otherwise all paths are listed in the prompt.",
|
|
63
|
+
},
|
|
64
|
+
permissionMode: {
|
|
65
|
+
type: "string",
|
|
66
|
+
enum: ["default", "acceptEdits", "bypassPermissions"],
|
|
67
|
+
description: "Permission/sandbox level. Defaults to 'bypassPermissions' (full auto — needed so the agent's tools run unattended; there is no interactive approval loop here). For codex: default→read-only sandbox, acceptEdits→workspace-write, bypassPermissions→no sandbox. Pass 'default'/'acceptEdits' to gate.",
|
|
68
|
+
},
|
|
69
|
+
background: {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
description: "Defaults to TRUE: run in the background and notify you on completion (right for long tasks). Pass false to run in the foreground and get the result inline (only for quick tasks).",
|
|
72
|
+
},
|
|
49
73
|
},
|
|
50
74
|
required: ["prompt", "cwd"],
|
|
51
75
|
},
|
|
52
76
|
};
|
|
53
77
|
const defaultRunner = (opts) => {
|
|
54
78
|
const { adapter, command } = CLI_ADAPTERS[opts.cli];
|
|
55
|
-
return runAgentOnce(adapter, {
|
|
79
|
+
return runAgentOnce(adapter, {
|
|
80
|
+
command,
|
|
81
|
+
prompt: opts.prompt,
|
|
82
|
+
resumeSessionId: opts.resumeSessionId,
|
|
83
|
+
cwd: opts.cwd,
|
|
84
|
+
permissionMode: opts.permissionMode ?? "default",
|
|
85
|
+
imagePaths: opts.imagePaths,
|
|
86
|
+
}, opts.signal);
|
|
56
87
|
};
|
|
57
88
|
function newDriveJobId() {
|
|
58
89
|
return `cc-${process.hrtime.bigint().toString(36)}`;
|
|
@@ -72,6 +103,43 @@ function argSignal(args) {
|
|
|
72
103
|
function startRun(runner, opts) {
|
|
73
104
|
return Promise.resolve().then(() => runner(opts));
|
|
74
105
|
}
|
|
106
|
+
function resolveAttachmentPaths(raw, cwd) {
|
|
107
|
+
if (raw === undefined)
|
|
108
|
+
return { paths: [] };
|
|
109
|
+
if (!Array.isArray(raw))
|
|
110
|
+
return { paths: [], error: "attachmentPaths must be an array of strings" };
|
|
111
|
+
const cwdReal = realpathSync(cwd);
|
|
112
|
+
const paths = [];
|
|
113
|
+
for (const item of raw) {
|
|
114
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
115
|
+
return { paths: [], error: "attachmentPaths must contain only non-empty strings" };
|
|
116
|
+
}
|
|
117
|
+
const candidate = isAbsolute(item) ? item : resolve(cwd, item);
|
|
118
|
+
if (!existsSync(candidate))
|
|
119
|
+
return { paths: [], error: `attachment path not found: ${item}` };
|
|
120
|
+
const real = realpathSync(candidate);
|
|
121
|
+
const rel = relative(cwdReal, real);
|
|
122
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
123
|
+
return { paths: [], error: `attachment path is outside cwd: ${item}` };
|
|
124
|
+
}
|
|
125
|
+
const info = statSync(real);
|
|
126
|
+
if (!info.isFile())
|
|
127
|
+
return { paths: [], error: `attachment path is not a file: ${item}` };
|
|
128
|
+
paths.push(real);
|
|
129
|
+
}
|
|
130
|
+
return { paths };
|
|
131
|
+
}
|
|
132
|
+
const DRIVE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
133
|
+
function appendAttachmentPrompt(prompt, paths) {
|
|
134
|
+
if (paths.length === 0)
|
|
135
|
+
return prompt;
|
|
136
|
+
const lines = ["", "Attached files:"];
|
|
137
|
+
for (const path of paths) {
|
|
138
|
+
const kind = DRIVE_IMAGE_EXTS.has(extname(path).toLowerCase()) ? "image" : "file";
|
|
139
|
+
lines.push(`- ${path} (${kind})`);
|
|
140
|
+
}
|
|
141
|
+
return `${prompt}\n${lines.join("\n")}`;
|
|
142
|
+
}
|
|
75
143
|
function recordSuccessfulSession(store, cli, cwd, result) {
|
|
76
144
|
if (result.isError || !result.sessionId)
|
|
77
145
|
return;
|
|
@@ -106,14 +174,28 @@ function attachDriveCompletion(params) {
|
|
|
106
174
|
// (not just "a job finished"). Mirrors the video/sub-agent completion
|
|
107
175
|
// path — enqueue lands in the same notificationQueue the wakeup drains.
|
|
108
176
|
notificationQueue.enqueue(r.isError
|
|
109
|
-
? {
|
|
110
|
-
|
|
177
|
+
? {
|
|
178
|
+
agentId: jobId,
|
|
179
|
+
description: label,
|
|
180
|
+
status: "failed",
|
|
181
|
+
workKind: "cc",
|
|
182
|
+
error: r.finalText || "(no output)",
|
|
183
|
+
ccSessionId: r.sessionId || undefined,
|
|
184
|
+
enqueuedAt: Date.now(),
|
|
185
|
+
}
|
|
186
|
+
: {
|
|
187
|
+
agentId: jobId,
|
|
188
|
+
description: label,
|
|
189
|
+
status: "completed",
|
|
190
|
+
workKind: "cc",
|
|
191
|
+
finalText: r.finalText,
|
|
192
|
+
ccSessionId: r.sessionId || undefined,
|
|
193
|
+
enqueuedAt: Date.now(),
|
|
194
|
+
}, sessionId);
|
|
111
195
|
// Attribute the files the external agent changed by parsing its own
|
|
112
196
|
// transcript (#6) — those Edit/Write calls are invisible to the host's
|
|
113
197
|
// in-session aggregator. Best-effort; [] on any failure.
|
|
114
|
-
const changedFiles = r.sessionId
|
|
115
|
-
? readExternalChangedFiles(cli, cwd, r.sessionId)
|
|
116
|
-
: [];
|
|
198
|
+
const changedFiles = r.sessionId ? readExternalChangedFiles(cli, cwd, r.sessionId) : [];
|
|
117
199
|
// Retain the job in the panel with its result + the external CLI
|
|
118
200
|
// session id + changed files.
|
|
119
201
|
backgroundJobRegistry.finish(jobId, {
|
|
@@ -125,7 +207,14 @@ function attachDriveCompletion(params) {
|
|
|
125
207
|
})
|
|
126
208
|
.catch((err) => {
|
|
127
209
|
const msg = err?.message ?? String(err);
|
|
128
|
-
notificationQueue.enqueue({
|
|
210
|
+
notificationQueue.enqueue({
|
|
211
|
+
agentId: jobId,
|
|
212
|
+
description: label,
|
|
213
|
+
status: "failed",
|
|
214
|
+
workKind: "cc",
|
|
215
|
+
error: msg,
|
|
216
|
+
enqueuedAt: Date.now(),
|
|
217
|
+
}, sessionId);
|
|
129
218
|
backgroundJobRegistry.finish(jobId, { status: "failed", finalText: msg });
|
|
130
219
|
});
|
|
131
220
|
}
|
|
@@ -163,7 +252,12 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
163
252
|
const requestedCwd = normalizeCwdPath(rawRequestedCwd);
|
|
164
253
|
if (!prompt)
|
|
165
254
|
return "Error: prompt is required";
|
|
166
|
-
const cli = fixedCli ??
|
|
255
|
+
const cli = fixedCli ??
|
|
256
|
+
(args.cli === "codex"
|
|
257
|
+
? "codex"
|
|
258
|
+
: args.cli === "claude" || args.cli === undefined
|
|
259
|
+
? "claude"
|
|
260
|
+
: "invalid");
|
|
167
261
|
if (cli === "invalid")
|
|
168
262
|
return `Error: unknown cli "${String(args.cli)}" (expected "claude" or "codex")`;
|
|
169
263
|
const resumeSessionId = typeof args.resumeSessionId === "string" ? args.resumeSessionId : undefined;
|
|
@@ -202,9 +296,25 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
202
296
|
args.permissionMode === "bypassPermissions"
|
|
203
297
|
? args.permissionMode
|
|
204
298
|
: "bypassPermissions";
|
|
299
|
+
const resolvedAttachmentPaths = resolveAttachmentPaths(args.attachmentPaths, cwd);
|
|
300
|
+
if (resolvedAttachmentPaths.error)
|
|
301
|
+
return `Error: ${resolvedAttachmentPaths.error}`;
|
|
302
|
+
const attachmentPaths = resolvedAttachmentPaths.paths;
|
|
303
|
+
const promptWithAttachments = appendAttachmentPrompt(prompt, attachmentPaths);
|
|
304
|
+
const imagePaths = cli === "codex"
|
|
305
|
+
? attachmentPaths.filter((path) => DRIVE_IMAGE_EXTS.has(extname(path).toLowerCase()))
|
|
306
|
+
: [];
|
|
205
307
|
const cliName = cli === "codex" ? "Codex" : "Claude Code";
|
|
206
308
|
const label = `DriveAgent(${cli}): ${prompt.slice(0, 40)}`;
|
|
207
|
-
const runOpts = {
|
|
309
|
+
const runOpts = {
|
|
310
|
+
cli,
|
|
311
|
+
prompt: promptWithAttachments,
|
|
312
|
+
resumeSessionId,
|
|
313
|
+
cwd,
|
|
314
|
+
permissionMode,
|
|
315
|
+
signal: ctx?.signal ?? argSignal(args),
|
|
316
|
+
imagePaths,
|
|
317
|
+
};
|
|
208
318
|
const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
|
|
209
319
|
const isWritableRun = permissionMode !== "default";
|
|
210
320
|
// Background by default (these tasks are typically long). Only an explicit
|
|
@@ -232,7 +342,9 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
232
342
|
resumeNote,
|
|
233
343
|
`已在后台启动 ${cliName}(jobId ${tracked.jobId})。完成后会通知你结果,无需轮询。`,
|
|
234
344
|
tracked.warning,
|
|
235
|
-
]
|
|
345
|
+
]
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
.join("\n");
|
|
236
348
|
}
|
|
237
349
|
const run = startRun(runner, runOpts);
|
|
238
350
|
const result = await waitForForegroundOrHandoff(run, foregroundHandoffMs);
|
|
@@ -250,7 +362,9 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
250
362
|
resumeNote,
|
|
251
363
|
`${cliName} foreground run exceeded ${foregroundHandoffMs}ms; moved it to background (jobId ${tracked.jobId}). Completion will notify this session, so do not poll.`,
|
|
252
364
|
tracked.warning,
|
|
253
|
-
]
|
|
365
|
+
]
|
|
366
|
+
.filter(Boolean)
|
|
367
|
+
.join("\n");
|
|
254
368
|
}
|
|
255
369
|
const r = result.kind === "completed" ? result.result : await run;
|
|
256
370
|
recordSuccessfulSession(sessionStore, cli, cwd, r);
|
|
@@ -269,14 +383,19 @@ export const driveClaudeCodeToolDef = {
|
|
|
269
383
|
name: "DriveClaudeCode",
|
|
270
384
|
description: "(Alias of DriveAgent with cli:claude.) Delegate a task to the external Claude Code CLI " +
|
|
271
385
|
"(drives `claude` for one turn). Prefer DriveAgent for new calls; this name is kept for " +
|
|
272
|
-
"compatibility. " +
|
|
386
|
+
"compatibility. " +
|
|
387
|
+
driveAgentToolDef.description,
|
|
273
388
|
inputSchema: {
|
|
274
389
|
type: "object",
|
|
275
390
|
properties: {
|
|
276
391
|
// intentionally omit `cli` — this alias is always claude
|
|
277
392
|
prompt: driveAgentToolDef.inputSchema.properties.prompt,
|
|
278
|
-
resumeSessionId: {
|
|
393
|
+
resumeSessionId: {
|
|
394
|
+
type: "string",
|
|
395
|
+
description: "Existing CC session id to resume (keeps context). Omit for a fresh session.",
|
|
396
|
+
},
|
|
279
397
|
cwd: driveAgentToolDef.inputSchema.properties.cwd,
|
|
398
|
+
attachmentPaths: driveAgentToolDef.inputSchema.properties.attachmentPaths,
|
|
280
399
|
permissionMode: driveAgentToolDef.inputSchema.properties.permissionMode,
|
|
281
400
|
background: driveAgentToolDef.inputSchema.properties.background,
|
|
282
401
|
},
|
|
@@ -19,13 +19,18 @@ import type { ToolVisibilityContext } from "../context.js";
|
|
|
19
19
|
* registry 会把它放进 ToolResult.contentBlocks。可选的 `result` 字段是给
|
|
20
20
|
* transcript / 摘要用的纯文本镜像。沙箱执行类工具(Bash 等)可返回
|
|
21
21
|
* `{ result, sandbox }`,registry 把 sandbox 透传到 ToolResult.sandbox 供 UI 显示。
|
|
22
|
+
* 敏感结果可返回 `{ result, sensitive, displayResult, transcriptResult }`;
|
|
23
|
+
* `result` 仅供当前模型轮使用,显示/持久化路径必须使用占位字段。
|
|
22
24
|
*/
|
|
23
25
|
export type BuiltinToolResult = string | {
|
|
24
26
|
contentBlocks: import("../../types.js").ContentBlock[];
|
|
25
27
|
result?: string;
|
|
26
28
|
} | {
|
|
27
29
|
result: string;
|
|
28
|
-
sandbox
|
|
30
|
+
sandbox?: import("../../types.js").ToolResult["sandbox"];
|
|
31
|
+
sensitive?: boolean;
|
|
32
|
+
displayResult?: string;
|
|
33
|
+
transcriptResult?: string;
|
|
29
34
|
};
|
|
30
35
|
export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<BuiltinToolResult>;
|
|
31
36
|
export type BuiltinToolGuard = (ctx: ToolVisibilityContext) => boolean;
|
|
@@ -41,5 +46,5 @@ export declare const BUILTIN_TOOLS: BuiltinTool[];
|
|
|
41
46
|
* Keyed by the tool's `name` (must match the toolDef name).
|
|
42
47
|
*/
|
|
43
48
|
export declare const BUILTIN_TOOL_GUARDS: Map<string, BuiltinToolGuard>;
|
|
44
|
-
/** UseCredential is available when the cwd's
|
|
45
|
-
export declare function isUseCredentialAvailable(cwd: string): boolean;
|
|
49
|
+
/** UseCredential is available when the cwd's credential metadata has ≥1 entry. */
|
|
50
|
+
export declare function isUseCredentialAvailable(cwd: string, settingsScope?: import("../../settings/manager.js").SettingsScope): boolean;
|
|
@@ -39,9 +39,9 @@ import { cancelGoalToolDef, cancelGoalTool } from "./cancel-goal.js";
|
|
|
39
39
|
import { addMarketplaceToolDef, addMarketplaceTool } from "./add-marketplace.js";
|
|
40
40
|
import { bashOutputToolDef, bashOutputTool, killShellToolDef, killShellTool, listShellsToolDef, listShellsTool, } from "./background-shell-tools.js";
|
|
41
41
|
import { browserObserveToolDef, browserObserveTool, browserActToolDef, browserActTool, browserNavigateToolDef, browserNavigateTool, } from "./browser-tools.js";
|
|
42
|
-
import { useCredentialToolDef,
|
|
42
|
+
import { useCredentialToolDef, useCredentialBuiltinTool, } from "../../credentials/use-credential-tool.js";
|
|
43
43
|
import { injectCredentialToolDef, injectCredentialTool, isInjectCredentialAvailable, } from "../../credentials/inject-credential-tool.js";
|
|
44
|
-
import {
|
|
44
|
+
import { credentialAccessScope, getCredentialAccess } from "../../credentials/access.js";
|
|
45
45
|
export const BUILTIN_TOOLS = [
|
|
46
46
|
{
|
|
47
47
|
definition: {
|
|
@@ -69,7 +69,7 @@ export const BUILTIN_TOOLS = [
|
|
|
69
69
|
definition: {
|
|
70
70
|
...editModelCatalogToolDef,
|
|
71
71
|
source: "builtin",
|
|
72
|
-
permissionDefault: "ask", //
|
|
72
|
+
permissionDefault: "ask", // UI hint; default classifier fallback confirms writes.
|
|
73
73
|
isReadOnly: false,
|
|
74
74
|
isConcurrencySafe: false, // serializes writes to the single catalog file
|
|
75
75
|
},
|
|
@@ -444,6 +444,7 @@ export const BUILTIN_TOOLS = [
|
|
|
444
444
|
isReadOnly: false,
|
|
445
445
|
isConcurrencySafe: false,
|
|
446
446
|
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
447
|
+
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
447
448
|
},
|
|
448
449
|
execute: driveAgentTool,
|
|
449
450
|
},
|
|
@@ -457,6 +458,7 @@ export const BUILTIN_TOOLS = [
|
|
|
457
458
|
isReadOnly: false,
|
|
458
459
|
isConcurrencySafe: false,
|
|
459
460
|
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
461
|
+
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
460
462
|
},
|
|
461
463
|
execute: driveClaudeCodeTool,
|
|
462
464
|
},
|
|
@@ -650,7 +652,7 @@ export const BUILTIN_TOOLS = [
|
|
|
650
652
|
},
|
|
651
653
|
// Browser automation — 3 semantic tools driving the in-app webview via the
|
|
652
654
|
// BrowserBridge (CDP). All serial on one webview (isConcurrencySafe:false).
|
|
653
|
-
// browser_observe is read-only. browser_act
|
|
655
|
+
// browser_observe is read-only. browser_act declares a UI hint of allow; its
|
|
654
656
|
// sensitive actions (click/type/select) are escalated to "ask" by a preset
|
|
655
657
|
// PermissionRule keyed on argsPattern { action } (see preset/index.ts §4.6) —
|
|
656
658
|
// so one tool carries per-action gating. Sensitive-action + domain-whitelist
|
|
@@ -671,7 +673,7 @@ export const BUILTIN_TOOLS = [
|
|
|
671
673
|
definition: {
|
|
672
674
|
...browserActToolDef,
|
|
673
675
|
source: "builtin",
|
|
674
|
-
permissionDefault: "allow", // per-action gating
|
|
676
|
+
permissionDefault: "allow", // UI hint; per-action execution gating is the preset rule.
|
|
675
677
|
isReadOnly: false,
|
|
676
678
|
isConcurrencySafe: false,
|
|
677
679
|
timeoutMs: 30_000, // the wait action internally bounds; give RPC headroom
|
|
@@ -689,8 +691,8 @@ export const BUILTIN_TOOLS = [
|
|
|
689
691
|
execute: browserNavigateTool,
|
|
690
692
|
},
|
|
691
693
|
// ─── Credentials: AI 取用已存凭证(token/link/cookie) ──────────
|
|
692
|
-
// permissionDefault:"allow"
|
|
693
|
-
// (默认问/本会话记住/全自动)
|
|
694
|
+
// permissionDefault:"allow" 是展示/声明 hint;取用审批由工具内部的
|
|
695
|
+
// CredentialUseGate 负责(默认问/本会话记住/全自动),避免双重弹窗。
|
|
694
696
|
// 读取凭证库本身不写文件(cookie 物化成临时 cookies.txt 是用完即弃的副产物)。
|
|
695
697
|
{
|
|
696
698
|
definition: {
|
|
@@ -700,10 +702,10 @@ export const BUILTIN_TOOLS = [
|
|
|
700
702
|
isReadOnly: true,
|
|
701
703
|
isConcurrencySafe: true,
|
|
702
704
|
},
|
|
703
|
-
execute:
|
|
705
|
+
execute: useCredentialBuiltinTool,
|
|
704
706
|
},
|
|
705
707
|
// InjectCredential:把 cookie 凭证注入内置浏览器(恢复登录态)。审批由工具内部
|
|
706
|
-
// CredentialUseGate 负责(逐条 autoInjectByAI)
|
|
708
|
+
// CredentialUseGate 负责(逐条 autoInjectByAI),permissionDefault:"allow" 仅作展示 hint。
|
|
707
709
|
// 改浏览器登录态有副作用 → 非 read-only。
|
|
708
710
|
{
|
|
709
711
|
definition: {
|
|
@@ -730,17 +732,17 @@ export const BUILTIN_TOOL_GUARDS = new Map([
|
|
|
730
732
|
// of the tool list (and the context) for the common no-credentials case,
|
|
731
733
|
// matching the spec's "quiet when empty" intent (true ToolSearch-deferral for
|
|
732
734
|
// builtins isn't wired in the engine).
|
|
733
|
-
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
|
|
735
|
+
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd, ctx.settingsScope)],
|
|
734
736
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
735
737
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
736
738
|
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd, ctx.settingsScope)],
|
|
737
739
|
[completeGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
738
740
|
[cancelGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
739
741
|
]);
|
|
740
|
-
/** UseCredential is available when the cwd's
|
|
741
|
-
export function isUseCredentialAvailable(cwd) {
|
|
742
|
+
/** UseCredential is available when the cwd's credential metadata has ≥1 entry. */
|
|
743
|
+
export function isUseCredentialAvailable(cwd, settingsScope) {
|
|
742
744
|
try {
|
|
743
|
-
return
|
|
745
|
+
return getCredentialAccess().listMasked(cwd, credentialAccessScope(settingsScope)).length > 0;
|
|
744
746
|
}
|
|
745
747
|
catch {
|
|
746
748
|
return false;
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* centralized in {@link safeSpawn}; this file only carries the pwsh /
|
|
6
6
|
* powershell.exe selection + the PowerShell-specific output formatting.
|
|
7
7
|
*/
|
|
8
|
-
import type { ToolDefinition } from "../../types.js";
|
|
8
|
+
import type { ToolDefinition, ToolResult } from "../../types.js";
|
|
9
9
|
import type { ToolContext } from "../context.js";
|
|
10
10
|
export declare const powershellToolDef: ToolDefinition;
|
|
11
|
-
export declare function powershellTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string
|
|
11
|
+
export declare function powershellTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | {
|
|
12
|
+
result: string;
|
|
13
|
+
sandbox: ToolResult["sandbox"];
|
|
14
|
+
}>;
|
|
@@ -28,6 +28,10 @@ export const powershellToolDef = {
|
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
const PS_MAX_BUFFER = 5 * 1024 * 1024;
|
|
31
|
+
const POWERSHELL_SANDBOX_MARK = { backend: "off" };
|
|
32
|
+
function markPowerShellResult(result) {
|
|
33
|
+
return { result, sandbox: POWERSHELL_SANDBOX_MARK };
|
|
34
|
+
}
|
|
31
35
|
export async function powershellTool(args, ctx) {
|
|
32
36
|
const command = args.command;
|
|
33
37
|
// `??` only catches null/undefined — a non-positive timeout (0/-5) would slip
|
|
@@ -35,7 +39,7 @@ export async function powershellTool(args, ctx) {
|
|
|
35
39
|
const rawTimeout = args.timeout;
|
|
36
40
|
const timeout = typeof rawTimeout === "number" && rawTimeout > 0 ? rawTimeout : 120_000;
|
|
37
41
|
if (!command.trim()) {
|
|
38
|
-
return "Error: command is required.";
|
|
42
|
+
return markPowerShellResult("Error: command is required.");
|
|
39
43
|
}
|
|
40
44
|
// Determine PowerShell executable.
|
|
41
45
|
const psCmd = process.platform === "win32" ? "powershell.exe" : "pwsh";
|
|
@@ -52,19 +56,19 @@ export async function powershellTool(args, ctx) {
|
|
|
52
56
|
signal: ctx?.signal,
|
|
53
57
|
});
|
|
54
58
|
if (result.aborted) {
|
|
55
|
-
return wasAbortedBeforeStart
|
|
59
|
+
return markPowerShellResult(wasAbortedBeforeStart
|
|
56
60
|
? "PowerShell aborted before starting."
|
|
57
|
-
: "PowerShell aborted by signal.";
|
|
61
|
+
: "PowerShell aborted by signal.");
|
|
58
62
|
}
|
|
59
63
|
if (result.timedOut) {
|
|
60
|
-
return `PowerShell timed out after ${timeout}ms
|
|
64
|
+
return markPowerShellResult(`PowerShell timed out after ${timeout}ms`);
|
|
61
65
|
}
|
|
62
66
|
if (result.spawnFailed) {
|
|
63
|
-
return `PowerShell spawn error: ${result.error ?? "unknown error"}
|
|
67
|
+
return markPowerShellResult(`PowerShell spawn error: ${result.error ?? "unknown error"}`);
|
|
64
68
|
}
|
|
65
69
|
if (result.exitCode !== 0) {
|
|
66
70
|
const out = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
67
|
-
return `PowerShell error:\n${out || `exit code ${result.exitCode}`}
|
|
71
|
+
return markPowerShellResult(`PowerShell error:\n${out || `exit code ${result.exitCode}`}`);
|
|
68
72
|
}
|
|
69
|
-
return result.stdout.trim() || "(no output)";
|
|
73
|
+
return markPowerShellResult(result.stdout.trim() || "(no output)");
|
|
70
74
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in Read file tool.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFile, stat, open } from "node:fs/promises";
|
|
6
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
7
|
+
import { extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
8
|
import { fileCache } from "./file-cache.js";
|
|
8
9
|
import { toLf } from "./eol.js";
|
|
9
10
|
export const readToolDef = {
|
|
@@ -11,7 +12,8 @@ export const readToolDef = {
|
|
|
11
12
|
description: "Read a file from the local filesystem. Returns the file content with line numbers. " +
|
|
12
13
|
"By default reads up to 2000 lines from the beginning. " +
|
|
13
14
|
"Use offset and limit to read specific portions of large files. " +
|
|
14
|
-
"For
|
|
15
|
+
"For images and binary files, returns path/metadata instead of raw bytes. " +
|
|
16
|
+
"For large text files, consider using Grep first to find the relevant lines.\n\n" +
|
|
15
17
|
"Do NOT re-read a file you just edited to verify — Edit/Write would have errored " +
|
|
16
18
|
"if the change failed.\n" +
|
|
17
19
|
"Do NOT re-read a file (or the same range of a file) you've already read earlier in " +
|
|
@@ -28,6 +30,15 @@ export const readToolDef = {
|
|
|
28
30
|
},
|
|
29
31
|
};
|
|
30
32
|
const MAX_CONTENT_CHARS = 200_000;
|
|
33
|
+
const LARGE_TEXT_BYTES = 5 * 1024 * 1024;
|
|
34
|
+
const SAMPLE_BYTES = 8192;
|
|
35
|
+
const IMAGE_EXT_MIME = {
|
|
36
|
+
".png": "image/png",
|
|
37
|
+
".jpg": "image/jpeg",
|
|
38
|
+
".jpeg": "image/jpeg",
|
|
39
|
+
".gif": "image/gif",
|
|
40
|
+
".webp": "image/webp",
|
|
41
|
+
};
|
|
31
42
|
export async function readTool(args, ctx) {
|
|
32
43
|
const rawPath = args.file_path;
|
|
33
44
|
if (!rawPath)
|
|
@@ -40,8 +51,21 @@ export async function readTool(args, ctx) {
|
|
|
40
51
|
// Get file info first
|
|
41
52
|
const fileInfo = await stat(filePath);
|
|
42
53
|
const sizeKB = Math.round(fileInfo.size / 1024);
|
|
54
|
+
const sample = await readSample(filePath, Math.min(fileInfo.size, SAMPLE_BYTES));
|
|
55
|
+
const binary = detectBinary(filePath, sample);
|
|
56
|
+
if (binary) {
|
|
57
|
+
const sha256 = await sha256File(filePath);
|
|
58
|
+
return formatBinaryReadResult({
|
|
59
|
+
filePath,
|
|
60
|
+
cwd,
|
|
61
|
+
size: fileInfo.size,
|
|
62
|
+
sha256,
|
|
63
|
+
mime: binary.mime,
|
|
64
|
+
isImage: binary.kind === "image",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
43
67
|
// Skip binary or excessively large files
|
|
44
|
-
if (fileInfo.size >
|
|
68
|
+
if (fileInfo.size > LARGE_TEXT_BYTES) {
|
|
45
69
|
return `Error: File is too large (${sizeKB}KB). Use Grep to search for specific content, or provide offset and limit to read a portion.`;
|
|
46
70
|
}
|
|
47
71
|
// Try cache first, fall back to disk read
|
|
@@ -79,3 +103,88 @@ export async function readTool(args, ctx) {
|
|
|
79
103
|
return `Error reading file: ${err.message}`;
|
|
80
104
|
}
|
|
81
105
|
}
|
|
106
|
+
async function readSample(filePath, length) {
|
|
107
|
+
if (length <= 0)
|
|
108
|
+
return Buffer.alloc(0);
|
|
109
|
+
const handle = await open(filePath, "r");
|
|
110
|
+
try {
|
|
111
|
+
const buffer = Buffer.alloc(length);
|
|
112
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
113
|
+
return buffer.subarray(0, bytesRead);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function detectBinary(filePath, sample) {
|
|
120
|
+
const imageMime = detectImageMime(filePath, sample);
|
|
121
|
+
if (imageMime)
|
|
122
|
+
return { kind: "image", mime: imageMime };
|
|
123
|
+
if (sample.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (sample.includes(0))
|
|
126
|
+
return { kind: "binary" };
|
|
127
|
+
try {
|
|
128
|
+
new TextDecoder("utf-8", { fatal: true }).decode(sample);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return { kind: "binary" };
|
|
132
|
+
}
|
|
133
|
+
let controls = 0;
|
|
134
|
+
for (const byte of sample) {
|
|
135
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 12 && byte !== 13)
|
|
136
|
+
controls += 1;
|
|
137
|
+
}
|
|
138
|
+
return controls > Math.max(8, sample.length * 0.01) ? { kind: "binary" } : null;
|
|
139
|
+
}
|
|
140
|
+
function detectImageMime(filePath, sample) {
|
|
141
|
+
if (sample.length >= 8 &&
|
|
142
|
+
sample[0] === 0x89 &&
|
|
143
|
+
sample[1] === 0x50 &&
|
|
144
|
+
sample[2] === 0x4e &&
|
|
145
|
+
sample[3] === 0x47 &&
|
|
146
|
+
sample[4] === 0x0d &&
|
|
147
|
+
sample[5] === 0x0a &&
|
|
148
|
+
sample[6] === 0x1a &&
|
|
149
|
+
sample[7] === 0x0a) {
|
|
150
|
+
return "image/png";
|
|
151
|
+
}
|
|
152
|
+
if (sample.length >= 3 && sample[0] === 0xff && sample[1] === 0xd8 && sample[2] === 0xff) {
|
|
153
|
+
return "image/jpeg";
|
|
154
|
+
}
|
|
155
|
+
const header = sample.subarray(0, 12).toString("ascii");
|
|
156
|
+
if (header.startsWith("GIF87a") || header.startsWith("GIF89a"))
|
|
157
|
+
return "image/gif";
|
|
158
|
+
if (header.startsWith("RIFF") && header.slice(8, 12) === "WEBP")
|
|
159
|
+
return "image/webp";
|
|
160
|
+
return IMAGE_EXT_MIME[extname(filePath).toLowerCase()];
|
|
161
|
+
}
|
|
162
|
+
async function sha256File(filePath) {
|
|
163
|
+
const hash = createHash("sha256");
|
|
164
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
165
|
+
hash.update(chunk);
|
|
166
|
+
}
|
|
167
|
+
return hash.digest("hex");
|
|
168
|
+
}
|
|
169
|
+
function formatBinaryReadResult(input) {
|
|
170
|
+
const path = displayPath(input.filePath, input.cwd);
|
|
171
|
+
const lines = [
|
|
172
|
+
input.isImage ? "Image file (not displayed by Read)." : "Binary file (not displayed by Read).",
|
|
173
|
+
`Path: ${path}`,
|
|
174
|
+
`Absolute path: ${input.filePath}`,
|
|
175
|
+
...(input.mime ? [`MIME: ${input.mime}`] : []),
|
|
176
|
+
`Size: ${input.size} bytes`,
|
|
177
|
+
`SHA-256: ${input.sha256}`,
|
|
178
|
+
];
|
|
179
|
+
if (input.isImage) {
|
|
180
|
+
lines.push(`Use view_image({ path: "${path}" }) to inspect pixels.`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
function displayPath(filePath, cwd) {
|
|
185
|
+
const rel = relative(cwd, filePath);
|
|
186
|
+
if (rel && rel !== ".." && !rel.startsWith(`..${sep}`)) {
|
|
187
|
+
return sep === "\\" ? rel.replace(/\\/g, "/") : rel;
|
|
188
|
+
}
|
|
189
|
+
return filePath;
|
|
190
|
+
}
|
|
@@ -41,6 +41,11 @@ export const viewImageToolDef = {
|
|
|
41
41
|
type: "number",
|
|
42
42
|
description: "Image history number N from an earlier [image #N, already provided] placeholder.",
|
|
43
43
|
},
|
|
44
|
+
detail: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["low", "standard", "high"],
|
|
47
|
+
description: "Optional image detail preference. Accepted for compatibility; current providers use the runtime image detail default.",
|
|
48
|
+
},
|
|
44
49
|
},
|
|
45
50
|
},
|
|
46
51
|
};
|
|
@@ -52,6 +57,10 @@ export async function viewImageTool(args, ctx) {
|
|
|
52
57
|
if (hasPath === hasImageNumber) {
|
|
53
58
|
return "Error: provide exactly one of path or imageNumber";
|
|
54
59
|
}
|
|
60
|
+
const detail = args.detail;
|
|
61
|
+
if (detail !== undefined && detail !== "low" && detail !== "standard" && detail !== "high") {
|
|
62
|
+
return "Error: detail must be one of low, standard, high";
|
|
63
|
+
}
|
|
55
64
|
if (hasImageNumber) {
|
|
56
65
|
if (typeof rawImageNumber !== "number" ||
|
|
57
66
|
!Number.isSafeInteger(rawImageNumber) ||
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool executor — orchestrates permission checks, hooks, and execution.
|
|
3
3
|
*/
|
|
4
|
-
import type { ToolCall, ToolResult
|
|
4
|
+
import type { ToolCall, ToolResult } from "../types.js";
|
|
5
5
|
import type { HookRegistry } from "../hooks/registry.js";
|
|
6
6
|
import { ToolRegistry } from "./registry.js";
|
|
7
7
|
import { PermissionClassifier } from "./permission.js";
|
|
@@ -42,9 +42,5 @@ export declare class ToolExecutor {
|
|
|
42
42
|
private resolvePolicyOperation;
|
|
43
43
|
private enforceDeclaredPathPolicy;
|
|
44
44
|
private resolvePathPolicyTargets;
|
|
45
|
-
/**
|
|
46
|
-
* Convert tool results into Message entries for the transcript.
|
|
47
|
-
*/
|
|
48
|
-
resultsToMessages(toolCalls: ToolCall[], results: ToolResult[]): Message[];
|
|
49
45
|
}
|
|
50
46
|
export {};
|