@vtstech/pi-shared 1.0.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/format.js +116 -0
- package/index.js +4 -0
- package/ollama.js +130 -0
- package/package.json +19 -0
- package/security.js +383 -0
- package/types.js +95 -0
package/format.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// shared/format.ts
|
|
20
|
+
var format_exports = {};
|
|
21
|
+
__export(format_exports, {
|
|
22
|
+
bytesHuman: () => bytesHuman,
|
|
23
|
+
fail: () => fail,
|
|
24
|
+
fmtBytes: () => fmtBytes,
|
|
25
|
+
fmtDur: () => fmtDur,
|
|
26
|
+
info: () => info,
|
|
27
|
+
msHuman: () => msHuman,
|
|
28
|
+
ok: () => ok,
|
|
29
|
+
padRight: () => padRight,
|
|
30
|
+
pct: () => pct,
|
|
31
|
+
sanitizeForReport: () => sanitizeForReport,
|
|
32
|
+
section: () => section,
|
|
33
|
+
truncate: () => truncate,
|
|
34
|
+
warn: () => warn
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(format_exports);
|
|
37
|
+
function section(title) {
|
|
38
|
+
return `
|
|
39
|
+
\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(1, 60 - title.length - 4))}`;
|
|
40
|
+
}
|
|
41
|
+
function ok(msg) {
|
|
42
|
+
return ` \u2705 ${msg}`;
|
|
43
|
+
}
|
|
44
|
+
function fail(msg) {
|
|
45
|
+
return ` \u274C ${msg}`;
|
|
46
|
+
}
|
|
47
|
+
function warn(msg) {
|
|
48
|
+
return ` \u26A0\uFE0F ${msg}`;
|
|
49
|
+
}
|
|
50
|
+
function info(msg) {
|
|
51
|
+
return ` \u2139\uFE0F ${msg}`;
|
|
52
|
+
}
|
|
53
|
+
function bytesHuman(bytes) {
|
|
54
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (bytes >= 1024 && i < units.length - 1) {
|
|
57
|
+
bytes /= 1024;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
return `${bytes.toFixed(1)}${units[i]}`;
|
|
61
|
+
}
|
|
62
|
+
function msHuman(ms) {
|
|
63
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
64
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
65
|
+
return `${(ms / 6e4).toFixed(1)}m`;
|
|
66
|
+
}
|
|
67
|
+
function fmtBytes(b) {
|
|
68
|
+
if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)}G`;
|
|
69
|
+
if (b >= 1048576) return `${(b / 1048576).toFixed(0)}M`;
|
|
70
|
+
return `${(b / 1024).toFixed(0)}K`;
|
|
71
|
+
}
|
|
72
|
+
function fmtDur(ms) {
|
|
73
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
74
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
75
|
+
return `${Math.floor(ms / 6e4)}m${Math.round(ms % 6e4 / 1e3)}s`;
|
|
76
|
+
}
|
|
77
|
+
function pct(used, total) {
|
|
78
|
+
return `${(used / total * 100).toFixed(1)}%`;
|
|
79
|
+
}
|
|
80
|
+
function truncate(s, max) {
|
|
81
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeForReport(s, maxLines = 40) {
|
|
84
|
+
let cleaned = s.replace(/^\s*```[a-zA-Z]*[ \t]*\n?/gm, "");
|
|
85
|
+
cleaned = cleaned.replace(/^\s*```[ \t]*\n?/gm, "");
|
|
86
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
|
|
87
|
+
if (/<[a-z][\s\S]*>/i.test(cleaned) && cleaned.includes("</")) {
|
|
88
|
+
const firstLine = cleaned.split("\n")[0];
|
|
89
|
+
return truncate(firstLine, 200) + "\n \u2139\uFE0F (HTML response truncated)";
|
|
90
|
+
}
|
|
91
|
+
const lines = cleaned.split("\n");
|
|
92
|
+
if (lines.length > maxLines) {
|
|
93
|
+
cleaned = lines.slice(0, maxLines).join("\n") + `
|
|
94
|
+
\u2139\uFE0F (truncated, ${lines.length - maxLines} more lines)`;
|
|
95
|
+
}
|
|
96
|
+
return cleaned;
|
|
97
|
+
}
|
|
98
|
+
function padRight(s, n) {
|
|
99
|
+
return s + " ".repeat(Math.max(0, n - s.length));
|
|
100
|
+
}
|
|
101
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
102
|
+
0 && (module.exports = {
|
|
103
|
+
bytesHuman,
|
|
104
|
+
fail,
|
|
105
|
+
fmtBytes,
|
|
106
|
+
fmtDur,
|
|
107
|
+
info,
|
|
108
|
+
msHuman,
|
|
109
|
+
ok,
|
|
110
|
+
padRight,
|
|
111
|
+
pct,
|
|
112
|
+
sanitizeForReport,
|
|
113
|
+
section,
|
|
114
|
+
truncate,
|
|
115
|
+
warn
|
|
116
|
+
});
|
package/index.js
ADDED
package/ollama.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// shared/ollama.ts
|
|
30
|
+
var ollama_exports = {};
|
|
31
|
+
__export(ollama_exports, {
|
|
32
|
+
MODELS_JSON_PATH: () => MODELS_JSON_PATH,
|
|
33
|
+
detectModelFamily: () => detectModelFamily,
|
|
34
|
+
fetchOllamaModels: () => fetchOllamaModels,
|
|
35
|
+
getOllamaBaseUrl: () => getOllamaBaseUrl,
|
|
36
|
+
isReasoningModel: () => isReasoningModel,
|
|
37
|
+
readModelsJson: () => readModelsJson,
|
|
38
|
+
writeModelsJson: () => writeModelsJson
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(ollama_exports);
|
|
41
|
+
var fs = __toESM(require("node:fs"));
|
|
42
|
+
var path = __toESM(require("node:path"));
|
|
43
|
+
var import_node_os = __toESM(require("node:os"));
|
|
44
|
+
var MODELS_JSON_PATH = path.join(import_node_os.default.homedir(), ".pi", "agent", "models.json");
|
|
45
|
+
function getOllamaBaseUrl() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(MODELS_JSON_PATH)) {
|
|
48
|
+
const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
|
|
49
|
+
const config = JSON.parse(raw);
|
|
50
|
+
const baseUrl = config?.providers?.["ollama"]?.baseUrl;
|
|
51
|
+
if (baseUrl) {
|
|
52
|
+
return baseUrl.replace(/\/v1\/?$/, "");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
if (process.env.OLLAMA_HOST) {
|
|
58
|
+
return `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, "")}`;
|
|
59
|
+
}
|
|
60
|
+
return "http://localhost:11434";
|
|
61
|
+
}
|
|
62
|
+
function readModelsJson() {
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(MODELS_JSON_PATH)) {
|
|
65
|
+
const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
return { providers: {} };
|
|
71
|
+
}
|
|
72
|
+
function writeModelsJson(data) {
|
|
73
|
+
const dir = path.dirname(MODELS_JSON_PATH);
|
|
74
|
+
if (!fs.existsSync(dir)) {
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
78
|
+
}
|
|
79
|
+
async function fetchOllamaModels(baseUrl) {
|
|
80
|
+
const res = await fetch(`${baseUrl}/api/tags`, {
|
|
81
|
+
signal: AbortSignal.timeout(5e3)
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
return data.models ?? [];
|
|
86
|
+
}
|
|
87
|
+
function isReasoningModel(name) {
|
|
88
|
+
const lower = name.toLowerCase();
|
|
89
|
+
return lower.includes("deepseek-r1") || lower.includes("qwq") || lower.includes("o1") || lower.includes("o3") || lower.includes("think") || lower.includes("reason");
|
|
90
|
+
}
|
|
91
|
+
function detectModelFamily(modelName) {
|
|
92
|
+
const name = modelName.toLowerCase();
|
|
93
|
+
const families = [
|
|
94
|
+
["qwen3.5", "qwen35"],
|
|
95
|
+
["qwen3", "qwen3"],
|
|
96
|
+
["qwen2.5", "qwen2"],
|
|
97
|
+
["qwen2", "qwen2"],
|
|
98
|
+
["qwen", "qwen2"],
|
|
99
|
+
["llama3.3", "llama"],
|
|
100
|
+
["llama3.2", "llama"],
|
|
101
|
+
["llama3.1", "llama"],
|
|
102
|
+
["llama3", "llama"],
|
|
103
|
+
["llama", "llama"],
|
|
104
|
+
["gemma3", "gemma3"],
|
|
105
|
+
["gemma2", "gemma2"],
|
|
106
|
+
["gemma", "gemma2"],
|
|
107
|
+
["granite", "granite"],
|
|
108
|
+
["dolphin", "dolphin"],
|
|
109
|
+
["deepseek-r1", "deepseek-r1"],
|
|
110
|
+
["deepseek", "deepseek"],
|
|
111
|
+
["mistral", "qwen2"],
|
|
112
|
+
["phi", "llama"],
|
|
113
|
+
["tinyllama", "llama"],
|
|
114
|
+
["codestral", "qwen2"]
|
|
115
|
+
];
|
|
116
|
+
for (const [prefix, family] of families) {
|
|
117
|
+
if (name.includes(prefix)) return family;
|
|
118
|
+
}
|
|
119
|
+
return "unknown";
|
|
120
|
+
}
|
|
121
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
122
|
+
0 && (module.exports = {
|
|
123
|
+
MODELS_JSON_PATH,
|
|
124
|
+
detectModelFamily,
|
|
125
|
+
fetchOllamaModels,
|
|
126
|
+
getOllamaBaseUrl,
|
|
127
|
+
isReasoningModel,
|
|
128
|
+
readModelsJson,
|
|
129
|
+
writeModelsJson
|
|
130
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vtstech/pi-shared",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Shared utilities for Pi Coding Agent extensions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": ["pi-package", "pi-extensions"],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"access": "public",
|
|
10
|
+
"author": "VTSTech",
|
|
11
|
+
"homepage": "https://www.vts-tech.org",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/VTSTech/pi-coding-agent"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@mariozechner/pi-coding-agent": ">=0.66"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/security.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// shared/security.ts
|
|
30
|
+
var security_exports = {};
|
|
31
|
+
__export(security_exports, {
|
|
32
|
+
BLOCKED_COMMANDS: () => BLOCKED_COMMANDS,
|
|
33
|
+
BLOCKED_URL_PATTERNS: () => BLOCKED_URL_PATTERNS,
|
|
34
|
+
appendAuditEntry: () => appendAuditEntry,
|
|
35
|
+
checkBashToolInput: () => checkBashToolInput,
|
|
36
|
+
checkFileToolInput: () => checkFileToolInput,
|
|
37
|
+
checkHttpToolInput: () => checkHttpToolInput,
|
|
38
|
+
checkInjectionPatterns: () => checkInjectionPatterns,
|
|
39
|
+
isSafeUrl: () => isSafeUrl,
|
|
40
|
+
readRecentAuditEntries: () => readRecentAuditEntries,
|
|
41
|
+
sanitizeCommand: () => sanitizeCommand,
|
|
42
|
+
validatePath: () => validatePath
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(security_exports);
|
|
45
|
+
var fs = __toESM(require("node:fs"));
|
|
46
|
+
var path = __toESM(require("node:path"));
|
|
47
|
+
var import_node_os = __toESM(require("node:os"));
|
|
48
|
+
var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
49
|
+
// System modification
|
|
50
|
+
"rm",
|
|
51
|
+
"rmdir",
|
|
52
|
+
"del",
|
|
53
|
+
"format",
|
|
54
|
+
"fdisk",
|
|
55
|
+
"mkfs",
|
|
56
|
+
"dd",
|
|
57
|
+
"shred",
|
|
58
|
+
"wipe",
|
|
59
|
+
"srm",
|
|
60
|
+
// Privilege escalation
|
|
61
|
+
"sudo",
|
|
62
|
+
"su",
|
|
63
|
+
"doas",
|
|
64
|
+
"pkexec",
|
|
65
|
+
"gksudo",
|
|
66
|
+
"kdesu",
|
|
67
|
+
// Network attacks
|
|
68
|
+
"nmap",
|
|
69
|
+
"nc",
|
|
70
|
+
"netcat",
|
|
71
|
+
"telnet",
|
|
72
|
+
"wget",
|
|
73
|
+
"curl",
|
|
74
|
+
"ssh",
|
|
75
|
+
"scp",
|
|
76
|
+
"sftp",
|
|
77
|
+
"rsync",
|
|
78
|
+
// Package management
|
|
79
|
+
"apt",
|
|
80
|
+
"apt-get",
|
|
81
|
+
"yum",
|
|
82
|
+
"dnf",
|
|
83
|
+
"pacman",
|
|
84
|
+
"pip",
|
|
85
|
+
"npm",
|
|
86
|
+
"yarn",
|
|
87
|
+
"cargo",
|
|
88
|
+
// Process control
|
|
89
|
+
"kill",
|
|
90
|
+
"killall",
|
|
91
|
+
"pkill",
|
|
92
|
+
"xkill",
|
|
93
|
+
"systemctl",
|
|
94
|
+
"service",
|
|
95
|
+
// User management
|
|
96
|
+
"useradd",
|
|
97
|
+
"userdel",
|
|
98
|
+
"usermod",
|
|
99
|
+
"passwd",
|
|
100
|
+
"adduser",
|
|
101
|
+
"deluser",
|
|
102
|
+
// Dangerous shell features
|
|
103
|
+
"exec",
|
|
104
|
+
"eval",
|
|
105
|
+
"source",
|
|
106
|
+
".",
|
|
107
|
+
"alias",
|
|
108
|
+
// Filesystem
|
|
109
|
+
"mount",
|
|
110
|
+
"umount",
|
|
111
|
+
"chown",
|
|
112
|
+
"chmod",
|
|
113
|
+
"chattr",
|
|
114
|
+
"lsattr",
|
|
115
|
+
// Shell escapes
|
|
116
|
+
"vi",
|
|
117
|
+
"vim",
|
|
118
|
+
"nano",
|
|
119
|
+
"emacs",
|
|
120
|
+
"less",
|
|
121
|
+
"more",
|
|
122
|
+
"man"
|
|
123
|
+
]);
|
|
124
|
+
var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
|
|
125
|
+
// Loopback
|
|
126
|
+
"localhost",
|
|
127
|
+
"127.0.0.1",
|
|
128
|
+
"0.0.0.0",
|
|
129
|
+
"::1",
|
|
130
|
+
// RFC1918 private ranges
|
|
131
|
+
"10.",
|
|
132
|
+
"192.168.",
|
|
133
|
+
"172.16.",
|
|
134
|
+
"172.17.",
|
|
135
|
+
"172.18.",
|
|
136
|
+
"172.19.",
|
|
137
|
+
"172.20.",
|
|
138
|
+
"172.21.",
|
|
139
|
+
"172.22.",
|
|
140
|
+
"172.23.",
|
|
141
|
+
"172.24.",
|
|
142
|
+
"172.25.",
|
|
143
|
+
"172.26.",
|
|
144
|
+
"172.27.",
|
|
145
|
+
"172.28.",
|
|
146
|
+
"172.29.",
|
|
147
|
+
"172.30.",
|
|
148
|
+
"172.31.",
|
|
149
|
+
// Cloud metadata endpoints
|
|
150
|
+
"169.254.169.254",
|
|
151
|
+
// Internal service patterns
|
|
152
|
+
"internal.",
|
|
153
|
+
"local.",
|
|
154
|
+
"private.",
|
|
155
|
+
"intranet."
|
|
156
|
+
]);
|
|
157
|
+
var CRITICAL_SYSTEM_DIRS = [
|
|
158
|
+
"/etc",
|
|
159
|
+
"/root",
|
|
160
|
+
"/var",
|
|
161
|
+
"/usr",
|
|
162
|
+
"/bin",
|
|
163
|
+
"/sbin",
|
|
164
|
+
"/boot",
|
|
165
|
+
"/dev",
|
|
166
|
+
"/proc",
|
|
167
|
+
"/sys"
|
|
168
|
+
];
|
|
169
|
+
function validatePath(filePath, allowedDirs) {
|
|
170
|
+
if (!filePath) return { valid: false, error: "Path cannot be empty" };
|
|
171
|
+
if (filePath.startsWith("\\\\")) {
|
|
172
|
+
return { valid: false, error: "UNC paths not allowed" };
|
|
173
|
+
}
|
|
174
|
+
if (filePath.includes("../") || filePath.includes("..\\")) {
|
|
175
|
+
return { valid: false, error: "Path traversal detected: parent directory access not allowed" };
|
|
176
|
+
}
|
|
177
|
+
let resolved;
|
|
178
|
+
try {
|
|
179
|
+
resolved = path.resolve(filePath);
|
|
180
|
+
} catch {
|
|
181
|
+
return { valid: false, error: "Invalid path format" };
|
|
182
|
+
}
|
|
183
|
+
for (const critical of CRITICAL_SYSTEM_DIRS) {
|
|
184
|
+
if (resolved.startsWith(critical + "/") || resolved === critical) {
|
|
185
|
+
return { valid: false, error: `Access to system directory denied: ${critical}` };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const sensitivePaths = [
|
|
189
|
+
"/etc/shadow",
|
|
190
|
+
"/etc/passwd",
|
|
191
|
+
"/.ssh/",
|
|
192
|
+
"/.gnupg/",
|
|
193
|
+
path.join(import_node_os.default.homedir(), ".ssh"),
|
|
194
|
+
path.join(import_node_os.default.homedir(), ".gnupg"),
|
|
195
|
+
path.join(import_node_os.default.homedir(), ".pi", "agent", "models.json")
|
|
196
|
+
];
|
|
197
|
+
for (const sensitive of sensitivePaths) {
|
|
198
|
+
if (resolved.startsWith(sensitive) || resolved === sensitive) {
|
|
199
|
+
return { valid: false, error: `Access to sensitive path denied: ${sensitive}` };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const cwd = process.cwd();
|
|
203
|
+
const safePrefixes = ["/tmp", "/var/tmp", "/home", cwd];
|
|
204
|
+
for (const prefix of safePrefixes) {
|
|
205
|
+
if (resolved.startsWith(prefix)) return { valid: true, error: "" };
|
|
206
|
+
}
|
|
207
|
+
if (allowedDirs) {
|
|
208
|
+
for (const dir of allowedDirs) {
|
|
209
|
+
try {
|
|
210
|
+
const absDir = path.resolve(dir);
|
|
211
|
+
if (resolved.startsWith(absDir)) return { valid: true, error: "" };
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { valid: false, error: `Path not in allowed directories: ${filePath}` };
|
|
217
|
+
}
|
|
218
|
+
function isSafeUrl(url, blockSsrf = true) {
|
|
219
|
+
if (!url) return { safe: false, error: "URL cannot be empty" };
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = new URL(url);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return { safe: false, error: `Invalid URL format: ${e.message}` };
|
|
225
|
+
}
|
|
226
|
+
const scheme = parsed.protocol.replace(":", "").toLowerCase();
|
|
227
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
228
|
+
return { safe: false, error: `URL scheme not allowed: ${parsed.protocol}` };
|
|
229
|
+
}
|
|
230
|
+
if (!parsed.hostname) {
|
|
231
|
+
return { safe: false, error: "URL must have a hostname" };
|
|
232
|
+
}
|
|
233
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
234
|
+
if (blockSsrf) {
|
|
235
|
+
for (const pattern of BLOCKED_URL_PATTERNS) {
|
|
236
|
+
if (hostname === pattern || hostname.endsWith("." + pattern) || hostname.startsWith(pattern)) {
|
|
237
|
+
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { safe: true, error: "" };
|
|
242
|
+
}
|
|
243
|
+
var INJECTION_PATTERNS = [
|
|
244
|
+
/;\s*\w+/,
|
|
245
|
+
// Command chaining
|
|
246
|
+
/\|\s*\w+/,
|
|
247
|
+
// Piping to another command
|
|
248
|
+
/&&\s*\w+/,
|
|
249
|
+
// AND chaining
|
|
250
|
+
/\|\|\s*\w+/,
|
|
251
|
+
// OR chaining
|
|
252
|
+
/`[^`]+`/,
|
|
253
|
+
// Command substitution (backticks)
|
|
254
|
+
/\$\([^)]+\)/,
|
|
255
|
+
// Command substitution ($())
|
|
256
|
+
/\$\{[^}]+\}/,
|
|
257
|
+
// Variable expansion
|
|
258
|
+
/>\s*\S+/,
|
|
259
|
+
// Output redirection
|
|
260
|
+
/<\s*\S+/,
|
|
261
|
+
// Input redirection
|
|
262
|
+
/\|(?=[^\s|])/
|
|
263
|
+
// Bare pipe without space
|
|
264
|
+
];
|
|
265
|
+
function sanitizeCommand(command) {
|
|
266
|
+
if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
267
|
+
const parts = command.trim().split(/\s+/);
|
|
268
|
+
if (!parts.length) return { isSafe: false, error: "Command cannot be empty", command: "" };
|
|
269
|
+
let baseCmd = parts[0].toLowerCase();
|
|
270
|
+
if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
|
|
271
|
+
if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
|
|
272
|
+
if (BLOCKED_COMMANDS.has(baseCmd)) {
|
|
273
|
+
return { isSafe: false, error: `Blocked command: ${baseCmd}`, command: "" };
|
|
274
|
+
}
|
|
275
|
+
const stripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
|
|
276
|
+
if (stripped !== command) {
|
|
277
|
+
return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
|
|
278
|
+
}
|
|
279
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
280
|
+
if (pattern.test(command)) {
|
|
281
|
+
return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { isSafe: true, error: "", command };
|
|
285
|
+
}
|
|
286
|
+
var AUDIT_DIR = path.join(import_node_os.default.homedir(), ".pi", "agent");
|
|
287
|
+
var AUDIT_LOG_PATH = path.join(AUDIT_DIR, "audit.log");
|
|
288
|
+
function appendAuditEntry(entry) {
|
|
289
|
+
try {
|
|
290
|
+
if (!fs.existsSync(AUDIT_DIR)) {
|
|
291
|
+
fs.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
const line = JSON.stringify(entry) + "\n";
|
|
294
|
+
fs.appendFileSync(AUDIT_LOG_PATH, line, "utf-8");
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function readRecentAuditEntries(count = 50) {
|
|
299
|
+
try {
|
|
300
|
+
if (!fs.existsSync(AUDIT_LOG_PATH)) return [];
|
|
301
|
+
const content = fs.readFileSync(AUDIT_LOG_PATH, "utf-8");
|
|
302
|
+
const lines = content.trim().split("\n");
|
|
303
|
+
const recent = lines.slice(-count);
|
|
304
|
+
return recent.map((line) => {
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(line);
|
|
307
|
+
} catch {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
} catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function checkBashToolInput(input) {
|
|
316
|
+
const command = input.command ?? input.cmd ?? "";
|
|
317
|
+
if (!command) return { safe: true, rule: "", detail: "" };
|
|
318
|
+
const result = sanitizeCommand(command);
|
|
319
|
+
if (!result.isSafe) {
|
|
320
|
+
return { safe: false, rule: "command_blocklist", detail: result.error };
|
|
321
|
+
}
|
|
322
|
+
return { safe: true, rule: "", detail: "" };
|
|
323
|
+
}
|
|
324
|
+
function checkFileToolInput(input) {
|
|
325
|
+
const filePaths = [
|
|
326
|
+
input.file_path,
|
|
327
|
+
input.path,
|
|
328
|
+
input.output_path,
|
|
329
|
+
input.filePath,
|
|
330
|
+
input.inputPath
|
|
331
|
+
].filter((p) => typeof p === "string" && p.length > 0);
|
|
332
|
+
for (const filePath of filePaths) {
|
|
333
|
+
const result = validatePath(filePath);
|
|
334
|
+
if (!result.valid) {
|
|
335
|
+
return { safe: false, rule: "path_validation", detail: result.error };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { safe: true, rule: "", detail: "" };
|
|
339
|
+
}
|
|
340
|
+
function checkHttpToolInput(input) {
|
|
341
|
+
const url = input.url ?? input.uri ?? input.endpoint ?? "";
|
|
342
|
+
if (!url) return { safe: true, rule: "", detail: "" };
|
|
343
|
+
const result = isSafeUrl(url);
|
|
344
|
+
if (!result.safe) {
|
|
345
|
+
return { safe: false, rule: "ssrf_protection", detail: result.error };
|
|
346
|
+
}
|
|
347
|
+
return { safe: true, rule: "", detail: "" };
|
|
348
|
+
}
|
|
349
|
+
function checkInjectionPatterns(input) {
|
|
350
|
+
const dangerousPatterns = [
|
|
351
|
+
/;\s*(rm|sudo|chmod|chown|mkfs|dd|shred)\b/,
|
|
352
|
+
/\|\s*(rm|sudo|chmod|shred|mkfs)\b/,
|
|
353
|
+
/\$\([^)]*\)/,
|
|
354
|
+
/`[^`]+`/
|
|
355
|
+
];
|
|
356
|
+
for (const [key, value] of Object.entries(input)) {
|
|
357
|
+
if (typeof value !== "string") continue;
|
|
358
|
+
for (const pattern of dangerousPatterns) {
|
|
359
|
+
if (pattern.test(value)) {
|
|
360
|
+
return {
|
|
361
|
+
safe: false,
|
|
362
|
+
rule: "injection_detection",
|
|
363
|
+
detail: `Suspicious pattern in argument '${key}'`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { safe: true, rule: "", detail: "" };
|
|
369
|
+
}
|
|
370
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
371
|
+
0 && (module.exports = {
|
|
372
|
+
BLOCKED_COMMANDS,
|
|
373
|
+
BLOCKED_URL_PATTERNS,
|
|
374
|
+
appendAuditEntry,
|
|
375
|
+
checkBashToolInput,
|
|
376
|
+
checkFileToolInput,
|
|
377
|
+
checkHttpToolInput,
|
|
378
|
+
checkInjectionPatterns,
|
|
379
|
+
isSafeUrl,
|
|
380
|
+
readRecentAuditEntries,
|
|
381
|
+
sanitizeCommand,
|
|
382
|
+
validatePath
|
|
383
|
+
});
|
package/types.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
20
|
+
|
|
21
|
+
// shared/types.ts
|
|
22
|
+
var types_exports = {};
|
|
23
|
+
__export(types_exports, {
|
|
24
|
+
EmptyResponseError: () => EmptyResponseError,
|
|
25
|
+
ModelTimeoutError: () => ModelTimeoutError,
|
|
26
|
+
OllamaConnectionError: () => OllamaConnectionError,
|
|
27
|
+
SecurityBlockError: () => SecurityBlockError,
|
|
28
|
+
ToolParseError: () => ToolParseError
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(types_exports);
|
|
31
|
+
var OllamaConnectionError = class extends Error {
|
|
32
|
+
constructor(message, cause) {
|
|
33
|
+
super(message);
|
|
34
|
+
/** The underlying error that caused this connection failure, if any. */
|
|
35
|
+
__publicField(this, "cause");
|
|
36
|
+
this.name = "OllamaConnectionError";
|
|
37
|
+
this.cause = cause;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var ModelTimeoutError = class extends Error {
|
|
41
|
+
constructor(model, timeoutMs) {
|
|
42
|
+
super(`Model ${model} timed out after ${timeoutMs}ms`);
|
|
43
|
+
/** The name of the model that timed out. */
|
|
44
|
+
__publicField(this, "model");
|
|
45
|
+
/** The timeout duration in milliseconds. */
|
|
46
|
+
__publicField(this, "timeoutMs");
|
|
47
|
+
this.name = "ModelTimeoutError";
|
|
48
|
+
this.model = model;
|
|
49
|
+
this.timeoutMs = timeoutMs;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var EmptyResponseError = class extends Error {
|
|
53
|
+
constructor(model, details) {
|
|
54
|
+
super(`Empty response from model ${model}: ${details}`);
|
|
55
|
+
/** The name of the model that returned an empty response. */
|
|
56
|
+
__publicField(this, "model");
|
|
57
|
+
/** Additional details about why the response was considered empty. */
|
|
58
|
+
__publicField(this, "details");
|
|
59
|
+
this.name = "EmptyResponseError";
|
|
60
|
+
this.model = model;
|
|
61
|
+
this.details = details;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var SecurityBlockError = class extends Error {
|
|
65
|
+
constructor(rule, detail, input) {
|
|
66
|
+
super(`[SECURITY] ${detail} (rule: ${rule})`);
|
|
67
|
+
/** The name of the security rule that was triggered. */
|
|
68
|
+
__publicField(this, "rule");
|
|
69
|
+
/** Detailed explanation of why the operation was blocked. */
|
|
70
|
+
__publicField(this, "detail");
|
|
71
|
+
/** The original input that was blocked. */
|
|
72
|
+
__publicField(this, "input");
|
|
73
|
+
this.name = "SecurityBlockError";
|
|
74
|
+
this.rule = rule;
|
|
75
|
+
this.detail = detail;
|
|
76
|
+
this.input = input;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var ToolParseError = class extends Error {
|
|
80
|
+
constructor(message, rawText) {
|
|
81
|
+
super(`Tool parse error: ${message}`);
|
|
82
|
+
/** The raw text that failed to parse. */
|
|
83
|
+
__publicField(this, "rawText");
|
|
84
|
+
this.name = "ToolParseError";
|
|
85
|
+
this.rawText = rawText;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
89
|
+
0 && (module.exports = {
|
|
90
|
+
EmptyResponseError,
|
|
91
|
+
ModelTimeoutError,
|
|
92
|
+
OllamaConnectionError,
|
|
93
|
+
SecurityBlockError,
|
|
94
|
+
ToolParseError
|
|
95
|
+
});
|