@vtstech/pi-security 1.1.4 → 1.1.5

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.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/package.json +2 -2
  3. package/security.js +143 -9
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Security extension for the [Pi Coding Agent](https://github.com/badlogic/pi-mono).
4
4
 
5
- Command, path, and network security layer for Pi's tool execution. Automatically loaded no commands needed.
5
+ Command, path, and network security layer for Pi's tool execution with a configurable security mode. Automatically loaded.
6
6
 
7
7
  ## Install
8
8
 
@@ -12,11 +12,21 @@ pi install "npm:@vtstech/pi-security"
12
12
 
13
13
  ## Protection
14
14
 
15
- - **65 blocked commands** — system modification, privilege escalation, network attacks, package management, process control, shell escapes
16
- - **SSRF protection** — 29 blocked hostname patterns (full `127.0.0.0/8` loopback range, RFC1918 private ranges, cloud metadata endpoints, IPv4-mapped IPv6 `::ffff:127.0.0.1` and `::ffff:0.0.0.0`)
15
+ - **Partitioned command blocklist** — 41 CRITICAL commands (always blocked: system modification, privilege escalation, network attacks, shell escapes) + 25 EXTENDED commands (blocked in max mode: package management, process control, development tools)
16
+ - **Mode-aware SSRF protection** — 19 ALWAYS_BLOCKED URL patterns (loopback, RFC1918 private ranges, cloud metadata endpoints) + 7 MAX_ONLY patterns (localhost by name, broadcast, link-local, current network) that are allowed in basic mode
17
+ - **Security mode toggle** — switch between `basic` and `max` modes at runtime; persisted to `~/.pi/agent/security.json`
17
18
  - **Path validation** — prevents filesystem escape and access to critical system directories; symlinks are dereferenced via `fs.realpathSync()` to block `/tmp/evil → /etc/passwd` bypasses
18
19
  - **Shell injection detection** — regex patterns for command chaining, substitution, and redirection
19
- - **Audit logging** — JSON-lines audit log at `~/.pi/agent/audit.log` (path exported as `AUDIT_LOG_PATH` for cross-extension use)
20
+ - **Audit logging** — JSON-lines audit log at `~/.pi/agent/audit.log` with security mode recorded per entry (path exported as `AUDIT_LOG_PATH`)
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ /security mode basic # Relaxed — CRITICAL commands blocked, localhost URLs allowed
26
+ /security mode max # Full lockdown — all 66 commands blocked, strict SSRF
27
+ ```
28
+
29
+ **Default mode: `max`**. The current mode is shown in the status bar as `SEC:BASIC` or `SEC:MAX`.
20
30
 
21
31
  ## Links
22
32
 
@@ -25,4 +35,4 @@ pi install "npm:@vtstech/pi-security"
25
35
 
26
36
  ## License
27
37
 
28
- MIT — [VTSTech](https://www.vts-tech.org)
38
+ MIT — [VTSTech](https://www.vts-tech.org)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-security",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Security extension for Pi Coding Agent",
5
5
  "main": "security.js",
6
6
  "keywords": ["pi-extensions"],
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
15
  },
16
16
  "dependencies": {
17
- "@vtstech/pi-shared": "1.1.4"
17
+ "@vtstech/pi-shared": "1.1.5"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@mariozechner/pi-coding-agent": ">=0.66"
package/security.js CHANGED
@@ -6,9 +6,15 @@ import {
6
6
  checkInjectionPatterns,
7
7
  appendAuditEntry,
8
8
  readRecentAuditEntries,
9
- BLOCKED_COMMANDS,
10
- BLOCKED_URL_PATTERNS
9
+ CRITICAL_COMMANDS,
10
+ EXTENDED_COMMANDS,
11
+ BLOCKED_URL_ALWAYS,
12
+ BLOCKED_URL_MAX_ONLY,
13
+ getSecurityMode,
14
+ setSecurityMode,
15
+ SECURITY_CONFIG_PATH
11
16
  } from "@vtstech/pi-shared/security";
17
+ import { debugLog } from "@vtstech/pi-shared/debug";
12
18
  import { section, ok, fail, warn, info } from "@vtstech/pi-shared/format";
13
19
  import { EXTENSION_VERSION } from "@vtstech/pi-shared/ollama";
14
20
  function security_temp_default(pi) {
@@ -24,6 +30,120 @@ function security_temp_default(pi) {
24
30
  ` GitHub: https://github.com/VTSTech`,
25
31
  ` Website: www.vts-tech.org`
26
32
  ].join("\n");
