@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/cli.js
CHANGED
|
@@ -244,6 +244,7 @@ class DefaultModelClientSession {
|
|
|
244
244
|
systemMessage.cache_control = { type: "ephemeral" };
|
|
245
245
|
}
|
|
246
246
|
const messages = [systemMessage];
|
|
247
|
+
const declaredToolCallIds = new Set;
|
|
247
248
|
for (let i = 0;i < params.history.length; i++) {
|
|
248
249
|
const item = params.history[i];
|
|
249
250
|
if (item.type === "user_message") {
|
|
@@ -268,6 +269,9 @@ class DefaultModelClientSession {
|
|
|
268
269
|
j++;
|
|
269
270
|
}
|
|
270
271
|
if (toolCalls.length > 0) {
|
|
272
|
+
for (const tc of toolCalls) {
|
|
273
|
+
declaredToolCallIds.add(tc.id);
|
|
274
|
+
}
|
|
271
275
|
messages.push({
|
|
272
276
|
role: "assistant",
|
|
273
277
|
content: cleanedContent || null,
|
|
@@ -304,6 +308,9 @@ class DefaultModelClientSession {
|
|
|
304
308
|
});
|
|
305
309
|
j++;
|
|
306
310
|
}
|
|
311
|
+
for (const tc of toolCalls) {
|
|
312
|
+
declaredToolCallIds.add(tc.id);
|
|
313
|
+
}
|
|
307
314
|
messages.push({
|
|
308
315
|
role: "assistant",
|
|
309
316
|
content: null,
|
|
@@ -311,11 +318,13 @@ class DefaultModelClientSession {
|
|
|
311
318
|
});
|
|
312
319
|
i = j - 1;
|
|
313
320
|
} else if (item.type === "function_call_output") {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
if (declaredToolCallIds.has(item.callId)) {
|
|
322
|
+
messages.push({
|
|
323
|
+
role: "tool",
|
|
324
|
+
tool_call_id: item.callId,
|
|
325
|
+
content: item.output
|
|
326
|
+
});
|
|
327
|
+
}
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
const toolsPayload = typeof params.tools?.toModelToolsSchema === "function" ? params.tools.toModelToolsSchema() : typeof params.tools?.toOpenAISpec === "function" ? params.tools.toOpenAISpec() : Array.isArray(params.tools) ? params.tools : [];
|
|
@@ -705,14 +714,39 @@ function compactHistory(history, retainedRecentItems = 6) {
|
|
|
705
714
|
if (history.length <= retainedRecentItems) {
|
|
706
715
|
return [...history];
|
|
707
716
|
}
|
|
708
|
-
const
|
|
709
|
-
|
|
717
|
+
const targetCutoff = history.length - retainedRecentItems;
|
|
718
|
+
let bestCutoff = -1;
|
|
719
|
+
for (let i = targetCutoff;i >= 0; i--) {
|
|
720
|
+
if (history[i]?.type === "user_message") {
|
|
721
|
+
bestCutoff = i;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (bestCutoff <= 0) {
|
|
726
|
+
for (let i = targetCutoff + 1;i < history.length; i++) {
|
|
727
|
+
if (history[i]?.type === "user_message") {
|
|
728
|
+
bestCutoff = i;
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
let cutoffIndex = bestCutoff > 0 ? bestCutoff : targetCutoff;
|
|
734
|
+
while (cutoffIndex < history.length && history[cutoffIndex]?.type === "function_call_output") {
|
|
735
|
+
cutoffIndex++;
|
|
736
|
+
}
|
|
737
|
+
if (cutoffIndex <= 0 || cutoffIndex >= history.length) {
|
|
738
|
+
return [...history];
|
|
739
|
+
}
|
|
740
|
+
const itemsToCompact = history.slice(0, cutoffIndex);
|
|
741
|
+
const recentItems = history.slice(cutoffIndex);
|
|
710
742
|
const summaryParts = ["### Summary of previous conversation context:"];
|
|
711
743
|
for (const item of itemsToCompact) {
|
|
712
744
|
if (item.type === "user_message") {
|
|
713
745
|
summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
|
|
714
746
|
} else if (item.type === "function_call") {
|
|
715
747
|
summaryParts.push(`- Executed tool: ${item.name}`);
|
|
748
|
+
} else if (item.type === "function_call_output") {
|
|
749
|
+
summaryParts.push(` Tool output: ${item.output.slice(0, 100)}`);
|
|
716
750
|
} else if (item.type === "agent_message") {
|
|
717
751
|
summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
|
|
718
752
|
}
|
|
@@ -1110,7 +1144,7 @@ class EphemeralWorkspaceManager {
|
|
|
1110
1144
|
}
|
|
1111
1145
|
var globalEphemeralWorkspace = new EphemeralWorkspaceManager;
|
|
1112
1146
|
// src/verification/verifier.ts
|
|
1113
|
-
import { spawnSync } from "child_process";
|
|
1147
|
+
import { spawn, spawnSync } from "child_process";
|
|
1114
1148
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1115
1149
|
import { join as join6 } from "path";
|
|
1116
1150
|
|
|
@@ -1487,36 +1521,120 @@ class AutoVerifier {
|
|
|
1487
1521
|
this.customCommand = options.customCommand;
|
|
1488
1522
|
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
1489
1523
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1524
|
+
findTargetedTests(modifiedFiles = []) {
|
|
1525
|
+
const matchedTests = new Set;
|
|
1526
|
+
for (const rawFile of modifiedFiles) {
|
|
1527
|
+
const normalized = rawFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1528
|
+
if (/\.(test|spec)\.[jt]sx?$/i.test(normalized) || /(^|\/)test_[^/]+\.py$/i.test(normalized) || /_test\.py$/i.test(normalized) || /_test\.go$/i.test(normalized)) {
|
|
1529
|
+
if (existsSync7(join6(this.cwd, normalized))) {
|
|
1530
|
+
matchedTests.add(normalized);
|
|
1531
|
+
}
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const extMatch = normalized.match(/\.[^.]+$/);
|
|
1535
|
+
if (!extMatch)
|
|
1536
|
+
continue;
|
|
1537
|
+
const withoutExt = normalized.slice(0, -extMatch[0].length);
|
|
1538
|
+
const parts = withoutExt.split("/");
|
|
1539
|
+
const baseName = parts[parts.length - 1];
|
|
1540
|
+
const subPath = normalized.startsWith("src/") ? normalized.slice(4, -extMatch[0].length) : normalized.startsWith("lib/") ? normalized.slice(4, -extMatch[0].length) : withoutExt;
|
|
1541
|
+
const candidatePaths = [
|
|
1542
|
+
`tests/${subPath}.test.ts`,
|
|
1543
|
+
`tests/${subPath}.test.js`,
|
|
1544
|
+
`tests/${subPath}.test.tsx`,
|
|
1545
|
+
`tests/${subPath}.spec.ts`,
|
|
1546
|
+
`tests/${subPath}.spec.js`,
|
|
1547
|
+
`tests/${baseName}.test.ts`,
|
|
1548
|
+
`tests/${baseName}.test.js`,
|
|
1549
|
+
`tests/${baseName}.spec.ts`,
|
|
1550
|
+
`test/${subPath}.test.ts`,
|
|
1551
|
+
`test/${subPath}.test.js`,
|
|
1552
|
+
`test/${baseName}.test.ts`,
|
|
1553
|
+
`test/${baseName}.test.js`,
|
|
1554
|
+
`src/${subPath}.test.ts`,
|
|
1555
|
+
`src/${subPath}.spec.ts`,
|
|
1556
|
+
`${withoutExt}.test.ts`,
|
|
1557
|
+
`${withoutExt}.spec.ts`,
|
|
1558
|
+
`tests/test_${baseName}.py`,
|
|
1559
|
+
`test_${baseName}.py`,
|
|
1560
|
+
`tests/${baseName}_test.go`
|
|
1561
|
+
];
|
|
1562
|
+
for (const candidate of candidatePaths) {
|
|
1563
|
+
if (existsSync7(join6(this.cwd, candidate))) {
|
|
1564
|
+
matchedTests.add(candidate);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return Array.from(matchedTests);
|
|
1569
|
+
}
|
|
1570
|
+
resolveTargetedTestCommand(testFiles) {
|
|
1571
|
+
if (testFiles.length === 0)
|
|
1572
|
+
return null;
|
|
1573
|
+
const quotedFiles = testFiles.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ");
|
|
1574
|
+
if (existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock"))) {
|
|
1575
|
+
return `bun test ${quotedFiles}`;
|
|
1576
|
+
}
|
|
1577
|
+
const pkgPath = join6(this.cwd, "package.json");
|
|
1578
|
+
if (existsSync7(pkgPath)) {
|
|
1579
|
+
try {
|
|
1580
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1581
|
+
const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1582
|
+
if (allDeps.vitest) {
|
|
1583
|
+
return `npx vitest run ${quotedFiles}`;
|
|
1584
|
+
}
|
|
1585
|
+
if (allDeps.jest) {
|
|
1586
|
+
return `npx jest ${quotedFiles}`;
|
|
1587
|
+
}
|
|
1588
|
+
const pm = existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
1589
|
+
if (pkg.scripts?.test) {
|
|
1590
|
+
if (pkg.scripts.test.includes("bun test")) {
|
|
1591
|
+
return `bun test ${quotedFiles}`;
|
|
1592
|
+
}
|
|
1593
|
+
return `${pm} test -- ${quotedFiles}`;
|
|
1594
|
+
}
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for test runner:`, err);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (existsSync7(join6(this.cwd, "pyproject.toml")) || existsSync7(join6(this.cwd, "requirements.txt"))) {
|
|
1600
|
+
if (existsSync7(join6(this.cwd, "uv.lock"))) {
|
|
1601
|
+
return `uv run pytest ${quotedFiles}`;
|
|
1602
|
+
}
|
|
1603
|
+
return `pytest ${quotedFiles}`;
|
|
1493
1604
|
}
|
|
1605
|
+
if (existsSync7(join6(this.cwd, "go.mod"))) {
|
|
1606
|
+
return `go test ${quotedFiles}`;
|
|
1607
|
+
}
|
|
1608
|
+
if (existsSync7(join6(this.cwd, "Cargo.toml"))) {
|
|
1609
|
+
return `cargo test ${quotedFiles}`;
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
resolveStaticCommand() {
|
|
1494
1614
|
try {
|
|
1495
1615
|
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
1496
1616
|
const analysis = analyzer.analyze();
|
|
1497
1617
|
if (analysis.commands.typecheck) {
|
|
1498
1618
|
return analysis.commands.typecheck;
|
|
1499
1619
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (analysis.commands.test) {
|
|
1504
|
-
return analysis.commands.test;
|
|
1505
|
-
}
|
|
1506
|
-
} catch {}
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
|
|
1622
|
+
}
|
|
1507
1623
|
const pkgPath = join6(this.cwd, "package.json");
|
|
1508
1624
|
if (existsSync7(pkgPath)) {
|
|
1509
1625
|
try {
|
|
1510
1626
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1511
1627
|
if (pkg.scripts) {
|
|
1628
|
+
const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
1629
|
+
const runCmd = pm === "bun" || pm === "pnpm" || pm === "yarn" ? `${pm} run` : "npm run";
|
|
1512
1630
|
if (pkg.scripts.typecheck)
|
|
1513
|
-
return
|
|
1631
|
+
return `${runCmd} typecheck`;
|
|
1514
1632
|
if (pkg.scripts.check)
|
|
1515
|
-
return
|
|
1516
|
-
if (pkg.scripts.test)
|
|
1517
|
-
return "npm test";
|
|
1633
|
+
return `${runCmd} check`;
|
|
1518
1634
|
}
|
|
1519
|
-
} catch {
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
|
|
1637
|
+
}
|
|
1520
1638
|
}
|
|
1521
1639
|
if (existsSync7(join6(this.cwd, "tsconfig.json"))) {
|
|
1522
1640
|
return "npx tsc --noEmit";
|
|
@@ -1527,17 +1645,76 @@ class AutoVerifier {
|
|
|
1527
1645
|
if (existsSync7(join6(this.cwd, "go.mod"))) {
|
|
1528
1646
|
return "go vet ./...";
|
|
1529
1647
|
}
|
|
1530
|
-
if (existsSync7(join6(this.cwd, "
|
|
1531
|
-
|
|
1532
|
-
return "mypy .";
|
|
1533
|
-
}
|
|
1648
|
+
if (existsSync7(join6(this.cwd, "mypy.ini")) || existsSync7(join6(this.cwd, ".mypy.ini"))) {
|
|
1649
|
+
return "mypy .";
|
|
1534
1650
|
}
|
|
1535
1651
|
return null;
|
|
1536
1652
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1653
|
+
resolveVerificationCommand(modifiedFiles = []) {
|
|
1654
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
1655
|
+
return this.customCommand.trim();
|
|
1656
|
+
}
|
|
1657
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
1658
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
1659
|
+
const staticCmd = this.resolveStaticCommand();
|
|
1660
|
+
if (targetedTestCmd && staticCmd) {
|
|
1661
|
+
return `${staticCmd} && ${targetedTestCmd}`;
|
|
1662
|
+
}
|
|
1663
|
+
if (targetedTestCmd) {
|
|
1664
|
+
return targetedTestCmd;
|
|
1665
|
+
}
|
|
1666
|
+
if (staticCmd) {
|
|
1667
|
+
return staticCmd;
|
|
1668
|
+
}
|
|
1669
|
+
try {
|
|
1670
|
+
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
1671
|
+
const analysis = analyzer.analyze();
|
|
1672
|
+
if (analysis.commands.test)
|
|
1673
|
+
return analysis.commands.test;
|
|
1674
|
+
if (analysis.commands.lint)
|
|
1675
|
+
return analysis.commands.lint;
|
|
1676
|
+
} catch {}
|
|
1677
|
+
const pkgPath = join6(this.cwd, "package.json");
|
|
1678
|
+
if (existsSync7(pkgPath)) {
|
|
1679
|
+
try {
|
|
1680
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1681
|
+
if (pkg.scripts?.test) {
|
|
1682
|
+
const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : "npm";
|
|
1683
|
+
return pm === "bun" ? "bun test" : "npm test";
|
|
1684
|
+
}
|
|
1685
|
+
if (pkg.scripts?.lint) {
|
|
1686
|
+
return "npm run lint";
|
|
1687
|
+
}
|
|
1688
|
+
} catch {}
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
resolveVerificationStages(modifiedFiles = []) {
|
|
1693
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
1694
|
+
return [this.customCommand.trim()];
|
|
1695
|
+
}
|
|
1696
|
+
const stages = [];
|
|
1697
|
+
const staticCmd = this.resolveStaticCommand();
|
|
1698
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
1699
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
1700
|
+
if (staticCmd) {
|
|
1701
|
+
stages.push(staticCmd);
|
|
1702
|
+
}
|
|
1703
|
+
if (targetedTestCmd) {
|
|
1704
|
+
stages.push(targetedTestCmd);
|
|
1705
|
+
}
|
|
1706
|
+
if (stages.length === 0) {
|
|
1707
|
+
const fallback = this.resolveVerificationCommand(modifiedFiles);
|
|
1708
|
+
if (fallback) {
|
|
1709
|
+
stages.push(fallback);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return stages;
|
|
1713
|
+
}
|
|
1714
|
+
async verify(modifiedFiles = [], signal) {
|
|
1715
|
+
const stages = this.resolveVerificationStages(modifiedFiles);
|
|
1539
1716
|
const startTime = performance.now();
|
|
1540
|
-
if (
|
|
1717
|
+
if (stages.length === 0) {
|
|
1541
1718
|
return {
|
|
1542
1719
|
command: "none",
|
|
1543
1720
|
success: true,
|
|
@@ -1547,59 +1724,158 @@ class AutoVerifier {
|
|
|
1547
1724
|
reason: "NO_VERIFIER_DETECTED"
|
|
1548
1725
|
};
|
|
1549
1726
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1727
|
+
const combinedOutputs = [];
|
|
1728
|
+
let executedCommands = [];
|
|
1729
|
+
for (const cmd of stages) {
|
|
1730
|
+
if (signal?.aborted) {
|
|
1731
|
+
return {
|
|
1732
|
+
command: cmd,
|
|
1733
|
+
success: false,
|
|
1734
|
+
exitCode: 1,
|
|
1735
|
+
output: "Verification was aborted.",
|
|
1736
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
1737
|
+
reason: "ABORTED"
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
executedCommands.push(cmd);
|
|
1741
|
+
const stageResult = await this.executeCommandAsync(cmd, signal);
|
|
1742
|
+
combinedOutputs.push(`[${cmd}]
|
|
1743
|
+
${stageResult.output}`);
|
|
1744
|
+
if (!stageResult.success) {
|
|
1745
|
+
return {
|
|
1746
|
+
command: cmd,
|
|
1747
|
+
success: false,
|
|
1748
|
+
exitCode: stageResult.exitCode,
|
|
1749
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
1750
|
+
|
|
1751
|
+
`)),
|
|
1752
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return {
|
|
1757
|
+
command: executedCommands.join(" && "),
|
|
1758
|
+
success: true,
|
|
1759
|
+
exitCode: 0,
|
|
1760
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
1761
|
+
|
|
1762
|
+
`)),
|
|
1763
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
executeCommandAsync(command, signal) {
|
|
1767
|
+
return new Promise((resolve) => {
|
|
1768
|
+
let proc = null;
|
|
1769
|
+
let stdoutData = "";
|
|
1770
|
+
let stderrData = "";
|
|
1771
|
+
let isSettled = false;
|
|
1772
|
+
const finish = (success, exitCode, output) => {
|
|
1773
|
+
if (isSettled)
|
|
1774
|
+
return;
|
|
1775
|
+
isSettled = true;
|
|
1776
|
+
cleanup();
|
|
1777
|
+
resolve({ success, exitCode, output: output.trim() });
|
|
1778
|
+
};
|
|
1779
|
+
const killProcessTree = () => {
|
|
1780
|
+
if (!proc || !proc.pid)
|
|
1781
|
+
return;
|
|
1782
|
+
try {
|
|
1783
|
+
if (process.platform === "win32") {
|
|
1784
|
+
spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
|
|
1785
|
+
} else {
|
|
1786
|
+
proc.kill("SIGTERM");
|
|
1787
|
+
setTimeout(() => {
|
|
1788
|
+
try {
|
|
1789
|
+
proc?.kill("SIGKILL");
|
|
1790
|
+
} catch {}
|
|
1791
|
+
}, 500);
|
|
1792
|
+
}
|
|
1793
|
+
} catch {}
|
|
1794
|
+
};
|
|
1795
|
+
const onAbort = () => {
|
|
1796
|
+
killProcessTree();
|
|
1797
|
+
finish(false, 1, "Verification command aborted by user or session signal.");
|
|
1798
|
+
};
|
|
1799
|
+
if (signal?.aborted) {
|
|
1800
|
+
return finish(false, 1, "Verification aborted before launch.");
|
|
1801
|
+
}
|
|
1802
|
+
if (signal) {
|
|
1803
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1804
|
+
}
|
|
1805
|
+
const timer = setTimeout(() => {
|
|
1806
|
+
killProcessTree();
|
|
1807
|
+
const partial = (stdoutData + `
|
|
1808
|
+
` + stderrData).trim();
|
|
1809
|
+
const timeoutMsg = `[Verification Timeout Error]: Verification command '${command}' exceeded timeout limit of ${this.timeoutMs}ms and was terminated.`;
|
|
1810
|
+
finish(false, 1, partial ? `${partial}
|
|
1811
|
+
|
|
1812
|
+
${timeoutMsg}` : timeoutMsg);
|
|
1813
|
+
}, this.timeoutMs);
|
|
1814
|
+
const cleanup = () => {
|
|
1815
|
+
clearTimeout(timer);
|
|
1816
|
+
if (signal) {
|
|
1817
|
+
signal.removeEventListener("abort", onAbort);
|
|
1562
1818
|
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1819
|
+
};
|
|
1820
|
+
try {
|
|
1821
|
+
proc = spawn(command, {
|
|
1822
|
+
cwd: this.cwd,
|
|
1823
|
+
shell: true,
|
|
1824
|
+
env: {
|
|
1825
|
+
...process.env,
|
|
1826
|
+
CI: "true",
|
|
1827
|
+
FORCE_COLOR: "0"
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1831
|
+
stdoutData += chunk.toString();
|
|
1832
|
+
});
|
|
1833
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1834
|
+
stderrData += chunk.toString();
|
|
1835
|
+
});
|
|
1836
|
+
proc.on("error", (err) => {
|
|
1837
|
+
const partial = (stdoutData + `
|
|
1838
|
+
` + stderrData).trim();
|
|
1839
|
+
const spawnMsg = `[Verification Process Spawn Error]: Failed to spawn command '${command}': ${err.message}`;
|
|
1840
|
+
finish(false, 1, partial ? `${partial}
|
|
1841
|
+
|
|
1842
|
+
${spawnMsg}` : spawnMsg);
|
|
1843
|
+
});
|
|
1844
|
+
proc.on("close", (code) => {
|
|
1845
|
+
const exitCode = code ?? 0;
|
|
1846
|
+
const combined = (stdoutData + `
|
|
1847
|
+
` + stderrData).trim();
|
|
1848
|
+
if (exitCode === 0) {
|
|
1849
|
+
finish(true, 0, combined || "Verification passed cleanly.");
|
|
1850
|
+
} else {
|
|
1851
|
+
const errorFallback = `[Verification Failure]: Command '${command}' exited with code ${exitCode} and produced no output.`;
|
|
1852
|
+
finish(false, exitCode, combined ? `${combined}
|
|
1853
|
+
|
|
1854
|
+
[Process exited with non-zero code ${exitCode}]` : errorFallback);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
finish(false, 1, `[Verification Execution Exception]: ${err.message || String(err)}`);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
truncateOutput(output) {
|
|
1863
|
+
if (output.length > 4000) {
|
|
1864
|
+
const lines = output.split(`
|
|
1571
1865
|
`);
|
|
1572
|
-
|
|
1573
|
-
|
|
1866
|
+
if (lines.length > 70) {
|
|
1867
|
+
const head = lines.slice(0, 35).join(`
|
|
1574
1868
|
`);
|
|
1575
|
-
|
|
1869
|
+
const tail = lines.slice(-30).join(`
|
|
1576
1870
|
`);
|
|
1577
|
-
|
|
1871
|
+
return `${head}
|
|
1578
1872
|
|
|
1579
|
-
... [${lines.length -
|
|
1873
|
+
... [${lines.length - 65} lines truncated for context efficiency] ...
|
|
1580
1874
|
|
|
1581
1875
|
${tail}`;
|
|
1582
|
-
}
|
|
1583
1876
|
}
|
|
1584
|
-
const exitCode = proc.status ?? (proc.error ? 1 : 0);
|
|
1585
|
-
const success = exitCode === 0;
|
|
1586
|
-
return {
|
|
1587
|
-
command,
|
|
1588
|
-
success,
|
|
1589
|
-
exitCode,
|
|
1590
|
-
output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
|
|
1591
|
-
durationMs
|
|
1592
|
-
};
|
|
1593
|
-
} catch (err) {
|
|
1594
|
-
const durationMs = Math.round(performance.now() - startTime);
|
|
1595
|
-
return {
|
|
1596
|
-
command,
|
|
1597
|
-
success: false,
|
|
1598
|
-
exitCode: 1,
|
|
1599
|
-
output: `Verification execution error: ${err.message || String(err)}`,
|
|
1600
|
-
durationMs
|
|
1601
|
-
};
|
|
1602
1877
|
}
|
|
1878
|
+
return output;
|
|
1603
1879
|
}
|
|
1604
1880
|
}
|
|
1605
1881
|
// src/session/turn.ts
|
|
@@ -1723,6 +1999,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1723
1999
|
});
|
|
1724
2000
|
}
|
|
1725
2001
|
if (toolCallRequests.length > 0) {
|
|
2002
|
+
const validCalls = [];
|
|
1726
2003
|
for (const toolCall of toolCallRequests) {
|
|
1727
2004
|
if (!toolCall.name || !toolCall.name.trim())
|
|
1728
2005
|
continue;
|
|
@@ -1746,6 +2023,14 @@ async function runTurn(session, turnContext, input) {
|
|
|
1746
2023
|
toolName: toolCall.name,
|
|
1747
2024
|
arguments: toolCall.arguments
|
|
1748
2025
|
});
|
|
2026
|
+
validCalls.push({
|
|
2027
|
+
callId: toolCall.callId,
|
|
2028
|
+
name: toolCall.name,
|
|
2029
|
+
arguments: toolCall.arguments,
|
|
2030
|
+
functionCallItem
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
for (const toolCall of validCalls) {
|
|
1749
2034
|
const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
|
|
1750
2035
|
cwd: turnContext.environment.cwd,
|
|
1751
2036
|
turnId,
|
|
@@ -1753,6 +2038,12 @@ async function runTurn(session, turnContext, input) {
|
|
|
1753
2038
|
execPolicy: session.execPolicy,
|
|
1754
2039
|
mode: session.collaborationMode,
|
|
1755
2040
|
permissionMode: session.permissionMode,
|
|
2041
|
+
onFileModified: (p) => {
|
|
2042
|
+
if (p) {
|
|
2043
|
+
modifiedFiles.add(p);
|
|
2044
|
+
hasRunVerification = false;
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
1756
2047
|
onPlanUpdate: (plan, explanation) => {
|
|
1757
2048
|
session.emitEvent({
|
|
1758
2049
|
type: "PlanUpdated",
|
|
@@ -1768,7 +2059,8 @@ async function runTurn(session, turnContext, input) {
|
|
|
1768
2059
|
turnId,
|
|
1769
2060
|
toolName: toolCall.name,
|
|
1770
2061
|
description,
|
|
1771
|
-
command
|
|
2062
|
+
command,
|
|
2063
|
+
prefixRule
|
|
1772
2064
|
});
|
|
1773
2065
|
},
|
|
1774
2066
|
requestInput: async (question, options) => {
|
|
@@ -1819,7 +2111,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1819
2111
|
cwd,
|
|
1820
2112
|
customCommand: session.autoVerificationCommand
|
|
1821
2113
|
});
|
|
1822
|
-
const command = verifier.resolveVerificationCommand();
|
|
2114
|
+
const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
|
|
1823
2115
|
if (command) {
|
|
1824
2116
|
session.emitEvent({
|
|
1825
2117
|
type: "VerificationStarted",
|
|
@@ -1827,7 +2119,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1827
2119
|
command,
|
|
1828
2120
|
modifiedFiles: Array.from(modifiedFiles)
|
|
1829
2121
|
});
|
|
1830
|
-
const vResult = verifier.verify(Array.from(modifiedFiles));
|
|
2122
|
+
const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
|
|
1831
2123
|
session.emitEvent({
|
|
1832
2124
|
type: "VerificationCompleted",
|
|
1833
2125
|
turnId,
|
|
@@ -1990,8 +2282,133 @@ async function submissionLoop(session, queue) {
|
|
|
1990
2282
|
}
|
|
1991
2283
|
}
|
|
1992
2284
|
|
|
2285
|
+
// src/security/shell-parser.ts
|
|
2286
|
+
function parseShellCommand(commandLine) {
|
|
2287
|
+
const commands = [];
|
|
2288
|
+
const subshellCommands = [];
|
|
2289
|
+
let hasPipes = false;
|
|
2290
|
+
let currentSegment = "";
|
|
2291
|
+
let inSingleQuote = false;
|
|
2292
|
+
let inDoubleQuote = false;
|
|
2293
|
+
let isEscaped = false;
|
|
2294
|
+
const len = commandLine.length;
|
|
2295
|
+
for (let i = 0;i < len; i++) {
|
|
2296
|
+
const char = commandLine[i];
|
|
2297
|
+
if (isEscaped) {
|
|
2298
|
+
currentSegment += char;
|
|
2299
|
+
isEscaped = false;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (char === "\\") {
|
|
2303
|
+
isEscaped = true;
|
|
2304
|
+
currentSegment += char;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (char === "'" && !inDoubleQuote) {
|
|
2308
|
+
inSingleQuote = !inSingleQuote;
|
|
2309
|
+
currentSegment += char;
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (char === '"' && !inSingleQuote) {
|
|
2313
|
+
inDoubleQuote = !inDoubleQuote;
|
|
2314
|
+
currentSegment += char;
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
if (inSingleQuote) {
|
|
2318
|
+
currentSegment += char;
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (char === "`") {
|
|
2322
|
+
let endIdx = -1;
|
|
2323
|
+
for (let j = i + 1;j < len; j++) {
|
|
2324
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
2325
|
+
j++;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (commandLine[j] === "`") {
|
|
2329
|
+
endIdx = j;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (endIdx !== -1) {
|
|
2334
|
+
const innerCmd = commandLine.slice(i + 1, endIdx);
|
|
2335
|
+
if (innerCmd.trim()) {
|
|
2336
|
+
subshellCommands.push(innerCmd.trim());
|
|
2337
|
+
}
|
|
2338
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
2339
|
+
i = endIdx;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
if (char === "$" && i + 1 < len && commandLine[i + 1] === "(") {
|
|
2344
|
+
let depth = 1;
|
|
2345
|
+
let endIdx = -1;
|
|
2346
|
+
for (let j = i + 2;j < len; j++) {
|
|
2347
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
2348
|
+
j++;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (commandLine[j] === "(")
|
|
2352
|
+
depth++;
|
|
2353
|
+
else if (commandLine[j] === ")") {
|
|
2354
|
+
depth--;
|
|
2355
|
+
if (depth === 0) {
|
|
2356
|
+
endIdx = j;
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (endIdx !== -1) {
|
|
2362
|
+
const innerCmd = commandLine.slice(i + 2, endIdx);
|
|
2363
|
+
if (innerCmd.trim()) {
|
|
2364
|
+
subshellCommands.push(innerCmd.trim());
|
|
2365
|
+
}
|
|
2366
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
2367
|
+
i = endIdx;
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!inDoubleQuote) {
|
|
2372
|
+
if (char === "&" && commandLine[i + 1] === "&" || char === "|" && commandLine[i + 1] === "|") {
|
|
2373
|
+
if (currentSegment.trim()) {
|
|
2374
|
+
commands.push(currentSegment.trim());
|
|
2375
|
+
}
|
|
2376
|
+
currentSegment = "";
|
|
2377
|
+
i++;
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
if (char === "|") {
|
|
2381
|
+
hasPipes = true;
|
|
2382
|
+
if (currentSegment.trim()) {
|
|
2383
|
+
commands.push(currentSegment.trim());
|
|
2384
|
+
}
|
|
2385
|
+
currentSegment = "";
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (char === ";" || char === `
|
|
2389
|
+
` || char === "&") {
|
|
2390
|
+
if (currentSegment.trim()) {
|
|
2391
|
+
commands.push(currentSegment.trim());
|
|
2392
|
+
}
|
|
2393
|
+
currentSegment = "";
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
currentSegment += char;
|
|
2398
|
+
}
|
|
2399
|
+
if (currentSegment.trim()) {
|
|
2400
|
+
commands.push(currentSegment.trim());
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
commands: commands.filter(Boolean),
|
|
2404
|
+
subshellCommands: subshellCommands.filter(Boolean),
|
|
2405
|
+
hasPipes
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
|
|
1993
2409
|
// src/security/exec-policy.ts
|
|
1994
2410
|
class ExecPolicy {
|
|
2411
|
+
denyRules = [];
|
|
1995
2412
|
rules = [];
|
|
1996
2413
|
mode = "auto";
|
|
1997
2414
|
constructor(initialMode = "auto") {
|
|
@@ -2005,12 +2422,24 @@ class ExecPolicy {
|
|
|
2005
2422
|
this.mode = mode;
|
|
2006
2423
|
}
|
|
2007
2424
|
initDefaultRules() {
|
|
2008
|
-
this.
|
|
2009
|
-
this.
|
|
2010
|
-
this.
|
|
2011
|
-
this.addRule(/^(
|
|
2012
|
-
this.addRule(/^(
|
|
2013
|
-
this.addRule(/^(
|
|
2425
|
+
this.addDenyRule(/(?:^|[/\\])(mkfs|format|fdisk|parted)\b/i, "Destructive filesystem formatting operation");
|
|
2426
|
+
this.addDenyRule(/^dd\s+.*(of=\/dev\/|\/dev\/sd|\/dev\/nvme)/i, "Raw block device write attempt");
|
|
2427
|
+
this.addDenyRule(/(?:^|[/\\])(reboot|shutdown|poweroff|init\s+0)\b/i, "System power manipulation");
|
|
2428
|
+
this.addRule(/^(sudo|su|doas|runas)\b/i, "prompt", "Privilege escalation attempt");
|
|
2429
|
+
this.addRule(/^(rm|del|rmdir|shred|unlink)\b/i, "prompt", "Destructive file removal");
|
|
2430
|
+
this.addRule(/^(chmod|chown|kill|pkill|killall|systemctl|service|crontab)\b/i, "prompt", "System administration & process control");
|
|
2431
|
+
this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase|branch\s+-D|checkout\s+-f))\b/i, "prompt", "Destructive git operation");
|
|
2432
|
+
this.addRule(/^(curl|wget|fetch|ssh|scp|sftp|ftp|rsync|nc|ncat|netcat|socat|telnet)\b/i, "prompt", "Network & remote transfer");
|
|
2433
|
+
this.addRule(/^(bash|sh|zsh|dash|ksh|cmd(\.exe)?|powershell(\.exe)?|pwsh)\s+(-c|-command|\/c)\b/i, "prompt", "Arbitrary subshell command execution");
|
|
2434
|
+
this.addRule(/^(python|python3|node|bun|perl|ruby)\s+(-c|-e)\b/i, "prompt", "Inline arbitrary code evaluation");
|
|
2435
|
+
this.addRule(/^(eval|exec)\b/i, "prompt", "Dynamic code execution");
|
|
2436
|
+
this.addRule(/^(npm\s+publish|bun\s+publish|cargo\s+publish)\b/i, "prompt", "Package registry publication");
|
|
2437
|
+
this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote|describe))\b/i, "allow", "Safe git query");
|
|
2438
|
+
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");
|
|
2439
|
+
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");
|
|
2440
|
+
}
|
|
2441
|
+
addDenyRule(pattern, description) {
|
|
2442
|
+
this.denyRules.push({ pattern, decision: "deny", description });
|
|
2014
2443
|
}
|
|
2015
2444
|
addRule(pattern, decision, description) {
|
|
2016
2445
|
this.rules.unshift({ pattern, decision, description });
|
|
@@ -2033,8 +2462,49 @@ class ExecPolicy {
|
|
|
2033
2462
|
}
|
|
2034
2463
|
evaluate(command) {
|
|
2035
2464
|
const trimmed = command.trim();
|
|
2465
|
+
if (!trimmed) {
|
|
2466
|
+
return { decision: "allow" };
|
|
2467
|
+
}
|
|
2468
|
+
const parsed = parseShellCommand(trimmed);
|
|
2469
|
+
const subCommands = [...parsed.commands, ...parsed.subshellCommands].map((c) => c.trim()).filter(Boolean);
|
|
2470
|
+
if (subCommands.length > 1 || parsed.subshellCommands.length > 0) {
|
|
2471
|
+
for (const subCmd of subCommands) {
|
|
2472
|
+
const subResult = this.evaluateSingle(subCmd);
|
|
2473
|
+
if (subResult.decision === "deny") {
|
|
2474
|
+
return {
|
|
2475
|
+
decision: "deny",
|
|
2476
|
+
reason: `Chained command contains denied operation: '${subCmd}' (${subResult.reason || "Forbidden"})`
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
if (subResult.decision === "prompt") {
|
|
2480
|
+
return {
|
|
2481
|
+
decision: "prompt",
|
|
2482
|
+
reason: `Chained command contains operation requiring confirmation: '${subCmd}' (${subResult.reason || "Requires approval"})`
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (parsed.hasPipes && /\|\s*(ba|z|k|c)?sh\b/i.test(trimmed)) {
|
|
2487
|
+
return {
|
|
2488
|
+
decision: "prompt",
|
|
2489
|
+
reason: "Pipeline executes piped input directly into shell interpreter (| sh)"
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
return { decision: "allow", reason: "All chained sub-commands are permitted" };
|
|
2493
|
+
}
|
|
2494
|
+
return this.evaluateSingle(trimmed);
|
|
2495
|
+
}
|
|
2496
|
+
evaluateSingle(command) {
|
|
2497
|
+
const trimmed = command.trim();
|
|
2498
|
+
for (const rule of this.denyRules) {
|
|
2499
|
+
if (rule.pattern.test(trimmed)) {
|
|
2500
|
+
return {
|
|
2501
|
+
decision: "deny",
|
|
2502
|
+
reason: rule.description
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2036
2506
|
if (this.mode === "plan") {
|
|
2037
|
-
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);
|
|
2507
|
+
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);
|
|
2038
2508
|
if (isReadOnly) {
|
|
2039
2509
|
return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
|
|
2040
2510
|
}
|
|
@@ -2050,7 +2520,7 @@ class ExecPolicy {
|
|
|
2050
2520
|
};
|
|
2051
2521
|
}
|
|
2052
2522
|
if (this.mode === "accept-edits") {
|
|
2053
|
-
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);
|
|
2523
|
+
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);
|
|
2054
2524
|
if (isReadOnly) {
|
|
2055
2525
|
return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
|
|
2056
2526
|
}
|
|
@@ -2067,11 +2537,21 @@ class ExecPolicy {
|
|
|
2067
2537
|
};
|
|
2068
2538
|
}
|
|
2069
2539
|
}
|
|
2540
|
+
if (this.isRecognizedSafeDevCommand(trimmed)) {
|
|
2541
|
+
return {
|
|
2542
|
+
decision: "allow",
|
|
2543
|
+
reason: "Safe development workspace command"
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
const firstWord = trimmed.split(/\s+/)[0] || trimmed;
|
|
2070
2547
|
return {
|
|
2071
|
-
decision: "
|
|
2072
|
-
reason:
|
|
2548
|
+
decision: "prompt",
|
|
2549
|
+
reason: `Command '${firstWord}' is unclassified and requires confirmation in auto mode`
|
|
2073
2550
|
};
|
|
2074
2551
|
}
|
|
2552
|
+
isRecognizedSafeDevCommand(command) {
|
|
2553
|
+
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);
|
|
2554
|
+
}
|
|
2075
2555
|
}
|
|
2076
2556
|
|
|
2077
2557
|
// src/session/session.ts
|
|
@@ -2204,7 +2684,8 @@ class Session {
|
|
|
2204
2684
|
turnId: params.turnId,
|
|
2205
2685
|
toolName: params.toolName,
|
|
2206
2686
|
description: params.description,
|
|
2207
|
-
command: params.command
|
|
2687
|
+
command: params.command,
|
|
2688
|
+
prefixRule: params.prefixRule
|
|
2208
2689
|
});
|
|
2209
2690
|
this.emitEvent({
|
|
2210
2691
|
type: "StatusChanged",
|
|
@@ -2382,6 +2863,7 @@ var applyPatchTool = {
|
|
|
2382
2863
|
try {
|
|
2383
2864
|
mkdirSync4(dirname3(filePath), { recursive: true });
|
|
2384
2865
|
writeFileSync2(filePath, replacementContent, "utf8");
|
|
2866
|
+
ctx.onFileModified?.(rawPath);
|
|
2385
2867
|
return { output: `Successfully created new file '${rawPath}'` };
|
|
2386
2868
|
} catch (err) {
|
|
2387
2869
|
return {
|
|
@@ -2402,7 +2884,31 @@ var applyPatchTool = {
|
|
|
2402
2884
|
isError: true
|
|
2403
2885
|
};
|
|
2404
2886
|
}
|
|
2405
|
-
|
|
2887
|
+
let targetToFind = targetContent;
|
|
2888
|
+
let replacementToUse = replacementContent;
|
|
2889
|
+
const fileHasCrlf = originalFileContent.includes(`\r
|
|
2890
|
+
`);
|
|
2891
|
+
let firstIndex = originalFileContent.indexOf(targetToFind);
|
|
2892
|
+
if (firstIndex === -1 && fileHasCrlf) {
|
|
2893
|
+
const crlfTarget = targetContent.replace(/\r?\n/g, `\r
|
|
2894
|
+
`);
|
|
2895
|
+
firstIndex = originalFileContent.indexOf(crlfTarget);
|
|
2896
|
+
if (firstIndex !== -1) {
|
|
2897
|
+
targetToFind = crlfTarget;
|
|
2898
|
+
replacementToUse = replacementContent.replace(/\r?\n/g, `\r
|
|
2899
|
+
`);
|
|
2900
|
+
}
|
|
2901
|
+
} else if (firstIndex === -1 && !fileHasCrlf && targetContent.includes(`\r
|
|
2902
|
+
`)) {
|
|
2903
|
+
const lfTarget = targetContent.replace(/\r\n/g, `
|
|
2904
|
+
`);
|
|
2905
|
+
firstIndex = originalFileContent.indexOf(lfTarget);
|
|
2906
|
+
if (firstIndex !== -1) {
|
|
2907
|
+
targetToFind = lfTarget;
|
|
2908
|
+
replacementToUse = replacementContent.replace(/\r\n/g, `
|
|
2909
|
+
`);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2406
2912
|
if (firstIndex === -1) {
|
|
2407
2913
|
return {
|
|
2408
2914
|
output: `Error: targetContent was not found in '${rawPath}'.
|
|
@@ -2413,7 +2919,7 @@ var applyPatchTool = {
|
|
|
2413
2919
|
isError: true
|
|
2414
2920
|
};
|
|
2415
2921
|
}
|
|
2416
|
-
const secondIndex = originalFileContent.indexOf(
|
|
2922
|
+
const secondIndex = originalFileContent.indexOf(targetToFind, firstIndex + 1);
|
|
2417
2923
|
if (secondIndex !== -1) {
|
|
2418
2924
|
return {
|
|
2419
2925
|
output: `Error: targetContent matched multiple locations in '${rawPath}'.
|
|
@@ -2424,8 +2930,9 @@ var applyPatchTool = {
|
|
|
2424
2930
|
isError: true
|
|
2425
2931
|
};
|
|
2426
2932
|
}
|
|
2427
|
-
const newFileContent = originalFileContent.slice(0, firstIndex) +
|
|
2933
|
+
const newFileContent = originalFileContent.slice(0, firstIndex) + replacementToUse + originalFileContent.slice(firstIndex + targetToFind.length);
|
|
2428
2934
|
writeFileSync2(filePath, newFileContent, "utf8");
|
|
2935
|
+
ctx.onFileModified?.(rawPath);
|
|
2429
2936
|
return {
|
|
2430
2937
|
output: `Successfully applied patch to '${rawPath}'`
|
|
2431
2938
|
};
|
|
@@ -2537,8 +3044,12 @@ class LinuxSandbox {
|
|
|
2537
3044
|
}
|
|
2538
3045
|
this.hasBwrap = existsSync9("/usr/bin/bwrap") || existsSync9("/bin/bwrap") || existsSync9("/usr/local/bin/bwrap");
|
|
2539
3046
|
}
|
|
2540
|
-
wrapCommand(cmd, profile) {
|
|
2541
|
-
if (
|
|
3047
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
3048
|
+
if (profile.kind === "danger-unrestricted") {
|
|
3049
|
+
return cmd;
|
|
3050
|
+
}
|
|
3051
|
+
if (!this.hasBwrap) {
|
|
3052
|
+
onWarning?.("Linux kernel sandbox (Bubblewrap / bwrap) is not available. Command will execute without OS-level namespace isolation.");
|
|
2542
3053
|
return cmd;
|
|
2543
3054
|
}
|
|
2544
3055
|
const bwrapArgs = [
|
|
@@ -2554,6 +3065,23 @@ class LinuxSandbox {
|
|
|
2554
3065
|
"--tmpfs",
|
|
2555
3066
|
"/tmp"
|
|
2556
3067
|
];
|
|
3068
|
+
const homeDir = process.env.HOME || "/root";
|
|
3069
|
+
const sensitivePaths = [
|
|
3070
|
+
`${homeDir}/.ssh`,
|
|
3071
|
+
`${homeDir}/.aws`,
|
|
3072
|
+
`${homeDir}/.gnupg`,
|
|
3073
|
+
`${homeDir}/.azure`,
|
|
3074
|
+
`${homeDir}/.kube`,
|
|
3075
|
+
`${homeDir}/.docker`,
|
|
3076
|
+
`${homeDir}/.netrc`,
|
|
3077
|
+
"/etc/shadow",
|
|
3078
|
+
"/etc/sudoers"
|
|
3079
|
+
];
|
|
3080
|
+
for (const sensitive of sensitivePaths) {
|
|
3081
|
+
if (existsSync9(sensitive)) {
|
|
3082
|
+
bwrapArgs.push("--tmpfs", sensitive);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
2557
3085
|
for (const writableRoot of profile.writableRoots) {
|
|
2558
3086
|
bwrapArgs.push("--bind", writableRoot, writableRoot);
|
|
2559
3087
|
}
|
|
@@ -2603,11 +3131,21 @@ class MacOSSandbox {
|
|
|
2603
3131
|
} else {
|
|
2604
3132
|
rules.push("(deny network*)");
|
|
2605
3133
|
}
|
|
3134
|
+
const home = process.env.HOME || "/Users/Shared";
|
|
3135
|
+
rules.push(`(deny file-read* (subpath "${home}/.ssh"))`);
|
|
3136
|
+
rules.push(`(deny file-read* (subpath "${home}/.aws"))`);
|
|
3137
|
+
rules.push(`(deny file-read* (subpath "${home}/.gnupg"))`);
|
|
3138
|
+
rules.push(`(deny file-read* (subpath "${home}/.kube"))`);
|
|
3139
|
+
rules.push(`(deny file-read* (subpath "${home}/.docker"))`);
|
|
2606
3140
|
return rules.join(`
|
|
2607
3141
|
`);
|
|
2608
3142
|
}
|
|
2609
|
-
wrapCommand(cmd, profile) {
|
|
2610
|
-
if (
|
|
3143
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
3144
|
+
if (profile.kind === "danger-unrestricted") {
|
|
3145
|
+
return cmd;
|
|
3146
|
+
}
|
|
3147
|
+
if (!this.hasSandboxExec) {
|
|
3148
|
+
onWarning?.("macOS Seatbelt (sandbox-exec) is not available. Command will execute without OS-level namespace isolation.");
|
|
2611
3149
|
return cmd;
|
|
2612
3150
|
}
|
|
2613
3151
|
const policy = this.generateProfile(profile);
|
|
@@ -2640,7 +3178,7 @@ class KernelSandboxManager {
|
|
|
2640
3178
|
isSandboxingActive: this.windowsSandbox.isSupported() || this.linuxSandbox.isSupported() || this.macOsSandbox.isSupported()
|
|
2641
3179
|
};
|
|
2642
3180
|
}
|
|
2643
|
-
buildDefaultProfile(cwd, allowNetwork =
|
|
3181
|
+
buildDefaultProfile(cwd, allowNetwork = false) {
|
|
2644
3182
|
const normCwd = normalize(resolve6(cwd));
|
|
2645
3183
|
return {
|
|
2646
3184
|
kind: "workspace-write",
|
|
@@ -2655,11 +3193,15 @@ class KernelSandboxManager {
|
|
|
2655
3193
|
}
|
|
2656
3194
|
};
|
|
2657
3195
|
}
|
|
2658
|
-
wrapCommand(cmd, profile) {
|
|
3196
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
2659
3197
|
if (process.platform === "linux") {
|
|
2660
|
-
return this.linuxSandbox.wrapCommand(cmd, profile);
|
|
3198
|
+
return this.linuxSandbox.wrapCommand(cmd, profile, onWarning);
|
|
2661
3199
|
} else if (process.platform === "darwin") {
|
|
2662
|
-
return this.macOsSandbox.wrapCommand(cmd, profile);
|
|
3200
|
+
return this.macOsSandbox.wrapCommand(cmd, profile, onWarning);
|
|
3201
|
+
} else if (process.platform === "win32") {
|
|
3202
|
+
if (!this.windowsSandbox.isSupported() && profile.kind !== "danger-unrestricted") {
|
|
3203
|
+
onWarning?.("Windows JobObject isolation is not available in this environment. Command will execute without OS-level process limits.");
|
|
3204
|
+
}
|
|
2663
3205
|
}
|
|
2664
3206
|
return cmd;
|
|
2665
3207
|
}
|
|
@@ -2791,6 +3333,12 @@ class PrefixRulesStore {
|
|
|
2791
3333
|
matchesPrefix(cmdTokens, prefixTokens) {
|
|
2792
3334
|
if (prefixTokens.length > cmdTokens.length)
|
|
2793
3335
|
return false;
|
|
3336
|
+
const SHELL_OPERATORS = new Set([";", "&&", "||", "|", "&", ";;", "&|"]);
|
|
3337
|
+
for (const token of cmdTokens) {
|
|
3338
|
+
if (SHELL_OPERATORS.has(token) || token.includes(";") || token.includes("&&") || token.includes("||")) {
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2794
3342
|
for (let i = 0;i < prefixTokens.length; i++) {
|
|
2795
3343
|
const cmd = cmdTokens[i];
|
|
2796
3344
|
const prefix = prefixTokens[i];
|
|
@@ -2846,11 +3394,29 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2846
3394
|
}
|
|
2847
3395
|
const args = rawArgs;
|
|
2848
3396
|
const rulesStore = ctx.prefixRulesStore || globalPrefixRulesStore;
|
|
2849
|
-
const
|
|
3397
|
+
const activePolicy = ctx.execPolicy || policy;
|
|
3398
|
+
const policyDecision = activePolicy.evaluate(command);
|
|
3399
|
+
if (policyDecision.decision === "deny") {
|
|
3400
|
+
return {
|
|
3401
|
+
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
3402
|
+
isError: true
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
const parsed = parseShellCommand(command);
|
|
3406
|
+
const isCompound = parsed.commands.length > 1 || parsed.subshellCommands.length > 0 || parsed.hasPipes;
|
|
2850
3407
|
let isEscalated = false;
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2853
|
-
|
|
3408
|
+
if (!isCompound) {
|
|
3409
|
+
const cmdTokens = command.split(/\s+/).filter(Boolean);
|
|
3410
|
+
if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
|
|
3411
|
+
isEscalated = true;
|
|
3412
|
+
}
|
|
3413
|
+
} else {
|
|
3414
|
+
const allSubCommands = [...parsed.commands, ...parsed.subshellCommands];
|
|
3415
|
+
if (allSubCommands.length > 0 && allSubCommands.every((sub) => rulesStore.isApproved(ctx.cwd, sub.split(/\s+/).filter(Boolean)))) {
|
|
3416
|
+
isEscalated = true;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
if (!isEscalated && args.sandbox_permissions === "require_escalated") {
|
|
2854
3420
|
if (ctx.requestApproval) {
|
|
2855
3421
|
const promptDesc = args.justification || "Executing command with escalated permissions";
|
|
2856
3422
|
const approvalResult = await ctx.requestApproval(promptDesc, command, args.prefix_rule);
|
|
@@ -2868,16 +3434,8 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2868
3434
|
isEscalated = true;
|
|
2869
3435
|
}
|
|
2870
3436
|
}
|
|
2871
|
-
if (!isEscalated) {
|
|
2872
|
-
|
|
2873
|
-
const policyDecision = activePolicy.evaluate(command);
|
|
2874
|
-
if (policyDecision.decision === "deny") {
|
|
2875
|
-
return {
|
|
2876
|
-
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
2877
|
-
isError: true
|
|
2878
|
-
};
|
|
2879
|
-
}
|
|
2880
|
-
if (policyDecision.decision === "prompt" && ctx.requestApproval) {
|
|
3437
|
+
if (policyDecision.decision === "prompt" && !isEscalated) {
|
|
3438
|
+
if (ctx.requestApproval) {
|
|
2881
3439
|
const approvalResult = await ctx.requestApproval(policyDecision.reason || "Executing external command", command);
|
|
2882
3440
|
const isAllowed = typeof approvalResult === "boolean" ? approvalResult : approvalResult?.allowed;
|
|
2883
3441
|
if (!isAllowed) {
|
|
@@ -2886,17 +3444,22 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2886
3444
|
isError: true
|
|
2887
3445
|
};
|
|
2888
3446
|
}
|
|
3447
|
+
} else {
|
|
3448
|
+
return {
|
|
3449
|
+
output: `Error: Command execution requires user confirmation: '${command}' (${policyDecision.reason || "Requires approval"})`,
|
|
3450
|
+
isError: true
|
|
3451
|
+
};
|
|
2889
3452
|
}
|
|
2890
3453
|
}
|
|
2891
3454
|
const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : 30000;
|
|
2892
3455
|
const isWindows = process.platform === "win32";
|
|
2893
3456
|
const baseCmd = isWindows ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command];
|
|
2894
3457
|
const ephemeralScratchpad = globalEphemeralWorkspace.createScratchpad(ctx.turnId);
|
|
2895
|
-
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd);
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3458
|
+
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd, isEscalated);
|
|
3459
|
+
let sandboxNotice = null;
|
|
3460
|
+
const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile, (w) => {
|
|
3461
|
+
sandboxNotice = w;
|
|
3462
|
+
});
|
|
2900
3463
|
try {
|
|
2901
3464
|
const proc = Bun.spawn(wrappedCmd, {
|
|
2902
3465
|
cwd: ctx.cwd,
|
|
@@ -2950,10 +3513,38 @@ ${result.stderr.trim()}`);
|
|
|
2950
3513
|
2. If this is a test failure, trace the failure in source code and fix the root cause before re-running.
|
|
2951
3514
|
3. If this is a missing command/module, install or configure the prerequisite.`);
|
|
2952
3515
|
}
|
|
2953
|
-
|
|
3516
|
+
let rawOutput = outputParts.join(`
|
|
2954
3517
|
`) || "[Command completed with no output]";
|
|
3518
|
+
const lines = rawOutput.split(`
|
|
3519
|
+
`);
|
|
3520
|
+
const MAX_LINES = 250;
|
|
3521
|
+
const MAX_CHARS = 30000;
|
|
3522
|
+
if (lines.length > MAX_LINES) {
|
|
3523
|
+
const headLines = lines.slice(0, 125).join(`
|
|
3524
|
+
`);
|
|
3525
|
+
const tailLines = lines.slice(-100).join(`
|
|
3526
|
+
`);
|
|
3527
|
+
rawOutput = `${headLines}
|
|
3528
|
+
|
|
3529
|
+
... [${lines.length - 225} lines truncated for context efficiency] ...
|
|
3530
|
+
|
|
3531
|
+
${tailLines}`;
|
|
3532
|
+
}
|
|
3533
|
+
if (rawOutput.length > MAX_CHARS) {
|
|
3534
|
+
const headChars = rawOutput.slice(0, 16000);
|
|
3535
|
+
const tailChars = rawOutput.slice(-12000);
|
|
3536
|
+
rawOutput = `${headChars}
|
|
3537
|
+
|
|
3538
|
+
... [${rawOutput.length - 28000} characters truncated for context efficiency] ...
|
|
3539
|
+
|
|
3540
|
+
${tailChars}`;
|
|
3541
|
+
}
|
|
3542
|
+
if (sandboxNotice) {
|
|
3543
|
+
rawOutput = `[Sandbox Notice]: ${sandboxNotice}
|
|
3544
|
+
${rawOutput}`;
|
|
3545
|
+
}
|
|
2955
3546
|
return {
|
|
2956
|
-
output,
|
|
3547
|
+
output: rawOutput,
|
|
2957
3548
|
isError: result.code !== 0
|
|
2958
3549
|
};
|
|
2959
3550
|
} catch (err) {
|
|
@@ -2972,7 +3563,7 @@ ${result.stderr.trim()}`);
|
|
|
2972
3563
|
}
|
|
2973
3564
|
var shellTool = createShellTool();
|
|
2974
3565
|
// src/tools/handlers/file-ops.ts
|
|
2975
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, mkdirSync as mkdirSync6 } from "fs";
|
|
3566
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, lstatSync, mkdirSync as mkdirSync6 } from "fs";
|
|
2976
3567
|
import { resolve as resolve8, dirname as dirname5 } from "path";
|
|
2977
3568
|
var DEFAULT_MAX_UNPAGINATED_LINES = 250;
|
|
2978
3569
|
var readFileTool = {
|
|
@@ -3084,6 +3675,7 @@ var viewFileTool = {
|
|
|
3084
3675
|
name: "view_file",
|
|
3085
3676
|
description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
|
|
3086
3677
|
};
|
|
3678
|
+
var DEFAULT_MAX_DIR_ENTRIES = 500;
|
|
3087
3679
|
var listDirTool = {
|
|
3088
3680
|
name: "list_dir",
|
|
3089
3681
|
description: "List contents of a directory with file names and types.",
|
|
@@ -3100,13 +3692,31 @@ var listDirTool = {
|
|
|
3100
3692
|
}
|
|
3101
3693
|
try {
|
|
3102
3694
|
const entries = readdirSync4(dirPath);
|
|
3103
|
-
const
|
|
3695
|
+
const totalEntries = entries.length;
|
|
3696
|
+
const displayEntries = entries.slice(0, DEFAULT_MAX_DIR_ENTRIES);
|
|
3697
|
+
const formatted = displayEntries.map((entry) => {
|
|
3104
3698
|
const full = resolve8(dirPath, entry);
|
|
3105
|
-
|
|
3699
|
+
let isDir = false;
|
|
3700
|
+
try {
|
|
3701
|
+
isDir = statSync4(full).isDirectory();
|
|
3702
|
+
} catch {
|
|
3703
|
+
try {
|
|
3704
|
+
const lst = lstatSync(full);
|
|
3705
|
+
if (lst.isSymbolicLink())
|
|
3706
|
+
return `[SYMLINK] ${entry}`;
|
|
3707
|
+
} catch {}
|
|
3708
|
+
return `[FILE] ${entry}`;
|
|
3709
|
+
}
|
|
3106
3710
|
return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
|
|
3107
3711
|
});
|
|
3108
|
-
|
|
3109
|
-
`) || "[Empty directory]"
|
|
3712
|
+
let output = formatted.join(`
|
|
3713
|
+
`) || "[Empty directory]";
|
|
3714
|
+
if (totalEntries > DEFAULT_MAX_DIR_ENTRIES) {
|
|
3715
|
+
output += `
|
|
3716
|
+
|
|
3717
|
+
... [Truncated: ${totalEntries - DEFAULT_MAX_DIR_ENTRIES} more entries. Showing first ${DEFAULT_MAX_DIR_ENTRIES} of ${totalEntries}]`;
|
|
3718
|
+
}
|
|
3719
|
+
return { output };
|
|
3110
3720
|
} catch (err) {
|
|
3111
3721
|
return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
3112
3722
|
}
|
|
@@ -3147,6 +3757,7 @@ var writeFileTool = {
|
|
|
3147
3757
|
try {
|
|
3148
3758
|
mkdirSync6(dirname5(filePath), { recursive: true });
|
|
3149
3759
|
writeFileSync3(filePath, String(args.content ?? ""), "utf8");
|
|
3760
|
+
ctx.onFileModified?.(rawPath);
|
|
3150
3761
|
return { output: `Successfully wrote to '${args.path}'` };
|
|
3151
3762
|
} catch (err) {
|
|
3152
3763
|
return {
|
|
@@ -3640,6 +4251,12 @@ class CodeModeToolsProxy {
|
|
|
3640
4251
|
if (result.isError) {
|
|
3641
4252
|
throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
|
|
3642
4253
|
}
|
|
4254
|
+
if (normalizedName === "write_file" || normalizedName === "apply_patch" || tool.name === "write_file" || tool.name === "apply_patch") {
|
|
4255
|
+
const pathArg = String(args?.path || "");
|
|
4256
|
+
if (pathArg) {
|
|
4257
|
+
this.context.onFileModified?.(pathArg);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
3643
4260
|
return result.output;
|
|
3644
4261
|
};
|
|
3645
4262
|
}
|
|
@@ -3713,6 +4330,9 @@ class SandboxedWorkerHost {
|
|
|
3713
4330
|
const fetch = undefined;
|
|
3714
4331
|
const XMLHttpRequest = undefined;
|
|
3715
4332
|
const WebSocket = undefined;
|
|
4333
|
+
const globalThis = Object.freeze(Object.create(null));
|
|
4334
|
+
const global = undefined;
|
|
4335
|
+
const window = undefined;
|
|
3716
4336
|
|
|
3717
4337
|
return (async () => {
|
|
3718
4338
|
${cleanCode}
|
|
@@ -4307,7 +4927,11 @@ class AgentGraphStore {
|
|
|
4307
4927
|
if (dbPath !== ":memory:") {
|
|
4308
4928
|
const dir = resolve11(dbPath, "..");
|
|
4309
4929
|
if (!existsSync15(dir)) {
|
|
4310
|
-
|
|
4930
|
+
try {
|
|
4931
|
+
mkdirSync7(dir, { recursive: true });
|
|
4932
|
+
} catch (err) {
|
|
4933
|
+
console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
|
|
4934
|
+
}
|
|
4311
4935
|
}
|
|
4312
4936
|
}
|
|
4313
4937
|
this.db = new Database2(dbPath);
|
|
@@ -4389,25 +5013,45 @@ class AgentGraphStore {
|
|
|
4389
5013
|
close() {
|
|
4390
5014
|
try {
|
|
4391
5015
|
this.db.close();
|
|
4392
|
-
} catch {
|
|
5016
|
+
} catch (err) {
|
|
5017
|
+
console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
|
|
5018
|
+
}
|
|
4393
5019
|
}
|
|
4394
5020
|
}
|
|
4395
5021
|
|
|
4396
5022
|
// src/agents/spawner.ts
|
|
4397
5023
|
class AgentSpawner {
|
|
4398
5024
|
parentSession;
|
|
5025
|
+
depth;
|
|
4399
5026
|
subAgents = new Map;
|
|
4400
5027
|
nextAgentId = 1;
|
|
4401
5028
|
roleRegistry;
|
|
4402
5029
|
parentIdentity;
|
|
4403
5030
|
graphStore;
|
|
4404
|
-
|
|
5031
|
+
maxConcurrentAgents;
|
|
5032
|
+
maxDepth;
|
|
5033
|
+
maxRetainedCompleted;
|
|
5034
|
+
defaultTokenBudget;
|
|
5035
|
+
constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
|
|
4405
5036
|
this.parentSession = parentSession;
|
|
5037
|
+
this.depth = depth;
|
|
4406
5038
|
this.roleRegistry = roleRegistry || new AgentRoleRegistry;
|
|
4407
5039
|
this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
|
|
4408
5040
|
this.graphStore = graphStore || new AgentGraphStore;
|
|
5041
|
+
const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
|
|
5042
|
+
this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
|
|
5043
|
+
this.maxDepth = options?.maxDepth ?? 2;
|
|
5044
|
+
this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
|
|
5045
|
+
this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
|
|
4409
5046
|
}
|
|
4410
5047
|
async spawnAgent(params) {
|
|
5048
|
+
if (this.depth >= this.maxDepth) {
|
|
5049
|
+
throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
|
|
5050
|
+
}
|
|
5051
|
+
const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
|
|
5052
|
+
if (activeRunningCount >= this.maxConcurrentAgents) {
|
|
5053
|
+
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.`);
|
|
5054
|
+
}
|
|
4411
5055
|
const roleName = params.role || "default";
|
|
4412
5056
|
const roleConfig = this.roleRegistry.getRole(roleName);
|
|
4413
5057
|
const agentIndex = this.nextAgentId++;
|
|
@@ -4419,7 +5063,18 @@ class AgentSpawner {
|
|
|
4419
5063
|
|
|
4420
5064
|
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.`);
|
|
4421
5065
|
const baseTools = params.tools || this.parentSession.tools;
|
|
4422
|
-
|
|
5066
|
+
let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
|
|
5067
|
+
if (this.depth + 1 >= this.maxDepth) {
|
|
5068
|
+
const sanitizedRouter = new ToolRouter;
|
|
5069
|
+
for (const t of effectiveTools.list()) {
|
|
5070
|
+
if (t.name !== "spawn_agent") {
|
|
5071
|
+
sanitizedRouter.register(t);
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
effectiveTools = sanitizedRouter;
|
|
5075
|
+
}
|
|
5076
|
+
const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
|
|
5077
|
+
let accumulatedTokens = 0;
|
|
4423
5078
|
const childSession = new Session({
|
|
4424
5079
|
threadId: agentId,
|
|
4425
5080
|
model: effectiveModel,
|
|
@@ -4443,45 +5098,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4443
5098
|
createdAt: Date.now(),
|
|
4444
5099
|
identity: childIdentity,
|
|
4445
5100
|
session: childSession,
|
|
4446
|
-
promise: taskPromise
|
|
5101
|
+
promise: taskPromise,
|
|
5102
|
+
depth: this.depth + 1,
|
|
5103
|
+
tokenBudget,
|
|
5104
|
+
totalTokens: 0
|
|
4447
5105
|
};
|
|
4448
5106
|
try {
|
|
4449
5107
|
this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
|
|
4450
|
-
} catch {
|
|
5108
|
+
} catch (err) {
|
|
5109
|
+
console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
|
|
5110
|
+
}
|
|
4451
5111
|
let collectedAgentText = "";
|
|
4452
5112
|
childSession.onEvent((event) => {
|
|
4453
5113
|
if (event.msg.type === "AgentMessageDelta") {
|
|
4454
5114
|
collectedAgentText += event.msg.delta;
|
|
5115
|
+
accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
|
|
5116
|
+
handle.totalTokens = accumulatedTokens;
|
|
5117
|
+
if (accumulatedTokens > tokenBudget && handle.status === "running") {
|
|
5118
|
+
handle.status = "error";
|
|
5119
|
+
handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
|
|
5120
|
+
childSession.interrupt();
|
|
5121
|
+
try {
|
|
5122
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5125
|
+
}
|
|
5126
|
+
rejectPromise(new Error(handle.error));
|
|
5127
|
+
}
|
|
4455
5128
|
} else if (event.msg.type === "TurnCompleted") {
|
|
4456
|
-
handle.status
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
5129
|
+
if (handle.status === "running") {
|
|
5130
|
+
handle.status = "completed";
|
|
5131
|
+
handle.lastOutput = collectedAgentText.trim();
|
|
5132
|
+
handle.totalTokens = accumulatedTokens;
|
|
5133
|
+
try {
|
|
5134
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5135
|
+
} catch (err) {
|
|
5136
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5137
|
+
}
|
|
5138
|
+
this.pruneCompletedAgents();
|
|
5139
|
+
resolvePromise(handle.lastOutput);
|
|
5140
|
+
}
|
|
4462
5141
|
} else if (event.msg.type === "Error") {
|
|
4463
|
-
handle.status
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
5142
|
+
if (handle.status === "running") {
|
|
5143
|
+
handle.status = "error";
|
|
5144
|
+
handle.error = event.msg.message;
|
|
5145
|
+
handle.totalTokens = accumulatedTokens;
|
|
5146
|
+
try {
|
|
5147
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5148
|
+
} catch (err) {
|
|
5149
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5150
|
+
}
|
|
5151
|
+
this.pruneCompletedAgents();
|
|
5152
|
+
rejectPromise(new Error(event.msg.message));
|
|
5153
|
+
}
|
|
4469
5154
|
} else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
|
|
4470
|
-
handle.status
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
5155
|
+
if (handle.status === "running") {
|
|
5156
|
+
handle.status = "interrupted";
|
|
5157
|
+
handle.totalTokens = accumulatedTokens;
|
|
5158
|
+
try {
|
|
5159
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5160
|
+
} catch (err) {
|
|
5161
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5162
|
+
}
|
|
5163
|
+
this.pruneCompletedAgents();
|
|
5164
|
+
resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
|
|
5165
|
+
}
|
|
4475
5166
|
}
|
|
4476
5167
|
});
|
|
4477
5168
|
this.subAgents.set(agentId, handle);
|
|
4478
5169
|
childSession.prompt(params.message).catch((err) => {
|
|
4479
|
-
handle.status
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
5170
|
+
if (handle.status === "running") {
|
|
5171
|
+
handle.status = "error";
|
|
5172
|
+
handle.error = err instanceof Error ? err.message : String(err);
|
|
5173
|
+
handle.totalTokens = accumulatedTokens;
|
|
5174
|
+
try {
|
|
5175
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5176
|
+
} catch (storeErr) {
|
|
5177
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
|
|
5178
|
+
}
|
|
5179
|
+
this.pruneCompletedAgents();
|
|
5180
|
+
rejectPromise(err);
|
|
5181
|
+
}
|
|
4485
5182
|
});
|
|
4486
5183
|
return {
|
|
4487
5184
|
id: handle.id,
|
|
@@ -4490,9 +5187,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4490
5187
|
role: handle.role,
|
|
4491
5188
|
status: handle.status,
|
|
4492
5189
|
createdAt: handle.createdAt,
|
|
4493
|
-
agentRuntimeId: childIdentity.agentRuntimeId
|
|
5190
|
+
agentRuntimeId: childIdentity.agentRuntimeId,
|
|
5191
|
+
depth: handle.depth,
|
|
5192
|
+
tokenBudget: handle.tokenBudget,
|
|
5193
|
+
totalTokens: handle.totalTokens
|
|
4494
5194
|
};
|
|
4495
5195
|
}
|
|
5196
|
+
pruneCompletedAgents() {
|
|
5197
|
+
const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
|
|
5198
|
+
if (finishedHandles.length > this.maxRetainedCompleted) {
|
|
5199
|
+
finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
|
|
5200
|
+
const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
|
|
5201
|
+
for (const h of toRemove) {
|
|
5202
|
+
this.subAgents.delete(h.id);
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
4496
5206
|
async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
|
|
4497
5207
|
if (!agentIdOrTaskName) {
|
|
4498
5208
|
const handles = Array.from(this.subAgents.values());
|
|
@@ -4546,9 +5256,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4546
5256
|
handle.status = "interrupted";
|
|
4547
5257
|
try {
|
|
4548
5258
|
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
4549
|
-
} catch {
|
|
5259
|
+
} catch (err) {
|
|
5260
|
+
console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
|
|
5261
|
+
}
|
|
5262
|
+
this.pruneCompletedAgents();
|
|
4550
5263
|
return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
|
|
4551
5264
|
}
|
|
5265
|
+
removeAgent(agentId) {
|
|
5266
|
+
const handle = this.subAgents.get(agentId);
|
|
5267
|
+
if (!handle)
|
|
5268
|
+
return false;
|
|
5269
|
+
if (handle.status === "running") {
|
|
5270
|
+
handle.session.interrupt();
|
|
5271
|
+
}
|
|
5272
|
+
try {
|
|
5273
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5274
|
+
} catch (err) {
|
|
5275
|
+
console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
|
|
5276
|
+
}
|
|
5277
|
+
return this.subAgents.delete(agentId);
|
|
5278
|
+
}
|
|
5279
|
+
clearCompleted() {
|
|
5280
|
+
let cleared = 0;
|
|
5281
|
+
for (const [id, handle] of this.subAgents.entries()) {
|
|
5282
|
+
if (handle.status !== "running") {
|
|
5283
|
+
this.subAgents.delete(id);
|
|
5284
|
+
cleared++;
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
return cleared;
|
|
5288
|
+
}
|
|
4552
5289
|
listAgents() {
|
|
4553
5290
|
const list = [];
|
|
4554
5291
|
for (const handle of this.subAgents.values()) {
|
|
@@ -4560,7 +5297,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4560
5297
|
status: handle.status,
|
|
4561
5298
|
createdAt: handle.createdAt,
|
|
4562
5299
|
agentRuntimeId: handle.identity.agentRuntimeId,
|
|
4563
|
-
lastOutput: handle.lastOutput
|
|
5300
|
+
lastOutput: handle.lastOutput,
|
|
5301
|
+
depth: handle.depth,
|
|
5302
|
+
tokenBudget: handle.tokenBudget,
|
|
5303
|
+
totalTokens: handle.totalTokens
|
|
4564
5304
|
});
|
|
4565
5305
|
}
|
|
4566
5306
|
return list;
|
|
@@ -4594,6 +5334,10 @@ function createMultiAgentTools(spawner) {
|
|
|
4594
5334
|
model: {
|
|
4595
5335
|
type: "string",
|
|
4596
5336
|
description: "Optional model override. Defaults to inheriting parent model."
|
|
5337
|
+
},
|
|
5338
|
+
max_tokens: {
|
|
5339
|
+
type: "number",
|
|
5340
|
+
description: "Optional token budget limit for the sub-agent (default: 50000)."
|
|
4597
5341
|
}
|
|
4598
5342
|
},
|
|
4599
5343
|
required: ["task_name", "message"]
|
|
@@ -4603,8 +5347,9 @@ function createMultiAgentTools(spawner) {
|
|
|
4603
5347
|
const message = String(args.message || "");
|
|
4604
5348
|
const role = args.role ? String(args.role) : undefined;
|
|
4605
5349
|
const model = args.model ? String(args.model) : undefined;
|
|
5350
|
+
const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
|
|
4606
5351
|
try {
|
|
4607
|
-
const handle = await spawner.spawnAgent({ taskName, message, role, model });
|
|
5352
|
+
const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
|
|
4608
5353
|
return {
|
|
4609
5354
|
output: `Successfully spawned sub-agent '${handle.id}' with role '${handle.role}' for task '${handle.taskName}' (status: ${handle.status}). Use 'wait_agent' to collect results.`
|
|
4610
5355
|
};
|
|
@@ -5898,7 +6643,8 @@ class SessionPersistenceManager {
|
|
|
5898
6643
|
loadSession(threadId) {
|
|
5899
6644
|
try {
|
|
5900
6645
|
return this.store.restoreSession(threadId);
|
|
5901
|
-
} catch {
|
|
6646
|
+
} catch (err) {
|
|
6647
|
+
console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
|
|
5902
6648
|
return null;
|
|
5903
6649
|
}
|
|
5904
6650
|
}
|
|
@@ -5906,7 +6652,9 @@ class SessionPersistenceManager {
|
|
|
5906
6652
|
for (const unsub of this.unsubscribers) {
|
|
5907
6653
|
try {
|
|
5908
6654
|
unsub();
|
|
5909
|
-
} catch {
|
|
6655
|
+
} catch (err) {
|
|
6656
|
+
console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
|
|
6657
|
+
}
|
|
5910
6658
|
}
|
|
5911
6659
|
this.unsubscribers = [];
|
|
5912
6660
|
}
|
|
@@ -5933,7 +6681,9 @@ class SessionPersistenceManager {
|
|
|
5933
6681
|
for (const unsub of this.unsubscribers) {
|
|
5934
6682
|
try {
|
|
5935
6683
|
unsub();
|
|
5936
|
-
} catch {
|
|
6684
|
+
} catch (err) {
|
|
6685
|
+
console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
|
|
6686
|
+
}
|
|
5937
6687
|
}
|
|
5938
6688
|
this.unsubscribers = [];
|
|
5939
6689
|
this.store.close();
|
|
@@ -6268,7 +7018,9 @@ class MemoryStore {
|
|
|
6268
7018
|
if (!existsSync20(dir)) {
|
|
6269
7019
|
try {
|
|
6270
7020
|
mkdirSync11(dir, { recursive: true });
|
|
6271
|
-
} catch {
|
|
7021
|
+
} catch (err) {
|
|
7022
|
+
console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
|
|
7023
|
+
}
|
|
6272
7024
|
}
|
|
6273
7025
|
return dir;
|
|
6274
7026
|
}
|
|
@@ -6277,7 +7029,9 @@ class MemoryStore {
|
|
|
6277
7029
|
if (!existsSync20(dir)) {
|
|
6278
7030
|
try {
|
|
6279
7031
|
mkdirSync11(dir, { recursive: true });
|
|
6280
|
-
} catch {
|
|
7032
|
+
} catch (err) {
|
|
7033
|
+
console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
|
|
7034
|
+
}
|
|
6281
7035
|
}
|
|
6282
7036
|
return dir;
|
|
6283
7037
|
}
|
|
@@ -6421,7 +7175,9 @@ class MemoryStore {
|
|
|
6421
7175
|
`)[0] || parsed.name,
|
|
6422
7176
|
file: f
|
|
6423
7177
|
});
|
|
6424
|
-
} catch {
|
|
7178
|
+
} catch (err) {
|
|
7179
|
+
console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
|
|
7180
|
+
}
|
|
6425
7181
|
}
|
|
6426
7182
|
const indexLines = [
|
|
6427
7183
|
"# Project Auto-Memory Index",
|
|
@@ -6463,7 +7219,9 @@ class MemoryStore {
|
|
|
6463
7219
|
try {
|
|
6464
7220
|
const full = join12(memoryDir, f);
|
|
6465
7221
|
list.push(this.parseTopicFile(readFileSync12(full, "utf8"), full));
|
|
6466
|
-
} catch {
|
|
7222
|
+
} catch (err) {
|
|
7223
|
+
console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
|
|
7224
|
+
}
|
|
6467
7225
|
}
|
|
6468
7226
|
return list;
|
|
6469
7227
|
}
|
|
@@ -7397,7 +8155,7 @@ function parsePatch(oldSrc, newSrc, contextLines = 3) {
|
|
|
7397
8155
|
// package.json
|
|
7398
8156
|
var package_default = {
|
|
7399
8157
|
name: "@pikaa-ai/pikaa",
|
|
7400
|
-
version: "0.
|
|
8158
|
+
version: "0.4.0",
|
|
7401
8159
|
description: "PIKAA CLI - AI coding agent that runs locally in your terminal.",
|
|
7402
8160
|
main: "./dist/index.js",
|
|
7403
8161
|
module: "./dist/index.js",
|
|
@@ -7429,14 +8187,14 @@ var package_default = {
|
|
|
7429
8187
|
prepublishOnly: "bun run build:js"
|
|
7430
8188
|
},
|
|
7431
8189
|
optionalDependencies: {
|
|
7432
|
-
"@pikaa-ai/pikaa-linux-x64": "0.
|
|
7433
|
-
"@pikaa-ai/pikaa-linux-x64-musl": "0.
|
|
7434
|
-
"@pikaa-ai/pikaa-linux-arm64": "0.
|
|
7435
|
-
"@pikaa-ai/pikaa-linux-arm64-musl": "0.
|
|
7436
|
-
"@pikaa-ai/pikaa-darwin-x64": "0.
|
|
7437
|
-
"@pikaa-ai/pikaa-darwin-arm64": "0.
|
|
7438
|
-
"@pikaa-ai/pikaa-windows-x64": "0.
|
|
7439
|
-
"@pikaa-ai/pikaa-windows-arm64": "0.
|
|
8190
|
+
"@pikaa-ai/pikaa-linux-x64": "0.4.0",
|
|
8191
|
+
"@pikaa-ai/pikaa-linux-x64-musl": "0.4.0",
|
|
8192
|
+
"@pikaa-ai/pikaa-linux-arm64": "0.4.0",
|
|
8193
|
+
"@pikaa-ai/pikaa-linux-arm64-musl": "0.4.0",
|
|
8194
|
+
"@pikaa-ai/pikaa-darwin-x64": "0.4.0",
|
|
8195
|
+
"@pikaa-ai/pikaa-darwin-arm64": "0.4.0",
|
|
8196
|
+
"@pikaa-ai/pikaa-windows-x64": "0.4.0",
|
|
8197
|
+
"@pikaa-ai/pikaa-windows-arm64": "0.4.0"
|
|
7440
8198
|
},
|
|
7441
8199
|
keywords: [
|
|
7442
8200
|
"ai",
|
|
@@ -11068,9 +11826,15 @@ class CliRepl {
|
|
|
11068
11826
|
try {
|
|
11069
11827
|
const decision = await promptToolApproval(msg);
|
|
11070
11828
|
if (decision === "always") {
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11829
|
+
if (msg.prefixRule && Array.isArray(msg.prefixRule) && msg.prefixRule.length > 0) {
|
|
11830
|
+
globalPrefixRulesStore.addRule(this.session.cwd, msg.prefixRule);
|
|
11831
|
+
this.session.resolveApproval(msg.approvalId, { allowed: true, rememberPrefix: true });
|
|
11832
|
+
this.spinner.start(`Executing approved action (saved prefix rule)...`, this.turnStartTime);
|
|
11833
|
+
} else {
|
|
11834
|
+
this.session.execPolicy.addRule(/.*/, "allow", "User allowed all standard actions for this session");
|
|
11835
|
+
this.session.resolveApproval(msg.approvalId, true);
|
|
11836
|
+
this.spinner.start(`Executing approved action (auto-approved for session)...`, this.turnStartTime);
|
|
11837
|
+
}
|
|
11074
11838
|
} else if (decision === "yes") {
|
|
11075
11839
|
this.session.resolveApproval(msg.approvalId, true);
|
|
11076
11840
|
this.spinner.start(`Executing approved action...`, this.turnStartTime);
|