mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
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:
1 parent
d87e2c2714
commit
f8e8dc3e7a
5 files changed
+35
-2
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user