@vtstech/pi-shared 1.1.2 → 1.1.4
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/debug.js +10 -0
- package/model-test-utils.js +325 -0
- package/ollama.js +18 -4
- package/package.json +1 -1
- package/react-parser.js +393 -0
- package/security.js +40 -21
package/debug.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// shared/debug.ts
|
|
2
|
+
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
3
|
+
function debugLog(module, message, ...args) {
|
|
4
|
+
if (!DEBUG_ENABLED) return;
|
|
5
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6
|
+
console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
debugLog
|
|
10
|
+
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// shared/model-test-utils.ts
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
var CONFIG = {
|
|
6
|
+
// General API settings
|
|
7
|
+
DEFAULT_TIMEOUT_MS: 999999,
|
|
8
|
+
// ~16.7 minutes — effectively unlimited for slow models
|
|
9
|
+
CONNECT_TIMEOUT_S: 60,
|
|
10
|
+
// 60 seconds to establish connection
|
|
11
|
+
MAX_RETRIES: 1,
|
|
12
|
+
// Single retry for transient failures
|
|
13
|
+
RETRY_DELAY_MS: 1e4,
|
|
14
|
+
// 10 seconds between retries
|
|
15
|
+
// Model generation settings
|
|
16
|
+
NUM_PREDICT: 1024,
|
|
17
|
+
// Max tokens in response
|
|
18
|
+
TEMPERATURE: 0.1,
|
|
19
|
+
// Low temperature for more deterministic output
|
|
20
|
+
// Test-specific settings
|
|
21
|
+
MIN_THINKING_LENGTH: 10,
|
|
22
|
+
// Minimum chars to consider thinking tokens valid
|
|
23
|
+
TOOL_TEST_TIMEOUT_MS: 999999,
|
|
24
|
+
// Effectively unlimited for slow tool usage tests
|
|
25
|
+
TOOL_SUPPORT_TIMEOUT_MS: 999999,
|
|
26
|
+
// Effectively unlimited for tool support detection
|
|
27
|
+
// Metadata retrieval
|
|
28
|
+
TAGS_TIMEOUT_MS: 15e3,
|
|
29
|
+
// 15 seconds for /api/tags
|
|
30
|
+
MODEL_INFO_TIMEOUT_MS: 3e4,
|
|
31
|
+
// 30 seconds for model info lookup
|
|
32
|
+
// Provider API settings
|
|
33
|
+
PROVIDER_TIMEOUT_MS: 999999,
|
|
34
|
+
// Effectively unlimited for cloud provider API calls
|
|
35
|
+
PROVIDER_TOOL_TIMEOUT_MS: 12e4,
|
|
36
|
+
// 120 seconds for tool usage tests on providers
|
|
37
|
+
// Rate limiting
|
|
38
|
+
TEST_DELAY_MS: 1e4
|
|
39
|
+
// 10 seconds between tests to avoid rate limiting
|
|
40
|
+
};
|
|
41
|
+
var WEATHER_TOOL_DEFINITION = {
|
|
42
|
+
type: "function",
|
|
43
|
+
function: {
|
|
44
|
+
name: "get_weather",
|
|
45
|
+
description: "Get the current weather for a location",
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
location: { type: "string", description: "City name" },
|
|
50
|
+
unit: { type: "string", enum: ["celsius", "fahrenheit"] }
|
|
51
|
+
},
|
|
52
|
+
required: ["location"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function scoreReasoning(msg) {
|
|
57
|
+
const allNumbers = msg.match(/\b(\d+)\b/g) || [];
|
|
58
|
+
const answer = allNumbers.length > 0 ? allNumbers[allNumbers.length - 1] : "?";
|
|
59
|
+
const isCorrect = answer === "8";
|
|
60
|
+
const reasoningPatterns = [
|
|
61
|
+
"because",
|
|
62
|
+
"therefore",
|
|
63
|
+
"since",
|
|
64
|
+
"step",
|
|
65
|
+
"subtract",
|
|
66
|
+
"minus",
|
|
67
|
+
"each day",
|
|
68
|
+
"each night",
|
|
69
|
+
"slides",
|
|
70
|
+
"climbs",
|
|
71
|
+
"night",
|
|
72
|
+
"reaches",
|
|
73
|
+
"finally",
|
|
74
|
+
"last day"
|
|
75
|
+
];
|
|
76
|
+
const hasReasoningWords = reasoningPatterns.some((w) => msg.toLowerCase().includes(w));
|
|
77
|
+
const hasNumberedSteps = /^\s*\d+\.\s/m.test(msg);
|
|
78
|
+
const hasReasoning = hasReasoningWords || hasNumberedSteps;
|
|
79
|
+
if (isCorrect && hasReasoning) return { score: "STRONG", pass: true };
|
|
80
|
+
if (isCorrect) return { score: "MODERATE", pass: true };
|
|
81
|
+
if (hasReasoning) return { score: "WEAK", pass: false };
|
|
82
|
+
return { score: "FAIL", pass: false };
|
|
83
|
+
}
|
|
84
|
+
function scoreNativeToolCall(fnName, args) {
|
|
85
|
+
const hasCorrectTool = fnName === "get_weather";
|
|
86
|
+
const hasLocation = typeof args.location === "string" && args.location.toLowerCase().includes("paris");
|
|
87
|
+
const unitValid = args.unit === void 0 || typeof args.unit === "string" && ["celsius", "fahrenheit"].includes(args.unit.toLowerCase());
|
|
88
|
+
if (hasCorrectTool && hasLocation && unitValid) return { score: "STRONG", pass: true };
|
|
89
|
+
if (hasCorrectTool && hasLocation) return { score: "MODERATE", pass: true };
|
|
90
|
+
return { score: "WEAK", pass: false };
|
|
91
|
+
}
|
|
92
|
+
function scoreTextToolCall(fnName, args) {
|
|
93
|
+
const isWeatherTool = fnName === "get_weather";
|
|
94
|
+
const hasLocation = typeof args.location === "string" && args.location.toLowerCase().includes("paris");
|
|
95
|
+
if (isWeatherTool && hasLocation) return { score: "STRONG", pass: true };
|
|
96
|
+
if (isWeatherTool) return { score: "MODERATE", pass: true };
|
|
97
|
+
return { score: "WEAK", pass: false };
|
|
98
|
+
}
|
|
99
|
+
function parseTextToolCall(content) {
|
|
100
|
+
const firstBrace = content.indexOf("{");
|
|
101
|
+
if (firstBrace === -1) return null;
|
|
102
|
+
const lastBrace = content.lastIndexOf("}");
|
|
103
|
+
if (lastBrace <= firstBrace) return null;
|
|
104
|
+
const jsonCandidate = content.slice(firstBrace, lastBrace + 1);
|
|
105
|
+
let textToolParsed = null;
|
|
106
|
+
try {
|
|
107
|
+
textToolParsed = JSON.parse(jsonCandidate);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!textToolParsed || typeof textToolParsed.name !== "string") return null;
|
|
112
|
+
const rawArgs = textToolParsed.arguments || { ...textToolParsed };
|
|
113
|
+
const { name: _, ...fnArgs } = rawArgs;
|
|
114
|
+
return { fnName: textToolParsed.name, args: fnArgs };
|
|
115
|
+
}
|
|
116
|
+
var TOOL_SUPPORT_CACHE_DIR = path.join(os.homedir(), ".pi", "agent", "cache");
|
|
117
|
+
var TOOL_SUPPORT_CACHE_PATH = path.join(TOOL_SUPPORT_CACHE_DIR, "tool_support.json");
|
|
118
|
+
var _toolSupportCacheInMemory = null;
|
|
119
|
+
function readToolSupportCache() {
|
|
120
|
+
try {
|
|
121
|
+
if (fs.existsSync(TOOL_SUPPORT_CACHE_PATH)) {
|
|
122
|
+
const raw = fs.readFileSync(TOOL_SUPPORT_CACHE_PATH, "utf-8");
|
|
123
|
+
return JSON.parse(raw);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
function writeToolSupportCache(cache) {
|
|
130
|
+
if (!fs.existsSync(TOOL_SUPPORT_CACHE_DIR)) {
|
|
131
|
+
fs.mkdirSync(TOOL_SUPPORT_CACHE_DIR, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
fs.writeFileSync(TOOL_SUPPORT_CACHE_PATH, JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
134
|
+
}
|
|
135
|
+
function getCachedToolSupport(model) {
|
|
136
|
+
const cache = _toolSupportCacheInMemory || readToolSupportCache();
|
|
137
|
+
if (!_toolSupportCacheInMemory) _toolSupportCacheInMemory = cache;
|
|
138
|
+
const entry = cache[model];
|
|
139
|
+
if (!entry) return null;
|
|
140
|
+
if (!entry.support || !["native", "react", "none"].includes(entry.support)) return null;
|
|
141
|
+
return entry;
|
|
142
|
+
}
|
|
143
|
+
function cacheToolSupport(model, support, family) {
|
|
144
|
+
const cache = _toolSupportCacheInMemory || readToolSupportCache();
|
|
145
|
+
cache[model] = {
|
|
146
|
+
support,
|
|
147
|
+
testedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
family
|
|
149
|
+
};
|
|
150
|
+
_toolSupportCacheInMemory = cache;
|
|
151
|
+
writeToolSupportCache(cache);
|
|
152
|
+
}
|
|
153
|
+
var REASONING_PROMPT = `A snail climbs 3 feet up a wall each day, but slides back 2 feet each night. The wall is 10 feet tall. How many days does it take the snail to reach the top? Think step by step and give the final answer on its own line like: ANSWER: <number>`;
|
|
154
|
+
var TOOL_SYSTEM_PROMPT = "You are a helpful assistant. Use the available tools when needed.";
|
|
155
|
+
var TOOL_USER_PROMPT = "What's the weather like in Paris right now?";
|
|
156
|
+
async function testToolUsageUnified(chatFn, model, options) {
|
|
157
|
+
const tools = options?.tools || [WEATHER_TOOL_DEFINITION];
|
|
158
|
+
const systemPrompt = options?.systemPrompt || TOOL_SYSTEM_PROMPT;
|
|
159
|
+
try {
|
|
160
|
+
const result = await chatFn(model, [
|
|
161
|
+
{ role: "system", content: systemPrompt },
|
|
162
|
+
{ role: "user", content: TOOL_USER_PROMPT }
|
|
163
|
+
], { tools });
|
|
164
|
+
const content = result.content;
|
|
165
|
+
const toolCalls = result.toolCalls;
|
|
166
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
167
|
+
const call = toolCalls[0];
|
|
168
|
+
const fn = call.function || {};
|
|
169
|
+
let args = {};
|
|
170
|
+
try {
|
|
171
|
+
args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments || {};
|
|
172
|
+
} catch {
|
|
173
|
+
return {
|
|
174
|
+
pass: true,
|
|
175
|
+
score: "WEAK",
|
|
176
|
+
hasToolCalls: true,
|
|
177
|
+
toolCall: `malformed args: ${String(fn.arguments)}`,
|
|
178
|
+
response: content,
|
|
179
|
+
elapsedMs: result.elapsedMs
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const { score, pass } = scoreNativeToolCall(fn.name || "", args);
|
|
183
|
+
return {
|
|
184
|
+
pass,
|
|
185
|
+
score,
|
|
186
|
+
hasToolCalls: true,
|
|
187
|
+
toolCall: `${fn.name}(${JSON.stringify(args)})`,
|
|
188
|
+
response: content,
|
|
189
|
+
elapsedMs: result.elapsedMs
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const textParsed = parseTextToolCall(content);
|
|
193
|
+
if (textParsed) {
|
|
194
|
+
const { score, pass } = scoreTextToolCall(textParsed.fnName, textParsed.args);
|
|
195
|
+
return {
|
|
196
|
+
pass,
|
|
197
|
+
score,
|
|
198
|
+
hasToolCalls: true,
|
|
199
|
+
toolCall: `${textParsed.fnName}(${JSON.stringify(textParsed.args)})`,
|
|
200
|
+
response: content,
|
|
201
|
+
elapsedMs: result.elapsedMs
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
pass: false,
|
|
206
|
+
score: "FAIL",
|
|
207
|
+
hasToolCalls: false,
|
|
208
|
+
toolCall: "none",
|
|
209
|
+
response: content,
|
|
210
|
+
elapsedMs: result.elapsedMs
|
|
211
|
+
};
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return { pass: false, score: "ERROR", hasToolCalls: false, toolCall: `error: ${e.message}`, response: "", elapsedMs: 0 };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function testReasoningUnified(chatFn, model) {
|
|
217
|
+
try {
|
|
218
|
+
const result = await chatFn(model, [
|
|
219
|
+
{ role: "user", content: REASONING_PROMPT }
|
|
220
|
+
]);
|
|
221
|
+
const msg = result.content.trim();
|
|
222
|
+
if (msg.length === 0) {
|
|
223
|
+
return { pass: false, score: "ERROR", reasoning: "Empty response", answer: "?", elapsedMs: result.elapsedMs };
|
|
224
|
+
}
|
|
225
|
+
const allNumbers = msg.match(/\b(\d+)\b/g) || [];
|
|
226
|
+
const answer = allNumbers.length > 0 ? allNumbers[allNumbers.length - 1] : "?";
|
|
227
|
+
const { score, pass } = scoreReasoning(msg);
|
|
228
|
+
return { pass, score, reasoning: msg, answer, elapsedMs: result.elapsedMs };
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return { pass: false, score: "ERROR", reasoning: e.message, answer: "?", elapsedMs: 0 };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function testInstructionFollowingUnified(chatFn, model) {
|
|
234
|
+
const prompt = `You must respond with ONLY a valid JSON object. No markdown, no explanation, no backticks, no extra text.
|
|
235
|
+
|
|
236
|
+
The JSON object must have exactly these 4 keys:
|
|
237
|
+
- "name" (string): your model name
|
|
238
|
+
- "can_count" (boolean): true
|
|
239
|
+
- "sum" (number): the result of 15 + 27
|
|
240
|
+
- "language" (string): the language you are responding in`;
|
|
241
|
+
try {
|
|
242
|
+
const result = await chatFn(model, [
|
|
243
|
+
{ role: "user", content: prompt }
|
|
244
|
+
]);
|
|
245
|
+
const msg = result.content.trim();
|
|
246
|
+
let parsed = null;
|
|
247
|
+
let repairNote = "";
|
|
248
|
+
try {
|
|
249
|
+
const cleaned = msg.replace(/```json?\s*/gi, "").replace(/```/g, "").trim();
|
|
250
|
+
parsed = JSON.parse(cleaned);
|
|
251
|
+
} catch {
|
|
252
|
+
const cleaned = msg.replace(/```json?\s*/gi, "").replace(/```/g, "").trim();
|
|
253
|
+
let braceDepth = 0, bracketDepth = 0;
|
|
254
|
+
let inString = false, escapeNext = false;
|
|
255
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
256
|
+
const c = cleaned[i];
|
|
257
|
+
if (escapeNext) {
|
|
258
|
+
escapeNext = false;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (c === "\\") {
|
|
262
|
+
if (inString) escapeNext = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (c === '"') {
|
|
266
|
+
inString = !inString;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (inString) continue;
|
|
270
|
+
if (c === "{") braceDepth++;
|
|
271
|
+
else if (c === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
272
|
+
else if (c === "[") bracketDepth++;
|
|
273
|
+
else if (c === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
274
|
+
}
|
|
275
|
+
if (braceDepth > 0 || bracketDepth > 0) {
|
|
276
|
+
const repaired = cleaned + "}".repeat(braceDepth) + "]".repeat(bracketDepth);
|
|
277
|
+
try {
|
|
278
|
+
parsed = JSON.parse(repaired);
|
|
279
|
+
repairNote = " (repaired truncated JSON)";
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!parsed) {
|
|
285
|
+
return { pass: false, score: "FAIL", output: msg, elapsedMs: result.elapsedMs };
|
|
286
|
+
}
|
|
287
|
+
const hasKeys = parsed.name && parsed.can_count !== void 0 && parsed.sum !== void 0 && parsed.language;
|
|
288
|
+
const correctSum = parsed.sum === 42;
|
|
289
|
+
const hasCorrectCount = parsed.can_count === true;
|
|
290
|
+
let score;
|
|
291
|
+
if (hasKeys && correctSum && hasCorrectCount) {
|
|
292
|
+
score = "STRONG";
|
|
293
|
+
} else if (hasKeys && (correctSum || hasCorrectCount)) {
|
|
294
|
+
score = "MODERATE";
|
|
295
|
+
} else if (parsed.sum !== void 0 || parsed.name) {
|
|
296
|
+
score = "WEAK";
|
|
297
|
+
} else {
|
|
298
|
+
score = "FAIL";
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
pass: hasKeys,
|
|
302
|
+
score,
|
|
303
|
+
output: JSON.stringify(parsed) + repairNote,
|
|
304
|
+
elapsedMs: result.elapsedMs
|
|
305
|
+
};
|
|
306
|
+
} catch (e) {
|
|
307
|
+
return { pass: false, score: "ERROR", output: e.message, elapsedMs: 0 };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export {
|
|
311
|
+
CONFIG,
|
|
312
|
+
TOOL_SUPPORT_CACHE_PATH,
|
|
313
|
+
WEATHER_TOOL_DEFINITION,
|
|
314
|
+
cacheToolSupport,
|
|
315
|
+
getCachedToolSupport,
|
|
316
|
+
parseTextToolCall,
|
|
317
|
+
readToolSupportCache,
|
|
318
|
+
scoreNativeToolCall,
|
|
319
|
+
scoreReasoning,
|
|
320
|
+
scoreTextToolCall,
|
|
321
|
+
testInstructionFollowingUnified,
|
|
322
|
+
testReasoningUnified,
|
|
323
|
+
testToolUsageUnified,
|
|
324
|
+
writeToolSupportCache
|
|
325
|
+
};
|
package/ollama.js
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// shared/debug.ts
|
|
7
|
+
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
8
|
+
function debugLog(module, message, ...args) {
|
|
9
|
+
if (!DEBUG_ENABLED) return;
|
|
10
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11
|
+
console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// shared/ollama.ts
|
|
15
|
+
var EXTENSION_VERSION = "1.1.4";
|
|
6
16
|
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
7
17
|
var _modelsJsonCache = null;
|
|
8
18
|
var _ollamaBaseUrlCache = null;
|
|
@@ -21,7 +31,8 @@ function getOllamaBaseUrl() {
|
|
|
21
31
|
return result;
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
|
-
} catch {
|
|
34
|
+
} catch (err) {
|
|
35
|
+
debugLog("ollama", "failed to parse models.json for base URL", err);
|
|
25
36
|
}
|
|
26
37
|
if (process.env.OLLAMA_HOST) {
|
|
27
38
|
const result = `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, "")}`;
|
|
@@ -42,7 +53,8 @@ function readModelsJson() {
|
|
|
42
53
|
_modelsJsonCache = { data, ts: now };
|
|
43
54
|
return data;
|
|
44
55
|
}
|
|
45
|
-
} catch {
|
|
56
|
+
} catch (err) {
|
|
57
|
+
debugLog("ollama", "failed to read/parse models.json", err);
|
|
46
58
|
}
|
|
47
59
|
const empty = { providers: {} };
|
|
48
60
|
_modelsJsonCache = { data: empty, ts: now };
|
|
@@ -53,7 +65,9 @@ function writeModelsJson(data) {
|
|
|
53
65
|
if (!fs.existsSync(dir)) {
|
|
54
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
55
67
|
}
|
|
56
|
-
|
|
68
|
+
const tmpPath = MODELS_JSON_PATH + ".tmp";
|
|
69
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
70
|
+
fs.renameSync(tmpPath, MODELS_JSON_PATH);
|
|
57
71
|
_modelsJsonCache = null;
|
|
58
72
|
_ollamaBaseUrlCache = null;
|
|
59
73
|
}
|
package/package.json
CHANGED
package/react-parser.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// shared/react-parser.ts
|
|
2
|
+
function sanitizeModelJson(text) {
|
|
3
|
+
text = text.replace(/:\s*True\b/g, ": true");
|
|
4
|
+
text = text.replace(/:\s*False\b/g, ": false");
|
|
5
|
+
text = text.replace(/:\s*None\b/g, ": null");
|
|
6
|
+
text = text.replace(/\[\s*True\b/g, "[true");
|
|
7
|
+
text = text.replace(/\[\s*False\b/g, "[false");
|
|
8
|
+
text = text.replace(/\[\s*None\b/g, "[null");
|
|
9
|
+
text = text.replace(/("(?:[^"\\]|\\.)*")\s*\+\s*[^,}'"\]\n]+/g, "$1");
|
|
10
|
+
text = text.replace(/,\s*([}\]])/g, "$1");
|
|
11
|
+
text = text.replace(/\\\\\\\\/g, "\\\\");
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
var REACT_DIALECTS = [
|
|
15
|
+
{
|
|
16
|
+
name: "react",
|
|
17
|
+
actionTag: "Action:",
|
|
18
|
+
inputTag: "Action Input:",
|
|
19
|
+
thoughtTag: "Thought:",
|
|
20
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Action:"],
|
|
21
|
+
finalTag: "Final Answer:"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "function",
|
|
25
|
+
actionTag: "Function:",
|
|
26
|
+
inputTag: "Function Input:",
|
|
27
|
+
thoughtTag: "Thought:",
|
|
28
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Function:", "Action:"],
|
|
29
|
+
finalTag: "Final Answer:"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "tool",
|
|
33
|
+
actionTag: "Tool:",
|
|
34
|
+
inputTag: "Tool Input:",
|
|
35
|
+
thoughtTag: "Thought:",
|
|
36
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Tool:", "Action:"],
|
|
37
|
+
finalTag: "Final Answer:"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "call",
|
|
41
|
+
actionTag: "Call:",
|
|
42
|
+
inputTag: "Input:",
|
|
43
|
+
thoughtTag: "Thought:",
|
|
44
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Call:", "Action:"],
|
|
45
|
+
finalTag: "Final Answer:"
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
function buildDialectPatterns(d) {
|
|
49
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
50
|
+
const aT = esc(d.actionTag);
|
|
51
|
+
const iT = esc(d.inputTag);
|
|
52
|
+
const stopAlt = d.stopTags.map(esc).join("|");
|
|
53
|
+
const tT = d.thoughtTag ? esc(d.thoughtTag) : void 0;
|
|
54
|
+
const fT = d.finalTag ? esc(d.finalTag) : void 0;
|
|
55
|
+
const thoughtRe = tT ? new RegExp(`${tT}\\s*(.*?)(?=${aT}|${fT}|$)`, "is") : void 0;
|
|
56
|
+
const actionRe = new RegExp(
|
|
57
|
+
`${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s*\\n?\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
58
|
+
"is"
|
|
59
|
+
);
|
|
60
|
+
const actionReSameline = new RegExp(
|
|
61
|
+
`${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s+${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
62
|
+
"is"
|
|
63
|
+
);
|
|
64
|
+
const actionReLoose = new RegExp(
|
|
65
|
+
`${aT}\\s*(.+?)\\n\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
66
|
+
"is"
|
|
67
|
+
);
|
|
68
|
+
const actionReParen = new RegExp(`${aT}\\s*(\\w+)\\s*\\(([^)]*)\\)`, "i");
|
|
69
|
+
const finalAnswerRe = fT ? new RegExp(`${fT}\\s*([\\s\\S]*?)$`, "i") : void 0;
|
|
70
|
+
return { thoughtRe, actionRe, actionReSameline, actionReLoose, actionReParen, finalAnswerRe, dialect: d };
|
|
71
|
+
}
|
|
72
|
+
var ALL_DIALECT_PATTERNS = REACT_DIALECTS.map(buildDialectPatterns);
|
|
73
|
+
var CLASSIC_PATTERNS = ALL_DIALECT_PATTERNS[0];
|
|
74
|
+
var THOUGHT_RE = CLASSIC_PATTERNS.thoughtRe;
|
|
75
|
+
var ACTION_RE = CLASSIC_PATTERNS.actionRe;
|
|
76
|
+
var ACTION_RE_SAMELINE = CLASSIC_PATTERNS.actionReSameline;
|
|
77
|
+
var ACTION_RE_LOOSE = CLASSIC_PATTERNS.actionReLoose;
|
|
78
|
+
var ACTION_RE_PAREN = CLASSIC_PATTERNS.actionReParen;
|
|
79
|
+
var FINAL_ANSWER_RE = CLASSIC_PATTERNS.finalAnswerRe;
|
|
80
|
+
function extractJsonArgs(rawArgs) {
|
|
81
|
+
const start = rawArgs.indexOf("{");
|
|
82
|
+
if (start === -1) return null;
|
|
83
|
+
let depth = 0;
|
|
84
|
+
let end = -1;
|
|
85
|
+
for (let i = start; i < rawArgs.length; i++) {
|
|
86
|
+
if (rawArgs[i] === "{") depth++;
|
|
87
|
+
else if (rawArgs[i] === "}") {
|
|
88
|
+
depth--;
|
|
89
|
+
if (depth === 0) {
|
|
90
|
+
end = i;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (end === -1) return null;
|
|
96
|
+
const jsonStr = rawArgs.slice(start, end + 1);
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(jsonStr);
|
|
99
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : { input: String(parsed) };
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const sanitized = sanitizeModelJson(jsonStr);
|
|
104
|
+
const parsed = JSON.parse(sanitized);
|
|
105
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : { input: String(parsed) };
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
const exprMatch = jsonStr.match(/['"]expression['"]:\s*['"]([^'"]+)['"]/);
|
|
109
|
+
if (exprMatch) return { expression: exprMatch[1] };
|
|
110
|
+
const cmdMatch = jsonStr.match(/['"]command['"]:\s*['"]([^'"]+)['"]/);
|
|
111
|
+
if (cmdMatch) return { command: cmdMatch[1] };
|
|
112
|
+
return { input: jsonStr };
|
|
113
|
+
}
|
|
114
|
+
function parseReact(text) {
|
|
115
|
+
for (const dp of ALL_DIALECT_PATTERNS) {
|
|
116
|
+
const result = parseReactWithPatterns(text, dp);
|
|
117
|
+
if (result) return result;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function parseReactWithPatterns(text, dp, tightLoose = false, availableTools) {
|
|
122
|
+
let thought;
|
|
123
|
+
if (dp.thoughtRe) {
|
|
124
|
+
const thoughtMatch = dp.thoughtRe.exec(text);
|
|
125
|
+
if (thoughtMatch) thought = thoughtMatch[1].trim();
|
|
126
|
+
}
|
|
127
|
+
let match = dp.actionRe.exec(text);
|
|
128
|
+
if (!match) match = dp.actionReSameline.exec(text);
|
|
129
|
+
let looseMatch = false;
|
|
130
|
+
if (!match) {
|
|
131
|
+
const looseResult = dp.actionReLoose.exec(text);
|
|
132
|
+
if (looseResult) {
|
|
133
|
+
if (tightLoose) {
|
|
134
|
+
const candidate = looseResult[1].trim().replace(/[`"']/g, "");
|
|
135
|
+
const isToolIdentifier = /^\w+$/.test(candidate) && (candidate.includes("_") || candidate.includes("-"));
|
|
136
|
+
const isKnownTool = /^(get_weather|calculate)$/i.test(candidate);
|
|
137
|
+
if (isToolIdentifier || isKnownTool) {
|
|
138
|
+
match = looseResult;
|
|
139
|
+
looseMatch = true;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
match = looseResult;
|
|
143
|
+
looseMatch = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let parenMatch = false;
|
|
148
|
+
if (!match) match = dp.actionReParen.exec(text), parenMatch = true;
|
|
149
|
+
if (match) {
|
|
150
|
+
let toolName = match[1].trim().replace(/[`"']/g, "");
|
|
151
|
+
if (looseMatch && !tightLoose && availableTools) {
|
|
152
|
+
const tools = availableTools || [];
|
|
153
|
+
for (const real of tools) {
|
|
154
|
+
const rl = real.toLowerCase().replace(/_/g, "");
|
|
155
|
+
if (toolName.toLowerCase().includes(rl)) {
|
|
156
|
+
toolName = real;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (toolName.includes(" ")) {
|
|
161
|
+
const words = toolName.split(/\s+/);
|
|
162
|
+
for (const w of words) {
|
|
163
|
+
const wc = w.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
164
|
+
if (wc.length < 3) continue;
|
|
165
|
+
for (const real of tools) {
|
|
166
|
+
const rl = real.toLowerCase().replace(/_/g, "");
|
|
167
|
+
if (rl.includes(wc.toLowerCase())) {
|
|
168
|
+
toolName = real;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!toolName.includes(" ")) break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const rawArgs = match[2].trim().replace(/^```\w*\s*/gm, "").replace(/```\s*$/gm, "").trim();
|
|
177
|
+
let args;
|
|
178
|
+
if (parenMatch && rawArgs && !rawArgs.startsWith("{")) {
|
|
179
|
+
const pairs = rawArgs.match(/(\w+)\s*:\s*("[^"]*"|'[^']*'|\S+)/g);
|
|
180
|
+
if (pairs) {
|
|
181
|
+
const obj = {};
|
|
182
|
+
for (const p of pairs) {
|
|
183
|
+
const colonIdx = p.indexOf(":");
|
|
184
|
+
const key = p.slice(0, colonIdx).trim();
|
|
185
|
+
let val = p.slice(colonIdx + 1).trim();
|
|
186
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
187
|
+
val = val.slice(1, -1);
|
|
188
|
+
}
|
|
189
|
+
obj[key] = val;
|
|
190
|
+
}
|
|
191
|
+
args = obj;
|
|
192
|
+
} else {
|
|
193
|
+
args = { input: rawArgs };
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
args = extractJsonArgs(rawArgs) || { input: rawArgs };
|
|
197
|
+
}
|
|
198
|
+
let finalAnswer;
|
|
199
|
+
if (dp.finalAnswerRe) {
|
|
200
|
+
const faMatch = dp.finalAnswerRe.exec(text);
|
|
201
|
+
if (faMatch) finalAnswer = faMatch[1].trim();
|
|
202
|
+
}
|
|
203
|
+
return { name: toolName, args, thought, finalAnswer, raw: match[0], dialect: dp.dialect.name };
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function detectReactDialect(text) {
|
|
208
|
+
for (const dp of ALL_DIALECT_PATTERNS) {
|
|
209
|
+
const tagPattern = new RegExp(`^\\s*${dp.dialect.actionTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "im");
|
|
210
|
+
if (tagPattern.test(text)) return dp.dialect;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function extractToolFromJson(obj) {
|
|
215
|
+
if (!obj || typeof obj !== "object") return null;
|
|
216
|
+
let name = obj.name || obj.function || obj.tool || obj.action;
|
|
217
|
+
let args = obj.arguments || obj.parameters || obj.args || obj.actionInput || {};
|
|
218
|
+
if (!name) {
|
|
219
|
+
for (const key of Object.keys(obj)) {
|
|
220
|
+
const kl = key.toLowerCase();
|
|
221
|
+
if (kl === "action" && typeof obj[key] === "string") {
|
|
222
|
+
name = obj[key];
|
|
223
|
+
}
|
|
224
|
+
if (kl === "action input" || kl === "actioninput" || kl === "action_input") {
|
|
225
|
+
const val = obj[key];
|
|
226
|
+
if (typeof val === "object" && val !== null) args = val;
|
|
227
|
+
else if (val) args = { input: val };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!name) {
|
|
232
|
+
const argToTool = { expression: "calculator", command: "shell" };
|
|
233
|
+
const nonToolKeys = /* @__PURE__ */ new Set(["response", "method", "answer", "result", "explanation", "output", "text"]);
|
|
234
|
+
const objKeys = Object.keys(obj);
|
|
235
|
+
if (!objKeys.some((k) => nonToolKeys.has(k))) {
|
|
236
|
+
for (const key of objKeys) {
|
|
237
|
+
if (key in argToTool) {
|
|
238
|
+
name = argToTool[key];
|
|
239
|
+
args = obj;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!name || typeof args !== "object" || args === null) return null;
|
|
246
|
+
return { name, args };
|
|
247
|
+
}
|
|
248
|
+
var FUZZY_MIN_PREFIX_LENGTH = 4;
|
|
249
|
+
var WORD_MAPPINGS = {
|
|
250
|
+
calculate: ["calculator"],
|
|
251
|
+
calc: ["calculator"],
|
|
252
|
+
math: ["calculator"],
|
|
253
|
+
compute: ["calculator"],
|
|
254
|
+
eval: ["calculator"],
|
|
255
|
+
expression: ["calculator"],
|
|
256
|
+
power: ["calculator"],
|
|
257
|
+
pow: ["calculator"],
|
|
258
|
+
sqrt: ["calculator"],
|
|
259
|
+
python: ["shell"],
|
|
260
|
+
repl: ["shell"],
|
|
261
|
+
code: ["shell"],
|
|
262
|
+
execute: ["shell"],
|
|
263
|
+
shell: ["bash"],
|
|
264
|
+
bash: ["bash"],
|
|
265
|
+
cmd: ["bash"],
|
|
266
|
+
command: ["bash"],
|
|
267
|
+
ls: ["bash"],
|
|
268
|
+
cat: ["bash"],
|
|
269
|
+
echo: ["bash"],
|
|
270
|
+
grep: ["bash"],
|
|
271
|
+
read: ["read"],
|
|
272
|
+
write: ["write"],
|
|
273
|
+
file: ["read"],
|
|
274
|
+
weather: ["get_weather"],
|
|
275
|
+
search: ["bash"]
|
|
276
|
+
};
|
|
277
|
+
function fuzzyMatchToolName(hallucinated, availableTools) {
|
|
278
|
+
const lower = hallucinated.toLowerCase().replace(/_/g, "");
|
|
279
|
+
if (availableTools.includes(hallucinated)) return hallucinated;
|
|
280
|
+
for (const real of availableTools) {
|
|
281
|
+
const rl = real.toLowerCase().replace(/_/g, "");
|
|
282
|
+
if (rl === lower || rl.includes(lower) || lower.includes(rl)) return real;
|
|
283
|
+
}
|
|
284
|
+
for (const [keyword, hints] of Object.entries(WORD_MAPPINGS)) {
|
|
285
|
+
if (lower.includes(keyword)) {
|
|
286
|
+
for (const hint of hints) {
|
|
287
|
+
for (const real of availableTools) {
|
|
288
|
+
if (real.includes(hint) || real === hint) return real;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (lower.length >= FUZZY_MIN_PREFIX_LENGTH) {
|
|
294
|
+
for (const real of availableTools) {
|
|
295
|
+
const rl = real.toLowerCase();
|
|
296
|
+
if (rl.length >= FUZZY_MIN_PREFIX_LENGTH && rl.slice(0, FUZZY_MIN_PREFIX_LENGTH) === lower.slice(0, FUZZY_MIN_PREFIX_LENGTH)) return real;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
var ARG_ALIASES = {
|
|
302
|
+
expression: ["expr", "exp", "formula", "calculation", "math"],
|
|
303
|
+
file_path: ["path", "filepath", "file", "filename", "location"],
|
|
304
|
+
content: ["text", "data", "body", "value"],
|
|
305
|
+
command: ["cmd", "shell", "script", "exec"],
|
|
306
|
+
url: ["uri", "link", "endpoint", "address"],
|
|
307
|
+
query: ["search", "term", "keywords", "q"],
|
|
308
|
+
input: ["value", "arg", "parameter"],
|
|
309
|
+
timeout: ["time_limit", "max_time", "seconds"]
|
|
310
|
+
};
|
|
311
|
+
function normalizeArguments(args, expectedParams) {
|
|
312
|
+
if (!args || typeof args !== "object") return args;
|
|
313
|
+
const expectedSet = new Set(expectedParams.map((p) => p.toLowerCase()));
|
|
314
|
+
const normalized = {};
|
|
315
|
+
const powerParts = {};
|
|
316
|
+
for (const [key, value] of Object.entries(args)) {
|
|
317
|
+
const keyLower = key.toLowerCase().replace(/-/g, "_");
|
|
318
|
+
let targetParam = null;
|
|
319
|
+
for (const param of expectedParams) {
|
|
320
|
+
if (param.toLowerCase() === keyLower) {
|
|
321
|
+
targetParam = param;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!targetParam) {
|
|
326
|
+
for (const [canonical, aliases] of Object.entries(ARG_ALIASES)) {
|
|
327
|
+
if (aliases.includes(keyLower) && expectedSet.has(canonical.toLowerCase())) {
|
|
328
|
+
targetParam = canonical;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!targetParam) {
|
|
334
|
+
for (const param of expectedParams) {
|
|
335
|
+
if (keyLower.includes(param.toLowerCase()) || keyLower.startsWith(param.toLowerCase())) {
|
|
336
|
+
targetParam = param;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (["base", "value", "x"].includes(keyLower) || ["exponent", "power", "n", "p", "exp"].includes(keyLower)) {
|
|
342
|
+
powerParts[keyLower] = value;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const finalKey = targetParam || key;
|
|
346
|
+
if (!(finalKey in normalized)) normalized[finalKey] = value;
|
|
347
|
+
}
|
|
348
|
+
if (powerParts && expectedSet.has("expression")) {
|
|
349
|
+
const base = powerParts.base ?? powerParts.value ?? powerParts.x;
|
|
350
|
+
const exp = powerParts.exponent ?? powerParts.power ?? powerParts.n ?? powerParts.p ?? powerParts.exp;
|
|
351
|
+
if (base !== void 0 && exp !== void 0) normalized.expression = `${base} ** ${exp}`;
|
|
352
|
+
else if (base !== void 0) normalized.expression = String(base);
|
|
353
|
+
}
|
|
354
|
+
return normalized;
|
|
355
|
+
}
|
|
356
|
+
function looksLikeSchemaDump(text) {
|
|
357
|
+
if (!text) return false;
|
|
358
|
+
const indicators = [
|
|
359
|
+
'{"function <nil>',
|
|
360
|
+
'"type":"function"',
|
|
361
|
+
'"parameters":{"type":"object"',
|
|
362
|
+
'[{"type":',
|
|
363
|
+
'"required":',
|
|
364
|
+
'"properties":'
|
|
365
|
+
];
|
|
366
|
+
const lower = text.toLowerCase();
|
|
367
|
+
const matches = indicators.filter((i) => lower.includes(i.toLowerCase())).length;
|
|
368
|
+
return matches >= 2;
|
|
369
|
+
}
|
|
370
|
+
export {
|
|
371
|
+
ACTION_RE,
|
|
372
|
+
ACTION_RE_LOOSE,
|
|
373
|
+
ACTION_RE_PAREN,
|
|
374
|
+
ACTION_RE_SAMELINE,
|
|
375
|
+
ALL_DIALECT_PATTERNS,
|
|
376
|
+
ARG_ALIASES,
|
|
377
|
+
CLASSIC_PATTERNS,
|
|
378
|
+
FINAL_ANSWER_RE,
|
|
379
|
+
FUZZY_MIN_PREFIX_LENGTH,
|
|
380
|
+
REACT_DIALECTS,
|
|
381
|
+
THOUGHT_RE,
|
|
382
|
+
WORD_MAPPINGS,
|
|
383
|
+
buildDialectPatterns,
|
|
384
|
+
detectReactDialect,
|
|
385
|
+
extractJsonArgs,
|
|
386
|
+
extractToolFromJson,
|
|
387
|
+
fuzzyMatchToolName,
|
|
388
|
+
looksLikeSchemaDump,
|
|
389
|
+
normalizeArguments,
|
|
390
|
+
parseReact,
|
|
391
|
+
parseReactWithPatterns,
|
|
392
|
+
sanitizeModelJson
|
|
393
|
+
};
|
package/security.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
// shared/debug.ts
|
|
7
|
+
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
8
|
+
function debugLog(module, message, ...args) {
|
|
9
|
+
if (!DEBUG_ENABLED) return;
|
|
10
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11
|
+
console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// shared/security.ts
|
|
15
|
+
var SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
5
16
|
var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
6
17
|
// System modification
|
|
7
18
|
"rm",
|
|
@@ -155,7 +166,11 @@ function validatePath(filePath, allowedDirs) {
|
|
|
155
166
|
"/.gnupg/",
|
|
156
167
|
path.join(os.homedir(), ".ssh"),
|
|
157
168
|
path.join(os.homedir(), ".gnupg"),
|
|
158
|
-
|
|
169
|
+
SETTINGS_PATH
|
|
170
|
+
// NOTE: models.json is intentionally excluded from sensitivePaths.
|
|
171
|
+
// Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
|
|
172
|
+
// for direct file I/O — not via Pi's tool system — so blocking it here
|
|
173
|
+
// would prevent legitimate model configuration updates.
|
|
159
174
|
];
|
|
160
175
|
for (const sensitive of sensitivePaths) {
|
|
161
176
|
if (resolved.startsWith(sensitive) || resolved === sensitive) {
|
|
@@ -195,9 +210,16 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
195
210
|
return { safe: false, error: "URL must have a hostname" };
|
|
196
211
|
}
|
|
197
212
|
const hostname = parsed.hostname.toLowerCase();
|
|
213
|
+
const normalized = hostname.replace(/\.$/, "");
|
|
214
|
+
if (/[^\x00-\x7F]/.test(normalized)) {
|
|
215
|
+
return { safe: false, error: "URL hostname contains non-ASCII characters" };
|
|
216
|
+
}
|
|
217
|
+
if (/^0x[0-9a-f]+$/i.test(normalized) || /^0[0-7]+$/i.test(normalized)) {
|
|
218
|
+
return { safe: false, error: "URL hostname uses non-decimal IP format" };
|
|
219
|
+
}
|
|
198
220
|
if (blockSsrf) {
|
|
199
221
|
for (const pattern of BLOCKED_URL_PATTERNS) {
|
|
200
|
-
if (
|
|
222
|
+
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
201
223
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
202
224
|
}
|
|
203
225
|
}
|
|
@@ -205,26 +227,20 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
205
227
|
return { safe: true, error: "" };
|
|
206
228
|
}
|
|
207
229
|
var INJECTION_PATTERNS = [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// OR chaining
|
|
230
|
+
// Command chaining with dangerous commands only
|
|
231
|
+
/;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
|
|
232
|
+
// Piping to dangerous commands
|
|
233
|
+
/\|\s*(rm|sudo|chmod|chown|shred|mkfs)\b/i,
|
|
234
|
+
// AND chaining with dangerous commands
|
|
235
|
+
/&&\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill)\b/i,
|
|
236
|
+
// Command substitution (backticks) — still dangerous
|
|
216
237
|
/`[^`]+`/,
|
|
217
|
-
// Command substitution (
|
|
238
|
+
// Command substitution ($()) — still dangerous
|
|
218
239
|
/\$\([^)]+\)/,
|
|
219
|
-
//
|
|
220
|
-
/\$\{
|
|
221
|
-
//
|
|
222
|
-
/>\s*\S+/,
|
|
223
|
-
// Output redirection
|
|
224
|
-
/<\s*\S+/,
|
|
225
|
-
// Input redirection
|
|
240
|
+
// Variable expansion targeting sensitive env vars
|
|
241
|
+
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i,
|
|
242
|
+
// Bare pipe without space (likely injection, not intentional piping)
|
|
226
243
|
/\|(?=[^\s|])/
|
|
227
|
-
// Bare pipe without space
|
|
228
244
|
];
|
|
229
245
|
function sanitizeCommand(command) {
|
|
230
246
|
if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
@@ -256,7 +272,8 @@ function appendAuditEntry(entry) {
|
|
|
256
272
|
}
|
|
257
273
|
const line = JSON.stringify(entry) + "\n";
|
|
258
274
|
fs.appendFileSync(AUDIT_LOG_PATH, line, "utf-8");
|
|
259
|
-
} catch {
|
|
275
|
+
} catch (err) {
|
|
276
|
+
debugLog("security", "audit log write failure", err);
|
|
260
277
|
}
|
|
261
278
|
}
|
|
262
279
|
function readRecentAuditEntries(count = 50) {
|
|
@@ -272,7 +289,8 @@ function readRecentAuditEntries(count = 50) {
|
|
|
272
289
|
return {};
|
|
273
290
|
}
|
|
274
291
|
});
|
|
275
|
-
} catch {
|
|
292
|
+
} catch (err) {
|
|
293
|
+
debugLog("security", "failed to read audit log", err);
|
|
276
294
|
return [];
|
|
277
295
|
}
|
|
278
296
|
}
|
|
@@ -335,6 +353,7 @@ export {
|
|
|
335
353
|
AUDIT_LOG_PATH,
|
|
336
354
|
BLOCKED_COMMANDS,
|
|
337
355
|
BLOCKED_URL_PATTERNS,
|
|
356
|
+
SETTINGS_PATH,
|
|
338
357
|
appendAuditEntry,
|
|
339
358
|
checkBashToolInput,
|
|
340
359
|
checkFileToolInput,
|