wave-agent-sdk 1.0.7 → 1.0.9
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/builtin/skills/artifact/SKILL.md +14 -0
- package/builtin/skills/settings/ENV.md +2 -1
- package/builtin/skills/settings/MODELS.md +3 -0
- package/builtin/skills/settings/SKILL.md +7 -0
- package/builtin/skills/settings/SUBAGENTS.md +1 -1
- package/builtin/subagents/vision.md +18 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/managers/aiManager.js +46 -4
- package/dist/managers/backgroundTaskManager.js +52 -4
- package/dist/managers/messageManager.js +8 -2
- package/dist/managers/permissionManager.d.ts +5 -0
- package/dist/managers/permissionManager.js +18 -2
- package/dist/managers/pluginManager.d.ts +9 -0
- package/dist/managers/pluginManager.js +20 -0
- package/dist/managers/skillManager.d.ts +13 -0
- package/dist/managers/skillManager.js +30 -0
- package/dist/managers/subagentManager.d.ts +12 -0
- package/dist/managers/subagentManager.js +32 -2
- package/dist/managers/toolManager.d.ts +8 -0
- package/dist/managers/toolManager.js +18 -0
- package/dist/services/aiService.d.ts +1 -1
- package/dist/services/aiService.js +3 -2
- package/dist/services/artifactAvailability.d.ts +9 -0
- package/dist/services/artifactAvailability.js +34 -0
- package/dist/services/artifactSession.d.ts +27 -0
- package/dist/services/artifactSession.js +52 -0
- package/dist/services/configurationService.d.ts +2 -1
- package/dist/services/configurationService.js +16 -1
- package/dist/services/initializationService.js +5 -0
- package/dist/services/remoteSettingsService.js +2 -0
- package/dist/tools/agentTool.js +2 -1
- package/dist/tools/artifactTool.d.ts +2 -0
- package/dist/tools/artifactTool.js +357 -0
- package/dist/tools/bashTool.js +24 -13
- package/dist/tools/editTool.js +7 -2
- package/dist/tools/types.d.ts +3 -1
- package/dist/tools/webFetchTool.js +141 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/configuration.d.ts +2 -0
- package/dist/types/messaging.d.ts +3 -1
- package/dist/types/permissions.d.ts +3 -1
- package/dist/types/permissions.js +2 -1
- package/dist/utils/bashParser.d.ts +14 -0
- package/dist/utils/bashParser.js +45 -1
- package/dist/utils/containerSetup.js +8 -0
- package/dist/utils/convertMessagesForAPI.js +51 -4
- package/dist/utils/encoding.d.ts +28 -0
- package/dist/utils/encoding.js +99 -0
- package/dist/utils/messageOperations.d.ts +4 -2
- package/dist/utils/messageOperations.js +23 -7
- package/dist/utils/subagentParser.d.ts +5 -2
- package/dist/utils/subagentParser.js +14 -4
- package/package.json +2 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artifact
|
|
3
|
+
description: Publish a local HTML or Markdown file as a shareable web page
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Artifact: Publish a File as a Shareable Web Page
|
|
8
|
+
|
|
9
|
+
Publish a local `.html` or `.md` file as a default-private, shareable web page.
|
|
10
|
+
|
|
11
|
+
- If a file path was provided ($ARGUMENTS / $1), use it directly as the `file_path`.
|
|
12
|
+
- Otherwise, infer which file to publish from the conversation context; if it is not clear, ask the user which file to publish.
|
|
13
|
+
|
|
14
|
+
Call the `Artifact` tool with the resolved `file_path` (and `favicon` if relevant), then report the resulting URL to the user.
|
|
@@ -23,10 +23,11 @@ Wave uses several environment variables to control its core functionality. Varia
|
|
|
23
23
|
| :--- | :--- | :--- |
|
|
24
24
|
| `WAVE_API_KEY` | API key for the AI gateway. | - |
|
|
25
25
|
| `WAVE_BASE_URL` | Base URL for the AI gateway. | - |
|
|
26
|
-
| `WAVE_SERVER_URL` | Server URL for SSO authentication.
|
|
26
|
+
| `WAVE_SERVER_URL` | Server URL for SSO authentication. Resolution order: `options.serverUrl` → `process.env.WAVE_SERVER_URL` → default. Unlike other `WAVE_*` vars, a settings.json `env` value is also mirrored to `process.env` so process-level singletons (AuthService) see it without a per-session snapshot. | `https://codechat.codewave.163.com` |
|
|
27
27
|
| `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
|
|
28
28
|
| `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
|
|
29
29
|
| `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
|
|
30
|
+
| `WAVE_VISION_MODEL` | Vision-capable model used by the built-in `vision` subagent for image recognition. When set, the built-in `vision` subagent is registered (its frontmatter `model: visionModel` resolves to this value); when unset, the subagent is not loaded. Useful when the main model is fast but non-vision (e.g. DeepSeek). | - (not registered) |
|
|
30
31
|
| `WAVE_MAX_INPUT_TOKENS` | Maximum number of input tokens allowed. | `200000` |
|
|
31
32
|
| `WAVE_MAX_OUTPUT_TOKENS` | Maximum number of output tokens allowed. | `32000` |
|
|
32
33
|
| `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
|
|
@@ -105,12 +105,15 @@ You can also set the default models Wave uses via environment variables in `sett
|
|
|
105
105
|
"env": {
|
|
106
106
|
"WAVE_MODEL": "gemini-3-flash",
|
|
107
107
|
"WAVE_FAST_MODEL": "gemini-2.5-flash",
|
|
108
|
+
"WAVE_VISION_MODEL": "qwen-vl-max",
|
|
108
109
|
"WAVE_MAX_INPUT_TOKENS": "100000",
|
|
109
110
|
"WAVE_MAX_OUTPUT_TOKENS": "4096"
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
```
|
|
113
114
|
|
|
115
|
+
`WAVE_VISION_MODEL` names a vision-capable model for the built-in `vision` subagent. Setting it registers the subagent, whose frontmatter `model: visionModel` resolves to this value — so a fast non-vision main model can delegate image recognition. Leave it unset to disable the built-in `vision` subagent.
|
|
116
|
+
|
|
114
117
|
## Live Reload
|
|
115
118
|
|
|
116
119
|
Model configurations support **live reload**. When you modify the `models` field or model-related environment variables in `settings.json`, the changes take effect immediately without restarting Wave.
|
|
@@ -104,6 +104,13 @@ For detailed guidance on creating plugins and marketplaces, see [PLUGINS.md](${W
|
|
|
104
104
|
- `language`: Preferred language for agent communication (e.g., `"en"`, `"zh"`).
|
|
105
105
|
- `autoMemoryEnabled`: Enable or disable auto-memory (default: `true`).
|
|
106
106
|
- `autoMemoryFrequency`: Frequency of auto-memory extraction turns (default: `1`).
|
|
107
|
+
- `enableArtifact`: Enable the Artifact tool, which publishes local `.html`/`.md` files as shareable (default-private) web pages. Defaults to `false` while the frame backend is not live; set to `true` to register the tool and enable WebFetch interception for artifact URLs. Toggling it hot-reloads the tool registry.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"enableArtifact": true
|
|
112
|
+
}
|
|
113
|
+
```
|
|
107
114
|
|
|
108
115
|
## How to use this skill
|
|
109
116
|
|
|
@@ -35,7 +35,7 @@ You are a specialized subagent for a specific task. Your goal is to:
|
|
|
35
35
|
- `name`: (Required) Unique identifier.
|
|
36
36
|
- `description`: (Required) Explains the subagent's expertise and when to use it.
|
|
37
37
|
- `tools`: (Optional) List of tools the subagent can use.
|
|
38
|
-
- `model`: (Optional) Overrides the default model for this subagent.
|
|
38
|
+
- `model`: (Optional) Overrides the default model for this subagent. The special values `fastModel` and `visionModel` resolve to the `WAVE_FAST_MODEL` / `WAVE_VISION_MODEL` env vars respectively. Built-in subagents declaring `model: visionModel` (e.g. the built-in `vision` agent) are only registered when `WAVE_VISION_MODEL` is set; for user-defined subagents the value simply resolves to the configured vision model.
|
|
39
39
|
|
|
40
40
|
## Subagent Locations
|
|
41
41
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vision
|
|
3
|
+
description: 'Image recognition specialist that runs on the model specified by the WAVE_VISION_MODEL environment variable. Use this when the current model does not support image recognition but the user has shared an image (identify the image by its "[Image source: <path>]" metadata). Pass the image file path(s) in the prompt; this agent reads the image with the Read tool and returns a detailed text description of its contents.'
|
|
4
|
+
tools: [Read]
|
|
5
|
+
model: visionModel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an image recognition specialist. You run on a vision-capable model and your job is to look at image files and return detailed text descriptions of their contents.
|
|
9
|
+
|
|
10
|
+
When given image file path(s):
|
|
11
|
+
- Use the Read tool on each image path to load it. The Read tool returns the image as base64 image data that you can see directly.
|
|
12
|
+
- Describe the image contents in detail: what is shown, any visible text (transcribe verbatim where relevant), layout, colors, objects, and anything else the caller asked about.
|
|
13
|
+
- If an image cannot be read (file missing, not an image, or too large), report the error clearly and state which path failed.
|
|
14
|
+
- Do not invent or guess content you cannot see — only describe what the image actually shows.
|
|
15
|
+
- Return your description directly as a text message. Do NOT create files.
|
|
16
|
+
- Avoid using emojis in your response.
|
|
17
|
+
|
|
18
|
+
Complete the image recognition task and report your findings clearly.
|
|
@@ -22,3 +22,4 @@ export declare const WEB_FETCH_TOOL_NAME = "WebFetch";
|
|
|
22
22
|
export declare const ENTER_WORKTREE_TOOL_NAME = "EnterWorktree";
|
|
23
23
|
export declare const EXIT_WORKTREE_TOOL_NAME = "ExitWorktree";
|
|
24
24
|
export declare const WORKFLOW_TOOL_NAME = "Workflow";
|
|
25
|
+
export declare const ARTIFACT_TOOL_NAME = "Artifact";
|
package/dist/constants/tools.js
CHANGED
|
@@ -20,6 +20,14 @@ import { logOTelEvent } from "../telemetry/events.js";
|
|
|
20
20
|
const MAX_FORK_TURNS = 3;
|
|
21
21
|
/** Max turns for the auto-memory extraction fork. */
|
|
22
22
|
const MAX_AUTO_MEMORY_FORK_TURNS = 5;
|
|
23
|
+
/**
|
|
24
|
+
* Max consecutive auto-resumes after `finish_reason === "length"` when the
|
|
25
|
+
* truncated turn produced NO tool calls (aligned with Claude Code's
|
|
26
|
+
* MAX_OUTPUT_TOKENS_RECOVERY_LIMIT). A tool call resets the counter — it is
|
|
27
|
+
* real progress. Exhausting the limit terminates the turn with an error
|
|
28
|
+
* instead of re-planning forever.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3;
|
|
23
31
|
// Truncate text to `max` chars and append a "… [+N chars]" marker when exceeded.
|
|
24
32
|
// Used for background_tasks description/command fields (≤1000 chars per spec FR-063).
|
|
25
33
|
function truncateWithMarker(text, max) {
|
|
@@ -138,6 +146,9 @@ export class AIManager {
|
|
|
138
146
|
if (this.modelOverride === "fastModel") {
|
|
139
147
|
modelToUse = parentModelConfig.fastModel;
|
|
140
148
|
}
|
|
149
|
+
else if (this.modelOverride === "visionModel") {
|
|
150
|
+
modelToUse = parentModelConfig.visionModel;
|
|
151
|
+
}
|
|
141
152
|
else if (this.modelOverride !== "inherit") {
|
|
142
153
|
modelToUse = this.modelOverride;
|
|
143
154
|
}
|
|
@@ -1217,6 +1228,10 @@ ${question}`;
|
|
|
1217
1228
|
toolAbortController = this.toolAbortController;
|
|
1218
1229
|
}
|
|
1219
1230
|
let turnDepth = turnOffset;
|
|
1231
|
+
// Consecutive truncation recoveries within this turn (no tool calls
|
|
1232
|
+
// in between). Reset to 0 on tool calls / at each sendAIMessage entry,
|
|
1233
|
+
// matching Claude Code's maxOutputTokensRecoveryCount semantics.
|
|
1234
|
+
let maxOutputTokensRecoveryCount = 0;
|
|
1220
1235
|
inner: while (true) {
|
|
1221
1236
|
let llmSpan;
|
|
1222
1237
|
try {
|
|
@@ -1300,10 +1315,15 @@ ${question}`;
|
|
|
1300
1315
|
// Use parametersChunk as compact param for better performance
|
|
1301
1316
|
// No need to extract params or generate compact params during streaming
|
|
1302
1317
|
// Update tool block with streaming parameters using parametersChunk as compact param
|
|
1318
|
+
// `parameters` is only present on start/running/end (authoritative); streaming
|
|
1319
|
+
// carries only `parametersChunk`, so don't forward `parameters: undefined`
|
|
1320
|
+
// (it would overwrite the accumulated block params in consumers)
|
|
1303
1321
|
this.messageManager.updateToolBlock({
|
|
1304
1322
|
id: toolCall.id,
|
|
1305
1323
|
name: toolCall.name,
|
|
1306
|
-
|
|
1324
|
+
...(toolCall.parameters !== undefined
|
|
1325
|
+
? { parameters: toolCall.parameters }
|
|
1326
|
+
: {}),
|
|
1307
1327
|
parametersChunk: toolCall.parametersChunk,
|
|
1308
1328
|
stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
|
|
1309
1329
|
});
|
|
@@ -1462,13 +1482,34 @@ ${question}`;
|
|
|
1462
1482
|
const lastMessage = this.messageManager.getMessages()[this.messageManager.getMessages().length - 1];
|
|
1463
1483
|
const toolBlocks = lastMessage?.blocks.filter((block) => block.type === "tool") || [];
|
|
1464
1484
|
const hasBackgrounded = toolBlocks.length > 0 &&
|
|
1465
|
-
toolBlocks.some((block) => block.
|
|
1485
|
+
toolBlocks.some((block) => block.backgroundedByUser);
|
|
1466
1486
|
if (hasBackgrounded) {
|
|
1467
1487
|
logger?.info("Some tools were manually backgrounded, stopping.");
|
|
1468
1488
|
}
|
|
1469
1489
|
else if (!isCurrentlyAborted) {
|
|
1490
|
+
// If the response was truncated WITHOUT any tool calls,
|
|
1491
|
+
// enforce the consecutive recovery limit (aligned with Claude
|
|
1492
|
+
// Code's MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3). Re-planning
|
|
1493
|
+
// loops that never produce output would otherwise continue
|
|
1494
|
+
// until the platform kills the task.
|
|
1495
|
+
if (result.finish_reason === "length" &&
|
|
1496
|
+
toolCalls.length === 0 &&
|
|
1497
|
+
maxOutputTokensRecoveryCount >=
|
|
1498
|
+
MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
|
|
1499
|
+
this.messageManager.addErrorBlock(`Response exceeded the output token limit ${MAX_OUTPUT_TOKENS_RECOVERY_LIMIT + 1} consecutive times without producing output or tool calls. Stopped to avoid an infinite loop. Break the remaining work into smaller pieces and try again.`);
|
|
1500
|
+
break inner;
|
|
1501
|
+
}
|
|
1470
1502
|
// If response was truncated, add a hidden continuation message
|
|
1471
1503
|
if (result.finish_reason === "length") {
|
|
1504
|
+
if (toolCalls.length === 0) {
|
|
1505
|
+
// Pure truncation — count toward the recovery limit.
|
|
1506
|
+
maxOutputTokensRecoveryCount++;
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
// Truncated with tool calls — real progress, reset the
|
|
1510
|
+
// consecutive counter.
|
|
1511
|
+
maxOutputTokensRecoveryCount = 0;
|
|
1512
|
+
}
|
|
1472
1513
|
this.messageManager.addUserMessage({
|
|
1473
1514
|
content: "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
|
|
1474
1515
|
isMeta: true,
|
|
@@ -1881,7 +1922,9 @@ ${question}`;
|
|
|
1881
1922
|
name: toolName,
|
|
1882
1923
|
compactParams,
|
|
1883
1924
|
shortResult: toolResult.shortResult,
|
|
1884
|
-
|
|
1925
|
+
backgroundTaskId: toolResult.backgroundTaskId,
|
|
1926
|
+
backgroundedByUser: toolResult.backgroundedByUser,
|
|
1927
|
+
assistantAutoBackgrounded: toolResult.assistantAutoBackgrounded,
|
|
1885
1928
|
startLineNumber: toolResult.startLineNumber,
|
|
1886
1929
|
images: toolResult.images,
|
|
1887
1930
|
timestamp: Date.now(),
|
|
@@ -1900,7 +1943,6 @@ ${question}`;
|
|
|
1900
1943
|
stage: "end",
|
|
1901
1944
|
name: toolName,
|
|
1902
1945
|
compactParams,
|
|
1903
|
-
isManuallyBackgrounded: false,
|
|
1904
1946
|
timestamp: Date.now(),
|
|
1905
1947
|
});
|
|
1906
1948
|
}
|
|
@@ -3,6 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
6
|
+
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
6
7
|
import { logger } from "../utils/globalLogger.js";
|
|
7
8
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
8
9
|
export class BackgroundTaskManager {
|
|
@@ -116,8 +117,12 @@ export class BackgroundTaskManager {
|
|
|
116
117
|
}
|
|
117
118
|
}, timeout);
|
|
118
119
|
}
|
|
120
|
+
// On Windows, native tools (taskkill, powershell, ...) write GBK (cp936)
|
|
121
|
+
// instead of UTF-8; decode their byte streams accordingly (issue #1753).
|
|
122
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
123
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
119
124
|
const onStdout = (data) => {
|
|
120
|
-
const stripped = stripAnsiColors(data.toString());
|
|
125
|
+
const stripped = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
121
126
|
shell.stdout += stripped;
|
|
122
127
|
if (logStream.writable) {
|
|
123
128
|
logStream.write(stripped);
|
|
@@ -125,7 +130,7 @@ export class BackgroundTaskManager {
|
|
|
125
130
|
this.notifyTasksChange();
|
|
126
131
|
};
|
|
127
132
|
const onStderr = (data) => {
|
|
128
|
-
const stripped = stripAnsiColors(data.toString());
|
|
133
|
+
const stripped = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
129
134
|
shell.stderr += stripped;
|
|
130
135
|
if (logStream.writable) {
|
|
131
136
|
logStream.write(stripped);
|
|
@@ -136,6 +141,26 @@ export class BackgroundTaskManager {
|
|
|
136
141
|
if (timeoutHandle) {
|
|
137
142
|
clearTimeout(timeoutHandle);
|
|
138
143
|
}
|
|
144
|
+
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
145
|
+
// character split across the last chunk).
|
|
146
|
+
if (stdoutDecoder) {
|
|
147
|
+
const rest = stdoutDecoder.flush();
|
|
148
|
+
if (rest) {
|
|
149
|
+
shell.stdout += rest;
|
|
150
|
+
if (logStream.writable) {
|
|
151
|
+
logStream.write(rest);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (stderrDecoder) {
|
|
156
|
+
const rest = stderrDecoder.flush();
|
|
157
|
+
if (rest) {
|
|
158
|
+
shell.stderr += rest;
|
|
159
|
+
if (logStream.writable) {
|
|
160
|
+
logStream.write(rest);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
139
164
|
if (logStream.writable) {
|
|
140
165
|
logStream.end();
|
|
141
166
|
}
|
|
@@ -256,8 +281,12 @@ export class BackgroundTaskManager {
|
|
|
256
281
|
};
|
|
257
282
|
this.tasks.set(id, shell);
|
|
258
283
|
this.notifyTasksChange();
|
|
284
|
+
// On Windows, native tools write GBK (cp936) instead of UTF-8; decode
|
|
285
|
+
// their byte streams accordingly (issue #1753).
|
|
286
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
287
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
259
288
|
child.stdout?.on("data", (data) => {
|
|
260
|
-
const stripped = stripAnsiColors(data.toString());
|
|
289
|
+
const stripped = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
261
290
|
shell.stdout += stripped;
|
|
262
291
|
if (logStream.writable) {
|
|
263
292
|
logStream.write(stripped);
|
|
@@ -265,7 +294,7 @@ export class BackgroundTaskManager {
|
|
|
265
294
|
this.notifyTasksChange();
|
|
266
295
|
});
|
|
267
296
|
child.stderr?.on("data", (data) => {
|
|
268
|
-
const stripped = stripAnsiColors(data.toString());
|
|
297
|
+
const stripped = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
269
298
|
shell.stderr += stripped;
|
|
270
299
|
if (logStream.writable) {
|
|
271
300
|
logStream.write(stripped);
|
|
@@ -273,6 +302,25 @@ export class BackgroundTaskManager {
|
|
|
273
302
|
this.notifyTasksChange();
|
|
274
303
|
});
|
|
275
304
|
child.on("exit", (code) => {
|
|
305
|
+
// Decode any bytes still held at stream end
|
|
306
|
+
if (stdoutDecoder) {
|
|
307
|
+
const rest = stdoutDecoder.flush();
|
|
308
|
+
if (rest) {
|
|
309
|
+
shell.stdout += rest;
|
|
310
|
+
if (logStream.writable) {
|
|
311
|
+
logStream.write(rest);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (stderrDecoder) {
|
|
316
|
+
const rest = stderrDecoder.flush();
|
|
317
|
+
if (rest) {
|
|
318
|
+
shell.stderr += rest;
|
|
319
|
+
if (logStream.writable) {
|
|
320
|
+
logStream.write(rest);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
276
324
|
if (logStream.writable) {
|
|
277
325
|
logStream.end();
|
|
278
326
|
}
|
|
@@ -686,7 +686,9 @@ export class MessageManager {
|
|
|
686
686
|
stage: "end",
|
|
687
687
|
success: false,
|
|
688
688
|
error: errorMessage,
|
|
689
|
-
|
|
689
|
+
backgroundTaskId: block.backgroundTaskId,
|
|
690
|
+
backgroundedByUser: block.backgroundedByUser,
|
|
691
|
+
assistantAutoBackgrounded: block.assistantAutoBackgrounded,
|
|
690
692
|
timestamp,
|
|
691
693
|
});
|
|
692
694
|
}
|
|
@@ -730,7 +732,11 @@ export class MessageManager {
|
|
|
730
732
|
// only sees messages from the latest compact summary forward — matching
|
|
731
733
|
// the compact and resume behaviors (which also fold memory).
|
|
732
734
|
this.setMessages(sliceFromLastCompact(newMessages));
|
|
733
|
-
|
|
735
|
+
// savedMessageCount tracks in-memory progress, so it must be the folded
|
|
736
|
+
// length. Using the full disk count here would make saveSession's
|
|
737
|
+
// slice(savedMessageCount) empty after a rewind past a compact boundary
|
|
738
|
+
// and silently drop every subsequent message from the session file.
|
|
739
|
+
this.savedMessageCount = this.messages.length;
|
|
734
740
|
}
|
|
735
741
|
/**
|
|
736
742
|
* Rewrite the session file with the current messages.
|
|
@@ -82,6 +82,11 @@ export declare class PermissionManager {
|
|
|
82
82
|
* Get all instance-specific denied rules
|
|
83
83
|
*/
|
|
84
84
|
getInstanceDeniedRules(): string[];
|
|
85
|
+
/**
|
|
86
|
+
* Add an instance-level allowed rule (session-level, in-memory only).
|
|
87
|
+
* Unlike addPermissionRule, this does NOT persist to settings.local.json.
|
|
88
|
+
*/
|
|
89
|
+
addInstanceAllowedRule(rule: string): void;
|
|
85
90
|
/**
|
|
86
91
|
* Get all additional directories
|
|
87
92
|
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { minimatch } from "minimatch";
|
|
10
10
|
import { RESTRICTED_TOOLS } from "../types/permissions.js";
|
|
11
|
-
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
|
|
11
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, stripGitScopePrefix, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
|
|
12
12
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
13
13
|
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
|
|
14
14
|
const DEFAULT_ALLOWED_RULES = [
|
|
@@ -140,6 +140,15 @@ export class PermissionManager {
|
|
|
140
140
|
getInstanceDeniedRules() {
|
|
141
141
|
return [...this.instanceDeniedRules];
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Add an instance-level allowed rule (session-level, in-memory only).
|
|
145
|
+
* Unlike addPermissionRule, this does NOT persist to settings.local.json.
|
|
146
|
+
*/
|
|
147
|
+
addInstanceAllowedRule(rule) {
|
|
148
|
+
if (!this.instanceAllowedRules.includes(rule)) {
|
|
149
|
+
this.instanceAllowedRules.push(rule);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
143
152
|
/**
|
|
144
153
|
* Get all additional directories
|
|
145
154
|
*/
|
|
@@ -676,7 +685,14 @@ export class PermissionManager {
|
|
|
676
685
|
.replace(/\*/g, ".*"); // Replace * with .*
|
|
677
686
|
const regex = new RegExp(`^${regexPattern}$`, "s");
|
|
678
687
|
const matched = regex.test(processedPart);
|
|
679
|
-
|
|
688
|
+
if (matched)
|
|
689
|
+
return true;
|
|
690
|
+
// Leading git global scope flags (e.g. `git -C <path>`) only change the
|
|
691
|
+
// target repository, not the subcommand being run, so rules like
|
|
692
|
+
// Bash(git status*) also cover `git -C <path> status`. The raw command
|
|
693
|
+
// is checked first so path-specific rules (e.g. deny rules on
|
|
694
|
+
// `git -C /secret status`) still match.
|
|
695
|
+
return regex.test(stripGitScopePrefix(processedPart));
|
|
680
696
|
}
|
|
681
697
|
// Handle path-based rules (e.g., "Read(**/*.env)")
|
|
682
698
|
const pathTools = [READ_TOOL_NAME, WRITE_TOOL_NAME, EDIT_TOOL_NAME];
|
|
@@ -6,10 +6,19 @@ export interface PluginManagerOptions {
|
|
|
6
6
|
}
|
|
7
7
|
export declare class PluginManager {
|
|
8
8
|
private container;
|
|
9
|
+
/**
|
|
10
|
+
* Read-only helper scripts shipped by builtin plugins that the agent runs via
|
|
11
|
+
* Bash. When such a plugin is enabled, its rules are registered as instance
|
|
12
|
+
* level allow rules (in-memory only, never persisted) so the scripts run
|
|
13
|
+
* without a permission prompt. Wildcards keep the rules valid across install
|
|
14
|
+
* locations; each rule anchors on the script filename.
|
|
15
|
+
*/
|
|
16
|
+
private static readonly BUILTIN_PLUGIN_ALLOW_RULES;
|
|
9
17
|
private plugins;
|
|
10
18
|
private workdir;
|
|
11
19
|
private enabledPlugins;
|
|
12
20
|
constructor(container: Container, options: PluginManagerOptions);
|
|
21
|
+
private get permissionManager();
|
|
13
22
|
private get skillManager();
|
|
14
23
|
private get hookManager();
|
|
15
24
|
private get lspManager();
|
|
@@ -11,6 +11,9 @@ export class PluginManager {
|
|
|
11
11
|
this.workdir = options.workdir;
|
|
12
12
|
this.enabledPlugins = options.enabledPlugins || {};
|
|
13
13
|
}
|
|
14
|
+
get permissionManager() {
|
|
15
|
+
return this.container.get("PermissionManager");
|
|
16
|
+
}
|
|
14
17
|
get skillManager() {
|
|
15
18
|
return this.container.get("SkillManager");
|
|
16
19
|
}
|
|
@@ -221,6 +224,13 @@ export class PluginManager {
|
|
|
221
224
|
if (this.enabledPlugins[`${entry.name}@builtin`] !== true)
|
|
222
225
|
continue;
|
|
223
226
|
await this.loadSinglePlugin(path.join(builtinDir, entry.name));
|
|
227
|
+
// Register allow rules only after the plugin actually loaded, so a
|
|
228
|
+
// failed load never leaves permission grants behind.
|
|
229
|
+
if (this.plugins.has(entry.name)) {
|
|
230
|
+
for (const rule of PluginManager.BUILTIN_PLUGIN_ALLOW_RULES[entry.name] || []) {
|
|
231
|
+
this.permissionManager?.addInstanceAllowedRule(rule);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
catch (error) {
|
|
@@ -240,3 +250,13 @@ export class PluginManager {
|
|
|
240
250
|
return this.plugins.get(name);
|
|
241
251
|
}
|
|
242
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Read-only helper scripts shipped by builtin plugins that the agent runs via
|
|
255
|
+
* Bash. When such a plugin is enabled, its rules are registered as instance
|
|
256
|
+
* level allow rules (in-memory only, never persisted) so the scripts run
|
|
257
|
+
* without a permission prompt. Wildcards keep the rules valid across install
|
|
258
|
+
* locations; each rule anchors on the script filename.
|
|
259
|
+
*/
|
|
260
|
+
PluginManager.BUILTIN_PLUGIN_ALLOW_RULES = {
|
|
261
|
+
sdd: ["Bash(node *spec-count.js*)"],
|
|
262
|
+
};
|
|
@@ -66,6 +66,19 @@ export declare class SkillManager extends EventEmitter {
|
|
|
66
66
|
* Find all directories that could contain skills
|
|
67
67
|
*/
|
|
68
68
|
private findSkillDirectories;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a discovered skill is a feature-gated builtin skill whose gate
|
|
71
|
+
* is currently off. Mirrors ToolManager's gate for the Artifact tool so
|
|
72
|
+
* the /artifact skill command stays in sync with enableArtifact.
|
|
73
|
+
*/
|
|
74
|
+
private isSkillGatedOff;
|
|
75
|
+
/**
|
|
76
|
+
* Re-evaluate feature-gated builtin skills after a live configuration
|
|
77
|
+
* reload (same hook as ToolManager.reloadFeatureGatedTools). Re-scans
|
|
78
|
+
* skill directories and emits "refreshed" so slash-command registration
|
|
79
|
+
* follows the gate.
|
|
80
|
+
*/
|
|
81
|
+
reloadFeatureGatedSkills(): Promise<void>;
|
|
69
82
|
/**
|
|
70
83
|
* Execute a skill by name
|
|
71
84
|
*/
|
|
@@ -7,6 +7,7 @@ import { parseSkillFile, formatSkillError } from "../utils/skillParser.js";
|
|
|
7
7
|
import { substituteCommandParameters } from "../utils/commandArgumentParser.js";
|
|
8
8
|
import { parseBashCommands, replaceBashCommandsWithOutput, executeBashCommands, } from "../utils/markdownParser.js";
|
|
9
9
|
import { getBuiltinSkillsDir } from "../utils/configPaths.js";
|
|
10
|
+
import { isArtifactEnabled } from "../services/artifactAvailability.js";
|
|
10
11
|
import { logger } from "../utils/globalLogger.js";
|
|
11
12
|
/**
|
|
12
13
|
* Manages skill discovery and loading
|
|
@@ -247,6 +248,12 @@ export class SkillManager extends EventEmitter {
|
|
|
247
248
|
...parsed.skillMetadata,
|
|
248
249
|
type: collection.type,
|
|
249
250
|
};
|
|
251
|
+
// Feature-gated builtin skills (e.g. artifact behind
|
|
252
|
+
// enableArtifact) are skipped while the gate is off, mirroring
|
|
253
|
+
// ToolManager's registration gate for the Artifact tool.
|
|
254
|
+
if (this.isSkillGatedOff(skillMetadata)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
250
257
|
// Create full skill object with content
|
|
251
258
|
const skill = {
|
|
252
259
|
...skillMetadata,
|
|
@@ -304,6 +311,29 @@ export class SkillManager extends EventEmitter {
|
|
|
304
311
|
}
|
|
305
312
|
return directories;
|
|
306
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether a discovered skill is a feature-gated builtin skill whose gate
|
|
316
|
+
* is currently off. Mirrors ToolManager's gate for the Artifact tool so
|
|
317
|
+
* the /artifact skill command stays in sync with enableArtifact.
|
|
318
|
+
*/
|
|
319
|
+
isSkillGatedOff(metadata) {
|
|
320
|
+
if (metadata.type !== "builtin") {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (metadata.name === "artifact") {
|
|
324
|
+
return !isArtifactEnabled(this.workdir);
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Re-evaluate feature-gated builtin skills after a live configuration
|
|
330
|
+
* reload (same hook as ToolManager.reloadFeatureGatedTools). Re-scans
|
|
331
|
+
* skill directories and emits "refreshed" so slash-command registration
|
|
332
|
+
* follows the gate.
|
|
333
|
+
*/
|
|
334
|
+
async reloadFeatureGatedSkills() {
|
|
335
|
+
await this.refreshSkills();
|
|
336
|
+
}
|
|
307
337
|
/**
|
|
308
338
|
* Execute a skill by name
|
|
309
339
|
*/
|
|
@@ -87,6 +87,18 @@ export declare class SubagentManager {
|
|
|
87
87
|
* Load all available subagent configurations and cache them
|
|
88
88
|
*/
|
|
89
89
|
loadConfigurations(): Promise<SubagentConfiguration[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Rebuild the cached subagent configurations. Called after settings.json
|
|
92
|
+
* env becomes available (post loadMergedConfiguration) so conditional
|
|
93
|
+
* builtin subagents (e.g. `model: visionModel` requiring WAVE_VISION_MODEL)
|
|
94
|
+
* register correctly. Plugin agents are preserved across the rebuild.
|
|
95
|
+
*/
|
|
96
|
+
refreshConfigurations(): Promise<SubagentConfiguration[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Get the merged environment (OS env overlaid with settings.json env) used
|
|
99
|
+
* for conditional subagent registration (e.g. WAVE_VISION_MODEL).
|
|
100
|
+
*/
|
|
101
|
+
private getMergedEnv;
|
|
90
102
|
/**
|
|
91
103
|
* Get cached configurations synchronously (must call loadConfigurations first)
|
|
92
104
|
*/
|
|
@@ -70,10 +70,40 @@ export class SubagentManager {
|
|
|
70
70
|
async loadConfigurations() {
|
|
71
71
|
if (this.cachedConfigurations === null) {
|
|
72
72
|
const { loadSubagentConfigurations } = await import("../utils/subagentParser.js");
|
|
73
|
-
this.cachedConfigurations = await loadSubagentConfigurations(this.workdir);
|
|
73
|
+
this.cachedConfigurations = await loadSubagentConfigurations(this.workdir, this.getMergedEnv());
|
|
74
74
|
}
|
|
75
75
|
return this.cachedConfigurations;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Rebuild the cached subagent configurations. Called after settings.json
|
|
79
|
+
* env becomes available (post loadMergedConfiguration) so conditional
|
|
80
|
+
* builtin subagents (e.g. `model: visionModel` requiring WAVE_VISION_MODEL)
|
|
81
|
+
* register correctly. Plugin agents are preserved across the rebuild.
|
|
82
|
+
*/
|
|
83
|
+
async refreshConfigurations() {
|
|
84
|
+
// Preserve plugin agents (namespaced `pluginName:agentName`) across the rebuild
|
|
85
|
+
const pluginAgents = (this.cachedConfigurations ?? []).filter((config) => config.name.includes(":"));
|
|
86
|
+
this.cachedConfigurations = null;
|
|
87
|
+
await this.loadConfigurations();
|
|
88
|
+
for (const agent of pluginAgents) {
|
|
89
|
+
this.cachedConfigurations.push(agent);
|
|
90
|
+
}
|
|
91
|
+
// Re-sort by priority then name (matches registerPluginAgents)
|
|
92
|
+
this.cachedConfigurations.sort((a, b) => {
|
|
93
|
+
if (a.priority !== b.priority)
|
|
94
|
+
return a.priority - b.priority;
|
|
95
|
+
return a.name.localeCompare(b.name);
|
|
96
|
+
});
|
|
97
|
+
return this.cachedConfigurations;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the merged environment (OS env overlaid with settings.json env) used
|
|
101
|
+
* for conditional subagent registration (e.g. WAVE_VISION_MODEL).
|
|
102
|
+
*/
|
|
103
|
+
getMergedEnv() {
|
|
104
|
+
return (this.configurationService?.getMergedEnv?.() ??
|
|
105
|
+
process.env);
|
|
106
|
+
}
|
|
77
107
|
/**
|
|
78
108
|
* Get cached configurations synchronously (must call loadConfigurations first)
|
|
79
109
|
*/
|
|
@@ -95,7 +125,7 @@ export class SubagentManager {
|
|
|
95
125
|
}
|
|
96
126
|
// Fall back to filesystem scan for non-plugin agents
|
|
97
127
|
const { findSubagentByName } = await import("../utils/subagentParser.js");
|
|
98
|
-
return findSubagentByName(name, this.workdir);
|
|
128
|
+
return findSubagentByName(name, this.workdir, this.getMergedEnv());
|
|
99
129
|
}
|
|
100
130
|
/**
|
|
101
131
|
* Register plugin agents into the cached configurations.
|
|
@@ -53,6 +53,14 @@ declare class ToolManager {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
initializeBuiltInTools(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Re-evaluate feature-gated built-in tools after a live configuration
|
|
58
|
+
* reload. Currently gates the Artifact tool on settings.json
|
|
59
|
+
* `enableArtifact`. Safe to call multiple times: gated tools are removed
|
|
60
|
+
* from the registry first, then initializeBuiltInTools() re-registers them
|
|
61
|
+
* only if still enabled (so toggling the flag off actually unregisters).
|
|
62
|
+
*/
|
|
63
|
+
reloadFeatureGatedTools(): void;
|
|
56
64
|
/**
|
|
57
65
|
* Check if a tool should be enabled based on tools configuration and permission rules
|
|
58
66
|
*/
|
|
@@ -9,6 +9,8 @@ import { cronCreateTool } from "../tools/cronCreateTool.js";
|
|
|
9
9
|
import { cronDeleteTool } from "../tools/cronDeleteTool.js";
|
|
10
10
|
import { cronListTool } from "../tools/cronListTool.js";
|
|
11
11
|
import { webFetchTool } from "../tools/webFetchTool.js";
|
|
12
|
+
import { artifactTool } from "../tools/artifactTool.js";
|
|
13
|
+
import { isArtifactEnabled } from "../services/artifactAvailability.js";
|
|
12
14
|
// New tools
|
|
13
15
|
import { globTool } from "../tools/globTool.js";
|
|
14
16
|
import { grepTool } from "../tools/grepTool.js";
|
|
@@ -95,6 +97,11 @@ class ToolManager {
|
|
|
95
97
|
exitWorktreeTool,
|
|
96
98
|
workflowTool,
|
|
97
99
|
];
|
|
100
|
+
// Artifact is a feature-gated tool: not registered at all while the frame
|
|
101
|
+
// backend is not live, unless settings.json opts in via enableArtifact: true.
|
|
102
|
+
if (isArtifactEnabled(this.container.get("Workdir"))) {
|
|
103
|
+
builtInTools.push(artifactTool);
|
|
104
|
+
}
|
|
98
105
|
for (const tool of builtInTools) {
|
|
99
106
|
if (this.shouldEnableTool(tool.name)) {
|
|
100
107
|
this.toolsRegistry.set(tool.name, tool);
|
|
@@ -107,6 +114,17 @@ class ToolManager {
|
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Re-evaluate feature-gated built-in tools after a live configuration
|
|
119
|
+
* reload. Currently gates the Artifact tool on settings.json
|
|
120
|
+
* `enableArtifact`. Safe to call multiple times: gated tools are removed
|
|
121
|
+
* from the registry first, then initializeBuiltInTools() re-registers them
|
|
122
|
+
* only if still enabled (so toggling the flag off actually unregisters).
|
|
123
|
+
*/
|
|
124
|
+
reloadFeatureGatedTools() {
|
|
125
|
+
this.toolsRegistry.delete(artifactTool.name);
|
|
126
|
+
this.initializeBuiltInTools();
|
|
127
|
+
}
|
|
110
128
|
/**
|
|
111
129
|
* Check if a tool should be enabled based on tools configuration and permission rules
|
|
112
130
|
*/
|