@iinm/plain-agent 1.11.5 → 1.11.7
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/README.md +1 -1
- package/config/config.predefined.json +27 -72
- package/package.json +1 -1
- package/src/cli/formatter.mjs +26 -10
- package/src/cli/interactive.mjs +4 -1
- package/src/main.mjs +3 -1
- package/src/providers/anthropic.mjs +1 -1
- package/src/providers/bedrock.mjs +1 -1
- package/src/providers/gemini.mjs +17 -9
- package/src/providers/openai.mjs +1 -1
- package/src/providers/openaiCompatible.mjs +20 -13
- package/src/tool.d.ts +14 -0
- package/src/tools/execCommand.mjs +18 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://deepwiki.com/iinm/plain-agent)
|
|
4
4
|
[](https://www.npmjs.com/package/@iinm/plain-agent)
|
|
5
5
|
[](https://packagephobia.com/result?p=@iinm/plain-agent)
|
|
6
|
-
[](https://socket.dev/npm/package/@iinm/plain-agent)
|
|
7
7
|
[](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
|
|
8
8
|
|
|
9
9
|
A lightweight terminal-based coding agent focused on safety and low token cost
|
|
@@ -1553,6 +1553,21 @@
|
|
|
1553
1553
|
|
|
1554
1554
|
{
|
|
1555
1555
|
"name": "kimi-k2.6",
|
|
1556
|
+
"variant": "azure",
|
|
1557
|
+
"platform": {
|
|
1558
|
+
"name": "azure",
|
|
1559
|
+
"variant": "default"
|
|
1560
|
+
},
|
|
1561
|
+
"model": {
|
|
1562
|
+
"format": "openai-messages",
|
|
1563
|
+
"config": {
|
|
1564
|
+
"model": "Kimi-K2.6",
|
|
1565
|
+
"reasoning_effort": "high"
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
"name": "kimi-k2.7-code",
|
|
1556
1571
|
"variant": "fireworks",
|
|
1557
1572
|
"platform": {
|
|
1558
1573
|
"name": "openai-compatible",
|
|
@@ -1561,7 +1576,7 @@
|
|
|
1561
1576
|
"model": {
|
|
1562
1577
|
"format": "openai-messages",
|
|
1563
1578
|
"config": {
|
|
1564
|
-
"model": "accounts/fireworks/models/kimi-
|
|
1579
|
+
"model": "accounts/fireworks/models/kimi-k2p7-code"
|
|
1565
1580
|
}
|
|
1566
1581
|
},
|
|
1567
1582
|
"cost": {
|
|
@@ -1569,13 +1584,13 @@
|
|
|
1569
1584
|
"unit": "1M",
|
|
1570
1585
|
"prices": {
|
|
1571
1586
|
"prompt_tokens": 0.95,
|
|
1572
|
-
"prompt_tokens_details.cached_tokens": -0.
|
|
1587
|
+
"prompt_tokens_details.cached_tokens": -0.76,
|
|
1573
1588
|
"completion_tokens": 4
|
|
1574
1589
|
}
|
|
1575
1590
|
}
|
|
1576
1591
|
},
|
|
1577
1592
|
{
|
|
1578
|
-
"name": "kimi-k2.
|
|
1593
|
+
"name": "kimi-k2.7-code",
|
|
1579
1594
|
"variant": "novita",
|
|
1580
1595
|
"platform": {
|
|
1581
1596
|
"name": "openai-compatible",
|
|
@@ -1584,7 +1599,7 @@
|
|
|
1584
1599
|
"model": {
|
|
1585
1600
|
"format": "openai-messages",
|
|
1586
1601
|
"config": {
|
|
1587
|
-
"model": "moonshotai/kimi-k2.
|
|
1602
|
+
"model": "moonshotai/kimi-k2.7-code"
|
|
1588
1603
|
}
|
|
1589
1604
|
},
|
|
1590
1605
|
"cost": {
|
|
@@ -1592,26 +1607,11 @@
|
|
|
1592
1607
|
"unit": "1M",
|
|
1593
1608
|
"prices": {
|
|
1594
1609
|
"prompt_tokens": 0.95,
|
|
1595
|
-
"prompt_tokens_details.cached_tokens": -0.
|
|
1610
|
+
"prompt_tokens_details.cached_tokens": -0.76,
|
|
1596
1611
|
"completion_tokens": 4
|
|
1597
1612
|
}
|
|
1598
1613
|
}
|
|
1599
1614
|
},
|
|
1600
|
-
{
|
|
1601
|
-
"name": "kimi-k2.6",
|
|
1602
|
-
"variant": "azure",
|
|
1603
|
-
"platform": {
|
|
1604
|
-
"name": "azure",
|
|
1605
|
-
"variant": "default"
|
|
1606
|
-
},
|
|
1607
|
-
"model": {
|
|
1608
|
-
"format": "openai-messages",
|
|
1609
|
-
"config": {
|
|
1610
|
-
"model": "Kimi-K2.6",
|
|
1611
|
-
"reasoning_effort": "high"
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
},
|
|
1615
1615
|
|
|
1616
1616
|
{
|
|
1617
1617
|
"name": "deepseek-v4-pro",
|
|
@@ -1676,7 +1676,7 @@
|
|
|
1676
1676
|
},
|
|
1677
1677
|
|
|
1678
1678
|
{
|
|
1679
|
-
"name": "minimax-
|
|
1679
|
+
"name": "minimax-m3",
|
|
1680
1680
|
"variant": "fireworks",
|
|
1681
1681
|
"platform": {
|
|
1682
1682
|
"name": "openai-compatible",
|
|
@@ -1685,30 +1685,7 @@
|
|
|
1685
1685
|
"model": {
|
|
1686
1686
|
"format": "openai-messages",
|
|
1687
1687
|
"config": {
|
|
1688
|
-
"model": "accounts/fireworks/models/minimax-
|
|
1689
|
-
}
|
|
1690
|
-
},
|
|
1691
|
-
"cost": {
|
|
1692
|
-
"currency": "USD",
|
|
1693
|
-
"unit": "1M",
|
|
1694
|
-
"prices": {
|
|
1695
|
-
"prompt_tokens": 0.3,
|
|
1696
|
-
"prompt_tokens_details.cached_tokens": -0.24,
|
|
1697
|
-
"completion_tokens": 1.2
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
"name": "minimax-m2.7",
|
|
1703
|
-
"variant": "novita",
|
|
1704
|
-
"platform": {
|
|
1705
|
-
"name": "openai-compatible",
|
|
1706
|
-
"variant": "novita"
|
|
1707
|
-
},
|
|
1708
|
-
"model": {
|
|
1709
|
-
"format": "openai-messages",
|
|
1710
|
-
"config": {
|
|
1711
|
-
"model": "minimax/minimax-m2.7"
|
|
1688
|
+
"model": "accounts/fireworks/models/minimax-m3"
|
|
1712
1689
|
}
|
|
1713
1690
|
},
|
|
1714
1691
|
"cost": {
|
|
@@ -1746,7 +1723,7 @@
|
|
|
1746
1723
|
},
|
|
1747
1724
|
|
|
1748
1725
|
{
|
|
1749
|
-
"name": "qwen3.
|
|
1726
|
+
"name": "qwen3.7-plus",
|
|
1750
1727
|
"variant": "fireworks",
|
|
1751
1728
|
"platform": {
|
|
1752
1729
|
"name": "openai-compatible",
|
|
@@ -1755,38 +1732,16 @@
|
|
|
1755
1732
|
"model": {
|
|
1756
1733
|
"format": "openai-messages",
|
|
1757
1734
|
"config": {
|
|
1758
|
-
"model": "accounts/fireworks/models/
|
|
1759
|
-
}
|
|
1760
|
-
},
|
|
1761
|
-
"cost": {
|
|
1762
|
-
"currency": "USD",
|
|
1763
|
-
"unit": "1M",
|
|
1764
|
-
"prices": {
|
|
1765
|
-
"prompt_tokens": 0.5,
|
|
1766
|
-
"prompt_tokens_details.cached_tokens": -0.4,
|
|
1767
|
-
"completion_tokens": 3
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
},
|
|
1771
|
-
{
|
|
1772
|
-
"name": "qwen3.6-27b",
|
|
1773
|
-
"variant": "novita",
|
|
1774
|
-
"platform": {
|
|
1775
|
-
"name": "openai-compatible",
|
|
1776
|
-
"variant": "novita"
|
|
1777
|
-
},
|
|
1778
|
-
"model": {
|
|
1779
|
-
"format": "openai-messages",
|
|
1780
|
-
"config": {
|
|
1781
|
-
"model": "qwen/qwen3.6-27b"
|
|
1735
|
+
"model": "accounts/fireworks/models/qwen3p7-plus"
|
|
1782
1736
|
}
|
|
1783
1737
|
},
|
|
1784
1738
|
"cost": {
|
|
1785
1739
|
"currency": "USD",
|
|
1786
1740
|
"unit": "1M",
|
|
1787
1741
|
"prices": {
|
|
1788
|
-
"prompt_tokens": 0.
|
|
1789
|
-
"
|
|
1742
|
+
"prompt_tokens": 0.4,
|
|
1743
|
+
"prompt_tokens_details.cached_tokens": -0.32,
|
|
1744
|
+
"completion_tokens": 1.6
|
|
1790
1745
|
}
|
|
1791
1746
|
}
|
|
1792
1747
|
},
|
package/package.json
CHANGED
package/src/cli/formatter.mjs
CHANGED
|
@@ -18,6 +18,9 @@ import { noThrow } from "../utils/noThrow.mjs";
|
|
|
18
18
|
/** Length above which a single-line arg forces block-form rendering. */
|
|
19
19
|
const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
20
20
|
|
|
21
|
+
/** Total JSON length above which args are rendered in block form even if each individual arg is short. */
|
|
22
|
+
const ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD = 160;
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Format an args array for display.
|
|
23
26
|
* Uses compact JSON for short single-line args; switches to a YAML-style
|
|
@@ -32,11 +35,13 @@ export function formatCommandArgs(args) {
|
|
|
32
35
|
return `args: ${JSON.stringify(args ?? [])}`;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
const needsBlock =
|
|
36
|
-
(
|
|
37
|
-
|
|
38
|
-
(a
|
|
39
|
-
|
|
38
|
+
const needsBlock =
|
|
39
|
+
JSON.stringify(args).length > ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD ||
|
|
40
|
+
args.some(
|
|
41
|
+
(a) =>
|
|
42
|
+
typeof a === "string" &&
|
|
43
|
+
(a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
|
|
44
|
+
);
|
|
40
45
|
if (!needsBlock) {
|
|
41
46
|
return `args: ${highlightCommandArgs(JSON.stringify(args))}`;
|
|
42
47
|
}
|
|
@@ -78,19 +83,29 @@ function highlightCommandArgs(args) {
|
|
|
78
83
|
);
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @import { SandboxModeProvider } from "../tool"
|
|
88
|
+
*/
|
|
89
|
+
|
|
81
90
|
/**
|
|
82
91
|
* Format tool use for display.
|
|
83
92
|
* @param {MessageContentToolUse} toolUse
|
|
93
|
+
* @param {{ execCommandTool?: SandboxModeProvider }} [options]
|
|
84
94
|
* @returns {Promise<string>}
|
|
85
95
|
*/
|
|
86
|
-
export async function formatToolUse(toolUse) {
|
|
96
|
+
export async function formatToolUse(toolUse, options = {}) {
|
|
87
97
|
const { toolName, input } = toolUse;
|
|
88
98
|
|
|
89
99
|
if (toolName === "exec_command") {
|
|
90
100
|
/** @type {Partial<ExecCommandInput>} */
|
|
91
101
|
const execCommandInput = input;
|
|
102
|
+
const mode = options.execCommandTool?.getSandboxMode?.(input);
|
|
103
|
+
const toolNameLine =
|
|
104
|
+
mode === "unsandboxed"
|
|
105
|
+
? `${toolName}${styleText("yellow", " [unsandboxed]")}`
|
|
106
|
+
: toolName;
|
|
92
107
|
return [
|
|
93
|
-
|
|
108
|
+
toolNameLine,
|
|
94
109
|
`command: ${JSON.stringify(execCommandInput.command)}`,
|
|
95
110
|
formatCommandArgs(execCommandInput.args),
|
|
96
111
|
].join("\n");
|
|
@@ -375,9 +390,10 @@ export function formatCostForBatch(summary) {
|
|
|
375
390
|
/**
|
|
376
391
|
* Print a message to the console.
|
|
377
392
|
* @param {Message} message
|
|
393
|
+
* @param {{ execCommandTool?: SandboxModeProvider }} [options]
|
|
378
394
|
* @returns {Promise<void>}
|
|
379
395
|
*/
|
|
380
|
-
export async function printMessage(message) {
|
|
396
|
+
export async function printMessage(message, options = {}) {
|
|
381
397
|
switch (message.role) {
|
|
382
398
|
case "assistant": {
|
|
383
399
|
// console.log(styleText("bold", "\nAgent:"));
|
|
@@ -386,7 +402,7 @@ export async function printMessage(message) {
|
|
|
386
402
|
(part) => part.type === "tool_use",
|
|
387
403
|
);
|
|
388
404
|
const formattedToolUses = await Promise.all(
|
|
389
|
-
toolUseParts.map((part) => formatToolUse(part)),
|
|
405
|
+
toolUseParts.map((part) => formatToolUse(part, options)),
|
|
390
406
|
);
|
|
391
407
|
let toolUseIndex = 0;
|
|
392
408
|
for (const part of message.content) {
|
|
@@ -405,7 +421,7 @@ export async function printMessage(message) {
|
|
|
405
421
|
// console.log(part.text);
|
|
406
422
|
// break;
|
|
407
423
|
case "tool_use":
|
|
408
|
-
console.log(styleText("bold", "\nTool
|
|
424
|
+
console.log(styleText("bold", "\nTool use:"));
|
|
409
425
|
console.log(formattedToolUses[toolUseIndex++]);
|
|
410
426
|
break;
|
|
411
427
|
}
|
package/src/cli/interactive.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
|
|
3
3
|
* @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
|
|
4
|
+
* @import { Tool, SandboxModeProvider } from "../tool"
|
|
4
5
|
* @import { VoiceInputConfig } from "../voice/input.mjs"
|
|
5
6
|
* @import { VoiceSession } from "../voice/session.mjs"
|
|
6
7
|
*/
|
|
@@ -67,6 +68,7 @@ const HELP_MESSAGE = [
|
|
|
67
68
|
* @property {() => Promise<void>} onStop
|
|
68
69
|
* @property {ClaudeCodePlugin[]} [claudeCodePlugins]
|
|
69
70
|
* @property {VoiceInputConfig} [voiceInput]
|
|
71
|
+
* @property {Tool & SandboxModeProvider} [execCommandTool]
|
|
70
72
|
*/
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -111,6 +113,7 @@ export function startInteractiveSession({
|
|
|
111
113
|
onStop,
|
|
112
114
|
claudeCodePlugins,
|
|
113
115
|
voiceInput,
|
|
116
|
+
execCommandTool,
|
|
114
117
|
}) {
|
|
115
118
|
/** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string, toolSpinnerIndex: number, toolSpinnerLastTime: number }} */
|
|
116
119
|
const state = {
|
|
@@ -517,7 +520,7 @@ export function startInteractiveSession({
|
|
|
517
520
|
|
|
518
521
|
agentEventEmitter.on("message", (message) => {
|
|
519
522
|
enqueueOutput(() =>
|
|
520
|
-
printMessage(message).catch((err) => {
|
|
523
|
+
printMessage(message, { execCommandTool }).catch((err) => {
|
|
521
524
|
console.error(
|
|
522
525
|
styleText("red", `Error rendering message: ${err.message}`),
|
|
523
526
|
);
|
package/src/main.mjs
CHANGED
|
@@ -308,8 +308,9 @@ export async function main(argv = process.argv) {
|
|
|
308
308
|
skills: Array.from(prompts.values()).filter((p) => p.isSkill),
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
+
const execCommandTool = createExecCommandTool({ sandbox: appConfig.sandbox });
|
|
311
312
|
const builtinTools = [
|
|
312
|
-
|
|
313
|
+
execCommandTool,
|
|
313
314
|
readFileTool,
|
|
314
315
|
writeFileTool,
|
|
315
316
|
createPatchFileTool(),
|
|
@@ -443,6 +444,7 @@ export async function main(argv = process.argv) {
|
|
|
443
444
|
} else {
|
|
444
445
|
startInteractiveSession({
|
|
445
446
|
...sessionOptions,
|
|
447
|
+
execCommandTool,
|
|
446
448
|
notifyCmd: appConfig.notifyCmd,
|
|
447
449
|
claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
|
|
448
450
|
voiceInput: appConfig.voiceInput,
|
|
@@ -145,7 +145,7 @@ export async function callAnthropicModel(
|
|
|
145
145
|
|
|
146
146
|
const response = await runFetch();
|
|
147
147
|
|
|
148
|
-
if (response.status === 429 || response.status >= 500) {
|
|
148
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
149
149
|
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
150
150
|
console.error(
|
|
151
151
|
styleText(
|
|
@@ -107,7 +107,7 @@ export async function callBedrockConverseModel(
|
|
|
107
107
|
(response.status === 429 ||
|
|
108
108
|
response.status === 502 ||
|
|
109
109
|
response.status === 503) &&
|
|
110
|
-
retryCount <
|
|
110
|
+
retryCount < 5
|
|
111
111
|
) {
|
|
112
112
|
const retryInterval = Math.min(2 * 2 ** retryCount, 16);
|
|
113
113
|
console.error(
|
package/src/providers/gemini.mjs
CHANGED
|
@@ -145,7 +145,10 @@ export function createCacheEnabledGeminiModelCaller(
|
|
|
145
145
|
signal: AbortSignal.timeout(8 * 60 * 1000),
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
(response.status === 429 || response.status >= 500) &&
|
|
150
|
+
retryCount < 5
|
|
151
|
+
) {
|
|
149
152
|
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
150
153
|
console.error(
|
|
151
154
|
styleText(
|
|
@@ -219,15 +222,20 @@ export function createCacheEnabledGeminiModelCaller(
|
|
|
219
222
|
message instanceof GeminiNoCandidateError ||
|
|
220
223
|
message instanceof GeminiMalformedFunctionCallError
|
|
221
224
|
) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
if (retryCount < 5) {
|
|
226
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
227
|
+
console.error(
|
|
228
|
+
styleText(
|
|
229
|
+
"yellow",
|
|
230
|
+
`${message.name}: Retrying in ${interval} seconds...`,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
234
|
+
return modelCaller(config, input, retryCount + 1);
|
|
235
|
+
}
|
|
236
|
+
return new Error(
|
|
237
|
+
`${message.name}: Retry limit (${retryCount}) exceeded: ${message.message}`,
|
|
228
238
|
);
|
|
229
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
230
|
-
return modelCaller(config, input, retryCount + 1);
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
// Create context cache for next request
|
package/src/providers/openai.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export async function callOpenAIModel(
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
const retryInterval = Math.min(2 * 2 ** retryCount, 16);
|
|
71
|
-
if (response.status === 429 || response.status >= 500) {
|
|
71
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
72
72
|
console.error(
|
|
73
73
|
styleText(
|
|
74
74
|
"yellow",
|
|
@@ -179,7 +179,7 @@ export async function callOpenAICompatibleModel(
|
|
|
179
179
|
maxAttempt: 5,
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
if (response.status === 429 || response.status >= 500) {
|
|
182
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
183
183
|
console.error(
|
|
184
184
|
styleText(
|
|
185
185
|
"yellow",
|
|
@@ -236,18 +236,25 @@ export async function callOpenAICompatibleModel(
|
|
|
236
236
|
|
|
237
237
|
const chatCompletion = convertOpenAIStreamDataToChatCompletion(dataList);
|
|
238
238
|
if (chatCompletion instanceof Error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
if (retryCount < 5) {
|
|
240
|
+
console.error(
|
|
241
|
+
styleText(
|
|
242
|
+
"yellow",
|
|
243
|
+
`Failed to process stream: ${chatCompletion.message}; Retry in ${retryInterval} seconds...`,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
await new Promise((resolve) =>
|
|
247
|
+
setTimeout(resolve, retryInterval * 1000),
|
|
248
|
+
);
|
|
249
|
+
return callOpenAICompatibleModel(
|
|
250
|
+
platformConfig,
|
|
251
|
+
modelConfig,
|
|
252
|
+
input,
|
|
253
|
+
retryCount + 1,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to process OpenAI compatible stream after ${retryCount} retries: ${chatCompletion.message}`,
|
|
251
258
|
);
|
|
252
259
|
}
|
|
253
260
|
|
package/src/tool.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export type Tool = {
|
|
|
10
10
|
injectImpl?: (impl: ToolImplementation) => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type SandboxMode = "sandbox" | "unsandboxed" | null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implemented by tools that can report the sandbox mode for a given input.
|
|
17
|
+
* - `null` if the tool has no sandbox configuration
|
|
18
|
+
* - `"unsandboxed"` if a sandbox rule matched with `mode: "unsandboxed"`
|
|
19
|
+
* - `"sandbox"` otherwise (sandbox config exists, default execution is sandboxed)
|
|
20
|
+
* Used by the CLI to display an `[unsandboxed]` badge for tool calls that
|
|
21
|
+
* will execute outside the sandbox.
|
|
22
|
+
*/
|
|
23
|
+
export type SandboxModeProvider = {
|
|
24
|
+
getSandboxMode: (input: unknown) => SandboxMode;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
export type ToolDefinition = {
|
|
14
28
|
name: string;
|
|
15
29
|
description: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
2
|
+
* @import { Tool, SandboxModeProvider } from '../tool'
|
|
3
3
|
* @import { ExecCommandConfig, ExecCommandInput, ExecCommandSanboxConfig } from './execCommand'
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -13,10 +13,10 @@ const OUTPUT_TRUNCATED_LENGTH = 1024 * 2;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {ExecCommandConfig=} config
|
|
16
|
-
* @returns {Tool}
|
|
16
|
+
* @returns {Tool & SandboxModeProvider}
|
|
17
17
|
*/
|
|
18
18
|
export function createExecCommandTool(config) {
|
|
19
|
-
/** @type {Tool} */
|
|
19
|
+
/** @type {Tool & SandboxModeProvider} */
|
|
20
20
|
return {
|
|
21
21
|
def: {
|
|
22
22
|
name: "exec_command",
|
|
@@ -190,6 +190,21 @@ Examples:
|
|
|
190
190
|
child.stdin?.end();
|
|
191
191
|
});
|
|
192
192
|
}),
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Report the sandbox mode for a given tool input. Mirrors
|
|
196
|
+
* `rewriteInputForSandbox`'s rule-matching logic so the CLI can preview
|
|
197
|
+
* the mode before execution.
|
|
198
|
+
* @param {unknown} input
|
|
199
|
+
* @returns {"sandbox" | "unsandboxed" | null}
|
|
200
|
+
*/
|
|
201
|
+
getSandboxMode: (input) => {
|
|
202
|
+
if (!config?.sandbox) return null;
|
|
203
|
+
const matchedRule = (config.sandbox.rules || []).find((rule) =>
|
|
204
|
+
matchValue(/** @type {ExecCommandInput} */ (input), rule.pattern),
|
|
205
|
+
);
|
|
206
|
+
return matchedRule?.mode === "unsandboxed" ? "unsandboxed" : "sandbox";
|
|
207
|
+
},
|
|
193
208
|
};
|
|
194
209
|
}
|
|
195
210
|
|