@4onstudios/iris-agent 0.1.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/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/api/acp/acpServer.d.ts +23 -0
- package/dist/api/acp/acpServer.js +814 -0
- package/dist/api/acp/index.d.ts +2 -0
- package/dist/api/acp/index.js +2 -0
- package/dist/api/acp/irisClient.d.ts +58 -0
- package/dist/api/acp/irisClient.js +214 -0
- package/dist/api/agent.d.ts +14 -0
- package/dist/api/agent.js +4171 -0
- package/dist/api/backendService.d.ts +4 -0
- package/dist/api/backendService.js +14 -0
- package/dist/api/core/agent/host/AgentContract.d.ts +82 -0
- package/dist/api/core/agent/host/AgentContract.js +1 -0
- package/dist/api/core/agent/host/AgentRegistry.d.ts +24 -0
- package/dist/api/core/agent/host/AgentRegistry.js +125 -0
- package/dist/api/core/agent/host/defaultRegistry.d.ts +15 -0
- package/dist/api/core/agent/host/defaultRegistry.js +46 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.d.ts +65 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.js +117 -0
- package/dist/api/core/agent/host/hostSessionManager.d.ts +111 -0
- package/dist/api/core/agent/host/hostSessionManager.js +352 -0
- package/dist/api/core/agent/host/index.d.ts +5 -0
- package/dist/api/core/agent/host/index.js +5 -0
- package/dist/api/core/agent/index.d.ts +87 -0
- package/dist/api/core/agent/index.js +1024 -0
- package/dist/api/core/agent/publicContracts.d.ts +35 -0
- package/dist/api/core/agent/publicContracts.js +103 -0
- package/dist/api/core/agent/tools/applyDiff.d.ts +51 -0
- package/dist/api/core/agent/tools/applyDiff.js +131 -0
- package/dist/api/core/agent/tools/backgroundTasks.d.ts +240 -0
- package/dist/api/core/agent/tools/backgroundTasks.js +313 -0
- package/dist/api/core/agent/tools/createDirectory.d.ts +43 -0
- package/dist/api/core/agent/tools/createDirectory.js +77 -0
- package/dist/api/core/agent/tools/deleteFile.d.ts +37 -0
- package/dist/api/core/agent/tools/deleteFile.js +66 -0
- package/dist/api/core/agent/tools/editFile.d.ts +73 -0
- package/dist/api/core/agent/tools/editFile.js +173 -0
- package/dist/api/core/agent/tools/executeCommand.d.ts +59 -0
- package/dist/api/core/agent/tools/executeCommand.js +250 -0
- package/dist/api/core/agent/tools/fileContent.d.ts +54 -0
- package/dist/api/core/agent/tools/fileContent.js +227 -0
- package/dist/api/core/agent/tools/findDefinition.d.ts +72 -0
- package/dist/api/core/agent/tools/findDefinition.js +109 -0
- package/dist/api/core/agent/tools/findReferences.d.ts +86 -0
- package/dist/api/core/agent/tools/findReferences.js +135 -0
- package/dist/api/core/agent/tools/formatDocument.d.ts +67 -0
- package/dist/api/core/agent/tools/formatDocument.js +90 -0
- package/dist/api/core/agent/tools/getCodeActions.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeActions.js +97 -0
- package/dist/api/core/agent/tools/getCodeCompletion.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeCompletion.js +174 -0
- package/dist/api/core/agent/tools/getCodeContext.d.ts +14 -0
- package/dist/api/core/agent/tools/getCodeContext.js +282 -0
- package/dist/api/core/agent/tools/getSignatureHelp.d.ts +61 -0
- package/dist/api/core/agent/tools/getSignatureHelp.js +85 -0
- package/dist/api/core/agent/tools/getSymbols.d.ts +50 -0
- package/dist/api/core/agent/tools/getSymbols.js +164 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.d.ts +58 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.js +147 -0
- package/dist/api/core/agent/tools/getTypeInfo.d.ts +70 -0
- package/dist/api/core/agent/tools/getTypeInfo.js +84 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.d.ts +80 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.js +281 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.d.ts +53 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.js +97 -0
- package/dist/api/core/agent/tools/grepSearch.d.ts +100 -0
- package/dist/api/core/agent/tools/grepSearch.js +211 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.d.ts +13 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.js +22 -0
- package/dist/api/core/agent/tools/listDirectory.d.ts +66 -0
- package/dist/api/core/agent/tools/listDirectory.js +161 -0
- package/dist/api/core/agent/tools/mcpTools.d.ts +48 -0
- package/dist/api/core/agent/tools/mcpTools.js +442 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.d.ts +120 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.js +306 -0
- package/dist/api/core/agent/tools/readFile.d.ts +92 -0
- package/dist/api/core/agent/tools/readFile.js +337 -0
- package/dist/api/core/agent/tools/renameFile.d.ts +49 -0
- package/dist/api/core/agent/tools/renameFile.js +86 -0
- package/dist/api/core/agent/tools/renameSymbol.d.ts +70 -0
- package/dist/api/core/agent/tools/renameSymbol.js +106 -0
- package/dist/api/core/agent/tools/runTerminalCommand.d.ts +72 -0
- package/dist/api/core/agent/tools/runTerminalCommand.js +69 -0
- package/dist/api/core/agent/tools/searchFiles.d.ts +82 -0
- package/dist/api/core/agent/tools/searchFiles.js +280 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.d.ts +3 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.js +676 -0
- package/dist/api/core/agent/tools/treeTraversal.d.ts +60 -0
- package/dist/api/core/agent/tools/treeTraversal.js +217 -0
- package/dist/api/core/agent/tools/webSearch.d.ts +37 -0
- package/dist/api/core/agent/tools/webSearch.js +80 -0
- package/dist/api/core/agent/tools/writeFile.d.ts +59 -0
- package/dist/api/core/agent/tools/writeFile.js +97 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.d.ts +12 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.js +67 -0
- package/dist/api/core/agent/utils/diffUtils.d.ts +14 -0
- package/dist/api/core/agent/utils/diffUtils.js +39 -0
- package/dist/api/core/agent/utils/environmentSnapshot.d.ts +42 -0
- package/dist/api/core/agent/utils/environmentSnapshot.js +213 -0
- package/dist/api/core/agent/utils/errorRecovery.d.ts +49 -0
- package/dist/api/core/agent/utils/errorRecovery.js +229 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.d.ts +8 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.js +238 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.d.ts +12 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.js +34 -0
- package/dist/api/core/agent/utils/pathRecovery.d.ts +1 -0
- package/dist/api/core/agent/utils/pathRecovery.js +29 -0
- package/dist/api/core/agent/utils/repoMapIndex.d.ts +3 -0
- package/dist/api/core/agent/utils/repoMapIndex.js +197 -0
- package/dist/api/core/agent/utils/skillsDiscovery.d.ts +10 -0
- package/dist/api/core/agent/utils/skillsDiscovery.js +50 -0
- package/dist/api/core/agent/utils/toolCallBudget.d.ts +9 -0
- package/dist/api/core/agent/utils/toolCallBudget.js +55 -0
- package/dist/api/core/agent/utils/toolLifecycle.d.ts +30 -0
- package/dist/api/core/agent/utils/toolLifecycle.js +361 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.d.ts +12 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.js +57 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.d.ts +13 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.js +149 -0
- package/dist/api/core/agent/utils/workspacePathGuard.d.ts +7 -0
- package/dist/api/core/agent/utils/workspacePathGuard.js +79 -0
- package/dist/api/core/containers/chat/toolResultSerialization.d.ts +8 -0
- package/dist/api/core/containers/chat/toolResultSerialization.js +71 -0
- package/dist/api/core/library/BrowserManager.d.ts +11 -0
- package/dist/api/core/library/BrowserManager.js +66 -0
- package/dist/api/core/library/desktopWorkspace.d.ts +1 -0
- package/dist/api/core/library/desktopWorkspace.js +3 -0
- package/dist/api/core/library/extensionManager.d.ts +10 -0
- package/dist/api/core/library/extensionManager.js +27 -0
- package/dist/api/core/library/knowledgeGraph.d.ts +268 -0
- package/dist/api/core/library/knowledgeGraph.js +989 -0
- package/dist/api/core/library/languageModelTools.d.ts +141 -0
- package/dist/api/core/library/languageModelTools.js +207 -0
- package/dist/api/core/library/localRuntime.d.ts +23 -0
- package/dist/api/core/library/localRuntime.js +144 -0
- package/dist/api/core/library/lsp/coreLsp.d.ts +367 -0
- package/dist/api/core/library/lsp/coreLsp.js +2076 -0
- package/dist/api/core/library/lsp/protocol.d.ts +25 -0
- package/dist/api/core/library/lsp/protocol.js +1 -0
- package/dist/api/core/library/lsp/serverManager.d.ts +37 -0
- package/dist/api/core/library/lsp/serverManager.js +427 -0
- package/dist/api/core/library/mcpServerProvider.d.ts +69 -0
- package/dist/api/core/library/mcpServerProvider.js +152 -0
- package/dist/api/core/library/mcpSettings.d.ts +20 -0
- package/dist/api/core/library/mcpSettings.js +131 -0
- package/dist/api/core/library/patternMatcher.d.ts +66 -0
- package/dist/api/core/library/patternMatcher.js +284 -0
- package/dist/api/core/library/regexEscape.d.ts +4 -0
- package/dist/api/core/library/regexEscape.js +4 -0
- package/dist/api/core/library/runtimeEventBus.d.ts +27 -0
- package/dist/api/core/library/runtimeEventBus.js +78 -0
- package/dist/api/core/library/safetyMiddleware.d.ts +98 -0
- package/dist/api/core/library/safetyMiddleware.js +215 -0
- package/dist/api/core/library/tauri.d.ts +71 -0
- package/dist/api/core/library/tauri.js +222 -0
- package/dist/api/core/library/tauriImport.d.ts +5 -0
- package/dist/api/core/library/tauriImport.js +7 -0
- package/dist/api/core/library/terminalAutoApproveSettings.d.ts +32 -0
- package/dist/api/core/library/terminalAutoApproveSettings.js +529 -0
- package/dist/api/core/library/workspaceIdentity.d.ts +10 -0
- package/dist/api/core/library/workspaceIdentity.js +49 -0
- package/dist/api/core/library/workspaceSummary.d.ts +19 -0
- package/dist/api/core/library/workspaceSummary.js +35 -0
- package/dist/api/core/skills/agent-customization/SKILL.md +27 -0
- package/dist/api/core/skills/bug-fix/SKILL.md +21 -0
- package/dist/api/core/skills/create-pr/SKILL.md +32 -0
- package/dist/api/core/skills/dev-server/SKILL.md +20 -0
- package/dist/api/core/skills/fix-suggestions/SKILL.md +21 -0
- package/dist/api/core/skills/github-search/SKILL.md +21 -0
- package/dist/api/core/skills/github-summary/SKILL.md +25 -0
- package/dist/api/core/skills/integration-tests/SKILL.md +29 -0
- package/dist/api/core/skills/pr-comments/SKILL.md +23 -0
- package/dist/api/core/skills/project-setup/SKILL.md +20 -0
- package/dist/api/core/skills/search-integration/SKILL.md +19 -0
- package/dist/api/core/skills/search-results/SKILL.md +19 -0
- package/dist/api/core/skills/typescript-upgrade/SKILL.md +21 -0
- package/dist/api/data/runStore.d.ts +46 -0
- package/dist/api/data/runStore.js +244 -0
- package/dist/api/helpers/agentUtils.d.ts +11 -0
- package/dist/api/helpers/agentUtils.js +23 -0
- package/dist/api/helpers/modelTokenLimits.d.ts +2 -0
- package/dist/api/helpers/modelTokenLimits.js +158 -0
- package/dist/api/helpers/observationalMemory.d.ts +10 -0
- package/dist/api/helpers/observationalMemory.js +36 -0
- package/dist/api/helpers/promptBudget.d.ts +30 -0
- package/dist/api/helpers/promptBudget.js +216 -0
- package/dist/api/helpers/resolveImageMessageParts.d.ts +9 -0
- package/dist/api/helpers/resolveImageMessageParts.js +140 -0
- package/dist/api/helpers/slashCommands.d.ts +31 -0
- package/dist/api/helpers/slashCommands.js +129 -0
- package/dist/api/helpers/tokenUsage.d.ts +19 -0
- package/dist/api/helpers/tokenUsage.js +81 -0
- package/dist/api/routes/fileRoutes.d.ts +2 -0
- package/dist/api/routes/fileRoutes.js +108 -0
- package/dist/api/routes/lspDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspDocumentRoutes.js +84 -0
- package/dist/api/routes/lspHierarchyRoutes.d.ts +4 -0
- package/dist/api/routes/lspHierarchyRoutes.js +122 -0
- package/dist/api/routes/lspPositionRoutes.d.ts +4 -0
- package/dist/api/routes/lspPositionRoutes.js +263 -0
- package/dist/api/routes/lspQueryRoutes.d.ts +4 -0
- package/dist/api/routes/lspQueryRoutes.js +679 -0
- package/dist/api/routes/lspResolveRoutes.d.ts +4 -0
- package/dist/api/routes/lspResolveRoutes.js +143 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.js +98 -0
- package/dist/api/routes/semanticRoutes.d.ts +2 -0
- package/dist/api/routes/semanticRoutes.js +126 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +38 -0
- package/package.json +100 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const TERMINAL_STATUSES = new Set([
|
|
2
|
+
"completed",
|
|
3
|
+
"failed",
|
|
4
|
+
"error",
|
|
5
|
+
"success",
|
|
6
|
+
"ok",
|
|
7
|
+
"rejected",
|
|
8
|
+
]);
|
|
9
|
+
const IN_PROGRESS_STATUSES = new Set([
|
|
10
|
+
"in_progress",
|
|
11
|
+
"pending",
|
|
12
|
+
"queued",
|
|
13
|
+
"running",
|
|
14
|
+
"started",
|
|
15
|
+
]);
|
|
16
|
+
const stableStringify = (value) => {
|
|
17
|
+
if (value === null || typeof value !== "object") {
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
22
|
+
}
|
|
23
|
+
const record = value;
|
|
24
|
+
const keys = Object.keys(record).sort();
|
|
25
|
+
return `{${keys
|
|
26
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
27
|
+
.join(",")}}`;
|
|
28
|
+
};
|
|
29
|
+
export const resolveToolExecutionStatus = (result) => {
|
|
30
|
+
if (typeof result === "string")
|
|
31
|
+
return "completed";
|
|
32
|
+
if (result === null || result === undefined)
|
|
33
|
+
return "unknown";
|
|
34
|
+
if (typeof result !== "object")
|
|
35
|
+
return "completed";
|
|
36
|
+
const direct = result;
|
|
37
|
+
const nested = direct.value;
|
|
38
|
+
const directStatus = typeof direct.status === "string" ? direct.status.toLowerCase() : null;
|
|
39
|
+
const nestedStatus = typeof nested?.status === "string" ? nested.status.toLowerCase() : null;
|
|
40
|
+
const rawStatus = directStatus ?? nestedStatus;
|
|
41
|
+
const successFlag = direct.success ?? nested?.success;
|
|
42
|
+
if (typeof successFlag === "boolean") {
|
|
43
|
+
if (successFlag)
|
|
44
|
+
return "completed";
|
|
45
|
+
return "failed";
|
|
46
|
+
}
|
|
47
|
+
if (rawStatus === "pending_confirmation")
|
|
48
|
+
return rawStatus;
|
|
49
|
+
if (rawStatus && ["pending", "queued", "started"].includes(rawStatus))
|
|
50
|
+
return "pending";
|
|
51
|
+
if (rawStatus && ["in_progress", "running"].includes(rawStatus))
|
|
52
|
+
return "in_progress";
|
|
53
|
+
if (rawStatus && ["completed", "success", "ok"].includes(rawStatus))
|
|
54
|
+
return "completed";
|
|
55
|
+
if (rawStatus && ["failed", "error", "rejected"].includes(rawStatus))
|
|
56
|
+
return "failed";
|
|
57
|
+
const hasErrorFlag = direct.isError === true ||
|
|
58
|
+
direct.is_error === true ||
|
|
59
|
+
nested?.isError === true ||
|
|
60
|
+
nested?.is_error === true;
|
|
61
|
+
const hasErrorMessage = Boolean(direct.error || nested?.error);
|
|
62
|
+
if (hasErrorFlag || hasErrorMessage)
|
|
63
|
+
return "failed";
|
|
64
|
+
return "completed";
|
|
65
|
+
};
|
|
66
|
+
const isTerminalResult = (result) => {
|
|
67
|
+
return TERMINAL_STATUSES.has(resolveToolExecutionStatus(result));
|
|
68
|
+
};
|
|
69
|
+
const isSettledForPendingReconciliation = (result) => {
|
|
70
|
+
const status = resolveToolExecutionStatus(result);
|
|
71
|
+
return status === "pending_confirmation" || TERMINAL_STATUSES.has(status);
|
|
72
|
+
};
|
|
73
|
+
const getResultRank = (result) => {
|
|
74
|
+
const status = resolveToolExecutionStatus(result);
|
|
75
|
+
if (status === "unknown")
|
|
76
|
+
return 1;
|
|
77
|
+
if (TERMINAL_STATUSES.has(status))
|
|
78
|
+
return 3;
|
|
79
|
+
if (IN_PROGRESS_STATUSES.has(status))
|
|
80
|
+
return 0;
|
|
81
|
+
return 2;
|
|
82
|
+
};
|
|
83
|
+
const buildDedupKey = (name, args, toolCallId, catalog) => {
|
|
84
|
+
const signatureKey = buildSignatureKey(name, args || {});
|
|
85
|
+
if (toolCallId) {
|
|
86
|
+
return resolveIdentifiedKey(name, args || {}, toolCallId, catalog);
|
|
87
|
+
}
|
|
88
|
+
return signatureKey;
|
|
89
|
+
};
|
|
90
|
+
const buildSignatureKey = (name, args) => `sig:${name}:${stableStringify(args)}`;
|
|
91
|
+
export const getToolCallSignature = (name, args) => buildSignatureKey(name, args);
|
|
92
|
+
const hasArgumentDetails = (args) => Object.keys(args || {}).length > 0;
|
|
93
|
+
const buildToolInvocationCatalog = (entries) => {
|
|
94
|
+
const catalog = new Map();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.toolCallId)
|
|
97
|
+
continue;
|
|
98
|
+
const signatureKey = buildSignatureKey(entry.name, entry.args || {});
|
|
99
|
+
const existing = catalog.get(entry.toolCallId) || [];
|
|
100
|
+
existing.push({
|
|
101
|
+
toolCallId: entry.toolCallId,
|
|
102
|
+
name: entry.name,
|
|
103
|
+
args: entry.args || {},
|
|
104
|
+
signatureKey,
|
|
105
|
+
hasArgumentDetails: hasArgumentDetails(entry.args || {}),
|
|
106
|
+
});
|
|
107
|
+
catalog.set(entry.toolCallId, existing);
|
|
108
|
+
}
|
|
109
|
+
return catalog;
|
|
110
|
+
};
|
|
111
|
+
const resolveUniqueDetailedInvocation = (name, toolCallId, catalog) => {
|
|
112
|
+
if (!catalog)
|
|
113
|
+
return undefined;
|
|
114
|
+
const candidates = (catalog.get(toolCallId) || []).filter((entry) => entry.name === name && entry.hasArgumentDetails);
|
|
115
|
+
const candidateSignatures = new Set(candidates.map((entry) => entry.signatureKey));
|
|
116
|
+
if (candidateSignatures.size !== 1) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return candidates[0];
|
|
120
|
+
};
|
|
121
|
+
const resolveIdentifiedKey = (name, args, toolCallId, catalog) => {
|
|
122
|
+
const signatureKey = buildSignatureKey(name, args || {});
|
|
123
|
+
if (!catalog || hasArgumentDetails(args || {})) {
|
|
124
|
+
return `id:${toolCallId}:${signatureKey}`;
|
|
125
|
+
}
|
|
126
|
+
const resolvedInvocation = resolveUniqueDetailedInvocation(name, toolCallId, catalog);
|
|
127
|
+
if (resolvedInvocation) {
|
|
128
|
+
return `id:${toolCallId}:${resolvedInvocation.signatureKey}`;
|
|
129
|
+
}
|
|
130
|
+
return `id:${toolCallId}:${signatureKey}`;
|
|
131
|
+
};
|
|
132
|
+
const resolveIdentifiedArgs = (name, args, toolCallId, catalog) => {
|
|
133
|
+
if (!toolCallId || hasArgumentDetails(args || {})) {
|
|
134
|
+
return args || {};
|
|
135
|
+
}
|
|
136
|
+
const resolvedInvocation = resolveUniqueDetailedInvocation(name, toolCallId, catalog);
|
|
137
|
+
return resolvedInvocation?.args || (args || {});
|
|
138
|
+
};
|
|
139
|
+
const extractSignatureFromIdentifiedKey = (key) => {
|
|
140
|
+
const marker = ":sig:";
|
|
141
|
+
const markerIndex = key.indexOf(marker);
|
|
142
|
+
if (markerIndex === -1) {
|
|
143
|
+
return key;
|
|
144
|
+
}
|
|
145
|
+
return `sig:${key.slice(markerIndex + marker.length)}`;
|
|
146
|
+
};
|
|
147
|
+
const hasSameLifecycleStep = (current, previous) => current.lifecycleStepIndex === undefined ||
|
|
148
|
+
previous.lifecycleStepIndex === undefined ||
|
|
149
|
+
current.lifecycleStepIndex === previous.lifecycleStepIndex;
|
|
150
|
+
const canReuseSameResultKey = (current, previous) => {
|
|
151
|
+
if (isTerminalResult(current.result) && isTerminalResult(previous.result)) {
|
|
152
|
+
return hasSameLifecycleStep(current, previous);
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
};
|
|
156
|
+
export const countUniqueToolCalls = (pendingToolCalls, executedToolResults) => {
|
|
157
|
+
const identifiedCatalog = buildToolInvocationCatalog([
|
|
158
|
+
...pendingToolCalls,
|
|
159
|
+
...executedToolResults,
|
|
160
|
+
]);
|
|
161
|
+
const seenIds = new Set();
|
|
162
|
+
const identifiedCounts = new Map();
|
|
163
|
+
const pendingAnonymousCounts = new Map();
|
|
164
|
+
const executedAnonymousCounts = new Map();
|
|
165
|
+
for (const call of [...pendingToolCalls, ...executedToolResults]) {
|
|
166
|
+
if (call.toolCallId) {
|
|
167
|
+
const key = buildDedupKey(call.name, call.args || {}, call.toolCallId, identifiedCatalog);
|
|
168
|
+
if (seenIds.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
seenIds.add(key);
|
|
171
|
+
const signatureKey = extractSignatureFromIdentifiedKey(key);
|
|
172
|
+
identifiedCounts.set(signatureKey, (identifiedCounts.get(signatureKey) || 0) + 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const call of pendingToolCalls) {
|
|
176
|
+
if (call.toolCallId)
|
|
177
|
+
continue;
|
|
178
|
+
const signatureKey = buildSignatureKey(call.name, call.args || {});
|
|
179
|
+
pendingAnonymousCounts.set(signatureKey, (pendingAnonymousCounts.get(signatureKey) || 0) + 1);
|
|
180
|
+
}
|
|
181
|
+
for (const result of executedToolResults) {
|
|
182
|
+
if (result.toolCallId)
|
|
183
|
+
continue;
|
|
184
|
+
const signatureKey = buildSignatureKey(result.name, result.args || {});
|
|
185
|
+
const counts = executedAnonymousCounts.get(signatureKey) || {
|
|
186
|
+
terminal: 0,
|
|
187
|
+
inProgress: 0,
|
|
188
|
+
unknown: 0,
|
|
189
|
+
};
|
|
190
|
+
const status = resolveToolExecutionStatus("result" in result ? result.result : undefined);
|
|
191
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
192
|
+
counts.terminal += 1;
|
|
193
|
+
}
|
|
194
|
+
else if (IN_PROGRESS_STATUSES.has(status)) {
|
|
195
|
+
counts.inProgress += 1;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
counts.unknown += 1;
|
|
199
|
+
}
|
|
200
|
+
executedAnonymousCounts.set(signatureKey, counts);
|
|
201
|
+
}
|
|
202
|
+
const signatures = new Set([
|
|
203
|
+
...identifiedCounts.keys(),
|
|
204
|
+
...pendingAnonymousCounts.keys(),
|
|
205
|
+
...executedAnonymousCounts.keys(),
|
|
206
|
+
]);
|
|
207
|
+
return Array.from(signatures).reduce((total, signatureKey) => {
|
|
208
|
+
const executedAnonymous = executedAnonymousCounts.get(signatureKey);
|
|
209
|
+
// Identified calls and anonymous pending calls are distinct invocations
|
|
210
|
+
// even when they share a signature, so they contribute additively.
|
|
211
|
+
const invocations = (identifiedCounts.get(signatureKey) || 0) +
|
|
212
|
+
(pendingAnonymousCounts.get(signatureKey) || 0);
|
|
213
|
+
// Anonymous result snapshots are overlapping evidence of invocations
|
|
214
|
+
// already counted above, so they only raise the floor.
|
|
215
|
+
const anonymousResultEvidence = Math.max(executedAnonymous?.terminal || 0, executedAnonymous?.inProgress || 0, executedAnonymous?.unknown || 0);
|
|
216
|
+
return total + Math.max(invocations, anonymousResultEvidence);
|
|
217
|
+
}, 0);
|
|
218
|
+
};
|
|
219
|
+
export const normalizeToolLifecycle = (pendingToolCalls, executedToolResults) => {
|
|
220
|
+
const identifiedCatalog = buildToolInvocationCatalog([
|
|
221
|
+
...pendingToolCalls,
|
|
222
|
+
...executedToolResults,
|
|
223
|
+
]);
|
|
224
|
+
const executedByKey = new Map();
|
|
225
|
+
const anonymousResultKeysBySignature = new Map();
|
|
226
|
+
// Include identified pending calls when establishing cardinality: an
|
|
227
|
+
// anonymous (missing-ID) executed result can settle an identified pending
|
|
228
|
+
// call (see the pending-reconciliation pass below), so an identified
|
|
229
|
+
// pending call also represents a known invocation slot for its signature.
|
|
230
|
+
const pendingSignatureCounts = new Map();
|
|
231
|
+
for (const call of pendingToolCalls) {
|
|
232
|
+
const signatureKey = buildSignatureKey(call.name, call.args || {});
|
|
233
|
+
pendingSignatureCounts.set(signatureKey, (pendingSignatureCounts.get(signatureKey) || 0) + 1);
|
|
234
|
+
}
|
|
235
|
+
for (const item of executedToolResults) {
|
|
236
|
+
const resolvedArgs = resolveIdentifiedArgs(item.name, item.args || {}, item.toolCallId, identifiedCatalog);
|
|
237
|
+
const normalizedItem = resolvedArgs === (item.args || {})
|
|
238
|
+
? item
|
|
239
|
+
: { ...item, args: resolvedArgs };
|
|
240
|
+
const signatureKey = buildSignatureKey(normalizedItem.name, normalizedItem.args || {});
|
|
241
|
+
const anonymousKeys = anonymousResultKeysBySignature.get(signatureKey) || [];
|
|
242
|
+
const pendingCount = pendingSignatureCounts.get(signatureKey) || 0;
|
|
243
|
+
// Prefer completing an outstanding lower-rank result (transitionKey) before
|
|
244
|
+
// collapsing into an identical terminal snapshot (sameResultKey). Only
|
|
245
|
+
// treat an identical payload as a duplicate once we already have at least
|
|
246
|
+
// as many results as known pending invocations (identified or anonymous)
|
|
247
|
+
// for this signature. A pendingCount of 0 means there is no established
|
|
248
|
+
// invocation cardinality for anonymous calls (e.g. only executed results,
|
|
249
|
+
// no pending calls at all), so these same-rank collapsing rules must not
|
|
250
|
+
// apply — otherwise distinct anonymous results with the same signature
|
|
251
|
+
// would be merged as if they were snapshots of a single invocation.
|
|
252
|
+
const hasEstablishedCardinality = pendingCount > 0 && anonymousKeys.length >= pendingCount;
|
|
253
|
+
const transitionKey = anonymousKeys.find((candidateKey) => {
|
|
254
|
+
const previous = executedByKey.get(candidateKey);
|
|
255
|
+
return (previous &&
|
|
256
|
+
getResultRank(normalizedItem.result) > getResultRank(previous.result));
|
|
257
|
+
});
|
|
258
|
+
const sameResultKey = hasEstablishedCardinality
|
|
259
|
+
? anonymousKeys.find((candidateKey) => {
|
|
260
|
+
const previous = executedByKey.get(candidateKey);
|
|
261
|
+
return (previous &&
|
|
262
|
+
canReuseSameResultKey(normalizedItem, previous) &&
|
|
263
|
+
stableStringify(previous.result) ===
|
|
264
|
+
stableStringify(normalizedItem.result));
|
|
265
|
+
})
|
|
266
|
+
: undefined;
|
|
267
|
+
const sameRankProgressKey = hasEstablishedCardinality
|
|
268
|
+
? anonymousKeys.find((candidateKey) => {
|
|
269
|
+
const previous = executedByKey.get(candidateKey);
|
|
270
|
+
return (previous &&
|
|
271
|
+
resolveToolExecutionStatus(previous.result) === "in_progress" &&
|
|
272
|
+
resolveToolExecutionStatus(normalizedItem.result) === "in_progress");
|
|
273
|
+
})
|
|
274
|
+
: undefined;
|
|
275
|
+
const sameRankTerminalKey = hasEstablishedCardinality
|
|
276
|
+
? anonymousKeys.find((candidateKey) => {
|
|
277
|
+
const previous = executedByKey.get(candidateKey);
|
|
278
|
+
return (previous &&
|
|
279
|
+
hasSameLifecycleStep(normalizedItem, previous) &&
|
|
280
|
+
isTerminalResult(previous.result) &&
|
|
281
|
+
isTerminalResult(normalizedItem.result) &&
|
|
282
|
+
getResultRank(normalizedItem.result) ===
|
|
283
|
+
getResultRank(previous.result));
|
|
284
|
+
})
|
|
285
|
+
: undefined;
|
|
286
|
+
const key = normalizedItem.toolCallId
|
|
287
|
+
? buildDedupKey(normalizedItem.name, normalizedItem.args || {}, normalizedItem.toolCallId, identifiedCatalog)
|
|
288
|
+
: transitionKey ||
|
|
289
|
+
sameResultKey ||
|
|
290
|
+
sameRankProgressKey ||
|
|
291
|
+
sameRankTerminalKey ||
|
|
292
|
+
`${signatureKey}:${anonymousKeys.length}`;
|
|
293
|
+
const previous = executedByKey.get(key);
|
|
294
|
+
if (!previous) {
|
|
295
|
+
executedByKey.set(key, normalizedItem);
|
|
296
|
+
if (!normalizedItem.toolCallId) {
|
|
297
|
+
const keys = anonymousResultKeysBySignature.get(signatureKey) || [];
|
|
298
|
+
keys.push(key);
|
|
299
|
+
anonymousResultKeysBySignature.set(signatureKey, keys);
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const prevRank = getResultRank(previous.result);
|
|
304
|
+
const nextRank = getResultRank(normalizedItem.result);
|
|
305
|
+
const isInProgressUpdate = resolveToolExecutionStatus(previous.result) === "in_progress" &&
|
|
306
|
+
resolveToolExecutionStatus(normalizedItem.result) === "in_progress";
|
|
307
|
+
// Once two results share this key, they are already confirmed to be the
|
|
308
|
+
// same invocation (via toolCallId, or via transitionKey/sameResultKey/
|
|
309
|
+
// sameRankProgressKey/sameRankTerminalKey for anonymous calls). So an
|
|
310
|
+
// equal-rank update should always replace the prior snapshot with the
|
|
311
|
+
// latest one, even when the payloads differ (e.g. failed -> completed).
|
|
312
|
+
if (nextRank >= prevRank || isInProgressUpdate) {
|
|
313
|
+
executedByKey.set(key, normalizedItem);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const normalizedExecuted = Array.from(executedByKey.values());
|
|
317
|
+
const settledExecutedKeys = new Set(normalizedExecuted
|
|
318
|
+
.filter((result) => isSettledForPendingReconciliation(result.result))
|
|
319
|
+
.map((result) => {
|
|
320
|
+
const key = buildDedupKey(result.name, result.args || {}, result.toolCallId, identifiedCatalog);
|
|
321
|
+
return key;
|
|
322
|
+
}));
|
|
323
|
+
const pendingByKey = new Map();
|
|
324
|
+
const settledAnonymousResultsBySignature = new Map();
|
|
325
|
+
for (const result of normalizedExecuted) {
|
|
326
|
+
if (!result.toolCallId &&
|
|
327
|
+
isSettledForPendingReconciliation(result.result)) {
|
|
328
|
+
const signatureKey = buildSignatureKey(result.name, result.args || {});
|
|
329
|
+
settledAnonymousResultsBySignature.set(signatureKey, (settledAnonymousResultsBySignature.get(signatureKey) || 0) + 1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [index, item] of pendingToolCalls.entries()) {
|
|
333
|
+
const key = buildDedupKey(item.name, item.args || {}, item.toolCallId, identifiedCatalog);
|
|
334
|
+
if (item.toolCallId && settledExecutedKeys.has(key)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const signatureKey = buildSignatureKey(item.name, item.args || {});
|
|
338
|
+
const matchingResultWithoutId = normalizedExecuted.filter((result) => !result.toolCallId &&
|
|
339
|
+
buildSignatureKey(result.name, result.args || {}) === signatureKey);
|
|
340
|
+
const sameSignaturePendingCalls = pendingToolCalls.filter((pending) => buildSignatureKey(pending.name, pending.args || {}) === signatureKey);
|
|
341
|
+
const hasSettledMatch = matchingResultWithoutId.some((result) => isSettledForPendingReconciliation(result.result));
|
|
342
|
+
if (hasSettledMatch) {
|
|
343
|
+
const remainingSettledResults = settledAnonymousResultsBySignature.get(signatureKey) || 0;
|
|
344
|
+
if (remainingSettledResults > 0) {
|
|
345
|
+
settledAnonymousResultsBySignature.set(signatureKey, remainingSettledResults - 1);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const preserveAnonymousMultiplicity = !item.toolCallId && sameSignaturePendingCalls.length > 1;
|
|
350
|
+
const pendingKey = preserveAnonymousMultiplicity
|
|
351
|
+
? `${signatureKey}:${index}`
|
|
352
|
+
: item.toolCallId || sameSignaturePendingCalls.length === 1
|
|
353
|
+
? key
|
|
354
|
+
: signatureKey;
|
|
355
|
+
pendingByKey.set(pendingKey, item);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
pendingToolCalls: Array.from(pendingByKey.values()),
|
|
359
|
+
executedToolResults: normalizedExecuted,
|
|
360
|
+
};
|
|
361
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Processor } from "@mastra/core/processors";
|
|
2
|
+
type RedactionSummary = {
|
|
3
|
+
result: unknown;
|
|
4
|
+
redactionCount: number;
|
|
5
|
+
redactionTypes: Set<string>;
|
|
6
|
+
};
|
|
7
|
+
export declare const redactToolResult: (value: unknown, seen?: WeakMap<object, unknown>) => RedactionSummary;
|
|
8
|
+
export declare class ToolResultSafetyProcessor implements Processor {
|
|
9
|
+
readonly id = "tool-result-safety";
|
|
10
|
+
processToolResult({ toolName, toolCallId, args, result, messageList, }: Parameters<NonNullable<Processor["processToolResult"]>>[0]): Promise<import("@mastra/core/agent").MessageList>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { redact } from "../../library/safetyMiddleware.js";
|
|
2
|
+
export const redactToolResult = (value, seen = new WeakMap()) => {
|
|
3
|
+
if (typeof value === "string") {
|
|
4
|
+
const { output, redactions } = redact(value);
|
|
5
|
+
return {
|
|
6
|
+
result: output,
|
|
7
|
+
redactionCount: redactions.length,
|
|
8
|
+
redactionTypes: new Set(redactions.map((entry) => entry.type)),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (value === null || typeof value !== "object") {
|
|
12
|
+
return { result: value, redactionCount: 0, redactionTypes: new Set() };
|
|
13
|
+
}
|
|
14
|
+
const existing = seen.get(value);
|
|
15
|
+
if (existing !== undefined) {
|
|
16
|
+
return { result: existing, redactionCount: 0, redactionTypes: new Set() };
|
|
17
|
+
}
|
|
18
|
+
const output = Array.isArray(value)
|
|
19
|
+
? []
|
|
20
|
+
: {};
|
|
21
|
+
seen.set(value, output);
|
|
22
|
+
let redactionCount = 0;
|
|
23
|
+
const redactionTypes = new Set();
|
|
24
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
25
|
+
const redacted = redactToolResult(entry, seen);
|
|
26
|
+
output[key] = redacted.result;
|
|
27
|
+
redactionCount += redacted.redactionCount;
|
|
28
|
+
redacted.redactionTypes.forEach((type) => redactionTypes.add(type));
|
|
29
|
+
}
|
|
30
|
+
return { result: output, redactionCount, redactionTypes };
|
|
31
|
+
};
|
|
32
|
+
export class ToolResultSafetyProcessor {
|
|
33
|
+
id = "tool-result-safety";
|
|
34
|
+
async processToolResult({ toolName, toolCallId, args, result, messageList, }) {
|
|
35
|
+
const redacted = redactToolResult(result);
|
|
36
|
+
if (redacted.redactionCount === 0) {
|
|
37
|
+
return messageList;
|
|
38
|
+
}
|
|
39
|
+
messageList.updateToolInvocation({
|
|
40
|
+
type: "tool-invocation",
|
|
41
|
+
toolInvocation: {
|
|
42
|
+
state: "result",
|
|
43
|
+
toolCallId,
|
|
44
|
+
toolName,
|
|
45
|
+
args,
|
|
46
|
+
result: redacted.result,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
console.warn("Redacted sensitive data from tool result", {
|
|
50
|
+
processorId: this.id,
|
|
51
|
+
toolName,
|
|
52
|
+
redactionCount: redacted.redactionCount,
|
|
53
|
+
redactionTypes: Array.from(redacted.redactionTypes).sort(),
|
|
54
|
+
});
|
|
55
|
+
return messageList;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Processor } from "@mastra/core/processors";
|
|
2
|
+
import { type WorkspaceToolHooks } from "@mastra/core/workspace";
|
|
3
|
+
type ValidationResult = Record<string, unknown> | null;
|
|
4
|
+
export declare class WorkspaceMutationBridge implements Processor {
|
|
5
|
+
private readonly basePath;
|
|
6
|
+
private readonly validate;
|
|
7
|
+
readonly id = "workspace-mutation-bridge";
|
|
8
|
+
private readonly snapshots;
|
|
9
|
+
constructor(basePath: string, validate: () => Promise<ValidationResult>);
|
|
10
|
+
readonly hooks: WorkspaceToolHooks;
|
|
11
|
+
processToolResult({ toolName, toolCallId, args, result, messageList, }: Parameters<NonNullable<Processor["processToolResult"]>>[0]): Promise<import("@mastra/core/agent").MessageList>;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { WORKSPACE_TOOLS, } from "@mastra/core/workspace";
|
|
4
|
+
import { generateDiff } from "./diffUtils.js";
|
|
5
|
+
const MUTATION_TOOLS = new Set([
|
|
6
|
+
WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE,
|
|
7
|
+
WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE,
|
|
8
|
+
WORKSPACE_TOOLS.FILESYSTEM.DELETE,
|
|
9
|
+
WORKSPACE_TOOLS.FILESYSTEM.MKDIR,
|
|
10
|
+
]);
|
|
11
|
+
const VALIDATED_TOOLS = new Set([
|
|
12
|
+
WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE,
|
|
13
|
+
WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE,
|
|
14
|
+
]);
|
|
15
|
+
const getToolCallId = (context) => {
|
|
16
|
+
if (!context || typeof context !== "object")
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = context.toolCallId;
|
|
19
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
20
|
+
};
|
|
21
|
+
const getInputPath = (input) => {
|
|
22
|
+
if (!input || typeof input !== "object")
|
|
23
|
+
return undefined;
|
|
24
|
+
const value = input.path;
|
|
25
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
26
|
+
};
|
|
27
|
+
const isFailedOutput = (output) => {
|
|
28
|
+
if (typeof output === "string") {
|
|
29
|
+
return /^(error:|cannot edit|could not find|found \d+ occurrences)/i.test(output.trim());
|
|
30
|
+
}
|
|
31
|
+
if (!output || typeof output !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const structured = output;
|
|
34
|
+
return (structured.success === false ||
|
|
35
|
+
typeof structured.error === "string" ||
|
|
36
|
+
("value" in structured && isFailedOutput(structured.value)));
|
|
37
|
+
};
|
|
38
|
+
const getOutputText = (output) => {
|
|
39
|
+
if (typeof output === "string")
|
|
40
|
+
return output;
|
|
41
|
+
if (output && typeof output === "object") {
|
|
42
|
+
const structured = output;
|
|
43
|
+
if (typeof structured.error === "string")
|
|
44
|
+
return structured.error;
|
|
45
|
+
if ("value" in structured && isFailedOutput(structured.value)) {
|
|
46
|
+
return getOutputText(structured.value);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(output);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return String(output);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return String(output ?? "");
|
|
56
|
+
};
|
|
57
|
+
const readTextIfPresent = async (absolutePath) => {
|
|
58
|
+
try {
|
|
59
|
+
return { exists: true, content: await fs.readFile(absolutePath, "utf8") };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { exists: false };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export class WorkspaceMutationBridge {
|
|
66
|
+
basePath;
|
|
67
|
+
validate;
|
|
68
|
+
id = "workspace-mutation-bridge";
|
|
69
|
+
snapshots = new Map();
|
|
70
|
+
constructor(basePath, validate) {
|
|
71
|
+
this.basePath = basePath;
|
|
72
|
+
this.validate = validate;
|
|
73
|
+
}
|
|
74
|
+
hooks = {
|
|
75
|
+
beforeToolCall: async ({ workspaceToolName, input, context }) => {
|
|
76
|
+
if (!MUTATION_TOOLS.has(workspaceToolName))
|
|
77
|
+
return;
|
|
78
|
+
const toolCallId = getToolCallId(context);
|
|
79
|
+
const inputPath = getInputPath(input);
|
|
80
|
+
if (!toolCallId || !inputPath)
|
|
81
|
+
return;
|
|
82
|
+
const absolutePath = path.resolve(this.basePath, inputPath);
|
|
83
|
+
const relativePath = path.relative(this.basePath, absolutePath);
|
|
84
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath))
|
|
85
|
+
return;
|
|
86
|
+
const before = await readTextIfPresent(absolutePath);
|
|
87
|
+
this.snapshots.set(toolCallId, {
|
|
88
|
+
path: absolutePath,
|
|
89
|
+
fileExisted: before.exists,
|
|
90
|
+
oldContent: before.content ??
|
|
91
|
+
(workspaceToolName === WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE ? "" : undefined),
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
afterToolCall: async ({ workspaceToolName, context, output, error }) => {
|
|
95
|
+
if (!MUTATION_TOOLS.has(workspaceToolName))
|
|
96
|
+
return;
|
|
97
|
+
const toolCallId = getToolCallId(context);
|
|
98
|
+
if (!toolCallId)
|
|
99
|
+
return;
|
|
100
|
+
const snapshot = this.snapshots.get(toolCallId);
|
|
101
|
+
if (!snapshot || error || isFailedOutput(output))
|
|
102
|
+
return;
|
|
103
|
+
const after = await readTextIfPresent(snapshot.path);
|
|
104
|
+
snapshot.newContent = after.content;
|
|
105
|
+
if (VALIDATED_TOOLS.has(workspaceToolName)) {
|
|
106
|
+
snapshot.validation = await this.validate();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
async processToolResult({ toolName, toolCallId, args, result, messageList, }) {
|
|
111
|
+
const snapshot = this.snapshots.get(toolCallId);
|
|
112
|
+
if (!snapshot)
|
|
113
|
+
return messageList;
|
|
114
|
+
this.snapshots.delete(toolCallId);
|
|
115
|
+
const output = getOutputText(result);
|
|
116
|
+
const failed = isFailedOutput(result);
|
|
117
|
+
const normalizedResult = {
|
|
118
|
+
success: !failed,
|
|
119
|
+
output,
|
|
120
|
+
filePath: snapshot.path,
|
|
121
|
+
fileExisted: snapshot.fileExisted,
|
|
122
|
+
};
|
|
123
|
+
if (!failed && snapshot.oldContent !== undefined && snapshot.newContent !== undefined) {
|
|
124
|
+
const { diff, linesAdded, linesRemoved } = generateDiff(snapshot.oldContent, snapshot.newContent, snapshot.path);
|
|
125
|
+
Object.assign(normalizedResult, {
|
|
126
|
+
oldContent: snapshot.oldContent,
|
|
127
|
+
newContent: snapshot.newContent,
|
|
128
|
+
diff,
|
|
129
|
+
linesAdded,
|
|
130
|
+
linesRemoved,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (snapshot.validation)
|
|
134
|
+
normalizedResult.validation = snapshot.validation;
|
|
135
|
+
if (failed)
|
|
136
|
+
normalizedResult.error = output;
|
|
137
|
+
messageList.updateToolInvocation({
|
|
138
|
+
type: "tool-invocation",
|
|
139
|
+
toolInvocation: {
|
|
140
|
+
state: "result",
|
|
141
|
+
toolCallId,
|
|
142
|
+
toolName,
|
|
143
|
+
args,
|
|
144
|
+
result: normalizedResult,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
return messageList;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type WorkspaceMutationRejection = {
|
|
2
|
+
success: false;
|
|
3
|
+
error: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const isWorkspaceMutationPathAllowed: (workspaceBasePath: string, filePath: string) => Promise<boolean>;
|
|
6
|
+
export declare const rejectUnsafeWorkspaceMutation: (workspaceBasePath: string, isVirtualWorkspace: boolean, filePaths: string[]) => Promise<WorkspaceMutationRejection | null>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const isPathWithin = (basePath, targetPath) => {
|
|
4
|
+
const relative = path.relative(basePath, targetPath);
|
|
5
|
+
return !(path.isAbsolute(relative) ||
|
|
6
|
+
relative === ".." ||
|
|
7
|
+
relative.startsWith(`..${path.sep}`));
|
|
8
|
+
};
|
|
9
|
+
const isMissingPathError = (error) => error?.code === "ENOENT";
|
|
10
|
+
const resolveExistingPath = async (targetPath) => {
|
|
11
|
+
try {
|
|
12
|
+
await fs.lstat(targetPath);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissingPathError(error)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
return fs.realpath(targetPath);
|
|
21
|
+
};
|
|
22
|
+
export const isWorkspaceMutationPathAllowed = async (workspaceBasePath, filePath) => {
|
|
23
|
+
const resolvedWorkspace = path.resolve(workspaceBasePath);
|
|
24
|
+
const resolvedTarget = path.resolve(filePath);
|
|
25
|
+
if (!isPathWithin(resolvedWorkspace, resolvedTarget)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
let realWorkspace;
|
|
29
|
+
try {
|
|
30
|
+
realWorkspace = await fs.realpath(resolvedWorkspace);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const realTarget = await resolveExistingPath(resolvedTarget);
|
|
37
|
+
if (realTarget) {
|
|
38
|
+
return isPathWithin(realWorkspace, realTarget);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// New files do not have a realpath yet. Validate the nearest existing
|
|
45
|
+
// ancestor, rejecting dangling symlinks and non-ENOENT filesystem errors.
|
|
46
|
+
let ancestor = path.dirname(resolvedTarget);
|
|
47
|
+
while (isPathWithin(resolvedWorkspace, ancestor)) {
|
|
48
|
+
try {
|
|
49
|
+
const realAncestor = await resolveExistingPath(ancestor);
|
|
50
|
+
if (realAncestor) {
|
|
51
|
+
return isPathWithin(realWorkspace, realAncestor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const parent = path.dirname(ancestor);
|
|
58
|
+
if (parent === ancestor)
|
|
59
|
+
break;
|
|
60
|
+
ancestor = parent;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
export const rejectUnsafeWorkspaceMutation = async (workspaceBasePath, isVirtualWorkspace, filePaths) => {
|
|
65
|
+
if (isVirtualWorkspace) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: "Server-side filesystem mutations are disabled for virtual workspaces",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const allowed = await Promise.all(filePaths.map((filePath) => isWorkspaceMutationPathAllowed(workspaceBasePath, filePath)));
|
|
72
|
+
if (allowed.every(Boolean)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: "File path is outside the workspace boundary",
|
|
78
|
+
};
|
|
79
|
+
};
|