feat(cutter): name the plugin re-copy recovery for sessions whose plugin source is unknown or modified (#10950)

* feat(cutter): name the plugin re-copy recovery for sessions whose plugin source is unknown or modified

* test(cutter): assert recovery names the owner's platform plugin path
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 17:22:30 +08:00
1 parent d87e2c2714
commit f8e8dc3e7a
5 files changed
+35 -2

No files matched your search

+3
View File
@@ -275,6 +275,9 @@ REA's packaged `bridge/cutter/rea_bridge.py`, `modified` when it differs, and
`unknown` when the plugin predates this field. `cutter_command` refuses a
`modified` session before sending anything and names the packaged plugin to
copy into Cutter's `plugins/python` directory. An `unknown` session still runs.
For `modified` and `unknown` sessions, discovery also returns
`bridge_source_recovery`, which names the packaged plugin to copy before
restarting Cutter.
The client and plugin must support the current v2 handshake; older peers are
rejected before dispatch because they cannot preserve cross-caller recovery proof.
+6 -2
View File
@@ -44,7 +44,7 @@ import { err, ok, type Result } from "../domain/result.js";
import { cutterBridgeMac } from "./cutterBridgeMac.js";
import {
cutterBridgeSource,
packagedCutterPluginPath,
cutterBridgeSourceRecovery,
} from "./CutterBridgeSource.js";
import { CutterBridgeResponseFrames } from "./cutterAuthenticatedResponse.js";
import { requireWindowsNativeAuthority } from "../windows/WindowsNativeLoader.js";
@@ -106,7 +106,7 @@ const responseSchema = z.object({
});
/** A session runs a plugin whose bytes differ from the packaged copy. */
const modifiedPluginRefusal = (): AnalysisCapabilityUnavailableError => {
const remediation = `Copy ${packagedCutterPluginPath()} into Cutter's plugins/python directory, restart Cutter, and list sessions again. No command was sent.`;
const remediation = `${cutterBridgeSourceRecovery("modified")} No command was sent.`;
return new AnalysisCapabilityUnavailableError(
"cutter",
"cutter_command",
@@ -411,6 +411,7 @@ export class CutterBridgeClient {
descriptor.bridge_source_sha256,
"list_cutter_sessions",
);
const bridgeSourceRecovery = cutterBridgeSourceRecovery(bridgeSource);
try {
const status = responseSchema.safeParse(
await this.#request(
@@ -443,6 +444,9 @@ export class CutterBridgeClient {
status.data.cutter_version ?? descriptor.cutter_version,
identity_status: "partial" as const,
bridge_source: bridgeSource,
...(bridgeSourceRecovery === undefined
? {}
: { bridge_source_recovery: bridgeSourceRecovery }),
};
} catch (cause) {
if (cause instanceof AnalysisResourceConstraintError) throw cause;
+15
View File
@@ -14,6 +14,21 @@ const PACKAGED_PLUGIN = new URL(
export const packagedCutterPluginPath = (plugin = PACKAGED_PLUGIN): string =>
fileURLToPath(plugin);
/**
* What to do when a session does not run the packaged plugin. A plugin with
* no published digest still runs, but REA cannot tell which source it is.
*/
export const cutterBridgeSourceRecovery = (
source: CutterBridgeSource,
plugin = PACKAGED_PLUGIN,
): string | undefined => {
if (source === "packaged") return undefined;
const recopy = `Copy ${packagedCutterPluginPath(plugin)} into Cutter's plugins/python directory, restart Cutter, and list sessions again.`;
return source === "unknown"
? `The running plugin publishes no SHA-256, so its source is unknown. ${recopy}`
: recopy;
};
const packagedDigests = new Map<string, string>();
/**
+5
View File
@@ -20,6 +20,11 @@ export const cutterSessionSchema = z.strictObject({
cutter_version: z.string().nullable(),
identity_status: z.literal("partial"),
bridge_source: cutterBridgeSourceSchema,
/**
* How to run the packaged plugin, present when `bridge_source` is not
* `packaged`: the session's plugin is unknown or differs from this release.
*/
bridge_source_recovery: z.string().exactOptional(),
});
/** Discovery facts retained independently of descriptor cleanup. */
@@ -23,6 +23,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { CutterBridgeClient } from "../../../src/cutter/CutterBridgeClient.js";
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { CutterBridgeService } from "../../../src/cutter/CutterBridgeService.js";
import { packagedCutterPluginPath } from "../../../src/cutter/CutterBridgeSource.js";
const listen = async (
server: ReturnType<typeof createServer>,
@@ -348,6 +349,11 @@ describe("CutterBridgeClient: plugin source identity", () => {
}).listSessions();
expect(result.sessions).toMatchObject([{ bridge_source: source }]);
const recovery = result.sessions[0]?.bridge_source_recovery;
if (source === "packaged") expect(recovery).toBeUndefined();
else
expect(recovery).toContain(`${packagedCutterPluginPath()} into Cutter`);
if (source === "unknown") expect(recovery).toContain("source is unknown");
},
);