@vtstech/pi-shared 1.1.7 → 1.1.9
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/config-io.js +57 -0
- package/errors.js +56 -0
- package/format.js +1 -0
- package/model-test-utils.js +4 -0
- package/ollama.js +13 -3
- package/package.json +1 -1
- package/provider-sync.js +18 -0
- package/react-parser.js +18 -0
- package/security.js +162 -39
- package/test-report.js +89 -0
package/config-io.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// shared/config-io.ts
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
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/config-io.ts
|
|
15
|
+
var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
16
|
+
function readJsonConfig(filePath, defaultValue = {}) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(filePath)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
debugLog("config-io", `failed to read config: ${filePath}`, err);
|
|
23
|
+
}
|
|
24
|
+
return defaultValue;
|
|
25
|
+
}
|
|
26
|
+
function writeJsonConfig(filePath, data) {
|
|
27
|
+
const dir = path.dirname(filePath);
|
|
28
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const content = JSON.stringify(data, null, 2) + "\n";
|
|
30
|
+
const tmpPath = filePath + ".tmp";
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
33
|
+
fs.renameSync(tmpPath, filePath);
|
|
34
|
+
} catch {
|
|
35
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var SETTINGS_PATH = path.join(PI_AGENT_DIR, "settings.json");
|
|
39
|
+
var SECURITY_PATH = path.join(PI_AGENT_DIR, "security.json");
|
|
40
|
+
var REACT_MODE_PATH = path.join(PI_AGENT_DIR, "react-mode.json");
|
|
41
|
+
var MODEL_TEST_CONFIG_PATH = path.join(PI_AGENT_DIR, "model-test-config.json");
|
|
42
|
+
function readSettings() {
|
|
43
|
+
return readJsonConfig(SETTINGS_PATH);
|
|
44
|
+
}
|
|
45
|
+
function writeSettings(data) {
|
|
46
|
+
writeJsonConfig(SETTINGS_PATH, data);
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
MODEL_TEST_CONFIG_PATH,
|
|
50
|
+
REACT_MODE_PATH,
|
|
51
|
+
SECURITY_PATH,
|
|
52
|
+
SETTINGS_PATH,
|
|
53
|
+
readJsonConfig,
|
|
54
|
+
readSettings,
|
|
55
|
+
writeJsonConfig,
|
|
56
|
+
writeSettings
|
|
57
|
+
};
|
package/errors.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// shared/errors.ts
|
|
6
|
+
var ExtensionError = class extends Error {
|
|
7
|
+
constructor(message, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
__publicField(this, "code", code);
|
|
10
|
+
this.name = "ExtensionError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var ConfigError = class extends ExtensionError {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message, "CONFIG_ERROR");
|
|
16
|
+
this.name = "ConfigError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ApiError = class extends ExtensionError {
|
|
20
|
+
constructor(message, statusCode, url) {
|
|
21
|
+
super(message, "API_ERROR");
|
|
22
|
+
__publicField(this, "statusCode", statusCode);
|
|
23
|
+
__publicField(this, "url", url);
|
|
24
|
+
this.name = "ApiError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var ExtensionTimeoutError = class extends ExtensionError {
|
|
28
|
+
constructor(message, timeoutMs) {
|
|
29
|
+
super(message, "TIMEOUT");
|
|
30
|
+
__publicField(this, "timeoutMs", timeoutMs);
|
|
31
|
+
this.name = "ExtensionTimeoutError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var SecurityError = class extends ExtensionError {
|
|
35
|
+
constructor(message, rule, detail) {
|
|
36
|
+
super(message, "SECURITY_VIOLATION");
|
|
37
|
+
__publicField(this, "rule", rule);
|
|
38
|
+
__publicField(this, "detail", detail);
|
|
39
|
+
this.name = "SecurityError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var ToolError = class extends ExtensionError {
|
|
43
|
+
constructor(message, toolName) {
|
|
44
|
+
super(message, "TOOL_ERROR");
|
|
45
|
+
__publicField(this, "toolName", toolName);
|
|
46
|
+
this.name = "ToolError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export {
|
|
50
|
+
ApiError,
|
|
51
|
+
ConfigError,
|
|
52
|
+
ExtensionError,
|
|
53
|
+
ExtensionTimeoutError,
|
|
54
|
+
SecurityError,
|
|
55
|
+
ToolError
|
|
56
|
+
};
|
package/format.js
CHANGED
|
@@ -32,6 +32,7 @@ function msHuman(ms) {
|
|
|
32
32
|
}
|
|
33
33
|
function fmtBytes(b) {
|
|
34
34
|
if (b === 0) return "0B";
|
|
35
|
+
if (b < 1024) return `${b}B`;
|
|
35
36
|
if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)}G`;
|
|
36
37
|
if (b >= 1048576) return `${(b / 1048576).toFixed(0)}M`;
|
|
37
38
|
return `${(b / 1024).toFixed(0)}K`;
|
package/model-test-utils.js
CHANGED
|
@@ -34,6 +34,9 @@ var CONFIG = {
|
|
|
34
34
|
// Effectively unlimited for cloud provider API calls
|
|
35
35
|
PROVIDER_TOOL_TIMEOUT_MS: 12e4,
|
|
36
36
|
// 120 seconds for tool usage tests on providers
|
|
37
|
+
// Context length fetching
|
|
38
|
+
CONTEXT_BATCH_SIZE: 3,
|
|
39
|
+
// Concurrent requests when fetching model context lengths
|
|
37
40
|
// Rate limiting
|
|
38
41
|
TEST_DELAY_MS: 1e4
|
|
39
42
|
// 10 seconds between tests to avoid rate limiting
|
|
@@ -62,6 +65,7 @@ function getEffectiveConfig() {
|
|
|
62
65
|
TOOL_TEST_TIMEOUT_MS: userConfig.toolTestTimeoutMs ?? CONFIG.TOOL_TEST_TIMEOUT_MS,
|
|
63
66
|
PROVIDER_TIMEOUT_MS: userConfig.providerTimeoutMs ?? CONFIG.PROVIDER_TIMEOUT_MS,
|
|
64
67
|
PROVIDER_TOOL_TIMEOUT_MS: userConfig.providerToolTimeoutMs ?? CONFIG.PROVIDER_TOOL_TIMEOUT_MS,
|
|
68
|
+
CONTEXT_BATCH_SIZE: userConfig.contextBatchSize ?? CONFIG.CONTEXT_BATCH_SIZE,
|
|
65
69
|
NUM_PREDICT: userConfig.numPredict ?? CONFIG.NUM_PREDICT,
|
|
66
70
|
TEMPERATURE: userConfig.temperature ?? CONFIG.TEMPERATURE
|
|
67
71
|
};
|
package/ollama.js
CHANGED
|
@@ -12,7 +12,7 @@ function debugLog(module, message, ...args) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// shared/ollama.ts
|
|
15
|
-
var EXTENSION_VERSION = "1.1.
|
|
15
|
+
var EXTENSION_VERSION = "1.1.9";
|
|
16
16
|
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
17
17
|
var _modelsJsonCache = null;
|
|
18
18
|
var _ollamaBaseUrlCache = null;
|
|
@@ -179,7 +179,8 @@ async function fetchModelContextLength(baseUrl, modelName) {
|
|
|
179
179
|
}
|
|
180
180
|
const numCtx = data?.model_info?.["num_ctx"];
|
|
181
181
|
if (typeof numCtx === "number") return numCtx;
|
|
182
|
-
} catch {
|
|
182
|
+
} catch (err) {
|
|
183
|
+
debugLog("ollama", `failed to fetch context length for ${modelName}`, err);
|
|
183
184
|
return void 0;
|
|
184
185
|
}
|
|
185
186
|
return void 0;
|
|
@@ -213,7 +214,8 @@ var BUILTIN_PROVIDERS = {
|
|
|
213
214
|
xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1", envKey: "XAI_API_KEY" },
|
|
214
215
|
together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1", envKey: "TOGETHER_API_KEY" },
|
|
215
216
|
fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", envKey: "FIREWORKS_API_KEY" },
|
|
216
|
-
cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" }
|
|
217
|
+
cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" },
|
|
218
|
+
zai: { api: "openai-completions", baseUrl: "https://open.bigmodel.cn/api/paas/v4", envKey: "ZAI_API_KEY" }
|
|
217
219
|
};
|
|
218
220
|
function detectModelFamily(modelName) {
|
|
219
221
|
const name = modelName.toLowerCase();
|
|
@@ -233,6 +235,8 @@ function detectModelFamily(modelName) {
|
|
|
233
235
|
["gemma", "gemma2"],
|
|
234
236
|
["granite", "granite"],
|
|
235
237
|
["dolphin", "dolphin"],
|
|
238
|
+
["glm-4", "glm"],
|
|
239
|
+
["glm", "glm"],
|
|
236
240
|
["deepseek-r1", "deepseek-r1"],
|
|
237
241
|
["deepseek", "deepseek"],
|
|
238
242
|
["mistral", "qwen2"],
|
|
@@ -285,6 +289,11 @@ function detectProvider(ctx) {
|
|
|
285
289
|
}
|
|
286
290
|
return { kind: "unknown", name: providerName };
|
|
287
291
|
}
|
|
292
|
+
function isLocalProvider(baseUrl, providerName) {
|
|
293
|
+
if (providerName === "ollama") return true;
|
|
294
|
+
const url = baseUrl || "";
|
|
295
|
+
return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
|
|
296
|
+
}
|
|
288
297
|
export {
|
|
289
298
|
BUILTIN_PROVIDERS,
|
|
290
299
|
EXTENSION_VERSION,
|
|
@@ -296,6 +305,7 @@ export {
|
|
|
296
305
|
fetchModelContextLength,
|
|
297
306
|
fetchOllamaModels,
|
|
298
307
|
getOllamaBaseUrl,
|
|
308
|
+
isLocalProvider,
|
|
299
309
|
isReasoningModel,
|
|
300
310
|
readModelsJson,
|
|
301
311
|
readModifyWriteModelsJson,
|
package/package.json
CHANGED
package/provider-sync.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// shared/provider-sync.ts
|
|
2
|
+
function mergeModels(newModels, oldModels) {
|
|
3
|
+
const oldModelMap = new Map(oldModels.map((m) => [m.id, m]));
|
|
4
|
+
return newModels.map((m) => {
|
|
5
|
+
const old = oldModelMap.get(m.id);
|
|
6
|
+
if (old) {
|
|
7
|
+
const merged = { ...m };
|
|
8
|
+
for (const [k, v] of Object.entries(old)) {
|
|
9
|
+
if (!(k in m)) merged[k] = v;
|
|
10
|
+
}
|
|
11
|
+
return merged;
|
|
12
|
+
}
|
|
13
|
+
return m;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
mergeModels
|
|
18
|
+
};
|
package/react-parser.js
CHANGED
|
@@ -111,6 +111,23 @@ function extractJsonArgs(rawArgs) {
|
|
|
111
111
|
if (cmdMatch) return { command: cmdMatch[1] };
|
|
112
112
|
return { input: jsonStr };
|
|
113
113
|
}
|
|
114
|
+
function extractBraceJson(raw) {
|
|
115
|
+
const jsonStart = raw.indexOf("{");
|
|
116
|
+
if (jsonStart === -1) return "";
|
|
117
|
+
let depth = 0;
|
|
118
|
+
let jsonEnd = -1;
|
|
119
|
+
for (let i = jsonStart; i < raw.length; i++) {
|
|
120
|
+
if (raw[i] === "{") depth++;
|
|
121
|
+
else if (raw[i] === "}") {
|
|
122
|
+
depth--;
|
|
123
|
+
if (depth === 0) {
|
|
124
|
+
jsonEnd = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return jsonEnd !== -1 ? raw.slice(jsonStart, jsonEnd + 1) : "";
|
|
130
|
+
}
|
|
114
131
|
function parseReact(text) {
|
|
115
132
|
for (const dp of ALL_DIALECT_PATTERNS) {
|
|
116
133
|
const result = parseReactWithPatterns(text, dp);
|
|
@@ -382,6 +399,7 @@ export {
|
|
|
382
399
|
WORD_MAPPINGS,
|
|
383
400
|
buildDialectPatterns,
|
|
384
401
|
detectReactDialect,
|
|
402
|
+
extractBraceJson,
|
|
385
403
|
extractJsonArgs,
|
|
386
404
|
extractToolFromJson,
|
|
387
405
|
fuzzyMatchToolName,
|
package/security.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// shared/security.ts
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
3
|
+
import * as path2 from "node:path";
|
|
4
|
+
import os2 from "node:os";
|
|
5
5
|
|
|
6
6
|
// shared/debug.ts
|
|
7
7
|
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
@@ -13,8 +13,19 @@ function debugLog(module, message, ...args) {
|
|
|
13
13
|
|
|
14
14
|
// shared/security.ts
|
|
15
15
|
import dns from "node:dns";
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
// shared/config-io.ts
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
21
|
+
var SETTINGS_PATH = path.join(PI_AGENT_DIR, "settings.json");
|
|
22
|
+
var SECURITY_PATH = path.join(PI_AGENT_DIR, "security.json");
|
|
23
|
+
var REACT_MODE_PATH = path.join(PI_AGENT_DIR, "react-mode.json");
|
|
24
|
+
var MODEL_TEST_CONFIG_PATH = path.join(PI_AGENT_DIR, "model-test-config.json");
|
|
25
|
+
|
|
26
|
+
// shared/security.ts
|
|
27
|
+
var SETTINGS_PATH2 = SETTINGS_PATH;
|
|
28
|
+
var SECURITY_CONFIG_PATH = SECURITY_PATH;
|
|
18
29
|
function getSecurityMode() {
|
|
19
30
|
try {
|
|
20
31
|
if (!fs.existsSync(SECURITY_CONFIG_PATH)) return "max";
|
|
@@ -28,7 +39,7 @@ function getSecurityMode() {
|
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
function setSecurityMode(mode) {
|
|
31
|
-
const configDir =
|
|
42
|
+
const configDir = path2.dirname(SECURITY_CONFIG_PATH);
|
|
32
43
|
try {
|
|
33
44
|
if (!fs.existsSync(configDir)) {
|
|
34
45
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -159,6 +170,8 @@ var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
|
|
|
159
170
|
"172.29.",
|
|
160
171
|
"172.30.",
|
|
161
172
|
"172.31.",
|
|
173
|
+
// IPv6-mapped IPv4 cloud metadata (always blocked)
|
|
174
|
+
"::ffff:169.254.169.254",
|
|
162
175
|
// Internal service patterns
|
|
163
176
|
"internal.",
|
|
164
177
|
"private.",
|
|
@@ -172,6 +185,25 @@ var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
|
|
|
172
185
|
"::1",
|
|
173
186
|
"::ffff:127.0.0.1",
|
|
174
187
|
"::ffff:0.0.0.0",
|
|
188
|
+
// IPv6-mapped IPv4 private ranges (always blocked in max mode)
|
|
189
|
+
"::ffff:10.",
|
|
190
|
+
"::ffff:192.168.",
|
|
191
|
+
"::ffff:172.16.",
|
|
192
|
+
"::ffff:172.17.",
|
|
193
|
+
"::ffff:172.18.",
|
|
194
|
+
"::ffff:172.19.",
|
|
195
|
+
"::ffff:172.20.",
|
|
196
|
+
"::ffff:172.21.",
|
|
197
|
+
"::ffff:172.22.",
|
|
198
|
+
"::ffff:172.23.",
|
|
199
|
+
"::ffff:172.24.",
|
|
200
|
+
"::ffff:172.25.",
|
|
201
|
+
"::ffff:172.26.",
|
|
202
|
+
"::ffff:172.27.",
|
|
203
|
+
"::ffff:172.28.",
|
|
204
|
+
"::ffff:172.29.",
|
|
205
|
+
"::ffff:172.30.",
|
|
206
|
+
"::ffff:172.31.",
|
|
175
207
|
// Local/internal patterns
|
|
176
208
|
"local."
|
|
177
209
|
]);
|
|
@@ -201,7 +233,7 @@ function validatePath(filePath, allowedDirs) {
|
|
|
201
233
|
}
|
|
202
234
|
let resolved;
|
|
203
235
|
try {
|
|
204
|
-
resolved =
|
|
236
|
+
resolved = path2.resolve(filePath);
|
|
205
237
|
try {
|
|
206
238
|
resolved = fs.realpathSync(resolved);
|
|
207
239
|
} catch {
|
|
@@ -219,9 +251,9 @@ function validatePath(filePath, allowedDirs) {
|
|
|
219
251
|
"/etc/passwd",
|
|
220
252
|
"/.ssh/",
|
|
221
253
|
"/.gnupg/",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
254
|
+
path2.join(os2.homedir(), ".ssh"),
|
|
255
|
+
path2.join(os2.homedir(), ".gnupg"),
|
|
256
|
+
SETTINGS_PATH2,
|
|
225
257
|
SECURITY_CONFIG_PATH
|
|
226
258
|
// NOTE: models.json is intentionally excluded from sensitivePaths.
|
|
227
259
|
// Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
|
|
@@ -234,14 +266,14 @@ function validatePath(filePath, allowedDirs) {
|
|
|
234
266
|
}
|
|
235
267
|
}
|
|
236
268
|
const cwd = process.cwd();
|
|
237
|
-
const safePrefixes = ["/
|
|
269
|
+
const safePrefixes = ["/home", "/tmp", cwd];
|
|
238
270
|
for (const prefix of safePrefixes) {
|
|
239
|
-
if (resolved.startsWith(prefix)) return { valid: true, error: "" };
|
|
271
|
+
if (resolved.startsWith(prefix + "/") || resolved === prefix) return { valid: true, error: "" };
|
|
240
272
|
}
|
|
241
273
|
if (allowedDirs) {
|
|
242
274
|
for (const dir of allowedDirs) {
|
|
243
275
|
try {
|
|
244
|
-
const absDir =
|
|
276
|
+
const absDir = path2.resolve(dir);
|
|
245
277
|
if (resolved.startsWith(absDir)) return { valid: true, error: "" };
|
|
246
278
|
} catch {
|
|
247
279
|
}
|
|
@@ -249,16 +281,23 @@ function validatePath(filePath, allowedDirs) {
|
|
|
249
281
|
}
|
|
250
282
|
return { valid: false, error: `Path not in allowed directories: ${filePath}` };
|
|
251
283
|
}
|
|
284
|
+
function stripIpv6Mapped(ip) {
|
|
285
|
+
if (ip.startsWith("::ffff:") && !ip.startsWith("::ffff:0:0")) {
|
|
286
|
+
return ip.slice(7);
|
|
287
|
+
}
|
|
288
|
+
return ip;
|
|
289
|
+
}
|
|
252
290
|
function isLoopbackIp(ip) {
|
|
253
|
-
|
|
291
|
+
const norm = stripIpv6Mapped(ip);
|
|
292
|
+
if (norm.startsWith("127.") || norm === "0.0.0.0") return true;
|
|
254
293
|
if (ip === "::1" || ip === "::ffff:0.0.0.0") return true;
|
|
255
|
-
if (ip.startsWith("::ffff:127.")) return true;
|
|
256
294
|
return false;
|
|
257
295
|
}
|
|
258
296
|
function isPrivateIp(ip) {
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
if (
|
|
297
|
+
const norm = stripIpv6Mapped(ip);
|
|
298
|
+
if (norm.startsWith("10.") || norm.startsWith("192.168.")) return true;
|
|
299
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(norm)) return true;
|
|
300
|
+
if (norm === "169.254.169.254") return true;
|
|
262
301
|
if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
|
|
263
302
|
if (ip.startsWith("fe80:")) return true;
|
|
264
303
|
return false;
|
|
@@ -276,7 +315,8 @@ async function resolveAndCheckHostname(hostname, blockPrivate = true) {
|
|
|
276
315
|
}
|
|
277
316
|
for (const addr of addresses) {
|
|
278
317
|
const ip = addr.address;
|
|
279
|
-
|
|
318
|
+
const normIp = stripIpv6Mapped(ip);
|
|
319
|
+
if (normIp === "169.254.169.254") {
|
|
280
320
|
return { safe: false, error: `SSRF protection: hostname ${hostname} resolves to cloud metadata IP ${ip}` };
|
|
281
321
|
}
|
|
282
322
|
if (blockPrivate && (isLoopbackIp(ip) || isPrivateIp(ip))) {
|
|
@@ -316,12 +356,24 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
316
356
|
const mode = getSecurityMode();
|
|
317
357
|
for (const pattern of BLOCKED_URL_ALWAYS) {
|
|
318
358
|
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
359
|
+
if (/^\d|^::/.test(pattern)) {
|
|
360
|
+
const nextChar = normalized[pattern.length];
|
|
361
|
+
if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
319
365
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
320
366
|
}
|
|
321
367
|
}
|
|
322
368
|
if (mode === "max") {
|
|
323
369
|
for (const pattern of BLOCKED_URL_MAX_ONLY) {
|
|
324
370
|
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
371
|
+
if (/^\d|^::/.test(pattern)) {
|
|
372
|
+
const nextChar = normalized[pattern.length];
|
|
373
|
+
if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
325
377
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
|
|
326
378
|
}
|
|
327
379
|
}
|
|
@@ -330,25 +382,20 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
330
382
|
return { safe: true, error: "" };
|
|
331
383
|
}
|
|
332
384
|
var INJECTION_PATTERNS = [
|
|
333
|
-
//
|
|
385
|
+
// Semicolon chaining to dangerous commands — mode-independent.
|
|
386
|
+
// Unlike && (conditional), ; ALWAYS runs the second command.
|
|
334
387
|
/;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
|
|
335
|
-
// Piping to dangerous commands
|
|
336
|
-
/\|\s*(rm|sudo|chmod|chown|shred|mkfs)\b/i,
|
|
337
|
-
// AND chaining with dangerous commands
|
|
338
|
-
/&&\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill)\b/i,
|
|
339
388
|
// Command substitution (backticks) — still dangerous
|
|
340
389
|
/`[^`]+`/,
|
|
341
390
|
// Command substitution ($()) — still dangerous
|
|
342
391
|
/\$\([^)]+\)/,
|
|
343
392
|
// Variable expansion targeting sensitive env vars
|
|
344
|
-
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
|
|
345
|
-
// Bare pipe without space (likely injection, not intentional piping)
|
|
346
|
-
/\|(?=[^\s|])/
|
|
393
|
+
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
|
|
347
394
|
];
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
395
|
+
function checkSingleCommand(command, mode) {
|
|
396
|
+
const trimmed = command.trim();
|
|
397
|
+
if (!trimmed) return { isSafe: true, error: "", command: "" };
|
|
398
|
+
const parts = trimmed.split(/\s+/);
|
|
352
399
|
let baseCmd = parts[0].toLowerCase();
|
|
353
400
|
if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
|
|
354
401
|
if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
|
|
@@ -360,23 +407,57 @@ function sanitizeCommand(command) {
|
|
|
360
407
|
return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
|
|
361
408
|
}
|
|
362
409
|
}
|
|
363
|
-
const mode = getSecurityMode();
|
|
364
410
|
if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
|
|
365
411
|
return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
|
|
366
412
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
413
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
414
|
+
if (pattern.test(trimmed)) {
|
|
415
|
+
return { isSafe: false, error: `Potential injection pattern detected in: ${trimmed}`, command: "" };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { isSafe: true, error: "", command: trimmed };
|
|
419
|
+
}
|
|
420
|
+
function sanitizeCommand(command) {
|
|
421
|
+
if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
422
|
+
let normalizedCmd = command.normalize("NFKC");
|
|
423
|
+
normalizedCmd = normalizedCmd.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "");
|
|
424
|
+
const strippedForCompare = command.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "").normalize("NFKC");
|
|
425
|
+
if (normalizedCmd !== strippedForCompare) {
|
|
426
|
+
return { isSafe: false, error: `Command rejected: Unicode normalization variance detected (possible homoglyph bypass)`, command: "" };
|
|
427
|
+
}
|
|
428
|
+
command = normalizedCmd;
|
|
429
|
+
const trimmed = command.trim();
|
|
430
|
+
if (!trimmed) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
431
|
+
const newlineStripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
|
|
432
|
+
if (newlineStripped !== command) {
|
|
369
433
|
return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
|
|
370
434
|
}
|
|
371
435
|
for (const pattern of INJECTION_PATTERNS) {
|
|
372
|
-
if (pattern.test(
|
|
436
|
+
if (pattern.test(trimmed)) {
|
|
373
437
|
return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
|
|
374
438
|
}
|
|
375
439
|
}
|
|
440
|
+
const subCommands = [];
|
|
441
|
+
let remaining = trimmed;
|
|
442
|
+
const chainRegex = /&&|\|\||(?<!\|)\|(?!\|)/g;
|
|
443
|
+
let match;
|
|
444
|
+
let lastIndex = 0;
|
|
445
|
+
while ((match = chainRegex.exec(remaining)) !== null) {
|
|
446
|
+
subCommands.push(remaining.slice(lastIndex, match.index));
|
|
447
|
+
lastIndex = match.index + match[0].length;
|
|
448
|
+
}
|
|
449
|
+
subCommands.push(remaining.slice(lastIndex));
|
|
450
|
+
const mode = getSecurityMode();
|
|
451
|
+
for (const subCmd of subCommands) {
|
|
452
|
+
const result = checkSingleCommand(subCmd, mode);
|
|
453
|
+
if (!result.isSafe) {
|
|
454
|
+
return { isSafe: false, error: result.error, command: "" };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
376
457
|
return { isSafe: true, error: "", command };
|
|
377
458
|
}
|
|
378
|
-
var AUDIT_DIR =
|
|
379
|
-
var AUDIT_LOG_PATH =
|
|
459
|
+
var AUDIT_DIR = path2.join(os2.homedir(), ".pi", "agent");
|
|
460
|
+
var AUDIT_LOG_PATH = path2.join(AUDIT_DIR, "audit.log");
|
|
380
461
|
var AUDIT_BUFFER_MAX_ENTRIES = 50;
|
|
381
462
|
var AUDIT_FLUSH_INTERVAL_MS = 500;
|
|
382
463
|
var _auditBuffer = [];
|
|
@@ -409,6 +490,19 @@ function flushAuditBuffer() {
|
|
|
409
490
|
function appendAuditEntry(entry) {
|
|
410
491
|
try {
|
|
411
492
|
ensureAuditFlushTimer();
|
|
493
|
+
const AUDIT_LOG_MAX_SIZE = 5 * 1024 * 1024;
|
|
494
|
+
try {
|
|
495
|
+
if (fs.existsSync(AUDIT_LOG_PATH)) {
|
|
496
|
+
const stat = fs.statSync(AUDIT_LOG_PATH);
|
|
497
|
+
if (stat.size > AUDIT_LOG_MAX_SIZE) {
|
|
498
|
+
const entries = readRecentAuditEntries(1e3);
|
|
499
|
+
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
500
|
+
fs.writeFileSync(AUDIT_LOG_PATH, content, "utf-8");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
debugLog("security", "audit log rotation failed", err);
|
|
505
|
+
}
|
|
412
506
|
const enriched = { ...entry, securityMode: getSecurityMode() };
|
|
413
507
|
const line = JSON.stringify(enriched) + "\n";
|
|
414
508
|
_auditBuffer.push(line);
|
|
@@ -422,8 +516,31 @@ function appendAuditEntry(entry) {
|
|
|
422
516
|
function readRecentAuditEntries(count = 50) {
|
|
423
517
|
try {
|
|
424
518
|
if (!fs.existsSync(AUDIT_LOG_PATH)) return [];
|
|
425
|
-
const
|
|
426
|
-
|
|
519
|
+
const fileSize = fs.statSync(AUDIT_LOG_PATH).size;
|
|
520
|
+
if (fileSize === 0) return [];
|
|
521
|
+
const fd = fs.openSync(AUDIT_LOG_PATH, "r");
|
|
522
|
+
const bufferSize = 8192;
|
|
523
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
524
|
+
const lines = [];
|
|
525
|
+
let pos = fileSize;
|
|
526
|
+
let partial = "";
|
|
527
|
+
while (pos > 0 && lines.length < count) {
|
|
528
|
+
const readSize = Math.min(bufferSize, pos);
|
|
529
|
+
pos -= readSize;
|
|
530
|
+
fs.readSync(fd, buffer, 0, readSize, pos);
|
|
531
|
+
const chunk = buffer.slice(0, readSize).toString("utf-8");
|
|
532
|
+
partial = chunk + partial;
|
|
533
|
+
const lineBreak = partial.lastIndexOf("\n");
|
|
534
|
+
if (lineBreak !== -1) {
|
|
535
|
+
const complete = partial.slice(lineBreak + 1);
|
|
536
|
+
if (complete.trim()) lines.unshift(complete);
|
|
537
|
+
partial = partial.slice(0, lineBreak);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
fs.closeSync(fd);
|
|
541
|
+
if (partial.trim() && lines.length < count) {
|
|
542
|
+
lines.unshift(partial);
|
|
543
|
+
}
|
|
427
544
|
const recent = lines.slice(-count);
|
|
428
545
|
return recent.map((line) => {
|
|
429
546
|
try {
|
|
@@ -437,6 +554,12 @@ function readRecentAuditEntries(count = 50) {
|
|
|
437
554
|
return [];
|
|
438
555
|
}
|
|
439
556
|
}
|
|
557
|
+
process.on("exit", () => {
|
|
558
|
+
flushAuditBuffer();
|
|
559
|
+
});
|
|
560
|
+
process.on("SIGTERM", () => {
|
|
561
|
+
flushAuditBuffer();
|
|
562
|
+
});
|
|
440
563
|
function checkBashToolInput(input) {
|
|
441
564
|
const command = input.command ?? input.cmd ?? "";
|
|
442
565
|
if (!command) return { safe: true, rule: "", detail: "" };
|
|
@@ -501,7 +624,7 @@ export {
|
|
|
501
624
|
CRITICAL_COMMANDS,
|
|
502
625
|
EXTENDED_COMMANDS,
|
|
503
626
|
SECURITY_CONFIG_PATH,
|
|
504
|
-
SETTINGS_PATH,
|
|
627
|
+
SETTINGS_PATH2 as SETTINGS_PATH,
|
|
505
628
|
appendAuditEntry,
|
|
506
629
|
checkBashToolInput,
|
|
507
630
|
checkFileToolInput,
|
package/test-report.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// shared/format.ts
|
|
2
|
+
function section(title) {
|
|
3
|
+
return `
|
|
4
|
+
\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(1, 60 - title.length - 4))}`;
|
|
5
|
+
}
|
|
6
|
+
function ok(msg) {
|
|
7
|
+
return ` \u2705 ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
function fail(msg) {
|
|
10
|
+
return ` \u274C ${msg}`;
|
|
11
|
+
}
|
|
12
|
+
function warn(msg) {
|
|
13
|
+
return ` \u26A0\uFE0F ${msg}`;
|
|
14
|
+
}
|
|
15
|
+
function info(msg) {
|
|
16
|
+
return ` \u2139\uFE0F ${msg}`;
|
|
17
|
+
}
|
|
18
|
+
function msHuman(ms) {
|
|
19
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
20
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
21
|
+
return `${(ms / 6e4).toFixed(1)}m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// shared/ollama.ts
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
|
|
28
|
+
// shared/debug.ts
|
|
29
|
+
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
30
|
+
|
|
31
|
+
// shared/ollama.ts
|
|
32
|
+
var EXTENSION_VERSION = "1.1.9";
|
|
33
|
+
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
34
|
+
|
|
35
|
+
// shared/test-report.ts
|
|
36
|
+
var branding = [
|
|
37
|
+
` \u26A1 Pi Model Benchmark v${EXTENSION_VERSION}`,
|
|
38
|
+
` Written by VTSTech`,
|
|
39
|
+
` GitHub: https://github.com/VTSTech`,
|
|
40
|
+
` Website: www.vts-tech.org`
|
|
41
|
+
].join("\n");
|
|
42
|
+
function formatTestScore(score, label) {
|
|
43
|
+
switch (score) {
|
|
44
|
+
case "STRONG":
|
|
45
|
+
return ok(`${label} (${score})`);
|
|
46
|
+
case "MODERATE":
|
|
47
|
+
return ok(`${label} (${score})`);
|
|
48
|
+
case "WEAK":
|
|
49
|
+
return warn(`${label} (${score})`);
|
|
50
|
+
case "FAIL":
|
|
51
|
+
return fail(`${label} (${score})`);
|
|
52
|
+
case "ERROR":
|
|
53
|
+
return fail(`Error: ${label}`);
|
|
54
|
+
default:
|
|
55
|
+
return fail(`${label} (${score})`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function formatTestSummary(tests, totalMs) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
lines.push(section("SUMMARY"));
|
|
61
|
+
for (const t of tests) {
|
|
62
|
+
lines.push(t.pass ? ok(`${t.name}: ${t.score}`) : fail(`${t.name}: ${t.score}`));
|
|
63
|
+
}
|
|
64
|
+
lines.push(info(`Total time: ${msHuman(totalMs)}`));
|
|
65
|
+
const passed = tests.filter((t) => t.pass).length;
|
|
66
|
+
lines.push(info(`Score: ${passed}/${tests.length} tests passed`));
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
function formatRecommendation(model, passed, total, via) {
|
|
70
|
+
const suffix = via ? ` via ${via}` : "";
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push(section("RECOMMENDATION"));
|
|
73
|
+
if (passed === total) {
|
|
74
|
+
lines.push(ok(`${model} is a STRONG model${suffix} \u2014 full capability`));
|
|
75
|
+
} else if (passed > 0 && passed >= total - 1) {
|
|
76
|
+
lines.push(ok(`${model} is a GOOD model${suffix} \u2014 most capabilities work`));
|
|
77
|
+
} else if (passed > 0 && passed >= total - 2) {
|
|
78
|
+
lines.push(warn(`${model} is USABLE${suffix} \u2014 some capabilities are limited`));
|
|
79
|
+
} else {
|
|
80
|
+
lines.push(fail(`${model} is WEAK${suffix} \u2014 limited capabilities for agent use`));
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
branding,
|
|
86
|
+
formatRecommendation,
|
|
87
|
+
formatTestScore,
|
|
88
|
+
formatTestSummary
|
|
89
|
+
};
|