summrise-agent 1.2.466 → 1.2.468
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/bin/summrise.js +36 -2
- package/package.json +1 -1
- package/summrise-agent.exe +0 -0
- package/summrise-desktop-electron/src/main.js +73 -27
package/bin/summrise.js
CHANGED
|
@@ -64,6 +64,7 @@ exports.newestOf = newestOf;
|
|
|
64
64
|
exports.latestReleaseVersion = latestReleaseVersion;
|
|
65
65
|
exports.cdnBase = cdnBase;
|
|
66
66
|
exports.componentUrl = componentUrl;
|
|
67
|
+
exports.componentFetchUrl = componentFetchUrl;
|
|
67
68
|
exports.componentKey = componentKey;
|
|
68
69
|
exports.componentPins = componentPins;
|
|
69
70
|
exports.sha256File = sha256File;
|
|
@@ -908,6 +909,29 @@ function cdnBase() {
|
|
|
908
909
|
function componentUrl(name) {
|
|
909
910
|
return `${cdnBase()}/summrise-agent/${name}`;
|
|
910
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* The address to FETCH a boxed component from: the release manifest's own `url` for it, or
|
|
914
|
+
* `componentUrl`'s derived route when the manifest carries none.
|
|
915
|
+
*
|
|
916
|
+
* WHY THE MANIFEST IS THE AUTHOR. Every release since the pins existed has published a `url`
|
|
917
|
+
* beside each component's `sha256` in version.json, and this CLI read the digest while the
|
|
918
|
+
* address was retyped here — one fact, two authors. They agree today, which is why reading the
|
|
919
|
+
* manifest is a DRIFT GUARD rather than a fix: index/src/index.js rebuilds the published url
|
|
920
|
+
* against the origin of the request that asked for `/api/version`, so the address it hands back
|
|
921
|
+
* is the one this function would have derived — under $SUMMRISE_CDN too, where both move
|
|
922
|
+
* together. A release whose url and route disagreed (a renamed bundle, the round-115 shape)
|
|
923
|
+
* now fetches what it published; a silent manifest (an older release, or no network) keeps the
|
|
924
|
+
* derived route, which is what every release before this one did.
|
|
925
|
+
*
|
|
926
|
+
* Pure — the tests pin it. The digest check beside it is unchanged.
|
|
927
|
+
*/
|
|
928
|
+
function componentFetchUrl(name, pins) {
|
|
929
|
+
const key = componentKey(name);
|
|
930
|
+
const u = key ? (pins[key] || {}).url : "";
|
|
931
|
+
return typeof u === "string" && /^https?:\/\/\S+$/.test(u.trim())
|
|
932
|
+
? u.trim()
|
|
933
|
+
: componentUrl(name);
|
|
934
|
+
}
|
|
911
935
|
/** The release manifest's key for a component's FILE name (they are not the same string). */
|
|
912
936
|
function componentKey(fileName) {
|
|
913
937
|
if (fileName.startsWith("summrise-playwright"))
|
|
@@ -969,7 +993,14 @@ function resolveComponent(name, pkgPath) {
|
|
|
969
993
|
const dest = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "summrise-comp-")), name);
|
|
970
994
|
// -f: an HTTP error is a FAILURE, not a 404 page written to disk (the
|
|
971
995
|
// HTML-polluted download that round-54 exists to remember).
|
|
972
|
-
|
|
996
|
+
//
|
|
997
|
+
// THE MANIFEST IS READ ONCE, BEFORE THE FETCH, because it is the author of the DIGEST (below)
|
|
998
|
+
// and now of the ADDRESS too (componentFetchUrl). Same read, same object, same order of
|
|
999
|
+
// effects: an unreachable manifest yields `{}`, the fetch falls back to the derived route, and
|
|
1000
|
+
// the digest check then says "fetched WITHOUT a manifest pin — not verified", exactly as it
|
|
1001
|
+
// did when the address was derived unconditionally.
|
|
1002
|
+
const pins = componentPins();
|
|
1003
|
+
const r = (0, child_process_1.spawnSync)("curl", ["-fsSL", "-m", "300", "-o", dest, componentFetchUrl(name, pins)], { encoding: "utf8", timeout: 320000 });
|
|
973
1004
|
if (r.status !== 0 || !fs.existsSync(dest) || fs.statSync(dest).size === 0) {
|
|
974
1005
|
return null;
|
|
975
1006
|
}
|
|
@@ -981,8 +1012,11 @@ function resolveComponent(name, pkgPath) {
|
|
|
981
1012
|
// because the failure it prevents is a device running bytes nobody published.
|
|
982
1013
|
// No pin at all (an older release) is a warning, not a refusal: it must not make
|
|
983
1014
|
// an install impossible.
|
|
1015
|
+
//
|
|
1016
|
+
// `pins` is the read from above — ONE manifest read per component fetch, exactly as before; the
|
|
1017
|
+
// check itself is unchanged.
|
|
984
1018
|
const key = componentKey(name);
|
|
985
|
-
const pin = key ? (
|
|
1019
|
+
const pin = key ? (pins[key] || {}).sha256 || "" : "";
|
|
986
1020
|
if (pin) {
|
|
987
1021
|
const got = sha256File(dest);
|
|
988
1022
|
if (got !== pin) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summrise-agent",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.468",
|
|
4
4
|
"description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
|
|
5
5
|
"//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
|
|
6
6
|
"bin": {
|
package/summrise-agent.exe
CHANGED
|
Binary file
|
|
@@ -165,6 +165,32 @@ function probeImage(p) {
|
|
|
165
165
|
function frameOk(e) {
|
|
166
166
|
return (0, url_policy_1.frameUrlOk)(e.senderFrame?.url || "");
|
|
167
167
|
}
|
|
168
|
+
// THE ONE REFUSAL a forbidden frame gets. It used to have TWO shapes — seven
|
|
169
|
+
// handlers answered this one and the rest answered a bare `{ ok: false }`,
|
|
170
|
+
// which the SPA cannot tell apart from a dead view (`j?.ok` is falsy either
|
|
171
|
+
// way, and only this shape carries the reason). The preload's `invoke` callers
|
|
172
|
+
// were written against THIS shape, so every handler answers it now.
|
|
173
|
+
const FORBIDDEN_FRAME = Object.freeze({
|
|
174
|
+
ok: false,
|
|
175
|
+
error: "forbidden frame",
|
|
176
|
+
});
|
|
177
|
+
// THE ONE IPC DOOR: `ipcMain.handle` is called HERE and nowhere else, so the
|
|
178
|
+
// frame check above is applied ONCE per channel and no handler can forget it.
|
|
179
|
+
// A fourteenth handler that re-checked (or skipped) the frame was SILENT:
|
|
180
|
+
// main.ts imports electron, so no node suite can import it and notice — the
|
|
181
|
+
// pin is a SOURCE check instead (test/ipc-door.test.mjs counts the call sites).
|
|
182
|
+
//
|
|
183
|
+
// Handlers get the invoke ARGUMENTS only: every one of them used the event for
|
|
184
|
+
// nothing but the frame check. The refusal is decided before the handler runs,
|
|
185
|
+
// so a forbidden frame never reaches the implementation (no schtasks spawn, no
|
|
186
|
+
// browser window, no loadURL).
|
|
187
|
+
function ipcHandle(channel, fn) {
|
|
188
|
+
electron_1.ipcMain.handle(channel, (e, ...args) => {
|
|
189
|
+
if (!frameOk(e))
|
|
190
|
+
return FORBIDDEN_FRAME;
|
|
191
|
+
return fn(...args);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
168
194
|
// P1: CDP port for AI (playwright) to drive Summrise's own pages — the SAME
|
|
169
195
|
// Electron window the user watches. Summrise's playwright-mcp connects via
|
|
170
196
|
// connectOverCDP("http://127.0.0.1:9333") and drives this window's pages.
|
|
@@ -365,11 +391,30 @@ function buildMenu() {
|
|
|
365
391
|
return electron_1.Menu.buildFromTemplate(template);
|
|
366
392
|
}
|
|
367
393
|
// --- Browser-session control: shared core (used by both IPC and HTTP) ---
|
|
368
|
-
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
394
|
+
/** THE ONE LOAD DOOR (stage-n hardening): every raw URL this shell is asked to
|
|
395
|
+
* load — a browser-session window, the embedded real-render view, a
|
|
396
|
+
* window.open() from a page inside it — is decided HERE and nowhere else.
|
|
397
|
+
*
|
|
398
|
+
* Only http/https/about:blank survive; file:// and every other scheme
|
|
399
|
+
* collapse to "about:blank", because a loadable file:// would hand the AI a
|
|
400
|
+
* local-file read primitive via the shared CDP endpoint (:9333).
|
|
401
|
+
*
|
|
402
|
+
* The DECISION itself is url-policy.sanitizeBrowserUrl() — pure, and
|
|
403
|
+
* unit-tested in test/url-policy.test.mjs, since main.ts imports electron and
|
|
404
|
+
* no node suite can import it. This function is that predicate's ONE call site
|
|
405
|
+
* in the shell (test/ipc-door.test.mjs pins the count), so a fourth load site
|
|
406
|
+
* cannot quietly reach for a weaker policy of its own.
|
|
407
|
+
*
|
|
408
|
+
* Callers keep their own about:blank semantics: browserOpen() opens a blank
|
|
409
|
+
* window, embeddedNavigate() loads it, and the embedded view's window-open
|
|
410
|
+
* handler REFUSES it (an external link must not blank the page being read). */
|
|
411
|
+
function loadTarget(raw) {
|
|
412
|
+
return (0, url_policy_1.sanitizeBrowserUrl)(raw);
|
|
413
|
+
}
|
|
414
|
+
/** Open a browser-session window on a decided load target (loadTarget() is the
|
|
415
|
+
* ONE load door — the scheme policy lives there). */
|
|
371
416
|
function browserOpen(url) {
|
|
372
|
-
const target = (
|
|
417
|
+
const target = loadTarget(url);
|
|
373
418
|
// stage-n: reuse an existing window on the same URL instead of stacking
|
|
374
419
|
// duplicates (AI-driven browsing opens/closes sessions repeatedly); focus
|
|
375
420
|
// the existing window. Match against the window's INITIAL target (stored
|
|
@@ -433,9 +478,9 @@ function browserList() {
|
|
|
433
478
|
const list = [...browserSessions.entries()].map(([id, bw]) => ({ id, url: bw.webContents.getURL() }));
|
|
434
479
|
return { ok: true, sessions: list, cdp: `http://127.0.0.1:${CDP_PORT}` };
|
|
435
480
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
481
|
+
ipcHandle("browser-session:open", (url) => browserOpen(url));
|
|
482
|
+
ipcHandle("browser-session:close", (id) => browserClose(id));
|
|
483
|
+
ipcHandle("browser-session:list", () => browserList());
|
|
439
484
|
// ── Embedded real-render browser view (round-246) ──────────────────────────
|
|
440
485
|
// The SPA's Browser page used to show the BRIDGE's headless-chromium as a
|
|
441
486
|
// JPEG screencast (lossy q60-92 frames over a websocket) — never as sharp as
|
|
@@ -473,12 +518,13 @@ function embeddedViewEnsure() {
|
|
|
473
518
|
// window) were DENIED, so clicking them did nothing. The embedded view is
|
|
474
519
|
// a single-tab browser: intercept window.open and navigate the SAME view
|
|
475
520
|
// to the requested URL instead (the address bar follows via the nav
|
|
476
|
-
// events).
|
|
477
|
-
// (javascript:, file:, chrome:)
|
|
521
|
+
// events). The scheme allow-list is loadTarget()'s (the ONE load door):
|
|
522
|
+
// anything it refuses (javascript:, file:, chrome:) must NOT blank the page
|
|
523
|
+
// the user is reading, so a refused target is dropped here instead.
|
|
478
524
|
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
479
|
-
const
|
|
480
|
-
if (
|
|
481
|
-
embeddedNavigate(
|
|
525
|
+
const target = loadTarget(url);
|
|
526
|
+
if (target !== "about:blank") {
|
|
527
|
+
embeddedNavigate(target);
|
|
482
528
|
}
|
|
483
529
|
return { action: "deny" };
|
|
484
530
|
});
|
|
@@ -519,7 +565,9 @@ function embeddedViewPlace(bounds) {
|
|
|
519
565
|
win.contentView.addChildView(view);
|
|
520
566
|
}
|
|
521
567
|
function embeddedNavigate(raw) {
|
|
522
|
-
|
|
568
|
+
// Every raw URL the SPA/AI hands the embedded view is decided by the ONE
|
|
569
|
+
// load door before it reaches loadURL.
|
|
570
|
+
const url = loadTarget(raw);
|
|
523
571
|
const view = embeddedViewEnsure();
|
|
524
572
|
embeddedUrl = url;
|
|
525
573
|
view.webContents.loadURL(url).catch(() => { });
|
|
@@ -621,6 +669,8 @@ function embeddedRecover() {
|
|
|
621
669
|
const view = embeddedViewEnsure();
|
|
622
670
|
if (!view || view.webContents.isDestroyed())
|
|
623
671
|
return { ok: false };
|
|
672
|
+
// Already decided by loadTarget() when embeddedUrl was set (the ONE load
|
|
673
|
+
// door) — recovery re-loads that decision, it does not re-open the policy.
|
|
624
674
|
const url = embeddedUrl && embeddedUrl !== "about:blank" ? embeddedUrl : "https://www.bing.com";
|
|
625
675
|
view.webContents.loadURL(url).catch(() => { });
|
|
626
676
|
// Re-show if the SPA slot is live (bounds were placed before the crash).
|
|
@@ -631,16 +681,14 @@ function embeddedRecover() {
|
|
|
631
681
|
}
|
|
632
682
|
return { ok: true };
|
|
633
683
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
684
|
+
ipcHandle("embedded-browser:recover", () => embeddedRecover());
|
|
685
|
+
ipcHandle("embedded-browser:navigate", (url) => embeddedNavigate(String(url || "")));
|
|
686
|
+
ipcHandle("embedded-browser:back", () => embeddedGo(-1));
|
|
687
|
+
ipcHandle("embedded-browser:fwd", () => embeddedGo(1));
|
|
688
|
+
ipcHandle("embedded-browser:reload", () => embeddedReload());
|
|
639
689
|
/** round-251: zoom the REAL embedded view (webContents.setZoomFactor — the
|
|
640
690
|
* native browser zooms, not a CSS scale). factor: 0.5-3.0. */
|
|
641
|
-
|
|
642
|
-
if (!frameOk(e))
|
|
643
|
-
return { ok: false };
|
|
691
|
+
ipcHandle("embedded-browser:zoom", (factor) => {
|
|
644
692
|
const view = embeddedView;
|
|
645
693
|
if (!view || view.webContents.isDestroyed())
|
|
646
694
|
return { ok: false };
|
|
@@ -653,13 +701,11 @@ electron_1.ipcMain.handle("embedded-browser:zoom", (e, factor) => {
|
|
|
653
701
|
return { ok: false };
|
|
654
702
|
}
|
|
655
703
|
});
|
|
656
|
-
|
|
657
|
-
if (!frameOk(e))
|
|
658
|
-
return { ok: false };
|
|
704
|
+
ipcHandle("embedded-browser:place", (bounds) => {
|
|
659
705
|
embeddedViewPlace(bounds);
|
|
660
706
|
return { ok: true };
|
|
661
707
|
});
|
|
662
|
-
|
|
708
|
+
ipcHandle("embedded-browser:state", () => embeddedState());
|
|
663
709
|
// Desktop-app settings (auto-launch) — the Settings page toggles this. We
|
|
664
710
|
// manage a per-user scheduled task ("SummriseDesktop", onlogon) instead of
|
|
665
711
|
// Electron's setLoginItemSettings: in dev mode (electron .) the login-item
|
|
@@ -752,10 +798,10 @@ async function autoLaunchTaskSet(enabled) {
|
|
|
752
798
|
return { ok: false, error: String(e) };
|
|
753
799
|
}
|
|
754
800
|
}
|
|
755
|
-
|
|
801
|
+
ipcHandle("desktop:get-auto-launch", async () => ({ ok: true, enabled: await autoLaunchTaskExists() }));
|
|
756
802
|
// The auto-launch pair is the PERSISTENCE primitive (onlogon schtasks in an
|
|
757
803
|
// admin context) — it absolutely cannot be reachable from a foreign frame.
|
|
758
|
-
|
|
804
|
+
ipcHandle("desktop:set-auto-launch", async (enabled) => autoLaunchTaskSet(!!enabled));
|
|
759
805
|
// --- Agent lifecycle (stage-m: Rust owns it; the shell only probes/triggers) ---
|
|
760
806
|
// review #2 (HIGH): a raw TCP connect only proves the PORT is held — an
|
|
761
807
|
// agent that is wedged (deadlock / OOM-parked) but still listening keeps the
|