@pentect/pi 0.0.1
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/README.md +19 -0
- package/extensions/pentect.js +252 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @pentect/pi
|
|
2
|
+
|
|
3
|
+
Pentect protection for Pi.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install `pentect`, then run:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi install npm:@pentect/pi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Start Pi normally:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pi
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Pentect runs behind Pi's existing interface. No separate launcher is required.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
createBashTool,
|
|
4
|
+
createLocalBashOperations,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
const SAFE_TEXT = "[Pentect: content unavailable]";
|
|
8
|
+
const SAFE_RESULT = {
|
|
9
|
+
content: [{ type: "text", text: SAFE_TEXT }],
|
|
10
|
+
details: undefined,
|
|
11
|
+
isError: true,
|
|
12
|
+
};
|
|
13
|
+
const BRIDGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
14
|
+
const SESSION_ENVIRONMENT = new Set([
|
|
15
|
+
"PENTECT_BIN",
|
|
16
|
+
"PENTECT_MEMORY_STORE_ADDR",
|
|
17
|
+
"PENTECT_MEMORY_STORE_TOKEN",
|
|
18
|
+
"PENTECT_PROCESS_HOST_READ_TOKEN",
|
|
19
|
+
"PENTECT_PROCESS_HOST_WRITE_TOKEN",
|
|
20
|
+
"PENTECT_PROCESS_HOST_ROOT",
|
|
21
|
+
"PENTECT_AGENT_LAUNCHED",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function replaceObject(target, source) {
|
|
25
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
26
|
+
Object.assign(target, source);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createPentectBridge() {
|
|
30
|
+
const child = spawn(process.env.PENTECT_BIN || "pentect", ["bridge"], {
|
|
31
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
let nextId = 1;
|
|
35
|
+
const pending = new Map();
|
|
36
|
+
let buffered = "";
|
|
37
|
+
let closed = false;
|
|
38
|
+
|
|
39
|
+
const fail = () => {
|
|
40
|
+
if (closed) return;
|
|
41
|
+
closed = true;
|
|
42
|
+
for (const { reject, timer } of pending.values()) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(new Error("Pentect unavailable"));
|
|
45
|
+
}
|
|
46
|
+
pending.clear();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
child.stdout.setEncoding("utf8");
|
|
50
|
+
child.stdout.on("data", (chunk) => {
|
|
51
|
+
buffered += chunk;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const end = buffered.indexOf("\n");
|
|
54
|
+
if (end < 0) break;
|
|
55
|
+
const line = buffered.slice(0, end);
|
|
56
|
+
buffered = buffered.slice(end + 1);
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = JSON.parse(line);
|
|
60
|
+
} catch {
|
|
61
|
+
child.kill();
|
|
62
|
+
fail();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const waiter = pending.get(response.id);
|
|
66
|
+
if (!waiter) continue;
|
|
67
|
+
pending.delete(response.id);
|
|
68
|
+
clearTimeout(waiter.timer);
|
|
69
|
+
if (response.ok) waiter.resolve(response.value);
|
|
70
|
+
else waiter.reject(new Error("Pentect rejected the operation"));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
child.on("error", fail);
|
|
74
|
+
child.on("exit", fail);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
request(op, fields = {}) {
|
|
78
|
+
if (closed) return Promise.reject(new Error("Pentect unavailable"));
|
|
79
|
+
const id = nextId++;
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
if (!pending.delete(id)) return;
|
|
83
|
+
reject(new Error("Pentect unavailable"));
|
|
84
|
+
child.kill();
|
|
85
|
+
fail();
|
|
86
|
+
}, BRIDGE_REQUEST_TIMEOUT_MS);
|
|
87
|
+
pending.set(id, { resolve, reject, timer });
|
|
88
|
+
try {
|
|
89
|
+
child.stdin.write(
|
|
90
|
+
`${JSON.stringify({ id, op, ...fields })}\n`,
|
|
91
|
+
(error) => {
|
|
92
|
+
if (!error) return;
|
|
93
|
+
const waiter = pending.get(id);
|
|
94
|
+
if (!waiter) return;
|
|
95
|
+
pending.delete(id);
|
|
96
|
+
clearTimeout(waiter.timer);
|
|
97
|
+
reject(new Error("Pentect unavailable"));
|
|
98
|
+
child.kill();
|
|
99
|
+
fail();
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
pending.delete(id);
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
reject(new Error("Pentect unavailable"));
|
|
106
|
+
child.kill();
|
|
107
|
+
fail();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
close() {
|
|
112
|
+
if (closed) return;
|
|
113
|
+
for (const { reject, timer } of pending.values()) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
reject(new Error("Pentect unavailable"));
|
|
116
|
+
}
|
|
117
|
+
pending.clear();
|
|
118
|
+
closed = true;
|
|
119
|
+
child.kill();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readSessionEnvironment(values) {
|
|
125
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
126
|
+
throw new Error("Pentect returned an invalid session");
|
|
127
|
+
}
|
|
128
|
+
for (const [name, value] of Object.entries(values)) {
|
|
129
|
+
if (!SESSION_ENVIRONMENT.has(name) || typeof value !== "string" || !value) {
|
|
130
|
+
throw new Error("Pentect returned an invalid session");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const required of SESSION_ENVIRONMENT) {
|
|
134
|
+
if (required === "PENTECT_BIN") continue;
|
|
135
|
+
if (typeof values[required] !== "string" || !values[required]) {
|
|
136
|
+
throw new Error("Pentect returned an incomplete session");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({ ...values });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default function pentectExtension(pi) {
|
|
143
|
+
const localBash = createLocalBashOperations();
|
|
144
|
+
let bridge;
|
|
145
|
+
let contract = "";
|
|
146
|
+
let sessionEnvironment;
|
|
147
|
+
|
|
148
|
+
const activeBridge = () => {
|
|
149
|
+
if (!bridge) throw new Error("Pentect unavailable");
|
|
150
|
+
return bridge;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const protectedBash = {
|
|
154
|
+
async exec(command, cwd, options) {
|
|
155
|
+
const next = await activeBridge().request("before", {
|
|
156
|
+
tool: "bash",
|
|
157
|
+
value: { command },
|
|
158
|
+
});
|
|
159
|
+
if (!next || typeof next.command !== "string") {
|
|
160
|
+
throw new Error("Pentect rejected the command");
|
|
161
|
+
}
|
|
162
|
+
if (!sessionEnvironment) throw new Error("Pentect unavailable");
|
|
163
|
+
// The bridge wraps the command with `pentect exec`, which masks each
|
|
164
|
+
// streamed chunk before Pi's onData callback receives it.
|
|
165
|
+
return localBash.exec(next.command, cwd, {
|
|
166
|
+
...options,
|
|
167
|
+
env: { ...options.env, ...sessionEnvironment },
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
173
|
+
bridge?.close();
|
|
174
|
+
sessionEnvironment = undefined;
|
|
175
|
+
contract = "";
|
|
176
|
+
bridge = createPentectBridge();
|
|
177
|
+
try {
|
|
178
|
+
const session = await bridge.request("session");
|
|
179
|
+
if (!session || typeof session.contract !== "string") {
|
|
180
|
+
throw new Error("Pentect returned an invalid session");
|
|
181
|
+
}
|
|
182
|
+
sessionEnvironment = readSessionEnvironment(session.environment);
|
|
183
|
+
contract = session.contract;
|
|
184
|
+
pi.registerTool(
|
|
185
|
+
createBashTool(ctx.cwd, {
|
|
186
|
+
operations: protectedBash,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
bridge.close();
|
|
191
|
+
bridge = undefined;
|
|
192
|
+
throw new Error("Pentect unavailable");
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
pi.on("before_agent_start", async (event) => {
|
|
197
|
+
if (!contract || event.systemPrompt.includes(contract)) return {};
|
|
198
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${contract}` };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
pi.on("input", async (event, ctx) => {
|
|
202
|
+
try {
|
|
203
|
+
const text = await activeBridge().request("prompt", { value: event.text });
|
|
204
|
+
const images = event.images
|
|
205
|
+
? await activeBridge().request("media", { value: event.images })
|
|
206
|
+
: event.images;
|
|
207
|
+
return { action: "transform", text, images };
|
|
208
|
+
} catch {
|
|
209
|
+
if (ctx.hasUI) ctx.ui.notify("Pentect unavailable", "error");
|
|
210
|
+
return { action: "handled" };
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
pi.on("tool_call", async (event) => {
|
|
215
|
+
if (event.toolName === "bash") return {};
|
|
216
|
+
try {
|
|
217
|
+
const next = await activeBridge().request("before", {
|
|
218
|
+
tool: event.toolName,
|
|
219
|
+
value: event.input,
|
|
220
|
+
});
|
|
221
|
+
replaceObject(event.input, next);
|
|
222
|
+
return {};
|
|
223
|
+
} catch {
|
|
224
|
+
return { block: true, reason: "Pentect unavailable" };
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.on("tool_result", async (event) => {
|
|
229
|
+
try {
|
|
230
|
+
return await activeBridge().request("after", {
|
|
231
|
+
tool: event.toolName,
|
|
232
|
+
input: event.input,
|
|
233
|
+
value: {
|
|
234
|
+
content: event.content,
|
|
235
|
+
details: event.details,
|
|
236
|
+
isError: event.isError,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
return SAFE_RESULT;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
pi.on("user_bash", async () => ({ operations: protectedBash }));
|
|
245
|
+
|
|
246
|
+
pi.on("session_shutdown", async () => {
|
|
247
|
+
bridge?.close();
|
|
248
|
+
bridge = undefined;
|
|
249
|
+
contract = "";
|
|
250
|
+
sessionEnvironment = undefined;
|
|
251
|
+
});
|
|
252
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pentect/pi",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pentect protection for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/EdamAme-x/pentect.git",
|
|
10
|
+
"directory": "integrations/pi"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/EdamAme-x/pentect/tree/main/integrations/pi",
|
|
13
|
+
"bugs": "https://github.com/EdamAme-x/pentect/issues",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"pi-package",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"pentect"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"extensions"
|
|
24
|
+
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./extensions/pentect.js"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"check": "node --check extensions/pentect.js",
|
|
32
|
+
"test": "node --test"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": ">=0.80.7 <0.81.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@earendil-works/pi-coding-agent": "0.80.7"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.19.0"
|
|
42
|
+
}
|
|
43
|
+
}
|