@pikaa-ai/pikaa 0.3.28 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pikaa.js +5 -2
- package/dist/cli.js +941 -177
- package/dist/index.js +925 -166
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1356,6 +1356,7 @@ class DefaultModelClientSession {
|
|
|
1356
1356
|
systemMessage.cache_control = { type: "ephemeral" };
|
|
1357
1357
|
}
|
|
1358
1358
|
const messages = [systemMessage];
|
|
1359
|
+
const declaredToolCallIds = new Set;
|
|
1359
1360
|
for (let i = 0;i < params.history.length; i++) {
|
|
1360
1361
|
const item = params.history[i];
|
|
1361
1362
|
if (item.type === "user_message") {
|
|
@@ -1380,6 +1381,9 @@ class DefaultModelClientSession {
|
|
|
1380
1381
|
j++;
|
|
1381
1382
|
}
|
|
1382
1383
|
if (toolCalls.length > 0) {
|
|
1384
|
+
for (const tc of toolCalls) {
|
|
1385
|
+
declaredToolCallIds.add(tc.id);
|
|
1386
|
+
}
|
|
1383
1387
|
messages.push({
|
|
1384
1388
|
role: "assistant",
|
|
1385
1389
|
content: cleanedContent || null,
|
|
@@ -1416,6 +1420,9 @@ class DefaultModelClientSession {
|
|
|
1416
1420
|
});
|
|
1417
1421
|
j++;
|
|
1418
1422
|
}
|
|
1423
|
+
for (const tc of toolCalls) {
|
|
1424
|
+
declaredToolCallIds.add(tc.id);
|
|
1425
|
+
}
|
|
1419
1426
|
messages.push({
|
|
1420
1427
|
role: "assistant",
|
|
1421
1428
|
content: null,
|
|
@@ -1423,11 +1430,13 @@ class DefaultModelClientSession {
|
|
|
1423
1430
|
});
|
|
1424
1431
|
i = j - 1;
|
|
1425
1432
|
} else if (item.type === "function_call_output") {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1433
|
+
if (declaredToolCallIds.has(item.callId)) {
|
|
1434
|
+
messages.push({
|
|
1435
|
+
role: "tool",
|
|
1436
|
+
tool_call_id: item.callId,
|
|
1437
|
+
content: item.output
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1431
1440
|
}
|
|
1432
1441
|
}
|
|
1433
1442
|
const toolsPayload = typeof params.tools?.toModelToolsSchema === "function" ? params.tools.toModelToolsSchema() : typeof params.tools?.toOpenAISpec === "function" ? params.tools.toOpenAISpec() : Array.isArray(params.tools) ? params.tools : [];
|
|
@@ -1821,6 +1830,7 @@ var applyPatchTool = {
|
|
|
1821
1830
|
try {
|
|
1822
1831
|
mkdirSync3(dirname2(filePath), { recursive: true });
|
|
1823
1832
|
writeFileSync2(filePath, replacementContent, "utf8");
|
|
1833
|
+
ctx.onFileModified?.(rawPath);
|
|
1824
1834
|
return { output: `Successfully created new file '${rawPath}'` };
|
|
1825
1835
|
} catch (err) {
|
|
1826
1836
|
return {
|
|
@@ -1841,7 +1851,31 @@ var applyPatchTool = {
|
|
|
1841
1851
|
isError: true
|
|
1842
1852
|
};
|
|
1843
1853
|
}
|
|
1844
|
-
|
|
1854
|
+
let targetToFind = targetContent;
|
|
1855
|
+
let replacementToUse = replacementContent;
|
|
1856
|
+
const fileHasCrlf = originalFileContent.includes(`\r
|
|
1857
|
+
`);
|
|
1858
|
+
let firstIndex = originalFileContent.indexOf(targetToFind);
|
|
1859
|
+
if (firstIndex === -1 && fileHasCrlf) {
|
|
1860
|
+
const crlfTarget = targetContent.replace(/\r?\n/g, `\r
|
|
1861
|
+
`);
|
|
1862
|
+
firstIndex = originalFileContent.indexOf(crlfTarget);
|
|
1863
|
+
if (firstIndex !== -1) {
|
|
1864
|
+
targetToFind = crlfTarget;
|
|
1865
|
+
replacementToUse = replacementContent.replace(/\r?\n/g, `\r
|
|
1866
|
+
`);
|
|
1867
|
+
}
|
|
1868
|
+
} else if (firstIndex === -1 && !fileHasCrlf && targetContent.includes(`\r
|
|
1869
|
+
`)) {
|
|
1870
|
+
const lfTarget = targetContent.replace(/\r\n/g, `
|
|
1871
|
+
`);
|
|
1872
|
+
firstIndex = originalFileContent.indexOf(lfTarget);
|
|
1873
|
+
if (firstIndex !== -1) {
|
|
1874
|
+
targetToFind = lfTarget;
|
|
1875
|
+
replacementToUse = replacementContent.replace(/\r\n/g, `
|
|
1876
|
+
`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1845
1879
|
if (firstIndex === -1) {
|
|
1846
1880
|
return {
|
|
1847
1881
|
output: `Error: targetContent was not found in '${rawPath}'.
|
|
@@ -1852,7 +1886,7 @@ var applyPatchTool = {
|
|
|
1852
1886
|
isError: true
|
|
1853
1887
|
};
|
|
1854
1888
|
}
|
|
1855
|
-
const secondIndex = originalFileContent.indexOf(
|
|
1889
|
+
const secondIndex = originalFileContent.indexOf(targetToFind, firstIndex + 1);
|
|
1856
1890
|
if (secondIndex !== -1) {
|
|
1857
1891
|
return {
|
|
1858
1892
|
output: `Error: targetContent matched multiple locations in '${rawPath}'.
|
|
@@ -1863,8 +1897,9 @@ var applyPatchTool = {
|
|
|
1863
1897
|
isError: true
|
|
1864
1898
|
};
|
|
1865
1899
|
}
|
|
1866
|
-
const newFileContent = originalFileContent.slice(0, firstIndex) +
|
|
1900
|
+
const newFileContent = originalFileContent.slice(0, firstIndex) + replacementToUse + originalFileContent.slice(firstIndex + targetToFind.length);
|
|
1867
1901
|
writeFileSync2(filePath, newFileContent, "utf8");
|
|
1902
|
+
ctx.onFileModified?.(rawPath);
|
|
1868
1903
|
return {
|
|
1869
1904
|
output: `Successfully applied patch to '${rawPath}'`
|
|
1870
1905
|
};
|
|
@@ -1876,8 +1911,133 @@ var applyPatchTool = {
|
|
|
1876
1911
|
}
|
|
1877
1912
|
}
|
|
1878
1913
|
};
|
|
1914
|
+
// src/security/shell-parser.ts
|
|
1915
|
+
function parseShellCommand(commandLine) {
|
|
1916
|
+
const commands = [];
|
|
1917
|
+
const subshellCommands = [];
|
|
1918
|
+
let hasPipes = false;
|
|
1919
|
+
let currentSegment = "";
|
|
1920
|
+
let inSingleQuote = false;
|
|
1921
|
+
let inDoubleQuote = false;
|
|
1922
|
+
let isEscaped = false;
|
|
1923
|
+
const len = commandLine.length;
|
|
1924
|
+
for (let i = 0;i < len; i++) {
|
|
1925
|
+
const char = commandLine[i];
|
|
1926
|
+
if (isEscaped) {
|
|
1927
|
+
currentSegment += char;
|
|
1928
|
+
isEscaped = false;
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (char === "\\") {
|
|
1932
|
+
isEscaped = true;
|
|
1933
|
+
currentSegment += char;
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
if (char === "'" && !inDoubleQuote) {
|
|
1937
|
+
inSingleQuote = !inSingleQuote;
|
|
1938
|
+
currentSegment += char;
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
if (char === '"' && !inSingleQuote) {
|
|
1942
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1943
|
+
currentSegment += char;
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (inSingleQuote) {
|
|
1947
|
+
currentSegment += char;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (char === "`") {
|
|
1951
|
+
let endIdx = -1;
|
|
1952
|
+
for (let j = i + 1;j < len; j++) {
|
|
1953
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
1954
|
+
j++;
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
if (commandLine[j] === "`") {
|
|
1958
|
+
endIdx = j;
|
|
1959
|
+
break;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (endIdx !== -1) {
|
|
1963
|
+
const innerCmd = commandLine.slice(i + 1, endIdx);
|
|
1964
|
+
if (innerCmd.trim()) {
|
|
1965
|
+
subshellCommands.push(innerCmd.trim());
|
|
1966
|
+
}
|
|
1967
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
1968
|
+
i = endIdx;
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (char === "$" && i + 1 < len && commandLine[i + 1] === "(") {
|
|
1973
|
+
let depth = 1;
|
|
1974
|
+
let endIdx = -1;
|
|
1975
|
+
for (let j = i + 2;j < len; j++) {
|
|
1976
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
1977
|
+
j++;
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (commandLine[j] === "(")
|
|
1981
|
+
depth++;
|
|
1982
|
+
else if (commandLine[j] === ")") {
|
|
1983
|
+
depth--;
|
|
1984
|
+
if (depth === 0) {
|
|
1985
|
+
endIdx = j;
|
|
1986
|
+
break;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (endIdx !== -1) {
|
|
1991
|
+
const innerCmd = commandLine.slice(i + 2, endIdx);
|
|
1992
|
+
if (innerCmd.trim()) {
|
|
1993
|
+
subshellCommands.push(innerCmd.trim());
|
|
1994
|
+
}
|
|
1995
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
1996
|
+
i = endIdx;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (!inDoubleQuote) {
|
|
2001
|
+
if (char === "&" && commandLine[i + 1] === "&" || char === "|" && commandLine[i + 1] === "|") {
|
|
2002
|
+
if (currentSegment.trim()) {
|
|
2003
|
+
commands.push(currentSegment.trim());
|
|
2004
|
+
}
|
|
2005
|
+
currentSegment = "";
|
|
2006
|
+
i++;
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
if (char === "|") {
|
|
2010
|
+
hasPipes = true;
|
|
2011
|
+
if (currentSegment.trim()) {
|
|
2012
|
+
commands.push(currentSegment.trim());
|
|
2013
|
+
}
|
|
2014
|
+
currentSegment = "";
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
if (char === ";" || char === `
|
|
2018
|
+
` || char === "&") {
|
|
2019
|
+
if (currentSegment.trim()) {
|
|
2020
|
+
commands.push(currentSegment.trim());
|
|
2021
|
+
}
|
|
2022
|
+
currentSegment = "";
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
currentSegment += char;
|
|
2027
|
+
}
|
|
2028
|
+
if (currentSegment.trim()) {
|
|
2029
|
+
commands.push(currentSegment.trim());
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
commands: commands.filter(Boolean),
|
|
2033
|
+
subshellCommands: subshellCommands.filter(Boolean),
|
|
2034
|
+
hasPipes
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1879
2038
|
// src/security/exec-policy.ts
|
|
1880
2039
|
class ExecPolicy {
|
|
2040
|
+
denyRules = [];
|
|
1881
2041
|
rules = [];
|
|
1882
2042
|
mode = "auto";
|
|
1883
2043
|
constructor(initialMode = "auto") {
|
|
@@ -1891,12 +2051,24 @@ class ExecPolicy {
|
|
|
1891
2051
|
this.mode = mode;
|
|
1892
2052
|
}
|
|
1893
2053
|
initDefaultRules() {
|
|
1894
|
-
this.
|
|
1895
|
-
this.
|
|
1896
|
-
this.
|
|
1897
|
-
this.addRule(/^(
|
|
1898
|
-
this.addRule(/^(
|
|
1899
|
-
this.addRule(/^(
|
|
2054
|
+
this.addDenyRule(/(?:^|[/\\])(mkfs|format|fdisk|parted)\b/i, "Destructive filesystem formatting operation");
|
|
2055
|
+
this.addDenyRule(/^dd\s+.*(of=\/dev\/|\/dev\/sd|\/dev\/nvme)/i, "Raw block device write attempt");
|
|
2056
|
+
this.addDenyRule(/(?:^|[/\\])(reboot|shutdown|poweroff|init\s+0)\b/i, "System power manipulation");
|
|
2057
|
+
this.addRule(/^(sudo|su|doas|runas)\b/i, "prompt", "Privilege escalation attempt");
|
|
2058
|
+
this.addRule(/^(rm|del|rmdir|shred|unlink)\b/i, "prompt", "Destructive file removal");
|
|
2059
|
+
this.addRule(/^(chmod|chown|kill|pkill|killall|systemctl|service|crontab)\b/i, "prompt", "System administration & process control");
|
|
2060
|
+
this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase|branch\s+-D|checkout\s+-f))\b/i, "prompt", "Destructive git operation");
|
|
2061
|
+
this.addRule(/^(curl|wget|fetch|ssh|scp|sftp|ftp|rsync|nc|ncat|netcat|socat|telnet)\b/i, "prompt", "Network & remote transfer");
|
|
2062
|
+
this.addRule(/^(bash|sh|zsh|dash|ksh|cmd(\.exe)?|powershell(\.exe)?|pwsh)\s+(-c|-command|\/c)\b/i, "prompt", "Arbitrary subshell command execution");
|
|
2063
|
+
this.addRule(/^(python|python3|node|bun|perl|ruby)\s+(-c|-e)\b/i, "prompt", "Inline arbitrary code evaluation");
|
|
2064
|
+
this.addRule(/^(eval|exec)\b/i, "prompt", "Dynamic code execution");
|
|
2065
|
+
this.addRule(/^(npm\s+publish|bun\s+publish|cargo\s+publish)\b/i, "prompt", "Package registry publication");
|
|
2066
|
+
this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote|describe))\b/i, "allow", "Safe git query");
|
|
2067
|
+
this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where|stat|file|du|df)\b/i, "allow", "Safe read-only shell command");
|
|
2068
|
+
this.addRule(/^(bun\s+(test|run|--version|-v)|npm\s+(test|run|--version|-v)|npx\s+(tsc|eslint|oxlint)|tsc|cargo\s+(check|test|build)|go\s+(test|vet|build)|pytest|python\s+-m\s+unittest|node\s+(-v|--version|--test))\b/i, "allow", "Testing, typechecking & build verification");
|
|
2069
|
+
}
|
|
2070
|
+
addDenyRule(pattern, description) {
|
|
2071
|
+
this.denyRules.push({ pattern, decision: "deny", description });
|
|
1900
2072
|
}
|
|
1901
2073
|
addRule(pattern, decision, description) {
|
|
1902
2074
|
this.rules.unshift({ pattern, decision, description });
|
|
@@ -1919,8 +2091,49 @@ class ExecPolicy {
|
|
|
1919
2091
|
}
|
|
1920
2092
|
evaluate(command) {
|
|
1921
2093
|
const trimmed = command.trim();
|
|
2094
|
+
if (!trimmed) {
|
|
2095
|
+
return { decision: "allow" };
|
|
2096
|
+
}
|
|
2097
|
+
const parsed = parseShellCommand(trimmed);
|
|
2098
|
+
const subCommands = [...parsed.commands, ...parsed.subshellCommands].map((c) => c.trim()).filter(Boolean);
|
|
2099
|
+
if (subCommands.length > 1 || parsed.subshellCommands.length > 0) {
|
|
2100
|
+
for (const subCmd of subCommands) {
|
|
2101
|
+
const subResult = this.evaluateSingle(subCmd);
|
|
2102
|
+
if (subResult.decision === "deny") {
|
|
2103
|
+
return {
|
|
2104
|
+
decision: "deny",
|
|
2105
|
+
reason: `Chained command contains denied operation: '${subCmd}' (${subResult.reason || "Forbidden"})`
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
if (subResult.decision === "prompt") {
|
|
2109
|
+
return {
|
|
2110
|
+
decision: "prompt",
|
|
2111
|
+
reason: `Chained command contains operation requiring confirmation: '${subCmd}' (${subResult.reason || "Requires approval"})`
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (parsed.hasPipes && /\|\s*(ba|z|k|c)?sh\b/i.test(trimmed)) {
|
|
2116
|
+
return {
|
|
2117
|
+
decision: "prompt",
|
|
2118
|
+
reason: "Pipeline executes piped input directly into shell interpreter (| sh)"
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
return { decision: "allow", reason: "All chained sub-commands are permitted" };
|
|
2122
|
+
}
|
|
2123
|
+
return this.evaluateSingle(trimmed);
|
|
2124
|
+
}
|
|
2125
|
+
evaluateSingle(command) {
|
|
2126
|
+
const trimmed = command.trim();
|
|
2127
|
+
for (const rule of this.denyRules) {
|
|
2128
|
+
if (rule.pattern.test(trimmed)) {
|
|
2129
|
+
return {
|
|
2130
|
+
decision: "deny",
|
|
2131
|
+
reason: rule.description
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
1922
2135
|
if (this.mode === "plan") {
|
|
1923
|
-
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
|
|
2136
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
|
|
1924
2137
|
if (isReadOnly) {
|
|
1925
2138
|
return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
|
|
1926
2139
|
}
|
|
@@ -1936,7 +2149,7 @@ class ExecPolicy {
|
|
|
1936
2149
|
};
|
|
1937
2150
|
}
|
|
1938
2151
|
if (this.mode === "accept-edits") {
|
|
1939
|
-
const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
|
|
2152
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
|
|
1940
2153
|
if (isReadOnly) {
|
|
1941
2154
|
return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
|
|
1942
2155
|
}
|
|
@@ -1953,11 +2166,21 @@ class ExecPolicy {
|
|
|
1953
2166
|
};
|
|
1954
2167
|
}
|
|
1955
2168
|
}
|
|
2169
|
+
if (this.isRecognizedSafeDevCommand(trimmed)) {
|
|
2170
|
+
return {
|
|
2171
|
+
decision: "allow",
|
|
2172
|
+
reason: "Safe development workspace command"
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
const firstWord = trimmed.split(/\s+/)[0] || trimmed;
|
|
1956
2176
|
return {
|
|
1957
|
-
decision: "
|
|
1958
|
-
reason:
|
|
2177
|
+
decision: "prompt",
|
|
2178
|
+
reason: `Command '${firstWord}' is unclassified and requires confirmation in auto mode`
|
|
1959
2179
|
};
|
|
1960
2180
|
}
|
|
2181
|
+
isRecognizedSafeDevCommand(command) {
|
|
2182
|
+
return /^(git\s+(checkout|add|commit|stash|merge|pull|init)|bun\s+(install|add|remove)|npm\s+(install|i|add|remove)|yarn\s+(add|remove)|pnpm\s+(add|remove|install)|mkdir|touch|cp|copy|mv|move|clear|cls|echo|printf|node|bun|python|python3|cargo|go)\b/i.test(command);
|
|
2183
|
+
}
|
|
1961
2184
|
}
|
|
1962
2185
|
|
|
1963
2186
|
// src/security/kernel/windows.ts
|
|
@@ -2060,8 +2283,12 @@ class LinuxSandbox {
|
|
|
2060
2283
|
}
|
|
2061
2284
|
this.hasBwrap = existsSync4("/usr/bin/bwrap") || existsSync4("/bin/bwrap") || existsSync4("/usr/local/bin/bwrap");
|
|
2062
2285
|
}
|
|
2063
|
-
wrapCommand(cmd, profile) {
|
|
2064
|
-
if (
|
|
2286
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
2287
|
+
if (profile.kind === "danger-unrestricted") {
|
|
2288
|
+
return cmd;
|
|
2289
|
+
}
|
|
2290
|
+
if (!this.hasBwrap) {
|
|
2291
|
+
onWarning?.("Linux kernel sandbox (Bubblewrap / bwrap) is not available. Command will execute without OS-level namespace isolation.");
|
|
2065
2292
|
return cmd;
|
|
2066
2293
|
}
|
|
2067
2294
|
const bwrapArgs = [
|
|
@@ -2077,6 +2304,23 @@ class LinuxSandbox {
|
|
|
2077
2304
|
"--tmpfs",
|
|
2078
2305
|
"/tmp"
|
|
2079
2306
|
];
|
|
2307
|
+
const homeDir = process.env.HOME || "/root";
|
|
2308
|
+
const sensitivePaths = [
|
|
2309
|
+
`${homeDir}/.ssh`,
|
|
2310
|
+
`${homeDir}/.aws`,
|
|
2311
|
+
`${homeDir}/.gnupg`,
|
|
2312
|
+
`${homeDir}/.azure`,
|
|
2313
|
+
`${homeDir}/.kube`,
|
|
2314
|
+
`${homeDir}/.docker`,
|
|
2315
|
+
`${homeDir}/.netrc`,
|
|
2316
|
+
"/etc/shadow",
|
|
2317
|
+
"/etc/sudoers"
|
|
2318
|
+
];
|
|
2319
|
+
for (const sensitive of sensitivePaths) {
|
|
2320
|
+
if (existsSync4(sensitive)) {
|
|
2321
|
+
bwrapArgs.push("--tmpfs", sensitive);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2080
2324
|
for (const writableRoot of profile.writableRoots) {
|
|
2081
2325
|
bwrapArgs.push("--bind", writableRoot, writableRoot);
|
|
2082
2326
|
}
|
|
@@ -2126,11 +2370,21 @@ class MacOSSandbox {
|
|
|
2126
2370
|
} else {
|
|
2127
2371
|
rules.push("(deny network*)");
|
|
2128
2372
|
}
|
|
2373
|
+
const home = process.env.HOME || "/Users/Shared";
|
|
2374
|
+
rules.push(`(deny file-read* (subpath "${home}/.ssh"))`);
|
|
2375
|
+
rules.push(`(deny file-read* (subpath "${home}/.aws"))`);
|
|
2376
|
+
rules.push(`(deny file-read* (subpath "${home}/.gnupg"))`);
|
|
2377
|
+
rules.push(`(deny file-read* (subpath "${home}/.kube"))`);
|
|
2378
|
+
rules.push(`(deny file-read* (subpath "${home}/.docker"))`);
|
|
2129
2379
|
return rules.join(`
|
|
2130
2380
|
`);
|
|
2131
2381
|
}
|
|
2132
|
-
wrapCommand(cmd, profile) {
|
|
2133
|
-
if (
|
|
2382
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
2383
|
+
if (profile.kind === "danger-unrestricted") {
|
|
2384
|
+
return cmd;
|
|
2385
|
+
}
|
|
2386
|
+
if (!this.hasSandboxExec) {
|
|
2387
|
+
onWarning?.("macOS Seatbelt (sandbox-exec) is not available. Command will execute without OS-level namespace isolation.");
|
|
2134
2388
|
return cmd;
|
|
2135
2389
|
}
|
|
2136
2390
|
const policy = this.generateProfile(profile);
|
|
@@ -2163,7 +2417,7 @@ class KernelSandboxManager {
|
|
|
2163
2417
|
isSandboxingActive: this.windowsSandbox.isSupported() || this.linuxSandbox.isSupported() || this.macOsSandbox.isSupported()
|
|
2164
2418
|
};
|
|
2165
2419
|
}
|
|
2166
|
-
buildDefaultProfile(cwd, allowNetwork =
|
|
2420
|
+
buildDefaultProfile(cwd, allowNetwork = false) {
|
|
2167
2421
|
const normCwd = normalize(resolve4(cwd));
|
|
2168
2422
|
return {
|
|
2169
2423
|
kind: "workspace-write",
|
|
@@ -2178,11 +2432,15 @@ class KernelSandboxManager {
|
|
|
2178
2432
|
}
|
|
2179
2433
|
};
|
|
2180
2434
|
}
|
|
2181
|
-
wrapCommand(cmd, profile) {
|
|
2435
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
2182
2436
|
if (process.platform === "linux") {
|
|
2183
|
-
return this.linuxSandbox.wrapCommand(cmd, profile);
|
|
2437
|
+
return this.linuxSandbox.wrapCommand(cmd, profile, onWarning);
|
|
2184
2438
|
} else if (process.platform === "darwin") {
|
|
2185
|
-
return this.macOsSandbox.wrapCommand(cmd, profile);
|
|
2439
|
+
return this.macOsSandbox.wrapCommand(cmd, profile, onWarning);
|
|
2440
|
+
} else if (process.platform === "win32") {
|
|
2441
|
+
if (!this.windowsSandbox.isSupported() && profile.kind !== "danger-unrestricted") {
|
|
2442
|
+
onWarning?.("Windows JobObject isolation is not available in this environment. Command will execute without OS-level process limits.");
|
|
2443
|
+
}
|
|
2186
2444
|
}
|
|
2187
2445
|
return cmd;
|
|
2188
2446
|
}
|
|
@@ -2314,6 +2572,12 @@ class PrefixRulesStore {
|
|
|
2314
2572
|
matchesPrefix(cmdTokens, prefixTokens) {
|
|
2315
2573
|
if (prefixTokens.length > cmdTokens.length)
|
|
2316
2574
|
return false;
|
|
2575
|
+
const SHELL_OPERATORS = new Set([";", "&&", "||", "|", "&", ";;", "&|"]);
|
|
2576
|
+
for (const token of cmdTokens) {
|
|
2577
|
+
if (SHELL_OPERATORS.has(token) || token.includes(";") || token.includes("&&") || token.includes("||")) {
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2317
2581
|
for (let i = 0;i < prefixTokens.length; i++) {
|
|
2318
2582
|
const cmd = cmdTokens[i];
|
|
2319
2583
|
const prefix = prefixTokens[i];
|
|
@@ -2439,11 +2703,29 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2439
2703
|
}
|
|
2440
2704
|
const args = rawArgs;
|
|
2441
2705
|
const rulesStore = ctx.prefixRulesStore || globalPrefixRulesStore;
|
|
2442
|
-
const
|
|
2706
|
+
const activePolicy = ctx.execPolicy || policy;
|
|
2707
|
+
const policyDecision = activePolicy.evaluate(command);
|
|
2708
|
+
if (policyDecision.decision === "deny") {
|
|
2709
|
+
return {
|
|
2710
|
+
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
2711
|
+
isError: true
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
const parsed = parseShellCommand(command);
|
|
2715
|
+
const isCompound = parsed.commands.length > 1 || parsed.subshellCommands.length > 0 || parsed.hasPipes;
|
|
2443
2716
|
let isEscalated = false;
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2446
|
-
|
|
2717
|
+
if (!isCompound) {
|
|
2718
|
+
const cmdTokens = command.split(/\s+/).filter(Boolean);
|
|
2719
|
+
if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
|
|
2720
|
+
isEscalated = true;
|
|
2721
|
+
}
|
|
2722
|
+
} else {
|
|
2723
|
+
const allSubCommands = [...parsed.commands, ...parsed.subshellCommands];
|
|
2724
|
+
if (allSubCommands.length > 0 && allSubCommands.every((sub) => rulesStore.isApproved(ctx.cwd, sub.split(/\s+/).filter(Boolean)))) {
|
|
2725
|
+
isEscalated = true;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (!isEscalated && args.sandbox_permissions === "require_escalated") {
|
|
2447
2729
|
if (ctx.requestApproval) {
|
|
2448
2730
|
const promptDesc = args.justification || "Executing command with escalated permissions";
|
|
2449
2731
|
const approvalResult = await ctx.requestApproval(promptDesc, command, args.prefix_rule);
|
|
@@ -2461,16 +2743,8 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2461
2743
|
isEscalated = true;
|
|
2462
2744
|
}
|
|
2463
2745
|
}
|
|
2464
|
-
if (!isEscalated) {
|
|
2465
|
-
|
|
2466
|
-
const policyDecision = activePolicy.evaluate(command);
|
|
2467
|
-
if (policyDecision.decision === "deny") {
|
|
2468
|
-
return {
|
|
2469
|
-
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
2470
|
-
isError: true
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
if (policyDecision.decision === "prompt" && ctx.requestApproval) {
|
|
2746
|
+
if (policyDecision.decision === "prompt" && !isEscalated) {
|
|
2747
|
+
if (ctx.requestApproval) {
|
|
2474
2748
|
const approvalResult = await ctx.requestApproval(policyDecision.reason || "Executing external command", command);
|
|
2475
2749
|
const isAllowed = typeof approvalResult === "boolean" ? approvalResult : approvalResult?.allowed;
|
|
2476
2750
|
if (!isAllowed) {
|
|
@@ -2479,17 +2753,22 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2479
2753
|
isError: true
|
|
2480
2754
|
};
|
|
2481
2755
|
}
|
|
2756
|
+
} else {
|
|
2757
|
+
return {
|
|
2758
|
+
output: `Error: Command execution requires user confirmation: '${command}' (${policyDecision.reason || "Requires approval"})`,
|
|
2759
|
+
isError: true
|
|
2760
|
+
};
|
|
2482
2761
|
}
|
|
2483
2762
|
}
|
|
2484
2763
|
const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : 30000;
|
|
2485
2764
|
const isWindows = process.platform === "win32";
|
|
2486
2765
|
const baseCmd = isWindows ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command];
|
|
2487
2766
|
const ephemeralScratchpad = globalEphemeralWorkspace.createScratchpad(ctx.turnId);
|
|
2488
|
-
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd);
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2767
|
+
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd, isEscalated);
|
|
2768
|
+
let sandboxNotice = null;
|
|
2769
|
+
const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile, (w) => {
|
|
2770
|
+
sandboxNotice = w;
|
|
2771
|
+
});
|
|
2493
2772
|
try {
|
|
2494
2773
|
const proc = Bun.spawn(wrappedCmd, {
|
|
2495
2774
|
cwd: ctx.cwd,
|
|
@@ -2543,10 +2822,38 @@ ${result.stderr.trim()}`);
|
|
|
2543
2822
|
2. If this is a test failure, trace the failure in source code and fix the root cause before re-running.
|
|
2544
2823
|
3. If this is a missing command/module, install or configure the prerequisite.`);
|
|
2545
2824
|
}
|
|
2546
|
-
|
|
2825
|
+
let rawOutput = outputParts.join(`
|
|
2547
2826
|
`) || "[Command completed with no output]";
|
|
2827
|
+
const lines = rawOutput.split(`
|
|
2828
|
+
`);
|
|
2829
|
+
const MAX_LINES = 250;
|
|
2830
|
+
const MAX_CHARS = 30000;
|
|
2831
|
+
if (lines.length > MAX_LINES) {
|
|
2832
|
+
const headLines = lines.slice(0, 125).join(`
|
|
2833
|
+
`);
|
|
2834
|
+
const tailLines = lines.slice(-100).join(`
|
|
2835
|
+
`);
|
|
2836
|
+
rawOutput = `${headLines}
|
|
2837
|
+
|
|
2838
|
+
... [${lines.length - 225} lines truncated for context efficiency] ...
|
|
2839
|
+
|
|
2840
|
+
${tailLines}`;
|
|
2841
|
+
}
|
|
2842
|
+
if (rawOutput.length > MAX_CHARS) {
|
|
2843
|
+
const headChars = rawOutput.slice(0, 16000);
|
|
2844
|
+
const tailChars = rawOutput.slice(-12000);
|
|
2845
|
+
rawOutput = `${headChars}
|
|
2846
|
+
|
|
2847
|
+
... [${rawOutput.length - 28000} characters truncated for context efficiency] ...
|
|
2848
|
+
|
|
2849
|
+
${tailChars}`;
|
|
2850
|
+
}
|
|
2851
|
+
if (sandboxNotice) {
|
|
2852
|
+
rawOutput = `[Sandbox Notice]: ${sandboxNotice}
|
|
2853
|
+
${rawOutput}`;
|
|
2854
|
+
}
|
|
2548
2855
|
return {
|
|
2549
|
-
output,
|
|
2856
|
+
output: rawOutput,
|
|
2550
2857
|
isError: result.code !== 0
|
|
2551
2858
|
};
|
|
2552
2859
|
} catch (err) {
|
|
@@ -2565,7 +2872,7 @@ ${result.stderr.trim()}`);
|
|
|
2565
2872
|
}
|
|
2566
2873
|
var shellTool = createShellTool();
|
|
2567
2874
|
// src/tools/handlers/file-ops.ts
|
|
2568
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync as statSync3, mkdirSync as mkdirSync6 } from "fs";
|
|
2875
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync as statSync3, lstatSync, mkdirSync as mkdirSync6 } from "fs";
|
|
2569
2876
|
import { resolve as resolve6, dirname as dirname4 } from "path";
|
|
2570
2877
|
var DEFAULT_MAX_UNPAGINATED_LINES = 250;
|
|
2571
2878
|
var readFileTool = {
|
|
@@ -2677,6 +2984,7 @@ var viewFileTool = {
|
|
|
2677
2984
|
name: "view_file",
|
|
2678
2985
|
description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
|
|
2679
2986
|
};
|
|
2987
|
+
var DEFAULT_MAX_DIR_ENTRIES = 500;
|
|
2680
2988
|
var listDirTool = {
|
|
2681
2989
|
name: "list_dir",
|
|
2682
2990
|
description: "List contents of a directory with file names and types.",
|
|
@@ -2693,13 +3001,31 @@ var listDirTool = {
|
|
|
2693
3001
|
}
|
|
2694
3002
|
try {
|
|
2695
3003
|
const entries = readdirSync3(dirPath);
|
|
2696
|
-
const
|
|
3004
|
+
const totalEntries = entries.length;
|
|
3005
|
+
const displayEntries = entries.slice(0, DEFAULT_MAX_DIR_ENTRIES);
|
|
3006
|
+
const formatted = displayEntries.map((entry) => {
|
|
2697
3007
|
const full = resolve6(dirPath, entry);
|
|
2698
|
-
|
|
3008
|
+
let isDir = false;
|
|
3009
|
+
try {
|
|
3010
|
+
isDir = statSync3(full).isDirectory();
|
|
3011
|
+
} catch {
|
|
3012
|
+
try {
|
|
3013
|
+
const lst = lstatSync(full);
|
|
3014
|
+
if (lst.isSymbolicLink())
|
|
3015
|
+
return `[SYMLINK] ${entry}`;
|
|
3016
|
+
} catch {}
|
|
3017
|
+
return `[FILE] ${entry}`;
|
|
3018
|
+
}
|
|
2699
3019
|
return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
|
|
2700
3020
|
});
|
|
2701
|
-
|
|
2702
|
-
`) || "[Empty directory]"
|
|
3021
|
+
let output = formatted.join(`
|
|
3022
|
+
`) || "[Empty directory]";
|
|
3023
|
+
if (totalEntries > DEFAULT_MAX_DIR_ENTRIES) {
|
|
3024
|
+
output += `
|
|
3025
|
+
|
|
3026
|
+
... [Truncated: ${totalEntries - DEFAULT_MAX_DIR_ENTRIES} more entries. Showing first ${DEFAULT_MAX_DIR_ENTRIES} of ${totalEntries}]`;
|
|
3027
|
+
}
|
|
3028
|
+
return { output };
|
|
2703
3029
|
} catch (err) {
|
|
2704
3030
|
return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
2705
3031
|
}
|
|
@@ -2740,6 +3066,7 @@ var writeFileTool = {
|
|
|
2740
3066
|
try {
|
|
2741
3067
|
mkdirSync6(dirname4(filePath), { recursive: true });
|
|
2742
3068
|
writeFileSync3(filePath, String(args.content ?? ""), "utf8");
|
|
3069
|
+
ctx.onFileModified?.(rawPath);
|
|
2743
3070
|
return { output: `Successfully wrote to '${args.path}'` };
|
|
2744
3071
|
} catch (err) {
|
|
2745
3072
|
return {
|
|
@@ -3233,6 +3560,12 @@ class CodeModeToolsProxy {
|
|
|
3233
3560
|
if (result.isError) {
|
|
3234
3561
|
throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
|
|
3235
3562
|
}
|
|
3563
|
+
if (normalizedName === "write_file" || normalizedName === "apply_patch" || tool.name === "write_file" || tool.name === "apply_patch") {
|
|
3564
|
+
const pathArg = String(args?.path || "");
|
|
3565
|
+
if (pathArg) {
|
|
3566
|
+
this.context.onFileModified?.(pathArg);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3236
3569
|
return result.output;
|
|
3237
3570
|
};
|
|
3238
3571
|
}
|
|
@@ -3306,6 +3639,9 @@ class SandboxedWorkerHost {
|
|
|
3306
3639
|
const fetch = undefined;
|
|
3307
3640
|
const XMLHttpRequest = undefined;
|
|
3308
3641
|
const WebSocket = undefined;
|
|
3642
|
+
const globalThis = Object.freeze(Object.create(null));
|
|
3643
|
+
const global = undefined;
|
|
3644
|
+
const window = undefined;
|
|
3309
3645
|
|
|
3310
3646
|
return (async () => {
|
|
3311
3647
|
${cleanCode}
|
|
@@ -4316,14 +4652,39 @@ function compactHistory(history, retainedRecentItems = 6) {
|
|
|
4316
4652
|
if (history.length <= retainedRecentItems) {
|
|
4317
4653
|
return [...history];
|
|
4318
4654
|
}
|
|
4319
|
-
const
|
|
4320
|
-
|
|
4655
|
+
const targetCutoff = history.length - retainedRecentItems;
|
|
4656
|
+
let bestCutoff = -1;
|
|
4657
|
+
for (let i = targetCutoff;i >= 0; i--) {
|
|
4658
|
+
if (history[i]?.type === "user_message") {
|
|
4659
|
+
bestCutoff = i;
|
|
4660
|
+
break;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
if (bestCutoff <= 0) {
|
|
4664
|
+
for (let i = targetCutoff + 1;i < history.length; i++) {
|
|
4665
|
+
if (history[i]?.type === "user_message") {
|
|
4666
|
+
bestCutoff = i;
|
|
4667
|
+
break;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
let cutoffIndex = bestCutoff > 0 ? bestCutoff : targetCutoff;
|
|
4672
|
+
while (cutoffIndex < history.length && history[cutoffIndex]?.type === "function_call_output") {
|
|
4673
|
+
cutoffIndex++;
|
|
4674
|
+
}
|
|
4675
|
+
if (cutoffIndex <= 0 || cutoffIndex >= history.length) {
|
|
4676
|
+
return [...history];
|
|
4677
|
+
}
|
|
4678
|
+
const itemsToCompact = history.slice(0, cutoffIndex);
|
|
4679
|
+
const recentItems = history.slice(cutoffIndex);
|
|
4321
4680
|
const summaryParts = ["### Summary of previous conversation context:"];
|
|
4322
4681
|
for (const item of itemsToCompact) {
|
|
4323
4682
|
if (item.type === "user_message") {
|
|
4324
4683
|
summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
|
|
4325
4684
|
} else if (item.type === "function_call") {
|
|
4326
4685
|
summaryParts.push(`- Executed tool: ${item.name}`);
|
|
4686
|
+
} else if (item.type === "function_call_output") {
|
|
4687
|
+
summaryParts.push(` Tool output: ${item.output.slice(0, 100)}`);
|
|
4327
4688
|
} else if (item.type === "agent_message") {
|
|
4328
4689
|
summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
|
|
4329
4690
|
}
|
|
@@ -4363,7 +4724,7 @@ class TurnContext {
|
|
|
4363
4724
|
}
|
|
4364
4725
|
}
|
|
4365
4726
|
// src/verification/verifier.ts
|
|
4366
|
-
import { spawnSync } from "child_process";
|
|
4727
|
+
import { spawn, spawnSync } from "child_process";
|
|
4367
4728
|
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
4368
4729
|
import { join as join8 } from "path";
|
|
4369
4730
|
|
|
@@ -4740,36 +5101,120 @@ class AutoVerifier {
|
|
|
4740
5101
|
this.customCommand = options.customCommand;
|
|
4741
5102
|
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
4742
5103
|
}
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
5104
|
+
findTargetedTests(modifiedFiles = []) {
|
|
5105
|
+
const matchedTests = new Set;
|
|
5106
|
+
for (const rawFile of modifiedFiles) {
|
|
5107
|
+
const normalized = rawFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5108
|
+
if (/\.(test|spec)\.[jt]sx?$/i.test(normalized) || /(^|\/)test_[^/]+\.py$/i.test(normalized) || /_test\.py$/i.test(normalized) || /_test\.go$/i.test(normalized)) {
|
|
5109
|
+
if (existsSync14(join8(this.cwd, normalized))) {
|
|
5110
|
+
matchedTests.add(normalized);
|
|
5111
|
+
}
|
|
5112
|
+
continue;
|
|
5113
|
+
}
|
|
5114
|
+
const extMatch = normalized.match(/\.[^.]+$/);
|
|
5115
|
+
if (!extMatch)
|
|
5116
|
+
continue;
|
|
5117
|
+
const withoutExt = normalized.slice(0, -extMatch[0].length);
|
|
5118
|
+
const parts = withoutExt.split("/");
|
|
5119
|
+
const baseName = parts[parts.length - 1];
|
|
5120
|
+
const subPath = normalized.startsWith("src/") ? normalized.slice(4, -extMatch[0].length) : normalized.startsWith("lib/") ? normalized.slice(4, -extMatch[0].length) : withoutExt;
|
|
5121
|
+
const candidatePaths = [
|
|
5122
|
+
`tests/${subPath}.test.ts`,
|
|
5123
|
+
`tests/${subPath}.test.js`,
|
|
5124
|
+
`tests/${subPath}.test.tsx`,
|
|
5125
|
+
`tests/${subPath}.spec.ts`,
|
|
5126
|
+
`tests/${subPath}.spec.js`,
|
|
5127
|
+
`tests/${baseName}.test.ts`,
|
|
5128
|
+
`tests/${baseName}.test.js`,
|
|
5129
|
+
`tests/${baseName}.spec.ts`,
|
|
5130
|
+
`test/${subPath}.test.ts`,
|
|
5131
|
+
`test/${subPath}.test.js`,
|
|
5132
|
+
`test/${baseName}.test.ts`,
|
|
5133
|
+
`test/${baseName}.test.js`,
|
|
5134
|
+
`src/${subPath}.test.ts`,
|
|
5135
|
+
`src/${subPath}.spec.ts`,
|
|
5136
|
+
`${withoutExt}.test.ts`,
|
|
5137
|
+
`${withoutExt}.spec.ts`,
|
|
5138
|
+
`tests/test_${baseName}.py`,
|
|
5139
|
+
`test_${baseName}.py`,
|
|
5140
|
+
`tests/${baseName}_test.go`
|
|
5141
|
+
];
|
|
5142
|
+
for (const candidate of candidatePaths) {
|
|
5143
|
+
if (existsSync14(join8(this.cwd, candidate))) {
|
|
5144
|
+
matchedTests.add(candidate);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return Array.from(matchedTests);
|
|
5149
|
+
}
|
|
5150
|
+
resolveTargetedTestCommand(testFiles) {
|
|
5151
|
+
if (testFiles.length === 0)
|
|
5152
|
+
return null;
|
|
5153
|
+
const quotedFiles = testFiles.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ");
|
|
5154
|
+
if (existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock"))) {
|
|
5155
|
+
return `bun test ${quotedFiles}`;
|
|
5156
|
+
}
|
|
5157
|
+
const pkgPath = join8(this.cwd, "package.json");
|
|
5158
|
+
if (existsSync14(pkgPath)) {
|
|
5159
|
+
try {
|
|
5160
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
5161
|
+
const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
5162
|
+
if (allDeps.vitest) {
|
|
5163
|
+
return `npx vitest run ${quotedFiles}`;
|
|
5164
|
+
}
|
|
5165
|
+
if (allDeps.jest) {
|
|
5166
|
+
return `npx jest ${quotedFiles}`;
|
|
5167
|
+
}
|
|
5168
|
+
const pm = existsSync14(join8(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync14(join8(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5169
|
+
if (pkg.scripts?.test) {
|
|
5170
|
+
if (pkg.scripts.test.includes("bun test")) {
|
|
5171
|
+
return `bun test ${quotedFiles}`;
|
|
5172
|
+
}
|
|
5173
|
+
return `${pm} test -- ${quotedFiles}`;
|
|
5174
|
+
}
|
|
5175
|
+
} catch (err) {
|
|
5176
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for test runner:`, err);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
if (existsSync14(join8(this.cwd, "pyproject.toml")) || existsSync14(join8(this.cwd, "requirements.txt"))) {
|
|
5180
|
+
if (existsSync14(join8(this.cwd, "uv.lock"))) {
|
|
5181
|
+
return `uv run pytest ${quotedFiles}`;
|
|
5182
|
+
}
|
|
5183
|
+
return `pytest ${quotedFiles}`;
|
|
4746
5184
|
}
|
|
5185
|
+
if (existsSync14(join8(this.cwd, "go.mod"))) {
|
|
5186
|
+
return `go test ${quotedFiles}`;
|
|
5187
|
+
}
|
|
5188
|
+
if (existsSync14(join8(this.cwd, "Cargo.toml"))) {
|
|
5189
|
+
return `cargo test ${quotedFiles}`;
|
|
5190
|
+
}
|
|
5191
|
+
return null;
|
|
5192
|
+
}
|
|
5193
|
+
resolveStaticCommand() {
|
|
4747
5194
|
try {
|
|
4748
5195
|
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
4749
5196
|
const analysis = analyzer.analyze();
|
|
4750
5197
|
if (analysis.commands.typecheck) {
|
|
4751
5198
|
return analysis.commands.typecheck;
|
|
4752
5199
|
}
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
if (analysis.commands.test) {
|
|
4757
|
-
return analysis.commands.test;
|
|
4758
|
-
}
|
|
4759
|
-
} catch {}
|
|
5200
|
+
} catch (err) {
|
|
5201
|
+
console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
|
|
5202
|
+
}
|
|
4760
5203
|
const pkgPath = join8(this.cwd, "package.json");
|
|
4761
5204
|
if (existsSync14(pkgPath)) {
|
|
4762
5205
|
try {
|
|
4763
5206
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
4764
5207
|
if (pkg.scripts) {
|
|
5208
|
+
const pm = existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock")) ? "bun" : existsSync14(join8(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync14(join8(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5209
|
+
const runCmd = pm === "bun" || pm === "pnpm" || pm === "yarn" ? `${pm} run` : "npm run";
|
|
4765
5210
|
if (pkg.scripts.typecheck)
|
|
4766
|
-
return
|
|
5211
|
+
return `${runCmd} typecheck`;
|
|
4767
5212
|
if (pkg.scripts.check)
|
|
4768
|
-
return
|
|
4769
|
-
if (pkg.scripts.test)
|
|
4770
|
-
return "npm test";
|
|
5213
|
+
return `${runCmd} check`;
|
|
4771
5214
|
}
|
|
4772
|
-
} catch {
|
|
5215
|
+
} catch (err) {
|
|
5216
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
|
|
5217
|
+
}
|
|
4773
5218
|
}
|
|
4774
5219
|
if (existsSync14(join8(this.cwd, "tsconfig.json"))) {
|
|
4775
5220
|
return "npx tsc --noEmit";
|
|
@@ -4780,17 +5225,76 @@ class AutoVerifier {
|
|
|
4780
5225
|
if (existsSync14(join8(this.cwd, "go.mod"))) {
|
|
4781
5226
|
return "go vet ./...";
|
|
4782
5227
|
}
|
|
4783
|
-
if (existsSync14(join8(this.cwd, "
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
5228
|
+
if (existsSync14(join8(this.cwd, "mypy.ini")) || existsSync14(join8(this.cwd, ".mypy.ini"))) {
|
|
5229
|
+
return "mypy .";
|
|
5230
|
+
}
|
|
5231
|
+
return null;
|
|
5232
|
+
}
|
|
5233
|
+
resolveVerificationCommand(modifiedFiles = []) {
|
|
5234
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
5235
|
+
return this.customCommand.trim();
|
|
5236
|
+
}
|
|
5237
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
5238
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
5239
|
+
const staticCmd = this.resolveStaticCommand();
|
|
5240
|
+
if (targetedTestCmd && staticCmd) {
|
|
5241
|
+
return `${staticCmd} && ${targetedTestCmd}`;
|
|
5242
|
+
}
|
|
5243
|
+
if (targetedTestCmd) {
|
|
5244
|
+
return targetedTestCmd;
|
|
5245
|
+
}
|
|
5246
|
+
if (staticCmd) {
|
|
5247
|
+
return staticCmd;
|
|
5248
|
+
}
|
|
5249
|
+
try {
|
|
5250
|
+
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
5251
|
+
const analysis = analyzer.analyze();
|
|
5252
|
+
if (analysis.commands.test)
|
|
5253
|
+
return analysis.commands.test;
|
|
5254
|
+
if (analysis.commands.lint)
|
|
5255
|
+
return analysis.commands.lint;
|
|
5256
|
+
} catch {}
|
|
5257
|
+
const pkgPath = join8(this.cwd, "package.json");
|
|
5258
|
+
if (existsSync14(pkgPath)) {
|
|
5259
|
+
try {
|
|
5260
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
5261
|
+
if (pkg.scripts?.test) {
|
|
5262
|
+
const pm = existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock")) ? "bun" : "npm";
|
|
5263
|
+
return pm === "bun" ? "bun test" : "npm test";
|
|
5264
|
+
}
|
|
5265
|
+
if (pkg.scripts?.lint) {
|
|
5266
|
+
return "npm run lint";
|
|
5267
|
+
}
|
|
5268
|
+
} catch {}
|
|
4787
5269
|
}
|
|
4788
5270
|
return null;
|
|
4789
5271
|
}
|
|
4790
|
-
|
|
4791
|
-
|
|
5272
|
+
resolveVerificationStages(modifiedFiles = []) {
|
|
5273
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
5274
|
+
return [this.customCommand.trim()];
|
|
5275
|
+
}
|
|
5276
|
+
const stages = [];
|
|
5277
|
+
const staticCmd = this.resolveStaticCommand();
|
|
5278
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
5279
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
5280
|
+
if (staticCmd) {
|
|
5281
|
+
stages.push(staticCmd);
|
|
5282
|
+
}
|
|
5283
|
+
if (targetedTestCmd) {
|
|
5284
|
+
stages.push(targetedTestCmd);
|
|
5285
|
+
}
|
|
5286
|
+
if (stages.length === 0) {
|
|
5287
|
+
const fallback = this.resolveVerificationCommand(modifiedFiles);
|
|
5288
|
+
if (fallback) {
|
|
5289
|
+
stages.push(fallback);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
return stages;
|
|
5293
|
+
}
|
|
5294
|
+
async verify(modifiedFiles = [], signal) {
|
|
5295
|
+
const stages = this.resolveVerificationStages(modifiedFiles);
|
|
4792
5296
|
const startTime = performance.now();
|
|
4793
|
-
if (
|
|
5297
|
+
if (stages.length === 0) {
|
|
4794
5298
|
return {
|
|
4795
5299
|
command: "none",
|
|
4796
5300
|
success: true,
|
|
@@ -4800,59 +5304,158 @@ class AutoVerifier {
|
|
|
4800
5304
|
reason: "NO_VERIFIER_DETECTED"
|
|
4801
5305
|
};
|
|
4802
5306
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
5307
|
+
const combinedOutputs = [];
|
|
5308
|
+
let executedCommands = [];
|
|
5309
|
+
for (const cmd of stages) {
|
|
5310
|
+
if (signal?.aborted) {
|
|
5311
|
+
return {
|
|
5312
|
+
command: cmd,
|
|
5313
|
+
success: false,
|
|
5314
|
+
exitCode: 1,
|
|
5315
|
+
output: "Verification was aborted.",
|
|
5316
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
5317
|
+
reason: "ABORTED"
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
executedCommands.push(cmd);
|
|
5321
|
+
const stageResult = await this.executeCommandAsync(cmd, signal);
|
|
5322
|
+
combinedOutputs.push(`[${cmd}]
|
|
5323
|
+
${stageResult.output}`);
|
|
5324
|
+
if (!stageResult.success) {
|
|
5325
|
+
return {
|
|
5326
|
+
command: cmd,
|
|
5327
|
+
success: false,
|
|
5328
|
+
exitCode: stageResult.exitCode,
|
|
5329
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
5330
|
+
|
|
5331
|
+
`)),
|
|
5332
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
5333
|
+
};
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
return {
|
|
5337
|
+
command: executedCommands.join(" && "),
|
|
5338
|
+
success: true,
|
|
5339
|
+
exitCode: 0,
|
|
5340
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
5341
|
+
|
|
5342
|
+
`)),
|
|
5343
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
5344
|
+
};
|
|
5345
|
+
}
|
|
5346
|
+
executeCommandAsync(command, signal) {
|
|
5347
|
+
return new Promise((resolve) => {
|
|
5348
|
+
let proc = null;
|
|
5349
|
+
let stdoutData = "";
|
|
5350
|
+
let stderrData = "";
|
|
5351
|
+
let isSettled = false;
|
|
5352
|
+
const finish = (success, exitCode, output) => {
|
|
5353
|
+
if (isSettled)
|
|
5354
|
+
return;
|
|
5355
|
+
isSettled = true;
|
|
5356
|
+
cleanup();
|
|
5357
|
+
resolve({ success, exitCode, output: output.trim() });
|
|
5358
|
+
};
|
|
5359
|
+
const killProcessTree = () => {
|
|
5360
|
+
if (!proc || !proc.pid)
|
|
5361
|
+
return;
|
|
5362
|
+
try {
|
|
5363
|
+
if (process.platform === "win32") {
|
|
5364
|
+
spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
|
|
5365
|
+
} else {
|
|
5366
|
+
proc.kill("SIGTERM");
|
|
5367
|
+
setTimeout(() => {
|
|
5368
|
+
try {
|
|
5369
|
+
proc?.kill("SIGKILL");
|
|
5370
|
+
} catch {}
|
|
5371
|
+
}, 500);
|
|
5372
|
+
}
|
|
5373
|
+
} catch {}
|
|
5374
|
+
};
|
|
5375
|
+
const onAbort = () => {
|
|
5376
|
+
killProcessTree();
|
|
5377
|
+
finish(false, 1, "Verification command aborted by user or session signal.");
|
|
5378
|
+
};
|
|
5379
|
+
if (signal?.aborted) {
|
|
5380
|
+
return finish(false, 1, "Verification aborted before launch.");
|
|
5381
|
+
}
|
|
5382
|
+
if (signal) {
|
|
5383
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5384
|
+
}
|
|
5385
|
+
const timer = setTimeout(() => {
|
|
5386
|
+
killProcessTree();
|
|
5387
|
+
const partial = (stdoutData + `
|
|
5388
|
+
` + stderrData).trim();
|
|
5389
|
+
const timeoutMsg = `[Verification Timeout Error]: Verification command '${command}' exceeded timeout limit of ${this.timeoutMs}ms and was terminated.`;
|
|
5390
|
+
finish(false, 1, partial ? `${partial}
|
|
5391
|
+
|
|
5392
|
+
${timeoutMsg}` : timeoutMsg);
|
|
5393
|
+
}, this.timeoutMs);
|
|
5394
|
+
const cleanup = () => {
|
|
5395
|
+
clearTimeout(timer);
|
|
5396
|
+
if (signal) {
|
|
5397
|
+
signal.removeEventListener("abort", onAbort);
|
|
4815
5398
|
}
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
5399
|
+
};
|
|
5400
|
+
try {
|
|
5401
|
+
proc = spawn(command, {
|
|
5402
|
+
cwd: this.cwd,
|
|
5403
|
+
shell: true,
|
|
5404
|
+
env: {
|
|
5405
|
+
...process.env,
|
|
5406
|
+
CI: "true",
|
|
5407
|
+
FORCE_COLOR: "0"
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
proc.stdout?.on("data", (chunk) => {
|
|
5411
|
+
stdoutData += chunk.toString();
|
|
5412
|
+
});
|
|
5413
|
+
proc.stderr?.on("data", (chunk) => {
|
|
5414
|
+
stderrData += chunk.toString();
|
|
5415
|
+
});
|
|
5416
|
+
proc.on("error", (err) => {
|
|
5417
|
+
const partial = (stdoutData + `
|
|
5418
|
+
` + stderrData).trim();
|
|
5419
|
+
const spawnMsg = `[Verification Process Spawn Error]: Failed to spawn command '${command}': ${err.message}`;
|
|
5420
|
+
finish(false, 1, partial ? `${partial}
|
|
5421
|
+
|
|
5422
|
+
${spawnMsg}` : spawnMsg);
|
|
5423
|
+
});
|
|
5424
|
+
proc.on("close", (code) => {
|
|
5425
|
+
const exitCode = code ?? 0;
|
|
5426
|
+
const combined = (stdoutData + `
|
|
5427
|
+
` + stderrData).trim();
|
|
5428
|
+
if (exitCode === 0) {
|
|
5429
|
+
finish(true, 0, combined || "Verification passed cleanly.");
|
|
5430
|
+
} else {
|
|
5431
|
+
const errorFallback = `[Verification Failure]: Command '${command}' exited with code ${exitCode} and produced no output.`;
|
|
5432
|
+
finish(false, exitCode, combined ? `${combined}
|
|
5433
|
+
|
|
5434
|
+
[Process exited with non-zero code ${exitCode}]` : errorFallback);
|
|
5435
|
+
}
|
|
5436
|
+
});
|
|
5437
|
+
} catch (err) {
|
|
5438
|
+
finish(false, 1, `[Verification Execution Exception]: ${err.message || String(err)}`);
|
|
5439
|
+
}
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
truncateOutput(output) {
|
|
5443
|
+
if (output.length > 4000) {
|
|
5444
|
+
const lines = output.split(`
|
|
4824
5445
|
`);
|
|
4825
|
-
|
|
4826
|
-
|
|
5446
|
+
if (lines.length > 70) {
|
|
5447
|
+
const head = lines.slice(0, 35).join(`
|
|
4827
5448
|
`);
|
|
4828
|
-
|
|
5449
|
+
const tail = lines.slice(-30).join(`
|
|
4829
5450
|
`);
|
|
4830
|
-
|
|
5451
|
+
return `${head}
|
|
4831
5452
|
|
|
4832
|
-
... [${lines.length -
|
|
5453
|
+
... [${lines.length - 65} lines truncated for context efficiency] ...
|
|
4833
5454
|
|
|
4834
5455
|
${tail}`;
|
|
4835
|
-
}
|
|
4836
5456
|
}
|
|
4837
|
-
const exitCode = proc.status ?? (proc.error ? 1 : 0);
|
|
4838
|
-
const success = exitCode === 0;
|
|
4839
|
-
return {
|
|
4840
|
-
command,
|
|
4841
|
-
success,
|
|
4842
|
-
exitCode,
|
|
4843
|
-
output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
|
|
4844
|
-
durationMs
|
|
4845
|
-
};
|
|
4846
|
-
} catch (err) {
|
|
4847
|
-
const durationMs = Math.round(performance.now() - startTime);
|
|
4848
|
-
return {
|
|
4849
|
-
command,
|
|
4850
|
-
success: false,
|
|
4851
|
-
exitCode: 1,
|
|
4852
|
-
output: `Verification execution error: ${err.message || String(err)}`,
|
|
4853
|
-
durationMs
|
|
4854
|
-
};
|
|
4855
5457
|
}
|
|
5458
|
+
return output;
|
|
4856
5459
|
}
|
|
4857
5460
|
}
|
|
4858
5461
|
// src/session/turn.ts
|
|
@@ -4976,6 +5579,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
4976
5579
|
});
|
|
4977
5580
|
}
|
|
4978
5581
|
if (toolCallRequests.length > 0) {
|
|
5582
|
+
const validCalls = [];
|
|
4979
5583
|
for (const toolCall of toolCallRequests) {
|
|
4980
5584
|
if (!toolCall.name || !toolCall.name.trim())
|
|
4981
5585
|
continue;
|
|
@@ -4999,6 +5603,14 @@ async function runTurn(session, turnContext, input) {
|
|
|
4999
5603
|
toolName: toolCall.name,
|
|
5000
5604
|
arguments: toolCall.arguments
|
|
5001
5605
|
});
|
|
5606
|
+
validCalls.push({
|
|
5607
|
+
callId: toolCall.callId,
|
|
5608
|
+
name: toolCall.name,
|
|
5609
|
+
arguments: toolCall.arguments,
|
|
5610
|
+
functionCallItem
|
|
5611
|
+
});
|
|
5612
|
+
}
|
|
5613
|
+
for (const toolCall of validCalls) {
|
|
5002
5614
|
const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
|
|
5003
5615
|
cwd: turnContext.environment.cwd,
|
|
5004
5616
|
turnId,
|
|
@@ -5006,6 +5618,12 @@ async function runTurn(session, turnContext, input) {
|
|
|
5006
5618
|
execPolicy: session.execPolicy,
|
|
5007
5619
|
mode: session.collaborationMode,
|
|
5008
5620
|
permissionMode: session.permissionMode,
|
|
5621
|
+
onFileModified: (p) => {
|
|
5622
|
+
if (p) {
|
|
5623
|
+
modifiedFiles.add(p);
|
|
5624
|
+
hasRunVerification = false;
|
|
5625
|
+
}
|
|
5626
|
+
},
|
|
5009
5627
|
onPlanUpdate: (plan, explanation) => {
|
|
5010
5628
|
session.emitEvent({
|
|
5011
5629
|
type: "PlanUpdated",
|
|
@@ -5021,7 +5639,8 @@ async function runTurn(session, turnContext, input) {
|
|
|
5021
5639
|
turnId,
|
|
5022
5640
|
toolName: toolCall.name,
|
|
5023
5641
|
description,
|
|
5024
|
-
command
|
|
5642
|
+
command,
|
|
5643
|
+
prefixRule
|
|
5025
5644
|
});
|
|
5026
5645
|
},
|
|
5027
5646
|
requestInput: async (question, options) => {
|
|
@@ -5072,7 +5691,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
5072
5691
|
cwd,
|
|
5073
5692
|
customCommand: session.autoVerificationCommand
|
|
5074
5693
|
});
|
|
5075
|
-
const command = verifier.resolveVerificationCommand();
|
|
5694
|
+
const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
|
|
5076
5695
|
if (command) {
|
|
5077
5696
|
session.emitEvent({
|
|
5078
5697
|
type: "VerificationStarted",
|
|
@@ -5080,7 +5699,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
5080
5699
|
command,
|
|
5081
5700
|
modifiedFiles: Array.from(modifiedFiles)
|
|
5082
5701
|
});
|
|
5083
|
-
const vResult = verifier.verify(Array.from(modifiedFiles));
|
|
5702
|
+
const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
|
|
5084
5703
|
session.emitEvent({
|
|
5085
5704
|
type: "VerificationCompleted",
|
|
5086
5705
|
turnId,
|
|
@@ -5373,7 +5992,8 @@ class Session {
|
|
|
5373
5992
|
turnId: params.turnId,
|
|
5374
5993
|
toolName: params.toolName,
|
|
5375
5994
|
description: params.description,
|
|
5376
|
-
command: params.command
|
|
5995
|
+
command: params.command,
|
|
5996
|
+
prefixRule: params.prefixRule
|
|
5377
5997
|
});
|
|
5378
5998
|
this.emitEvent({
|
|
5379
5999
|
type: "StatusChanged",
|
|
@@ -9128,7 +9748,11 @@ class AgentGraphStore {
|
|
|
9128
9748
|
if (dbPath !== ":memory:") {
|
|
9129
9749
|
const dir = resolve19(dbPath, "..");
|
|
9130
9750
|
if (!existsSync19(dir)) {
|
|
9131
|
-
|
|
9751
|
+
try {
|
|
9752
|
+
mkdirSync10(dir, { recursive: true });
|
|
9753
|
+
} catch (err) {
|
|
9754
|
+
console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
|
|
9755
|
+
}
|
|
9132
9756
|
}
|
|
9133
9757
|
}
|
|
9134
9758
|
this.db = new Database3(dbPath);
|
|
@@ -9210,24 +9834,44 @@ class AgentGraphStore {
|
|
|
9210
9834
|
close() {
|
|
9211
9835
|
try {
|
|
9212
9836
|
this.db.close();
|
|
9213
|
-
} catch {
|
|
9837
|
+
} catch (err) {
|
|
9838
|
+
console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
|
|
9839
|
+
}
|
|
9214
9840
|
}
|
|
9215
9841
|
}
|
|
9216
9842
|
// src/agents/spawner.ts
|
|
9217
9843
|
class AgentSpawner {
|
|
9218
9844
|
parentSession;
|
|
9845
|
+
depth;
|
|
9219
9846
|
subAgents = new Map;
|
|
9220
9847
|
nextAgentId = 1;
|
|
9221
9848
|
roleRegistry;
|
|
9222
9849
|
parentIdentity;
|
|
9223
9850
|
graphStore;
|
|
9224
|
-
|
|
9851
|
+
maxConcurrentAgents;
|
|
9852
|
+
maxDepth;
|
|
9853
|
+
maxRetainedCompleted;
|
|
9854
|
+
defaultTokenBudget;
|
|
9855
|
+
constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
|
|
9225
9856
|
this.parentSession = parentSession;
|
|
9857
|
+
this.depth = depth;
|
|
9226
9858
|
this.roleRegistry = roleRegistry || new AgentRoleRegistry;
|
|
9227
9859
|
this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
|
|
9228
9860
|
this.graphStore = graphStore || new AgentGraphStore;
|
|
9861
|
+
const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
|
|
9862
|
+
this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
|
|
9863
|
+
this.maxDepth = options?.maxDepth ?? 2;
|
|
9864
|
+
this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
|
|
9865
|
+
this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
|
|
9229
9866
|
}
|
|
9230
9867
|
async spawnAgent(params) {
|
|
9868
|
+
if (this.depth >= this.maxDepth) {
|
|
9869
|
+
throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
|
|
9870
|
+
}
|
|
9871
|
+
const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
|
|
9872
|
+
if (activeRunningCount >= this.maxConcurrentAgents) {
|
|
9873
|
+
throw new GroupyError(`Resource limit exceeded: Maximum concurrent sub-agents limit (${this.maxConcurrentAgents}) reached. Please wait for running sub-agents to complete or close them.`);
|
|
9874
|
+
}
|
|
9231
9875
|
const roleName = params.role || "default";
|
|
9232
9876
|
const roleConfig = this.roleRegistry.getRole(roleName);
|
|
9233
9877
|
const agentIndex = this.nextAgentId++;
|
|
@@ -9239,7 +9883,18 @@ class AgentSpawner {
|
|
|
9239
9883
|
|
|
9240
9884
|
Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus strictly on this task.` : `You are a specialized sub-agent named ${nickname} tasked with: '${params.taskName}'. Focus strictly on this task.`);
|
|
9241
9885
|
const baseTools = params.tools || this.parentSession.tools;
|
|
9242
|
-
|
|
9886
|
+
let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
|
|
9887
|
+
if (this.depth + 1 >= this.maxDepth) {
|
|
9888
|
+
const sanitizedRouter = new ToolRouter;
|
|
9889
|
+
for (const t of effectiveTools.list()) {
|
|
9890
|
+
if (t.name !== "spawn_agent") {
|
|
9891
|
+
sanitizedRouter.register(t);
|
|
9892
|
+
}
|
|
9893
|
+
}
|
|
9894
|
+
effectiveTools = sanitizedRouter;
|
|
9895
|
+
}
|
|
9896
|
+
const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
|
|
9897
|
+
let accumulatedTokens = 0;
|
|
9243
9898
|
const childSession = new Session({
|
|
9244
9899
|
threadId: agentId,
|
|
9245
9900
|
model: effectiveModel,
|
|
@@ -9263,45 +9918,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
9263
9918
|
createdAt: Date.now(),
|
|
9264
9919
|
identity: childIdentity,
|
|
9265
9920
|
session: childSession,
|
|
9266
|
-
promise: taskPromise
|
|
9921
|
+
promise: taskPromise,
|
|
9922
|
+
depth: this.depth + 1,
|
|
9923
|
+
tokenBudget,
|
|
9924
|
+
totalTokens: 0
|
|
9267
9925
|
};
|
|
9268
9926
|
try {
|
|
9269
9927
|
this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
|
|
9270
|
-
} catch {
|
|
9928
|
+
} catch (err) {
|
|
9929
|
+
console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
|
|
9930
|
+
}
|
|
9271
9931
|
let collectedAgentText = "";
|
|
9272
9932
|
childSession.onEvent((event) => {
|
|
9273
9933
|
if (event.msg.type === "AgentMessageDelta") {
|
|
9274
9934
|
collectedAgentText += event.msg.delta;
|
|
9935
|
+
accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
|
|
9936
|
+
handle.totalTokens = accumulatedTokens;
|
|
9937
|
+
if (accumulatedTokens > tokenBudget && handle.status === "running") {
|
|
9938
|
+
handle.status = "error";
|
|
9939
|
+
handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
|
|
9940
|
+
childSession.interrupt();
|
|
9941
|
+
try {
|
|
9942
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9943
|
+
} catch (err) {
|
|
9944
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
9945
|
+
}
|
|
9946
|
+
rejectPromise(new Error(handle.error));
|
|
9947
|
+
}
|
|
9275
9948
|
} else if (event.msg.type === "TurnCompleted") {
|
|
9276
|
-
handle.status
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9949
|
+
if (handle.status === "running") {
|
|
9950
|
+
handle.status = "completed";
|
|
9951
|
+
handle.lastOutput = collectedAgentText.trim();
|
|
9952
|
+
handle.totalTokens = accumulatedTokens;
|
|
9953
|
+
try {
|
|
9954
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9955
|
+
} catch (err) {
|
|
9956
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
9957
|
+
}
|
|
9958
|
+
this.pruneCompletedAgents();
|
|
9959
|
+
resolvePromise(handle.lastOutput);
|
|
9960
|
+
}
|
|
9282
9961
|
} else if (event.msg.type === "Error") {
|
|
9283
|
-
handle.status
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9962
|
+
if (handle.status === "running") {
|
|
9963
|
+
handle.status = "error";
|
|
9964
|
+
handle.error = event.msg.message;
|
|
9965
|
+
handle.totalTokens = accumulatedTokens;
|
|
9966
|
+
try {
|
|
9967
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9968
|
+
} catch (err) {
|
|
9969
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
9970
|
+
}
|
|
9971
|
+
this.pruneCompletedAgents();
|
|
9972
|
+
rejectPromise(new Error(event.msg.message));
|
|
9973
|
+
}
|
|
9289
9974
|
} else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
|
|
9290
|
-
handle.status
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9975
|
+
if (handle.status === "running") {
|
|
9976
|
+
handle.status = "interrupted";
|
|
9977
|
+
handle.totalTokens = accumulatedTokens;
|
|
9978
|
+
try {
|
|
9979
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9980
|
+
} catch (err) {
|
|
9981
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
9982
|
+
}
|
|
9983
|
+
this.pruneCompletedAgents();
|
|
9984
|
+
resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
|
|
9985
|
+
}
|
|
9295
9986
|
}
|
|
9296
9987
|
});
|
|
9297
9988
|
this.subAgents.set(agentId, handle);
|
|
9298
9989
|
childSession.prompt(params.message).catch((err) => {
|
|
9299
|
-
handle.status
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9990
|
+
if (handle.status === "running") {
|
|
9991
|
+
handle.status = "error";
|
|
9992
|
+
handle.error = err instanceof Error ? err.message : String(err);
|
|
9993
|
+
handle.totalTokens = accumulatedTokens;
|
|
9994
|
+
try {
|
|
9995
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9996
|
+
} catch (storeErr) {
|
|
9997
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
|
|
9998
|
+
}
|
|
9999
|
+
this.pruneCompletedAgents();
|
|
10000
|
+
rejectPromise(err);
|
|
10001
|
+
}
|
|
9305
10002
|
});
|
|
9306
10003
|
return {
|
|
9307
10004
|
id: handle.id,
|
|
@@ -9310,9 +10007,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
9310
10007
|
role: handle.role,
|
|
9311
10008
|
status: handle.status,
|
|
9312
10009
|
createdAt: handle.createdAt,
|
|
9313
|
-
agentRuntimeId: childIdentity.agentRuntimeId
|
|
10010
|
+
agentRuntimeId: childIdentity.agentRuntimeId,
|
|
10011
|
+
depth: handle.depth,
|
|
10012
|
+
tokenBudget: handle.tokenBudget,
|
|
10013
|
+
totalTokens: handle.totalTokens
|
|
9314
10014
|
};
|
|
9315
10015
|
}
|
|
10016
|
+
pruneCompletedAgents() {
|
|
10017
|
+
const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
|
|
10018
|
+
if (finishedHandles.length > this.maxRetainedCompleted) {
|
|
10019
|
+
finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
|
|
10020
|
+
const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
|
|
10021
|
+
for (const h of toRemove) {
|
|
10022
|
+
this.subAgents.delete(h.id);
|
|
10023
|
+
}
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
9316
10026
|
async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
|
|
9317
10027
|
if (!agentIdOrTaskName) {
|
|
9318
10028
|
const handles = Array.from(this.subAgents.values());
|
|
@@ -9366,9 +10076,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
9366
10076
|
handle.status = "interrupted";
|
|
9367
10077
|
try {
|
|
9368
10078
|
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
9369
|
-
} catch {
|
|
10079
|
+
} catch (err) {
|
|
10080
|
+
console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
|
|
10081
|
+
}
|
|
10082
|
+
this.pruneCompletedAgents();
|
|
9370
10083
|
return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
|
|
9371
10084
|
}
|
|
10085
|
+
removeAgent(agentId) {
|
|
10086
|
+
const handle = this.subAgents.get(agentId);
|
|
10087
|
+
if (!handle)
|
|
10088
|
+
return false;
|
|
10089
|
+
if (handle.status === "running") {
|
|
10090
|
+
handle.session.interrupt();
|
|
10091
|
+
}
|
|
10092
|
+
try {
|
|
10093
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
10094
|
+
} catch (err) {
|
|
10095
|
+
console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
|
|
10096
|
+
}
|
|
10097
|
+
return this.subAgents.delete(agentId);
|
|
10098
|
+
}
|
|
10099
|
+
clearCompleted() {
|
|
10100
|
+
let cleared = 0;
|
|
10101
|
+
for (const [id, handle] of this.subAgents.entries()) {
|
|
10102
|
+
if (handle.status !== "running") {
|
|
10103
|
+
this.subAgents.delete(id);
|
|
10104
|
+
cleared++;
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
return cleared;
|
|
10108
|
+
}
|
|
9372
10109
|
listAgents() {
|
|
9373
10110
|
const list = [];
|
|
9374
10111
|
for (const handle of this.subAgents.values()) {
|
|
@@ -9380,7 +10117,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
9380
10117
|
status: handle.status,
|
|
9381
10118
|
createdAt: handle.createdAt,
|
|
9382
10119
|
agentRuntimeId: handle.identity.agentRuntimeId,
|
|
9383
|
-
lastOutput: handle.lastOutput
|
|
10120
|
+
lastOutput: handle.lastOutput,
|
|
10121
|
+
depth: handle.depth,
|
|
10122
|
+
tokenBudget: handle.tokenBudget,
|
|
10123
|
+
totalTokens: handle.totalTokens
|
|
9384
10124
|
});
|
|
9385
10125
|
}
|
|
9386
10126
|
return list;
|
|
@@ -9413,6 +10153,10 @@ function createMultiAgentTools(spawner) {
|
|
|
9413
10153
|
model: {
|
|
9414
10154
|
type: "string",
|
|
9415
10155
|
description: "Optional model override. Defaults to inheriting parent model."
|
|
10156
|
+
},
|
|
10157
|
+
max_tokens: {
|
|
10158
|
+
type: "number",
|
|
10159
|
+
description: "Optional token budget limit for the sub-agent (default: 50000)."
|
|
9416
10160
|
}
|
|
9417
10161
|
},
|
|
9418
10162
|
required: ["task_name", "message"]
|
|
@@ -9422,8 +10166,9 @@ function createMultiAgentTools(spawner) {
|
|
|
9422
10166
|
const message = String(args.message || "");
|
|
9423
10167
|
const role = args.role ? String(args.role) : undefined;
|
|
9424
10168
|
const model = args.model ? String(args.model) : undefined;
|
|
10169
|
+
const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
|
|
9425
10170
|
try {
|
|
9426
|
-
const handle = await spawner.spawnAgent({ taskName, message, role, model });
|
|
10171
|
+
const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
|
|
9427
10172
|
return {
|
|
9428
10173
|
output: `Successfully spawned sub-agent '${handle.id}' with role '${handle.role}' for task '${handle.taskName}' (status: ${handle.status}). Use 'wait_agent' to collect results.`
|
|
9429
10174
|
};
|
|
@@ -9770,7 +10515,8 @@ class SessionPersistenceManager {
|
|
|
9770
10515
|
loadSession(threadId) {
|
|
9771
10516
|
try {
|
|
9772
10517
|
return this.store.restoreSession(threadId);
|
|
9773
|
-
} catch {
|
|
10518
|
+
} catch (err) {
|
|
10519
|
+
console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
|
|
9774
10520
|
return null;
|
|
9775
10521
|
}
|
|
9776
10522
|
}
|
|
@@ -9778,7 +10524,9 @@ class SessionPersistenceManager {
|
|
|
9778
10524
|
for (const unsub of this.unsubscribers) {
|
|
9779
10525
|
try {
|
|
9780
10526
|
unsub();
|
|
9781
|
-
} catch {
|
|
10527
|
+
} catch (err) {
|
|
10528
|
+
console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
|
|
10529
|
+
}
|
|
9782
10530
|
}
|
|
9783
10531
|
this.unsubscribers = [];
|
|
9784
10532
|
}
|
|
@@ -9805,7 +10553,9 @@ class SessionPersistenceManager {
|
|
|
9805
10553
|
for (const unsub of this.unsubscribers) {
|
|
9806
10554
|
try {
|
|
9807
10555
|
unsub();
|
|
9808
|
-
} catch {
|
|
10556
|
+
} catch (err) {
|
|
10557
|
+
console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
|
|
10558
|
+
}
|
|
9809
10559
|
}
|
|
9810
10560
|
this.unsubscribers = [];
|
|
9811
10561
|
this.store.close();
|
|
@@ -10070,7 +10820,9 @@ class MemoryStore {
|
|
|
10070
10820
|
if (!existsSync22(dir)) {
|
|
10071
10821
|
try {
|
|
10072
10822
|
mkdirSync12(dir, { recursive: true });
|
|
10073
|
-
} catch {
|
|
10823
|
+
} catch (err) {
|
|
10824
|
+
console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
|
|
10825
|
+
}
|
|
10074
10826
|
}
|
|
10075
10827
|
return dir;
|
|
10076
10828
|
}
|
|
@@ -10079,7 +10831,9 @@ class MemoryStore {
|
|
|
10079
10831
|
if (!existsSync22(dir)) {
|
|
10080
10832
|
try {
|
|
10081
10833
|
mkdirSync12(dir, { recursive: true });
|
|
10082
|
-
} catch {
|
|
10834
|
+
} catch (err) {
|
|
10835
|
+
console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
|
|
10836
|
+
}
|
|
10083
10837
|
}
|
|
10084
10838
|
return dir;
|
|
10085
10839
|
}
|
|
@@ -10223,7 +10977,9 @@ class MemoryStore {
|
|
|
10223
10977
|
`)[0] || parsed.name,
|
|
10224
10978
|
file: f
|
|
10225
10979
|
});
|
|
10226
|
-
} catch {
|
|
10980
|
+
} catch (err) {
|
|
10981
|
+
console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
|
|
10982
|
+
}
|
|
10227
10983
|
}
|
|
10228
10984
|
const indexLines = [
|
|
10229
10985
|
"# Project Auto-Memory Index",
|
|
@@ -10265,7 +11021,9 @@ class MemoryStore {
|
|
|
10265
11021
|
try {
|
|
10266
11022
|
const full = join13(memoryDir, f);
|
|
10267
11023
|
list.push(this.parseTopicFile(readFileSync13(full, "utf8"), full));
|
|
10268
|
-
} catch {
|
|
11024
|
+
} catch (err) {
|
|
11025
|
+
console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
|
|
11026
|
+
}
|
|
10269
11027
|
}
|
|
10270
11028
|
return list;
|
|
10271
11029
|
}
|
|
@@ -10785,7 +11543,7 @@ function ClaudeLogo({
|
|
|
10785
11543
|
}, undefined, false, undefined, this);
|
|
10786
11544
|
}
|
|
10787
11545
|
function ClaudeHeader({
|
|
10788
|
-
version = "v0.
|
|
11546
|
+
version = "v0.4.0",
|
|
10789
11547
|
user = "Developer",
|
|
10790
11548
|
model = "Claude 3.7 Sonnet (Thinking)",
|
|
10791
11549
|
plan = "Pro",
|
|
@@ -12234,6 +12992,7 @@ export {
|
|
|
12234
12992
|
CredentialsStore,
|
|
12235
12993
|
DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS,
|
|
12236
12994
|
DEFAULT_MAX_CONTEXT_TOKENS,
|
|
12995
|
+
DEFAULT_MAX_DIR_ENTRIES,
|
|
12237
12996
|
DEFAULT_MAX_UNPAGINATED_LINES,
|
|
12238
12997
|
DEFAULT_WORKTREE_KEEP_COUNT,
|
|
12239
12998
|
DefaultModelClientSession,
|