@vtstech/pi-diag 1.2.2 → 1.2.3
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/diag.js +617 -44
- package/package.json +10 -6
package/diag.js
CHANGED
|
@@ -1,34 +1,607 @@
|
|
|
1
|
-
//
|
|
1
|
+
// extensions/diag.ts
|
|
2
|
+
import * as fs4 from "node:fs";
|
|
3
|
+
import * as os4 from "node:os";
|
|
4
|
+
import * as path4 from "node:path";
|
|
5
|
+
|
|
6
|
+
// shared/format.ts
|
|
7
|
+
function section(title) {
|
|
8
|
+
return `
|
|
9
|
+
\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(1, 60 - title.length - 4))}`;
|
|
10
|
+
}
|
|
11
|
+
function ok(msg) {
|
|
12
|
+
return ` \u2705 ${msg}`;
|
|
13
|
+
}
|
|
14
|
+
function fail(msg) {
|
|
15
|
+
return ` \u274C ${msg}`;
|
|
16
|
+
}
|
|
17
|
+
function warn(msg) {
|
|
18
|
+
return ` \u26A0\uFE0F ${msg}`;
|
|
19
|
+
}
|
|
20
|
+
function info(msg) {
|
|
21
|
+
return ` \u2139\uFE0F ${msg}`;
|
|
22
|
+
}
|
|
23
|
+
function bytesHuman(bytes) {
|
|
24
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
25
|
+
let i = 0;
|
|
26
|
+
let b = bytes;
|
|
27
|
+
while (b >= 1024 && i < units.length - 1) {
|
|
28
|
+
b /= 1024;
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
return `${b.toFixed(1)}${units[i]}`;
|
|
32
|
+
}
|
|
33
|
+
function msHuman(ms) {
|
|
34
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
35
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
36
|
+
return `${(ms / 6e4).toFixed(1)}m`;
|
|
37
|
+
}
|
|
38
|
+
function pct(used, total) {
|
|
39
|
+
if (total === 0) return "0.0%";
|
|
40
|
+
return `${(used / total * 100).toFixed(1)}%`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// shared/ollama.ts
|
|
2
44
|
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
45
|
import * as path from "node:path";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
import os from "node:os";
|
|
47
|
+
|
|
48
|
+
// shared/debug.ts
|
|
49
|
+
var DEBUG_ENABLED = process?.env?.PI_EXTENSIONS_DEBUG === "1";
|
|
50
|
+
function debugLog(module, message, ...args) {
|
|
51
|
+
if (!DEBUG_ENABLED) return;
|
|
52
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
53
|
+
console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// shared/ollama.ts
|
|
57
|
+
var EXTENSION_VERSION = "1.2.3";
|
|
58
|
+
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
59
|
+
var _modelsJsonCache = null;
|
|
60
|
+
var _ollamaBaseUrlCache = null;
|
|
61
|
+
var CACHE_TTL_MS = 2e3;
|
|
62
|
+
function getOllamaBaseUrl() {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (_ollamaBaseUrlCache && now - _ollamaBaseUrlCache.ts < CACHE_TTL_MS) return _ollamaBaseUrlCache.data;
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(MODELS_JSON_PATH)) {
|
|
67
|
+
const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
|
|
68
|
+
const config = JSON.parse(raw);
|
|
69
|
+
const baseUrl = config?.providers?.["ollama"]?.baseUrl;
|
|
70
|
+
if (baseUrl) {
|
|
71
|
+
const result = baseUrl.replace(/\/v1\/?$/, "");
|
|
72
|
+
_ollamaBaseUrlCache = { data: result, ts: now };
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
debugLog("ollama", "failed to parse models.json for base URL", err);
|
|
78
|
+
}
|
|
79
|
+
if (process.env.OLLAMA_HOST) {
|
|
80
|
+
const result = `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, "")}`;
|
|
81
|
+
_ollamaBaseUrlCache = { data: result, ts: now };
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
const fallback = "http://localhost:11434";
|
|
85
|
+
_ollamaBaseUrlCache = { data: fallback, ts: now };
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
function readModelsJson() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (_modelsJsonCache && now - _modelsJsonCache.ts < CACHE_TTL_MS) return _modelsJsonCache.data;
|
|
91
|
+
try {
|
|
92
|
+
if (fs.existsSync(MODELS_JSON_PATH)) {
|
|
93
|
+
const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
|
|
94
|
+
const data = JSON.parse(raw);
|
|
95
|
+
_modelsJsonCache = { data, ts: now };
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
debugLog("ollama", "failed to read/parse models.json", err);
|
|
100
|
+
}
|
|
101
|
+
const empty = { providers: {} };
|
|
102
|
+
_modelsJsonCache = { data: empty, ts: now };
|
|
103
|
+
return empty;
|
|
104
|
+
}
|
|
105
|
+
var BUILTIN_PROVIDERS = {
|
|
106
|
+
openrouter: { api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", envKey: "OPENROUTER_API_KEY" },
|
|
107
|
+
anthropic: { api: "anthropic-messages", baseUrl: "https://api.anthropic.com/v1", envKey: "ANTHROPIC_API_KEY" },
|
|
108
|
+
google: { api: "gemini", baseUrl: "https://generativelanguage.googleapis.com", envKey: "GOOGLE_API_KEY" },
|
|
109
|
+
openai: { api: "openai-completions", baseUrl: "https://api.openai.com/v1", envKey: "OPENAI_API_KEY" },
|
|
110
|
+
groq: { api: "openai-completions", baseUrl: "https://api.groq.com/v1", envKey: "GROQ_API_KEY" },
|
|
111
|
+
deepseek: { api: "openai-completions", baseUrl: "https://api.deepseek.com/v1", envKey: "DEEPSEEK_API_KEY" },
|
|
112
|
+
mistral: { api: "openai-completions", baseUrl: "https://api.mistral.ai/v1", envKey: "MISTRAL_API_KEY" },
|
|
113
|
+
xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1", envKey: "XAI_API_KEY" },
|
|
114
|
+
together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1", envKey: "TOGETHER_API_KEY" },
|
|
115
|
+
fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", envKey: "FIREWORKS_API_KEY" },
|
|
116
|
+
cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" },
|
|
117
|
+
zai: { api: "openai-completions", baseUrl: "https://open.bigmodel.cn/api/paas/v4", envKey: "ZAI_API_KEY" }
|
|
118
|
+
};
|
|
119
|
+
function isLocalProvider(baseUrl, providerName) {
|
|
120
|
+
if (providerName === "ollama") return true;
|
|
121
|
+
const url = baseUrl || "";
|
|
122
|
+
return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// shared/security.ts
|
|
126
|
+
import * as fs3 from "node:fs";
|
|
127
|
+
import * as path3 from "node:path";
|
|
128
|
+
import os3 from "node:os";
|
|
129
|
+
|
|
130
|
+
// shared/config-io.ts
|
|
131
|
+
import * as fs2 from "fs";
|
|
132
|
+
import * as path2 from "path";
|
|
133
|
+
import os2 from "os";
|
|
134
|
+
var PI_AGENT_DIR = path2.join(os2.homedir(), ".pi", "agent");
|
|
135
|
+
function readJsonConfig(filePath, defaultValue = {}) {
|
|
136
|
+
try {
|
|
137
|
+
if (fs2.existsSync(filePath)) {
|
|
138
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
debugLog("config-io", `failed to read config: ${filePath}`, err);
|
|
142
|
+
}
|
|
143
|
+
return defaultValue;
|
|
144
|
+
}
|
|
145
|
+
var SETTINGS_PATH = path2.join(PI_AGENT_DIR, "settings.json");
|
|
146
|
+
var SECURITY_PATH = path2.join(PI_AGENT_DIR, "security.json");
|
|
147
|
+
var REACT_MODE_PATH = path2.join(PI_AGENT_DIR, "react-mode.json");
|
|
148
|
+
var MODEL_TEST_CONFIG_PATH = path2.join(PI_AGENT_DIR, "model-test-config.json");
|
|
149
|
+
function readSettings() {
|
|
150
|
+
return readJsonConfig(SETTINGS_PATH);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// shared/security.ts
|
|
154
|
+
var SETTINGS_PATH2 = SETTINGS_PATH;
|
|
155
|
+
var SECURITY_CONFIG_PATH = SECURITY_PATH;
|
|
156
|
+
var securityModeCache = null;
|
|
157
|
+
var securityModeCacheTime = 0;
|
|
158
|
+
var SECURITY_CACHE_DURATION_MS = 3e4;
|
|
159
|
+
function getSecurityMode() {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
if (securityModeCache && now - securityModeCacheTime < SECURITY_CACHE_DURATION_MS) {
|
|
162
|
+
return securityModeCache;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!fs3.existsSync(SECURITY_CONFIG_PATH)) {
|
|
166
|
+
securityModeCache = "max";
|
|
167
|
+
securityModeCacheTime = now;
|
|
168
|
+
return "max";
|
|
169
|
+
}
|
|
170
|
+
const raw = fs3.readFileSync(SECURITY_CONFIG_PATH, "utf-8");
|
|
171
|
+
const config = JSON.parse(raw);
|
|
172
|
+
if (config.mode === "basic" || config.mode === "max" || config.mode === "off") {
|
|
173
|
+
securityModeCache = config.mode;
|
|
174
|
+
securityModeCacheTime = now;
|
|
175
|
+
return config.mode;
|
|
176
|
+
}
|
|
177
|
+
securityModeCache = "max";
|
|
178
|
+
securityModeCacheTime = now;
|
|
179
|
+
return "max";
|
|
180
|
+
} catch (err) {
|
|
181
|
+
debugLog("security", `failed to read security config at ${SECURITY_CONFIG_PATH}`, err);
|
|
182
|
+
securityModeCache = "max";
|
|
183
|
+
securityModeCacheTime = now;
|
|
184
|
+
return "max";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var CRITICAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
188
|
+
// Filesystem destruction (irrecoverable)
|
|
189
|
+
"mkfs",
|
|
190
|
+
"dd",
|
|
191
|
+
"shred",
|
|
192
|
+
"wipe",
|
|
193
|
+
"srm",
|
|
194
|
+
"format",
|
|
195
|
+
"fdisk",
|
|
196
|
+
// Privilege escalation (non-sudo)
|
|
197
|
+
"su",
|
|
198
|
+
"doas",
|
|
199
|
+
"pkexec",
|
|
200
|
+
"gksudo",
|
|
201
|
+
"kdesu",
|
|
202
|
+
// Network attack tools
|
|
203
|
+
"nmap",
|
|
204
|
+
"nc",
|
|
205
|
+
"netcat",
|
|
206
|
+
"telnet",
|
|
207
|
+
// Remote access
|
|
208
|
+
"ssh",
|
|
209
|
+
"scp",
|
|
210
|
+
"sftp",
|
|
211
|
+
"rsync",
|
|
212
|
+
// Process killing
|
|
213
|
+
"kill",
|
|
214
|
+
"killall",
|
|
215
|
+
"pkill",
|
|
216
|
+
"xkill",
|
|
217
|
+
// User management
|
|
218
|
+
"useradd",
|
|
219
|
+
"userdel",
|
|
220
|
+
"usermod",
|
|
221
|
+
"passwd",
|
|
222
|
+
"adduser",
|
|
223
|
+
"deluser",
|
|
224
|
+
// Dangerous shell features
|
|
225
|
+
"exec",
|
|
226
|
+
"eval",
|
|
227
|
+
"source",
|
|
228
|
+
".",
|
|
229
|
+
"alias",
|
|
230
|
+
// Filesystem control
|
|
231
|
+
"mount",
|
|
232
|
+
"umount",
|
|
233
|
+
"chattr",
|
|
234
|
+
"lsattr",
|
|
235
|
+
// Permission modification
|
|
236
|
+
"chown",
|
|
237
|
+
"chmod"
|
|
238
|
+
]);
|
|
239
|
+
var EXTENDED_COMMANDS = /* @__PURE__ */ new Set([
|
|
240
|
+
// File deletion
|
|
241
|
+
"rm",
|
|
242
|
+
"rmdir",
|
|
243
|
+
"del",
|
|
244
|
+
// Privilege escalation
|
|
245
|
+
"sudo",
|
|
246
|
+
// Download tools
|
|
247
|
+
"wget",
|
|
248
|
+
"curl",
|
|
249
|
+
// Package management
|
|
250
|
+
"apt",
|
|
251
|
+
"apt-get",
|
|
252
|
+
"yum",
|
|
253
|
+
"dnf",
|
|
254
|
+
"pacman",
|
|
255
|
+
"pip",
|
|
256
|
+
"npm",
|
|
257
|
+
"yarn",
|
|
258
|
+
"cargo",
|
|
259
|
+
// System service control
|
|
260
|
+
"systemctl",
|
|
261
|
+
"service",
|
|
262
|
+
// Interactive editors (shell escape risk)
|
|
263
|
+
"vi",
|
|
264
|
+
"vim",
|
|
265
|
+
"nano",
|
|
266
|
+
"emacs",
|
|
267
|
+
"less",
|
|
268
|
+
"more",
|
|
269
|
+
"man",
|
|
270
|
+
// Version control
|
|
271
|
+
"git"
|
|
272
|
+
]);
|
|
273
|
+
var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
274
|
+
...CRITICAL_COMMANDS,
|
|
275
|
+
...EXTENDED_COMMANDS
|
|
276
|
+
]);
|
|
277
|
+
var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
|
|
278
|
+
// Cloud metadata endpoints
|
|
279
|
+
"169.254.169.254",
|
|
280
|
+
// RFC1918 private ranges
|
|
281
|
+
"10.",
|
|
282
|
+
"192.168.",
|
|
283
|
+
"172.16.",
|
|
284
|
+
"172.17.",
|
|
285
|
+
"172.18.",
|
|
286
|
+
"172.19.",
|
|
287
|
+
"172.20.",
|
|
288
|
+
"172.21.",
|
|
289
|
+
"172.22.",
|
|
290
|
+
"172.23.",
|
|
291
|
+
"172.24.",
|
|
292
|
+
"172.25.",
|
|
293
|
+
"172.26.",
|
|
294
|
+
"172.27.",
|
|
295
|
+
"172.28.",
|
|
296
|
+
"172.29.",
|
|
297
|
+
"172.30.",
|
|
298
|
+
"172.31.",
|
|
299
|
+
// IPv6-mapped IPv4 cloud metadata (always blocked)
|
|
300
|
+
"::ffff:169.254.169.254",
|
|
301
|
+
// Internal service patterns
|
|
302
|
+
"internal.",
|
|
303
|
+
"private.",
|
|
304
|
+
"intranet."
|
|
305
|
+
]);
|
|
306
|
+
var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
|
|
307
|
+
// Loopback addresses (full 127.0.0.0/8 range)
|
|
308
|
+
"localhost",
|
|
309
|
+
"127.",
|
|
310
|
+
"0.0.0.0",
|
|
311
|
+
"::1",
|
|
312
|
+
"::ffff:127.0.0.1",
|
|
313
|
+
"::ffff:0.0.0.0",
|
|
314
|
+
// IPv6-mapped IPv4 private ranges (always blocked in max mode)
|
|
315
|
+
"::ffff:10.",
|
|
316
|
+
"::ffff:192.168.",
|
|
317
|
+
"::ffff:172.16.",
|
|
318
|
+
"::ffff:172.17.",
|
|
319
|
+
"::ffff:172.18.",
|
|
320
|
+
"::ffff:172.19.",
|
|
321
|
+
"::ffff:172.20.",
|
|
322
|
+
"::ffff:172.21.",
|
|
323
|
+
"::ffff:172.22.",
|
|
324
|
+
"::ffff:172.23.",
|
|
325
|
+
"::ffff:172.24.",
|
|
326
|
+
"::ffff:172.25.",
|
|
327
|
+
"::ffff:172.26.",
|
|
328
|
+
"::ffff:172.27.",
|
|
329
|
+
"::ffff:172.28.",
|
|
330
|
+
"::ffff:172.29.",
|
|
331
|
+
"::ffff:172.30.",
|
|
332
|
+
"::ffff:172.31.",
|
|
333
|
+
// Local/internal patterns
|
|
334
|
+
"local."
|
|
335
|
+
]);
|
|
336
|
+
var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
|
|
337
|
+
...BLOCKED_URL_ALWAYS,
|
|
338
|
+
...BLOCKED_URL_MAX_ONLY
|
|
339
|
+
]);
|
|
340
|
+
var CRITICAL_SYSTEM_DIRS = [
|
|
341
|
+
"/etc",
|
|
342
|
+
"/root",
|
|
343
|
+
"/var",
|
|
344
|
+
"/usr",
|
|
345
|
+
"/bin",
|
|
346
|
+
"/sbin",
|
|
347
|
+
"/boot",
|
|
348
|
+
"/dev",
|
|
349
|
+
"/proc",
|
|
350
|
+
"/sys"
|
|
351
|
+
];
|
|
352
|
+
function validatePath(filePath, allowedDirs) {
|
|
353
|
+
if (!filePath) return { valid: false, error: "Path cannot be empty" };
|
|
354
|
+
if (filePath.startsWith("\\\\")) {
|
|
355
|
+
return { valid: false, error: "UNC paths not allowed" };
|
|
356
|
+
}
|
|
357
|
+
if (filePath.includes("../") || filePath.includes("..\\")) {
|
|
358
|
+
return { valid: false, error: "Path traversal detected: parent directory access not allowed" };
|
|
359
|
+
}
|
|
360
|
+
let resolved;
|
|
361
|
+
try {
|
|
362
|
+
resolved = path3.resolve(filePath);
|
|
363
|
+
try {
|
|
364
|
+
resolved = fs3.realpathSync(resolved);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
return { valid: false, error: "Invalid path format" };
|
|
369
|
+
}
|
|
370
|
+
for (const critical of CRITICAL_SYSTEM_DIRS) {
|
|
371
|
+
if (resolved.startsWith(critical + "/") || resolved === critical) {
|
|
372
|
+
return { valid: false, error: `Access to system directory denied: ${critical}` };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const sensitivePaths = [
|
|
376
|
+
"/etc/shadow",
|
|
377
|
+
"/etc/passwd",
|
|
378
|
+
"/.ssh/",
|
|
379
|
+
"/.gnupg/",
|
|
380
|
+
path3.join(os3.homedir(), ".ssh"),
|
|
381
|
+
path3.join(os3.homedir(), ".gnupg"),
|
|
382
|
+
SETTINGS_PATH2,
|
|
383
|
+
SECURITY_CONFIG_PATH
|
|
384
|
+
// NOTE: models.json is intentionally excluded from sensitivePaths.
|
|
385
|
+
// Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
|
|
386
|
+
// for direct file I/O — not via Pi's tool system — so blocking it here
|
|
387
|
+
// would prevent legitimate model configuration updates.
|
|
388
|
+
];
|
|
389
|
+
for (const sensitive of sensitivePaths) {
|
|
390
|
+
if (resolved.startsWith(sensitive) || resolved === sensitive) {
|
|
391
|
+
return { valid: false, error: `Access to sensitive path denied: ${sensitive}` };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const cwd = process.cwd();
|
|
395
|
+
const safePrefixes = ["/home", "/tmp", cwd];
|
|
396
|
+
for (const prefix of safePrefixes) {
|
|
397
|
+
if (resolved.startsWith(prefix + "/") || resolved === prefix) return { valid: true, error: "" };
|
|
398
|
+
}
|
|
399
|
+
if (allowedDirs) {
|
|
400
|
+
for (const dir of allowedDirs) {
|
|
401
|
+
try {
|
|
402
|
+
const absDir = path3.resolve(dir);
|
|
403
|
+
if (resolved.startsWith(absDir)) return { valid: true, error: "" };
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { valid: false, error: `Path not in allowed directories: ${filePath}` };
|
|
409
|
+
}
|
|
410
|
+
function isSafeUrl(url, blockSsrf = true, mode = "max") {
|
|
411
|
+
if (!url) return { safe: false, error: "URL cannot be empty" };
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = new URL(url);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
417
|
+
return { safe: false, error: `Invalid URL format: ${msg}` };
|
|
418
|
+
}
|
|
419
|
+
const scheme = parsed.protocol.replace(":", "").toLowerCase();
|
|
420
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
421
|
+
return { safe: false, error: `URL scheme not allowed: ${parsed.protocol}` };
|
|
422
|
+
}
|
|
423
|
+
if (!parsed.hostname) {
|
|
424
|
+
return { safe: false, error: "URL must have a hostname" };
|
|
425
|
+
}
|
|
426
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
427
|
+
const normalized = hostname.replace(/\.$/, "");
|
|
428
|
+
if (/[^\x00-\x7F]/.test(normalized)) {
|
|
429
|
+
return { safe: false, error: "URL hostname contains non-ASCII characters" };
|
|
430
|
+
}
|
|
431
|
+
if (/^0x[0-9a-f]+$/i.test(normalized) || /^0[0-7]+$/i.test(normalized)) {
|
|
432
|
+
return { safe: false, error: "URL hostname uses non-decimal IP format" };
|
|
433
|
+
}
|
|
434
|
+
if (blockSsrf) {
|
|
435
|
+
if (mode === "off") {
|
|
436
|
+
return { safe: true, error: "" };
|
|
437
|
+
}
|
|
438
|
+
for (const pattern of BLOCKED_URL_ALWAYS) {
|
|
439
|
+
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
440
|
+
if (/^\d|^::/.test(pattern)) {
|
|
441
|
+
const nextChar = normalized[pattern.length];
|
|
442
|
+
if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (mode === "max") {
|
|
450
|
+
for (const pattern of BLOCKED_URL_MAX_ONLY) {
|
|
451
|
+
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
452
|
+
if (/^\d|^::/.test(pattern)) {
|
|
453
|
+
const nextChar = normalized[pattern.length];
|
|
454
|
+
if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { safe: true, error: "" };
|
|
464
|
+
}
|
|
465
|
+
var INJECTION_PATTERNS = [
|
|
466
|
+
// Semicolon chaining to dangerous commands — mode-independent.
|
|
467
|
+
// Unlike && (conditional), ; ALWAYS runs the second command.
|
|
468
|
+
/;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
|
|
469
|
+
// Command substitution (backticks) — still dangerous
|
|
470
|
+
/`[^`]+`/,
|
|
471
|
+
// Command substitution ($()) — still dangerous
|
|
472
|
+
/\$\([^)]+\)/,
|
|
473
|
+
// Variable expansion targeting sensitive env vars
|
|
474
|
+
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
|
|
475
|
+
];
|
|
476
|
+
function checkSingleCommand(command, mode) {
|
|
477
|
+
const trimmed = command.trim();
|
|
478
|
+
if (!trimmed) return { isSafe: true, error: "", command: "" };
|
|
479
|
+
const parts = trimmed.split(/\s+/);
|
|
480
|
+
let baseCmd = parts[0].toLowerCase();
|
|
481
|
+
if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
|
|
482
|
+
if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
|
|
483
|
+
for (const raw of parts) {
|
|
484
|
+
let word = raw.toLowerCase();
|
|
485
|
+
if (word.includes("/")) word = word.split("/").pop();
|
|
486
|
+
if (word.includes("\\")) word = word.split("\\").pop();
|
|
487
|
+
if (CRITICAL_COMMANDS.has(word)) {
|
|
488
|
+
return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
|
|
492
|
+
return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
|
|
493
|
+
}
|
|
494
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
495
|
+
if (pattern.test(trimmed)) {
|
|
496
|
+
return { isSafe: false, error: `Potential injection pattern detected in: ${trimmed}`, command: "" };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { isSafe: true, error: "", command: trimmed };
|
|
500
|
+
}
|
|
501
|
+
function sanitizeCommand(command) {
|
|
502
|
+
if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
503
|
+
let normalizedCmd = command.normalize("NFKC");
|
|
504
|
+
normalizedCmd = normalizedCmd.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "");
|
|
505
|
+
const strippedForCompare = command.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "").normalize("NFKC");
|
|
506
|
+
if (normalizedCmd !== strippedForCompare) {
|
|
507
|
+
return { isSafe: false, error: `Command rejected: Unicode normalization variance detected (possible homoglyph bypass)`, command: "" };
|
|
508
|
+
}
|
|
509
|
+
command = normalizedCmd;
|
|
510
|
+
const trimmed = command.trim();
|
|
511
|
+
if (!trimmed) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
512
|
+
const newlineStripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
|
|
513
|
+
if (newlineStripped !== command) {
|
|
514
|
+
return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
|
|
515
|
+
}
|
|
516
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
517
|
+
if (pattern.test(trimmed)) {
|
|
518
|
+
return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const subCommands = [];
|
|
522
|
+
let remaining = trimmed;
|
|
523
|
+
const chainRegex = /&&|\|\||(?<!\|)\|(?!\|)/g;
|
|
524
|
+
let match;
|
|
525
|
+
let lastIndex = 0;
|
|
526
|
+
while ((match = chainRegex.exec(remaining)) !== null) {
|
|
527
|
+
subCommands.push(remaining.slice(lastIndex, match.index));
|
|
528
|
+
lastIndex = match.index + match[0].length;
|
|
529
|
+
}
|
|
530
|
+
subCommands.push(remaining.slice(lastIndex));
|
|
531
|
+
const mode = getSecurityMode();
|
|
532
|
+
for (const subCmd of subCommands) {
|
|
533
|
+
const result = checkSingleCommand(subCmd, mode);
|
|
534
|
+
if (!result.isSafe) {
|
|
535
|
+
return { isSafe: false, error: result.error, command: "" };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { isSafe: true, error: "", command };
|
|
539
|
+
}
|
|
540
|
+
var AUDIT_DIR = path3.join(os3.homedir(), ".pi", "agent");
|
|
541
|
+
var AUDIT_LOG_PATH = path3.join(AUDIT_DIR, "audit.log");
|
|
542
|
+
var _auditBuffer = [];
|
|
543
|
+
function flushAuditBuffer() {
|
|
544
|
+
if (_auditBuffer.length === 0) return;
|
|
545
|
+
try {
|
|
546
|
+
if (!fs3.existsSync(AUDIT_DIR)) {
|
|
547
|
+
fs3.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
548
|
+
}
|
|
549
|
+
const batch = _auditBuffer.join("");
|
|
550
|
+
fs3.appendFileSync(AUDIT_LOG_PATH, batch, "utf-8");
|
|
551
|
+
} catch (err) {
|
|
552
|
+
debugLog("security", "audit buffer flush failure", err);
|
|
553
|
+
}
|
|
554
|
+
_auditBuffer = [];
|
|
555
|
+
}
|
|
556
|
+
function readRecentAuditEntries(count = 50) {
|
|
557
|
+
try {
|
|
558
|
+
if (!fs3.existsSync(AUDIT_LOG_PATH)) return [];
|
|
559
|
+
const fileSize = fs3.statSync(AUDIT_LOG_PATH).size;
|
|
560
|
+
if (fileSize === 0) return [];
|
|
561
|
+
const fd = fs3.openSync(AUDIT_LOG_PATH, "r");
|
|
562
|
+
const bufferSize = 8192;
|
|
563
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
564
|
+
const lines = [];
|
|
565
|
+
let pos = fileSize;
|
|
566
|
+
let partial = "";
|
|
567
|
+
while (pos > 0 && lines.length < count) {
|
|
568
|
+
const readSize = Math.min(bufferSize, pos);
|
|
569
|
+
pos -= readSize;
|
|
570
|
+
fs3.readSync(fd, buffer, 0, readSize, pos);
|
|
571
|
+
const chunk = buffer.slice(0, readSize).toString("utf-8");
|
|
572
|
+
partial = chunk + partial;
|
|
573
|
+
const lineBreak = partial.lastIndexOf("\n");
|
|
574
|
+
if (lineBreak !== -1) {
|
|
575
|
+
const complete = partial.slice(lineBreak + 1);
|
|
576
|
+
if (complete.trim()) lines.unshift(complete);
|
|
577
|
+
partial = partial.slice(0, lineBreak);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
fs3.closeSync(fd);
|
|
581
|
+
if (partial.trim() && lines.length < count) {
|
|
582
|
+
lines.unshift(partial);
|
|
583
|
+
}
|
|
584
|
+
const recent = lines.slice(-count);
|
|
585
|
+
return recent.map((line) => {
|
|
586
|
+
try {
|
|
587
|
+
return JSON.parse(line);
|
|
588
|
+
} catch {
|
|
589
|
+
return {};
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
} catch (err) {
|
|
593
|
+
debugLog("security", "failed to read audit log", err);
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
process.on("exit", () => {
|
|
598
|
+
flushAuditBuffer();
|
|
599
|
+
});
|
|
600
|
+
process.on("SIGTERM", () => {
|
|
601
|
+
flushAuditBuffer();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// extensions/diag.ts
|
|
32
605
|
var SECRET_KEY_PATTERNS = [
|
|
33
606
|
/key/i,
|
|
34
607
|
/token/i,
|
|
@@ -45,7 +618,7 @@ function redactValue(key, value) {
|
|
|
45
618
|
if (value.length > 20 && !value.includes(" ") && /^[A-Za-z0-9_\-+/=]+$/.test(value)) return value.slice(0, 8) + "...";
|
|
46
619
|
return value;
|
|
47
620
|
}
|
|
48
|
-
function
|
|
621
|
+
function diag_default(pi) {
|
|
49
622
|
let cachedSystemPrompt = null;
|
|
50
623
|
let cachedPayload = null;
|
|
51
624
|
pi.on("before_provider_request", (event) => {
|
|
@@ -79,15 +652,15 @@ function diag_temp_default(pi) {
|
|
|
79
652
|
}
|
|
80
653
|
};
|
|
81
654
|
lines.push(section("SYSTEM"));
|
|
82
|
-
const cpus2 =
|
|
83
|
-
const totalMem =
|
|
84
|
-
const freeMem =
|
|
655
|
+
const cpus2 = os4.cpus();
|
|
656
|
+
const totalMem = os4.totalmem();
|
|
657
|
+
const freeMem = os4.freemem();
|
|
85
658
|
const usedMem = totalMem - freeMem;
|
|
86
659
|
const memPct = pct(usedMem, totalMem);
|
|
87
|
-
lines.push(info(`OS: ${
|
|
660
|
+
lines.push(info(`OS: ${os4.type()} ${os4.release()} ${os4.arch()}`));
|
|
88
661
|
lines.push(info(`CPU: ${cpus2.length}x ${cpus2[0]?.model || "unknown"}`));
|
|
89
662
|
lines.push(info(`RAM: ${bytesHuman(usedMem)} / ${bytesHuman(totalMem)} (${memPct})`));
|
|
90
|
-
lines.push(info(`Uptime: ${msHuman(
|
|
663
|
+
lines.push(info(`Uptime: ${msHuman(os4.uptime() * 1e3)}`));
|
|
91
664
|
lines.push(info(`Node.js: ${process.version}`));
|
|
92
665
|
check(
|
|
93
666
|
totalMem >= 4 * 1024 * 1024 * 1024,
|
|
@@ -212,7 +785,7 @@ function diag_temp_default(pi) {
|
|
|
212
785
|
}
|
|
213
786
|
}
|
|
214
787
|
lines.push(section("MODELS.JSON"));
|
|
215
|
-
const agentDir =
|
|
788
|
+
const agentDir = path4.join(os4.homedir(), ".pi", "agent");
|
|
216
789
|
let configuredModels = [];
|
|
217
790
|
const modelsJson = readModelsJson();
|
|
218
791
|
if (modelsJson && Object.keys(modelsJson.providers || {}).length > 0) {
|
|
@@ -273,12 +846,12 @@ function diag_temp_default(pi) {
|
|
|
273
846
|
lines.push(fail(`settings.json read error: ${e.message}`));
|
|
274
847
|
}
|
|
275
848
|
lines.push(section("EXTENSIONS"));
|
|
276
|
-
const extensionsDir =
|
|
849
|
+
const extensionsDir = path4.join(agentDir, "extensions");
|
|
277
850
|
const activeTools = pi.getActiveTools();
|
|
278
851
|
const allTools = pi.getAllTools();
|
|
279
852
|
const builtinTools = /* @__PURE__ */ new Set(["read", "bash", "edit", "write"]);
|
|
280
853
|
const extensionToolCount = activeTools.filter((t) => !builtinTools.has(t)).length;
|
|
281
|
-
const localExtFiles =
|
|
854
|
+
const localExtFiles = fs4.existsSync(extensionsDir) ? fs4.readdirSync(extensionsDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")) : [];
|
|
282
855
|
lines.push(info(`Extension files in ${extensionsDir}: ${localExtFiles.length}`));
|
|
283
856
|
localExtFiles.forEach((f) => lines.push(info(` \u2022 ${f}`)));
|
|
284
857
|
if (localExtFiles.length > 0) {
|
|
@@ -295,15 +868,15 @@ function diag_temp_default(pi) {
|
|
|
295
868
|
}
|
|
296
869
|
lines.push(info(`Registered tools (all): ${allTools.length}`));
|
|
297
870
|
lines.push(section("THEMES"));
|
|
298
|
-
const themesDir =
|
|
299
|
-
if (
|
|
300
|
-
const themeFiles =
|
|
871
|
+
const themesDir = path4.join(agentDir, "themes");
|
|
872
|
+
if (fs4.existsSync(themesDir)) {
|
|
873
|
+
const themeFiles = fs4.readdirSync(themesDir).filter(
|
|
301
874
|
(f) => f.endsWith(".json")
|
|
302
875
|
);
|
|
303
876
|
lines.push(info(`Theme files: ${themeFiles.length}`));
|
|
304
877
|
themeFiles.forEach((f) => {
|
|
305
878
|
try {
|
|
306
|
-
const theme = JSON.parse(
|
|
879
|
+
const theme = JSON.parse(fs4.readFileSync(path4.join(themesDir, f), "utf-8"));
|
|
307
880
|
lines.push(info(` \u2022 ${f} (name: "${theme.name || "unnamed"}")`));
|
|
308
881
|
} catch {
|
|
309
882
|
lines.push(warn(` \u2022 ${f} \u2014 INVALID JSON`));
|
|
@@ -402,7 +975,7 @@ function diag_temp_default(pi) {
|
|
|
402
975
|
}
|
|
403
976
|
lines.push(info("Audit log status:"));
|
|
404
977
|
const auditEntries = readRecentAuditEntries(50);
|
|
405
|
-
if (
|
|
978
|
+
if (fs4.existsSync(AUDIT_LOG_PATH)) {
|
|
406
979
|
lines.push(ok(`Audit log exists: ${AUDIT_LOG_PATH}`));
|
|
407
980
|
if (auditEntries.length > 0) {
|
|
408
981
|
lines.push(info(` Recent entries: ${auditEntries.length} (last 50)`));
|
|
@@ -577,5 +1150,5 @@ function diag_temp_default(pi) {
|
|
|
577
1150
|
});
|
|
578
1151
|
}
|
|
579
1152
|
export {
|
|
580
|
-
|
|
1153
|
+
diag_default as default
|
|
581
1154
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtstech/pi-diag",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Diagnostics extension for Pi Coding Agent",
|
|
5
5
|
"main": "diag.js",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"pi-extensions"
|
|
11
|
+
],
|
|
7
12
|
"license": "MIT",
|
|
8
13
|
"access": "public",
|
|
9
14
|
"type": "module",
|
|
@@ -13,13 +18,12 @@
|
|
|
13
18
|
"type": "git",
|
|
14
19
|
"url": "https://github.com/VTSTech/pi-coding-agent"
|
|
15
20
|
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@vtstech/pi-shared": "1.2.2"
|
|
18
|
-
},
|
|
19
21
|
"peerDependencies": {
|
|
20
22
|
"@mariozechner/pi-coding-agent": ">=0.66"
|
|
21
23
|
},
|
|
22
24
|
"pi": {
|
|
23
|
-
"extensions": [
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./diag.js"
|
|
27
|
+
]
|
|
24
28
|
}
|
|
25
29
|
}
|