@evandu/super-pi 0.1.94 → 0.1.96
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/agents/super-pi-task/core.ts +6 -5
- package/agents/super-pi-task/index.ts +2 -1
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +3 -3
- package/dist/client/assets/{index-zbxFjL-O.js → index-gjLqMWYT.js} +2 -2
- package/dist/client/index.html +1 -1
- package/dist/extension.js +13 -3
- package/dist/server/agents.d.ts +18 -0
- package/dist/server/agents.js +39 -0
- package/dist/server/application/run-service.d.ts +47 -3
- package/dist/server/application/run-service.js +158 -88
- package/dist/server/capability.d.ts +28 -12
- package/dist/server/capability.js +54 -20
- package/dist/server/http-server.js +14 -6
- package/dist/server/index.js +10 -1
- package/dist/server/pi-command.d.ts +29 -1
- package/dist/server/pi-command.js +82 -5
- package/dist/server/pi-session-events.d.ts +111 -0
- package/dist/server/pi-session-events.js +291 -0
- package/dist/server/pi-session-pool.d.ts +166 -0
- package/dist/server/pi-session-pool.js +262 -0
- package/dist/server/pi-session.d.ts +204 -0
- package/dist/server/pi-session.js +226 -0
- package/dist/server/runtime.d.ts +262 -29
- package/dist/server/runtime.js +664 -60
- package/dist/server/store.js +4 -4
- package/dist/server/ws-server.js +5 -4
- package/package.json +7 -3
- package/scripts/patch-brace-expansion.mjs +179 -0
package/dist/server/store.js
CHANGED
|
@@ -1556,18 +1556,18 @@ export class Store {
|
|
|
1556
1556
|
const waiting = this.waitingMessages(conversation);
|
|
1557
1557
|
const from = waiting.indexOf(message);
|
|
1558
1558
|
const target = Math.max(0, Math.min(Math.trunc(toIndex), waiting.length - 1));
|
|
1559
|
-
if (target === from)
|
|
1560
|
-
return structuredClone(message);
|
|
1561
1559
|
// NOTHING MAY OCCUPY THE `sending` MESSAGE'S SLOT, in either direction. It is already in Pi's stdin and
|
|
1562
1560
|
// Pi's steering queue is FIFO, so it is delivered next whatever this method does - meaning a different
|
|
1563
1561
|
// message landing in front of it would be a move the UI performed and the transport then ignored, and
|
|
1564
1562
|
// moving the `sending` message itself off that spot would be a lie of the same kind.
|
|
1565
|
-
const
|
|
1566
|
-
if (
|
|
1563
|
+
const lastSendingIndex = waiting.reduce((last, item, index) => item.delivery === "sending" ? index : last, -1);
|
|
1564
|
+
if (lastSendingIndex >= 0 && (message.delivery === "sending" || target <= lastSendingIndex)) {
|
|
1567
1565
|
throw new Error(message.delivery === "sending"
|
|
1568
1566
|
? "这条消息已经交给 Agent 了,挪不动(Pi 会先回答已经收到的那条)"
|
|
1569
1567
|
: "前面那条消息已经交给 Agent 了,插不过去(Pi 按收到的顺序送达)");
|
|
1570
1568
|
}
|
|
1569
|
+
if (target === from)
|
|
1570
|
+
return structuredClone(message);
|
|
1571
1571
|
// Permute THE WAITING SLOTS rather than splicing a message to an index. The waiting messages are
|
|
1572
1572
|
// usually a suffix of the array, but they are not guaranteed to be one - an assistant message sits
|
|
1573
1573
|
// between two of them once a steer has been delivered mid-turn - and an offset derived from "where in
|
package/dist/server/ws-server.js
CHANGED
|
@@ -222,10 +222,11 @@ export class WsServer {
|
|
|
222
222
|
this.broadcaster.configureStreaming(socket, parsed.coalesce);
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
|
-
// A send
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
const runIndependently = parsed.type === "message.send" || parsed.type === "
|
|
225
|
+
// A send or selected queued turn is a streaming operation. It must leave the socket's command
|
|
226
|
+
// chain immediately: waiting for the model turn would block later priority/cancel commands and
|
|
227
|
+
// make a second send miss the durable queue. The service owns per-conversation run claims.
|
|
228
|
+
const runIndependently = parsed.type === "message.send" || parsed.type === "message.sendQueued"
|
|
229
|
+
|| parsed.type === "message.interrupt" || parsed.type === "run.stop";
|
|
229
230
|
// ...WHICH MEANS A SEND CAN OVERTAKE A REVERT SENT JUST BEFORE IT, because the revert below is
|
|
230
231
|
// queued behind whatever this socket was already doing. The service only learns it is cutting
|
|
231
232
|
// when the command EXECUTES, so a send handled in between would find the conversation open and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.96",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"bin",
|
|
20
20
|
"scripts/restart-server.mjs",
|
|
21
21
|
"README.md",
|
|
22
|
-
"assets/knowledge-model"
|
|
22
|
+
"assets/knowledge-model",
|
|
23
|
+
"scripts/patch-brace-expansion.mjs"
|
|
23
24
|
],
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=22.19.0"
|
|
@@ -35,11 +36,13 @@
|
|
|
35
36
|
"start": "node dist/server/index.js",
|
|
36
37
|
"restart:server": "node scripts/restart-server.mjs",
|
|
37
38
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
38
|
-
"check": "node scripts/run-check.mjs"
|
|
39
|
+
"check": "node scripts/run-check.mjs",
|
|
40
|
+
"postinstall": "node scripts/patch-brace-expansion.mjs"
|
|
39
41
|
},
|
|
40
42
|
"check:logic": [
|
|
41
43
|
"scripts/stream-checkpoint.test.mjs",
|
|
42
44
|
"scripts/stream-transport.test.mjs",
|
|
45
|
+
"scripts/pi-session.test.mjs",
|
|
43
46
|
"scripts/lan-access.test.mjs",
|
|
44
47
|
"scripts/terminal-pty.test.mjs",
|
|
45
48
|
"scripts/persistence-repository.test.mjs",
|
|
@@ -141,6 +144,7 @@
|
|
|
141
144
|
"scripts/knowledge-base-browser.test.mjs"
|
|
142
145
|
],
|
|
143
146
|
"dependencies": {
|
|
147
|
+
"@earendil-works/pi-coding-agent": "^0.86.1",
|
|
144
148
|
"@xterm/addon-fit": "^0.11.0",
|
|
145
149
|
"@xterm/xterm": "^6.0.0",
|
|
146
150
|
"node-pty": "^1.2.0-beta.15",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Repairs one vulnerable transitive dependency that Super Pi cannot fix from the outside.
|
|
2
|
+
//
|
|
3
|
+
// THE PROBLEM. Pi's package ships an `npm-shrinkwrap.json`, and a shrinkwrap pins a dependency's
|
|
4
|
+
// whole subtree. Pi's shrinkwrap pins `brace-expansion@5.0.9`, and npm honours a shrinkwrap over
|
|
5
|
+
// everything else - so neither `overrides` (in either the flat or the nested form) nor `npm audit fix`
|
|
6
|
+
// can move it. `npm audit` therefore reports "1 high severity vulnerability" to every person who
|
|
7
|
+
// installs Super Pi, and the finding is real: brace-expansion 5.0.9 is quadratic-time on `{a,b}` and
|
|
8
|
+
// recurses without bound on nested brace groups, so a pattern that reaches it can burn the CPU or the
|
|
9
|
+
// stack.
|
|
10
|
+
//
|
|
11
|
+
// THE REACHABILITY, so this is not a reflex. brace-expansion is reached through `minimatch`, and in
|
|
12
|
+
// Pi's tree `minimatch` is used by the package manager - to match the paths of installed extensions
|
|
13
|
+
// and skills. Those names come from files on disk, so a hostile workspace is enough to feed a pattern
|
|
14
|
+
// in. It is a denial of service, not a privilege escalation, but it is reachable by opening a
|
|
15
|
+
// repository, and a workspace is exactly what Super Pi is pointed at.
|
|
16
|
+
//
|
|
17
|
+
// THE FIX. `brace-expansion@5.0.12` is the patched release, and it has the same file layout as
|
|
18
|
+
// 5.0.9 (both are `tshy` builds with the same `dist/commonjs` + `dist/esm` shape), so replacing the
|
|
19
|
+
// installed copy is a file-level swap with no re-linking and no API change.
|
|
20
|
+
//
|
|
21
|
+
// WHY IT IS A POSTINSTALL AND NOT A DEPENDENCY. A dependency cannot express "a different version of
|
|
22
|
+
// my dependency's dependency", because the shrinkwrap is the whole point of a shrinkwrap. The only
|
|
23
|
+
// place left is the install itself.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IT WILL NOT DO. It never fails an install. A patched copy that cannot be fetched leaves the
|
|
26
|
+
// tree as it was, and the script says so in one line. An install that cannot finish because of a
|
|
27
|
+
// security patch is a worse outcome than an install that finishes with a known advisory, so the script
|
|
28
|
+
// only ever warns.
|
|
29
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, renameSync, cpSync } from "node:fs";
|
|
30
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { execFileSync } from "node:child_process";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
|
|
35
|
+
/** The patched release. 5.0.9 is what Pi's shrinkwrap pins; 5.0.12 carries the fix. */
|
|
36
|
+
const PATCHED_VERSION = "5.0.12";
|
|
37
|
+
/** The first fixed version. Below this, the advisory applies. */
|
|
38
|
+
const VULNERABLE_BELOW = "5.0.12";
|
|
39
|
+
|
|
40
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const repositoryRoot = resolve(here, "..");
|
|
42
|
+
|
|
43
|
+
/** The lockfile-relative path of a directory, in the form a lockfile's keys take. */
|
|
44
|
+
const lockKeyOf = (directory) => relative(repositoryRoot, directory).split(/[\\/]/).filter(Boolean).join("/");
|
|
45
|
+
|
|
46
|
+
/** Where Pi's package keeps its own `brace-expansion`, if it keeps one. */
|
|
47
|
+
function findVulnerableCopies() {
|
|
48
|
+
const found = [];
|
|
49
|
+
const candidates = [
|
|
50
|
+
join(repositoryRoot, "node_modules", "@earendil-works", "pi-coding-agent", "node_modules", "brace-expansion"),
|
|
51
|
+
join(repositoryRoot, "node_modules", "brace-expansion"),
|
|
52
|
+
];
|
|
53
|
+
for (const directory of candidates) {
|
|
54
|
+
const manifest = join(directory, "package.json");
|
|
55
|
+
if (!existsSync(manifest)) continue;
|
|
56
|
+
let version = "";
|
|
57
|
+
try { version = JSON.parse(readFileSync(manifest, "utf8")).version ?? ""; } catch { continue; }
|
|
58
|
+
if (!version) continue;
|
|
59
|
+
if (compareVersions(version, VULNERABLE_BELOW) >= 0) continue;
|
|
60
|
+
found.push({ directory, version, lockKey: lockKeyOf(directory) });
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Numeric version comparison that does not care about a leading `v` or a prerelease tag. */
|
|
66
|
+
function compareVersions(a, b) {
|
|
67
|
+
const parts = (value) => String(value).replace(/^v/, "").split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
68
|
+
const left = parts(a);
|
|
69
|
+
const right = parts(b);
|
|
70
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
71
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
72
|
+
if (difference !== 0) return difference < 0 ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Replaces one installed copy with the patched release.
|
|
79
|
+
*
|
|
80
|
+
* The patched copy is fetched with `npm install` into a throwaway directory - npm's own
|
|
81
|
+
* fetch-and-verify path - rather than `npm pack` plus `tar`, which needs a `tar` on PATH whose flags
|
|
82
|
+
* differ between platforms. It is laid out exactly the way npm would have laid it out.
|
|
83
|
+
*/
|
|
84
|
+
function patch(directory) {
|
|
85
|
+
const scratch = join(tmpdir(), `super-pi-brace-${process.pid}-${Date.now()}`);
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(scratch, { recursive: true });
|
|
88
|
+
const stage = join(scratch, "stage");
|
|
89
|
+
mkdirSync(stage, { recursive: true });
|
|
90
|
+
writeFileSync(join(stage, "package.json"), JSON.stringify({ name: "super-pi-brace-stage", version: "0.0.0", private: true }));
|
|
91
|
+
execFileSync("npm", ["install", `brace-expansion@${PATCHED_VERSION}`, "--no-audit", "--no-fund", "--silent", "--omit=dev"], {
|
|
92
|
+
cwd: stage,
|
|
93
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
94
|
+
shell: process.platform === "win32",
|
|
95
|
+
});
|
|
96
|
+
const unpacked = join(stage, "node_modules", "brace-expansion");
|
|
97
|
+
if (!existsSync(join(unpacked, "package.json"))) throw new Error("the staged install produced no package");
|
|
98
|
+
// The old copy is moved aside rather than deleted first, so a failure mid-swap cannot leave the
|
|
99
|
+
// tree with half a package in it.
|
|
100
|
+
const previous = `${directory}.super-pi-backup`;
|
|
101
|
+
rmSync(previous, { recursive: true, force: true });
|
|
102
|
+
renameSync(directory, previous);
|
|
103
|
+
try {
|
|
104
|
+
cpSync(unpacked, directory, { recursive: true });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// The swap failed halfway: put the old copy back rather than leaving the tree broken.
|
|
107
|
+
rmSync(directory, { recursive: true, force: true });
|
|
108
|
+
renameSync(previous, directory);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
rmSync(previous, { recursive: true, force: true });
|
|
112
|
+
return true;
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The integrity hash npm recorded for the patched version, so the lockfile still describes a tree npm
|
|
120
|
+
* can verify. It comes from the registry metadata npm itself trusts.
|
|
121
|
+
*/
|
|
122
|
+
function readIntegrity() {
|
|
123
|
+
try {
|
|
124
|
+
const output = execFileSync("npm", ["view", `brace-expansion@${PATCHED_VERSION}`, "dist.integrity", "--silent"], {
|
|
125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
126
|
+
shell: process.platform === "win32",
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
});
|
|
129
|
+
return output.trim() || undefined;
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Rewrites the lockfile's entry for a patched package.
|
|
137
|
+
*
|
|
138
|
+
* WITHOUT THIS THE PATCH IS INVISIBLE. `npm audit` reads the LOCKFILE, not the files on disk - it
|
|
139
|
+
* compares the resolved versions against the advisory database. So a tree that is genuinely patched
|
|
140
|
+
* still reports "1 high severity vulnerability", and the person who installed Super Pi is left looking
|
|
141
|
+
* at a warning that no amount of reinstalling will clear.
|
|
142
|
+
*/
|
|
143
|
+
function updateLockfile(lockKey, version, integrity) {
|
|
144
|
+
const lockPath = join(repositoryRoot, "package-lock.json");
|
|
145
|
+
if (!existsSync(lockPath)) return false;
|
|
146
|
+
let lock;
|
|
147
|
+
try { lock = JSON.parse(readFileSync(lockPath, "utf8")); }
|
|
148
|
+
catch { return false; }
|
|
149
|
+
const entry = lock.packages?.[lockKey];
|
|
150
|
+
if (!entry || entry.version === version) return false;
|
|
151
|
+
entry.version = version;
|
|
152
|
+
if (integrity) entry.integrity = integrity;
|
|
153
|
+
if (entry.resolved) entry.resolved = entry.resolved.replace(/-\/[^/]*\.tgz$/, `-/brace-expansion-${version}.tgz`);
|
|
154
|
+
writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const copies = findVulnerableCopies();
|
|
159
|
+
if (copies.length === 0) {
|
|
160
|
+
console.log("[super-pi] brace-expansion is not a vulnerable version here; nothing to patch.");
|
|
161
|
+
} else {
|
|
162
|
+
let patched = 0;
|
|
163
|
+
for (const copy of copies) {
|
|
164
|
+
try {
|
|
165
|
+
if (!patch(copy.directory)) continue;
|
|
166
|
+
patched += 1;
|
|
167
|
+
console.log(`[super-pi] patched brace-expansion ${copy.version} -> ${PATCHED_VERSION} (${copy.directory})`);
|
|
168
|
+
// And the lockfile, so `npm audit` stops reporting a tree that is no longer vulnerable.
|
|
169
|
+
if (updateLockfile(copy.lockKey, PATCHED_VERSION, readIntegrity())) {
|
|
170
|
+
console.log("[super-pi] the lockfile now records the patched version, so `npm audit` stops reporting it.");
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// NEVER fail the install over this. An install that cannot finish is worse than an install that
|
|
174
|
+
// finishes with a known advisory, and the advisory is reported by `npm audit` either way.
|
|
175
|
+
console.warn(`[super-pi] could not patch brace-expansion in ${copy.directory}: ${error instanceof Error ? error.message : String(error)}. Run \`npm audit fix\` or reinstall to try again.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (patched === 0) console.warn("[super-pi] brace-expansion is still at a vulnerable version; `npm audit` will keep reporting it. This is Pi's own pinned dependency tree.");
|
|
179
|
+
}
|