33
+ pi.registerCommand("security", {
34
+ description: "Manage security mode \u2014 usage: /security mode [basic|max]",
35
+ handler: async (args, ctx) => {
36
+ try {
37
+ const parts = args.trim().split(/\s+/);
38
+ const sub = parts[0]?.toLowerCase() || "";
39
+ if (sub === "mode") {
40
+ const value = parts[1]?.toLowerCase();
41
+ const currentMode = getSecurityMode();
42
+ if (!value) {
43
+ const lines2 = [branding];
44
+ lines2.push(section("SECURITY MODE"));
45
+ lines2.push(info(`Current mode: ${currentMode.toUpperCase()}`));
46
+ lines2.push(info(`Config path: ${SECURITY_CONFIG_PATH}`));
47
+ lines2.push(info(`Critical commands (always blocked): ${CRITICAL_COMMANDS.size}`));
48
+ lines2.push(info(`Extended commands (max only): ${EXTENDED_COMMANDS.size}`));
49
+ lines2.push(info(`Total blocked (max): ${CRITICAL_COMMANDS.size + EXTENDED_COMMANDS.size}`));
50
+ lines2.push(info(`URL patterns always blocked: ${BLOCKED_URL_ALWAYS.size}`));
51
+ lines2.push(info(`URL patterns (max only): ${BLOCKED_URL_MAX_ONLY.size}`));
52
+ lines2.push(section("MODE DIFFERENCES"));
53
+ lines2.push(info("Basic: critical commands blocked, localhost/127.x allowed"));
54
+ lines2.push(info("Max: all commands blocked, full SSRF protection"));
55
+ lines2.push(section("SWITCH MODE"));
56
+ lines2.push(info("/security mode basic \u2014 relax restrictions for development"));
57
+ lines2.push(info("/security mode max \u2014 full lockdown (default)"));
58
+ lines2.push(branding);
59
+ pi.sendMessage({
60
+ customType: "security-mode-info",
61
+ content: lines2.join("\n"),
62
+ display: { type: "content", content: lines2.join("\n") }
63
+ });
64
+ return;
65
+ }
66
+ if (value === "basic" || value === "max") {
67
+ if (value === currentMode) {
68
+ ctx.ui.notify(`Security mode is already ${value.toUpperCase()}`, "info");
69
+ return;
70
+ }
71
+ const writeOk = setSecurityMode(value);
72
+ if (!writeOk) {
73
+ ctx.ui.notify(`FAILED to persist security mode: could not write ${SECURITY_CONFIG_PATH}`, "error");
74
+ debugLog("security", `/security mode ${value}: write failed`, { path: SECURITY_CONFIG_PATH });
75
+ return;
76
+ }
77
+ ctx.ui.setStatus("status-sec", value.toUpperCase());
78
+ ctx.ui.notify(`Security mode set to ${value.toUpperCase()}`, "success");
79
+ appendAuditEntry({
80
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
81
+ toolName: "security-command",
82
+ toolCallId: "",
83
+ action: "allowed",
84
+ rule: "mode_change",
85
+ detail: `Security mode changed to ${value.toUpperCase()}`
86
+ });
87
+ const totalCmds = CRITICAL_COMMANDS.size + EXTENDED_COMMANDS.size;
88
+ const lines2 = [branding];
89
+ lines2.push(section("SECURITY MODE CHANGED"));
90
+ lines2.push(ok(`Mode: ${value.toUpperCase()}`));
91
+ lines2.push(info(`Previous: ${currentMode.toUpperCase()}`));
92
+ lines2.push(info(`Config: ${SECURITY_CONFIG_PATH}`));
93
+ if (value === "basic") {
94
+ lines2.push(warn("Extended commands are now ALLOWED: rm, sudo, npm, apt, git, curl, wget, etc."));
95
+ lines2.push(warn("Localhost and 127.x URLs are now ALLOWED for SSRF"));
96
+ lines2.push(ok("Critical commands remain blocked: dd, mkfs, shred, fdisk, ssh, etc."));
97
+ } else {
98
+ lines2.push(ok(`Full lockdown active \u2014 all ${totalCmds} commands blocked`));
99
+ lines2.push(ok("Full SSRF protection \u2014 localhost and private IPs blocked"));
100
+ }
101
+ lines2.push(branding);
102
+ pi.sendMessage({
103
+ customType: "security-mode-changed",
104
+ content: lines2.join("\n"),
105
+ display: { type: "content", content: lines2.join("\n") }
106
+ });
107
+ return;
108
+ }
109
+ ctx.ui.notify(`Invalid mode: "${value}". Use "basic" or "max".`, "error");
110
+ return;
111
+ }
112
+ const lines = [branding];
113
+ lines.push(section("SECURITY COMMANDS"));
114
+ lines.push(info("/security mode \u2014 show current security mode"));
115
+ lines.push(info("/security mode basic \u2014 relax to basic mode"));
116
+ lines.push(info("/security mode max \u2014 switch to max lockdown"));
117
+ lines.push(info("/security-audit \u2014 show security audit report"));
118
+ lines.push(branding);
119
+ pi.sendMessage({
120
+ customType: "security-usage",
121
+ content: lines.join("\n"),
122
+ display: { type: "content", content: lines.join("\n") }
123
+ });
124
+ } catch (e) {
125
+ debugLog("security", "/security command handler error", e);
126
+ ctx.ui.notify(`/security error: ${e.message}`, "error");
127
+ }
128
+ }
129
+ });
130
+ pi.registerCompletion?.("security", {
131
+ getCompletions: () => {
132
+ return [
133
+ { value: "mode", label: "mode", description: "View or change the security enforcement mode" }
134
+ ];
135
+ },
136
+ getArgumentCompletions: (args) => {
137
+ const sub = args[0]?.toLowerCase() || "";
138
+ if (sub === "mode" && args.length === 2) {
139
+ return [
140
+ { value: "basic", label: "basic", description: "Relax to basic mode \u2014 only critical commands blocked" },
141
+ { value: "max", label: "max", description: "Full lockdown \u2014 all commands blocked (default)" }
142
+ ];
143
+ }
144
+ return [];
145
+ }
146
+ });
27
147
  pi.on("tool_call", (event) => {
28
148
  const toolName = event.toolName;
29
149
  const input = event.input ?? {};
@@ -111,6 +231,17 @@ function security_temp_default(pi) {
111
231
  async function generateAuditReport() {
112
232
  const lines = [];
113
233
  lines.push(branding);
234
+ const currentMode = getSecurityMode();
235
+ lines.push(section("SECURITY MODE"));
236
+ lines.push(info(`Current mode: ${currentMode.toUpperCase()}`));
237
+ lines.push(info(`Config file: ${SECURITY_CONFIG_PATH}`));
238
+ lines.push(section("BLOCKLIST SUMMARY"));
239
+ lines.push(info(`Critical commands (always blocked): ${CRITICAL_COMMANDS.size}`));
240
+ lines.push(info(`Extended commands (max only): ${EXTENDED_COMMANDS.size}`));
241
+ lines.push(info(`Effective blocked commands: ${currentMode === "max" ? CRITICAL_COMMANDS.size + EXTENDED_COMMANDS.size : CRITICAL_COMMANDS.size}`));
242
+ lines.push(info(`URL patterns always blocked: ${BLOCKED_URL_ALWAYS.size}`));
243
+ lines.push(info(`URL patterns (max only): ${BLOCKED_URL_MAX_ONLY.size}`));
244
+ lines.push(info(`Effective blocked URL patterns: ${currentMode === "max" ? BLOCKED_URL_ALWAYS.size + BLOCKED_URL_MAX_ONLY.size : BLOCKED_URL_ALWAYS.size}`));
114
245
  lines.push(section("SESSION STATISTICS"));
115
246
  lines.push(info(`Tool calls allowed: ${stats.allowed}`));
116
247
  lines.push(info(`Tool calls blocked: ${stats.blocked}`));
@@ -132,10 +263,11 @@ function security_temp_default(pi) {
132
263
  lines.push(fail(`Detail: ${stats.lastBlocked.detail}`));
133
264
  lines.push(info(`Time: ${stats.lastBlocked.timestamp}`));
134
265
  }
135
- lines.push(section("SECURITY CONFIGURATION"));
136
- lines.push(info(`Blocked commands: ${BLOCKED_COMMANDS.size}`));
137
- lines.push(info(`Blocked URL patterns: ${BLOCKED_URL_PATTERNS.size}`));
138
- lines.push(info(`Active checks: command_blocklist, path_validation, ssrf_protection, injection_detection`));
266
+ lines.push(section("ACTIVE CHECKS"));
267
+ lines.push(info(`Command blocklist: critical always, extended in max mode`));
268
+ lines.push(info(`Path validation: sensitive directory protection`));
269
+ lines.push(info(`SSRF protection: ${currentMode === "max" ? "full (loopback + metadata + private)" : "metadata + private only"}`));
270
+ lines.push(info(`Injection detection: metacharacter scanning`));
139
271
  const recentEntries = readRecentAuditEntries(20);
140
272
  if (recentEntries.length > 0) {
141
273
  lines.push(section("RECENT AUDIT LOG (last 20)"));
@@ -145,12 +277,13 @@ function security_temp_default(pi) {
145
277
  const tool = entry.toolName;
146
278
  const rule = entry.rule;
147
279
  const detail = entry.detail;
280
+ const mode = entry.securityMode || currentMode;
148
281
  if (action === "blocked") {
149
- lines.push(fail(`[${ts}] ${tool} \u2192 BLOCKED (${rule}): ${detail}`));
282
+ lines.push(fail(`[${ts}][${mode.toUpperCase()}] ${tool} \u2192 BLOCKED (${rule}): ${detail}`));
150
283
  } else if (action === "warning") {
151
- lines.push(warn(`[${ts}] ${tool} \u2192 WARNING (${rule}): ${detail}`));
284
+ lines.push(warn(`[${ts}][${mode.toUpperCase()}] ${tool} \u2192 WARNING (${rule}): ${detail}`));
152
285
  } else {
153
- lines.push(ok(`[${ts}] ${tool} \u2192 allowed (${rule})`));
286
+ lines.push(ok(`[${ts}][${mode.toUpperCase()}] ${tool} \u2192 allowed (${rule})`));
154
287
  }
155
288
  }
156
289
  }
@@ -160,6 +293,7 @@ function security_temp_default(pi) {
160
293
  } else {
161
294
  lines.push(fail(`${stats.blocked} security violation(s) blocked`));
162
295
  }
296
+ lines.push(info(`Security mode: ${currentMode.toUpperCase()} \u2014 /security mode to change`));
163
297
  lines.push(branding);
164
298
  return lines.join("\n");
165
299
  }