agent-purge 0.2.7 → 0.2.9
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 +17 -24
- package/backends/index.mjs +3 -3
- package/bin/agent-purge.mjs +58 -25
- package/lib/armed.mjs +3 -2
- package/lib/caps.mjs +4 -4
- package/lib/dsh.mjs +123 -31
- package/lib/persist.mjs +33 -12
- package/lib/personas.mjs +10 -0
- package/lib/slashes.mjs +241 -25
- package/lib/spawn.mjs +3 -1
- package/lib/state.mjs +22 -4
- package/matrix.json +2 -2
- package/package.json +2 -3
- package/web/server.mjs +59 -7
- package/web/ui.html +17 -8
package/web/server.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { detect } from "../lib/detect.mjs";
|
|
7
7
|
import { oneshots } from "../backends/index.mjs";
|
|
8
8
|
import { createStore } from "../lib/personas.mjs";
|
|
9
|
-
import { MODES, armedOf, dshProfilesOf, loadState, patchState, setArmed, setDefault, setPersona, setPiAuto, setDshProfile } from "../lib/state.mjs";
|
|
9
|
+
import { MODES, armedOf, dshProfilesOf, loadState, patchState, setArmed, setDefault, setPersona, setPiAuto, setOmpAuto, setDshProfile } from "../lib/state.mjs";
|
|
10
10
|
import { install, status as persistStatus, uninstall } from "../lib/persist.mjs";
|
|
11
11
|
import { plans, summary as armorSummary } from "../lib/armor.mjs";
|
|
12
12
|
import { installShim, shimStatus, uninstallShim } from "../lib/shim.mjs";
|
|
@@ -31,7 +31,14 @@ const matrix = JSON.parse(readFileSync(join(ROOT, "matrix.json"), "utf8"));
|
|
|
31
31
|
const store = createStore(DATA);
|
|
32
32
|
|
|
33
33
|
export function serveWeb(port = 7788) {
|
|
34
|
-
|
|
34
|
+
// handle 是 async:任何异常都在这里收口,否则请求会挂着不响应
|
|
35
|
+
const server = createServer((req, res) => {
|
|
36
|
+
handle(req, res).catch((e) => {
|
|
37
|
+
try {
|
|
38
|
+
send(res, 500, { error: `服务端错误: ${e.message}` });
|
|
39
|
+
} catch {}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
35
42
|
server.on("error", (e) => {
|
|
36
43
|
// 端口被旧实例占着的时候,--detach 是静默的 → 这里必须说人话,否则用户看到的是旧 UI
|
|
37
44
|
if (e.code === "EADDRINUSE") {
|
|
@@ -51,11 +58,16 @@ async function handle(req, res) {
|
|
|
51
58
|
const url = new URL(req.url, "http://127.0.0.1");
|
|
52
59
|
const p = url.pathname;
|
|
53
60
|
|
|
61
|
+
// 防 CSRF:本机 Web 页面能 POST 就能写配置/改人设;跨站页面用 text/plain 也能发“简单请求”。
|
|
62
|
+
// 只放同源:Origin 的 host 必须和本次请求的 Host 完全一致(端口不同也挡住)。
|
|
63
|
+
if (isMutating(req.method) && !sameOrigin(req)) return send(res, 403, { error: "只接受本机同源请求" });
|
|
64
|
+
|
|
54
65
|
if (req.method === "GET" && (p === "/" || p === "/index.html")) return html(res);
|
|
55
66
|
if (req.method === "GET" && p === "/api/state") return send(res, 200, await state(url.searchParams.get("versions") === "1"));
|
|
56
67
|
if (req.method === "GET" && p === "/api/armor") return send(res, 200, armorState());
|
|
57
68
|
if (req.method === "POST" && p === "/api/defaults") return send(res, 200, saveDefaults(await body(req)));
|
|
58
69
|
if (req.method === "POST" && p === "/api/pi-auto") return send(res, 200, { piAuto: setPiAuto(DATA, (await body(req)).on === true) });
|
|
70
|
+
if (req.method === "POST" && p === "/api/omp-auto") return send(res, 200, { ompAuto: setOmpAuto(DATA, (await body(req)).on === true) });
|
|
59
71
|
if (req.method === "POST" && p === "/api/dsh-profile") return send(res, 200, saveDshProfile(await body(req)));
|
|
60
72
|
if (req.method === "POST" && p === "/api/persona") return send(res, 200, savePersona(await body(req)));
|
|
61
73
|
if (req.method === "DELETE" && p === "/api/persona") return send(res, 200, deletePersona(url.searchParams.get("id")));
|
|
@@ -144,6 +156,7 @@ async function state(withVersions) {
|
|
|
144
156
|
dshAll: DSH_PROFILES,
|
|
145
157
|
modes: MODES,
|
|
146
158
|
piAuto: st.piAuto === true,
|
|
159
|
+
ompAuto: st.ompAuto === true,
|
|
147
160
|
shimDir: shimStatus()[0]?.dir ?? null,
|
|
148
161
|
slashes: slashes,
|
|
149
162
|
};
|
|
@@ -228,7 +241,8 @@ function savePersona(payload) {
|
|
|
228
241
|
function deletePersona(id) {
|
|
229
242
|
if (!id) return { error: "缺少 id" };
|
|
230
243
|
if (store.list().length <= 1) return { error: "至少保留一个 persona" };
|
|
231
|
-
store.
|
|
244
|
+
if (!store.read(id)) return { error: "找不到这个 persona" };
|
|
245
|
+
if (!store.remove(id)) return { error: "非法 persona id" };
|
|
232
246
|
return { ok: true, active: store.activeId() };
|
|
233
247
|
}
|
|
234
248
|
|
|
@@ -251,6 +265,7 @@ function usePersona(payload) {
|
|
|
251
265
|
|
|
252
266
|
/** 全局总开关:所有 harness × 选定通道一次折腾完(= `agent-purge 注册 --all` / `disarm --all`)*/
|
|
253
267
|
function applyArmAll(payload) {
|
|
268
|
+
if (!payload.action) return { error: "缺少 action" };
|
|
254
269
|
const { action } = payload;
|
|
255
270
|
const channel = payload.channel ?? "all";
|
|
256
271
|
const verb = verbOf(action);
|
|
@@ -310,6 +325,7 @@ function verbOf(action) {
|
|
|
310
325
|
|
|
311
326
|
/** 裸命令接管(shim)、/purge(slash)、常驻(install):Web 上一个开关全部搞定 */
|
|
312
327
|
function applyArm(payload) {
|
|
328
|
+
if (!payload.action) return { error: "缺少 action" };
|
|
313
329
|
const { harness, channel, action } = payload;
|
|
314
330
|
const id = payload.persona ?? store.activeId();
|
|
315
331
|
if (!store.read(id)) return { error: "没有可用 persona" };
|
|
@@ -363,21 +379,57 @@ function armChannel(harness, channel, verb, id) {
|
|
|
363
379
|
return { error: `未知通道: ${channel}` };
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
const send = (res, code, obj) =>
|
|
382
|
+
const send = (res, code, obj) =>
|
|
383
|
+
res.writeHead(code, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }).end(JSON.stringify(obj));
|
|
367
384
|
const html = (res) =>
|
|
368
385
|
res
|
|
369
386
|
.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" })
|
|
370
387
|
.end(readFileSync(join(ROOT, "web", "ui.html"), "utf8"));
|
|
371
388
|
|
|
389
|
+
function isMutating(method) {
|
|
390
|
+
return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 浏览器跨站请求会带 Origin;curl/脚本没有,放行。有 Origin 就必须和 Host 同源。 */
|
|
394
|
+
function sameOrigin(req) {
|
|
395
|
+
// 先看浏览器声明的跨站属性:没有 Origin 的跨站请求(部分 fetch 场景)也要挡住
|
|
396
|
+
const site = req.headers["sec-fetch-site"];
|
|
397
|
+
if (site && site !== "same-origin" && site !== "none") return false;
|
|
398
|
+
const origin = req.headers.origin;
|
|
399
|
+
if (!origin) return true;
|
|
400
|
+
try {
|
|
401
|
+
const u = new URL(origin);
|
|
402
|
+
const loopback = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "::1" || u.hostname === "[::1]";
|
|
403
|
+
return loopback && u.host.toLowerCase() === String(req.headers.host ?? "").toLowerCase();
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
372
409
|
function body(req) {
|
|
373
410
|
return new Promise((resolve) => {
|
|
374
411
|
let raw = "";
|
|
375
|
-
|
|
412
|
+
let size = 0;
|
|
413
|
+
let done = false;
|
|
414
|
+
const finish = (value) => {
|
|
415
|
+
if (done) return;
|
|
416
|
+
done = true;
|
|
417
|
+
resolve(value);
|
|
418
|
+
};
|
|
419
|
+
req.on("data", (c) => {
|
|
420
|
+
size += c.length;
|
|
421
|
+
// 本机也不该接无上限的 body:一个坏页面能靠大 POST 把内存吃光
|
|
422
|
+
if (size > 2 * 1024 * 1024) {
|
|
423
|
+
req.destroy();
|
|
424
|
+
return finish({});
|
|
425
|
+
}
|
|
426
|
+
raw += c;
|
|
427
|
+
});
|
|
376
428
|
req.on("end", () => {
|
|
377
429
|
try {
|
|
378
|
-
|
|
430
|
+
finish(JSON.parse(raw || "{}"));
|
|
379
431
|
} catch {
|
|
380
|
-
|
|
432
|
+
finish({});
|
|
381
433
|
}
|
|
382
434
|
});
|
|
383
435
|
});
|
package/web/ui.html
CHANGED
|
@@ -567,8 +567,8 @@ const ARM_INFO = {
|
|
|
567
567
|
shim: { name: "接管裸命令", desc: (h) => h?.id === "pi"
|
|
568
568
|
? "PATH 最前放个同名壳:敲 pi 就注入。想要“裸启动也带人设”,开这个。"
|
|
569
569
|
: `往 PATH 前置一个同名小壳:直接敲 ${h?.id || "<harness>"} 也是接管版。` },
|
|
570
|
-
slash: { name: "会话内热切", desc: (h) => h?.id === "pi"
|
|
571
|
-
?
|
|
570
|
+
slash: { name: "会话内热切", desc: (h) => h?.id === "pi" || h?.id === "omp"
|
|
571
|
+
? `写 ${h.id} 扩展,只挂 /purge:不敲 /purge 就原样跑,动手才改写。`
|
|
572
572
|
: "写 harness 自己支持的扩展/命令,会话里 /purge 切换。" },
|
|
573
573
|
install: { name: "配置文件常驻", desc: () => "写进 harness 自己的指令文件:连别人启动、GUI 里点开都生效。" },
|
|
574
574
|
};
|
|
@@ -688,7 +688,7 @@ function renderOverview() {
|
|
|
688
688
|
const hid = S.defaults.harness;
|
|
689
689
|
const h = S.harnesses.find((x) => x.id === hid) || S.harnesses[0];
|
|
690
690
|
const persona = S.personas.find((p) => p.id === (selPersona || S.persona)) || S.personas[0];
|
|
691
|
-
const hot = S.harnesses.filter((h) => (h.armed || []).length).map((h) => h.id + "(" + h.armed.join("/") + (h.id === "pi" && S.piAuto ? "+裸启动" : "") + ")");
|
|
691
|
+
const hot = S.harnesses.filter((h) => (h.armed || []).length).map((h) => h.id + "(" + h.armed.join("/") + (((h.id === "pi" && S.piAuto) || (h.id === "omp" && S.ompAuto)) ? "+裸启动" : "") + ")");
|
|
692
692
|
|
|
693
693
|
const why = mode === "off"
|
|
694
694
|
? "总电源关着 —— 下面写的都只是配置,实际一条都不会注入。"
|
|
@@ -704,7 +704,7 @@ function renderOverview() {
|
|
|
704
704
|
const say = (c) => c === "shim"
|
|
705
705
|
? (h.id === "pi" ? "敲 pi 会注入(PATH 刚加的前置壳)" : "敲 " + h.id + " 会注入(PATH 前置壳)")
|
|
706
706
|
: c === "slash"
|
|
707
|
-
? (h.id === "pi" ? (S.piAuto ?
|
|
707
|
+
? (h.id === "pi" || h.id === "omp" ? ((h.id === "pi" ? S.piAuto : S.ompAuto) ? `裸敲 ${h.id} 也注入(扩展常驻)` : `裸敲 ${h.id} 不注入,只有会话里 /purge 才改写`) : "会话里 /purge 展开成用户消息")
|
|
708
708
|
: "改了它的配置文件,连别人启动的也生效";
|
|
709
709
|
return chans.map(say).join(";") + "。";
|
|
710
710
|
})()
|
|
@@ -812,6 +812,13 @@ function harnessCard(h) {
|
|
|
812
812
|
<small class="${smallCls}" ${path && !why ? `title="${esc(path)}"` : ""}>${esc(smallText)}</small></span></button>`;
|
|
813
813
|
}).join("");
|
|
814
814
|
|
|
815
|
+
// omp 的裸启动注入靠扩展常驻(ompAuto);只有扩展已装(slash 已武装)才给这个开关
|
|
816
|
+
const ompAutoBtn = h.id === "omp" && h.armed?.includes("slash")
|
|
817
|
+
? `<button class="chan ${S.ompAuto ? "on" : ""}" data-ompauto="${S.ompAuto ? "off" : "on"}" type="button" title="扩展常驻,不改 PATH:开关写在 state.json 的 ompAuto">
|
|
818
|
+
<span class="dot"></span><span><b>裸命令接管${S.ompAuto ? " · 开" : ""}</b>
|
|
819
|
+
<small>${S.ompAuto ? "扩展常驻:敲 omp 就注入(每次启动)" : "关着:只有 /purge 或 agent-purge omp 才注入"}</small></span></button>`
|
|
820
|
+
: "";
|
|
821
|
+
|
|
815
822
|
const notes = []
|
|
816
823
|
// 卡片上的徽章已经写了“SYS 通过/部分通过”,同义的行就不再重复
|
|
817
824
|
.concat((row?.warnings || []).filter((w) => !/^(sys|user) (通过|部分通过|未通过)$/.test(w)))
|
|
@@ -856,7 +863,7 @@ function harnessCard(h) {
|
|
|
856
863
|
<span><k>home</k>${esc(h.home || "—")}${h.homeExists === false ? "(不存在)" : ""}</span>
|
|
857
864
|
</div>
|
|
858
865
|
${notes.length ? `<div class="hnote">${notes.map(esc).join("<br>")}</div>` : ""}
|
|
859
|
-
<div class="chans">${chans}</div>
|
|
866
|
+
<div class="chans">${chans}${ompAutoBtn}</div>
|
|
860
867
|
${profileRow}
|
|
861
868
|
</div>`;
|
|
862
869
|
}
|
|
@@ -1047,11 +1054,13 @@ $("#hpick").addEventListener("click", (e) => {
|
|
|
1047
1054
|
selHarness = b.dataset.hpick; renderHarness();
|
|
1048
1055
|
});
|
|
1049
1056
|
$("#hsel").addEventListener("click", async (e) => {
|
|
1050
|
-
const b = e.target.closest("[data-piauto]"); if (!b) return;
|
|
1057
|
+
const b = e.target.closest("[data-piauto], [data-ompauto]"); if (!b) return;
|
|
1058
|
+
const isPi = Boolean(b.dataset.piauto);
|
|
1059
|
+
const want = (isPi ? b.dataset.piauto : b.dataset.ompauto) === "on";
|
|
1051
1060
|
await withBusy(b, async () => {
|
|
1052
1061
|
try {
|
|
1053
|
-
await api("/api/pi-auto", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ on:
|
|
1054
|
-
toast("裸启动自动注入 → " + (
|
|
1062
|
+
await api(isPi ? "/api/pi-auto" : "/api/omp-auto", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ on: want }) });
|
|
1063
|
+
toast((isPi ? "pi" : "omp") + " 裸启动自动注入 → " + (want ? "开" : "关"));
|
|
1055
1064
|
await load();
|
|
1056
1065
|
} catch (err) { toast("失败:" + err.message, true); }
|
|
1057
1066
|
});
|