mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix: keep session catalog mirroring within isolated profiles (#129638)
* fix: bind catalog HOME policy in active facade * fix: report isolated catalog mirroring
This commit is contained in:
@@ -55,6 +55,7 @@ function fakeCatalog(params: {
|
||||
hostKind?: string;
|
||||
onList?: () => unknown;
|
||||
onRead?: (threadId: string) => unknown;
|
||||
processHomeFallbackAllowed?: boolean;
|
||||
}): ActiveSessionCatalog {
|
||||
const host: SessionCatalogHost = {
|
||||
hostId: "gateway:local",
|
||||
@@ -77,6 +78,7 @@ function fakeCatalog(params: {
|
||||
pluginId: params.id,
|
||||
id: params.id,
|
||||
label: params.id,
|
||||
processHomeFallbackAllowed: params.processHomeFallbackAllowed ?? true,
|
||||
list: async () => {
|
||||
await params.onList?.();
|
||||
return [host];
|
||||
@@ -815,6 +817,28 @@ describe("createBeamMirrorRunner", () => {
|
||||
expect(sent).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("warns once when profile isolation disables process-HOME fallback", async () => {
|
||||
const warnings: string[] = [];
|
||||
const catalog = fakeCatalog({
|
||||
id: "claude",
|
||||
sessions: [],
|
||||
processHomeFallbackAllowed: false,
|
||||
});
|
||||
const runner = createBeamMirrorRunner({
|
||||
runtime: fakeRuntime(mirrorConfig({ catalogs: ["claude"] })),
|
||||
logger: { warn: (message) => warnings.push(message), info: () => {} },
|
||||
now: () => NOW,
|
||||
listCatalogs: () => [catalog],
|
||||
});
|
||||
|
||||
await runner.tick();
|
||||
await runner.tick();
|
||||
|
||||
expect(warnings).toEqual([
|
||||
"beam mirror process-HOME fallback disabled: isolated state; only explicit catalog roots can be mirrored",
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends one completed upload when a session leaves the active window", async () => {
|
||||
const sent: SentRequest[] = [];
|
||||
const recency = NOW - 60_000;
|
||||
|
||||
@@ -281,6 +281,7 @@ export function createBeamMirrorRunner(params: {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
let lastWarnAt = 0;
|
||||
let warnedProcessHomeIsolation = false;
|
||||
let redirectBlockedEndpoint: string | undefined;
|
||||
let activeTick: Promise<void> | undefined;
|
||||
let stopPromise: Promise<void> | undefined;
|
||||
@@ -458,6 +459,15 @@ export function createBeamMirrorRunner(params: {
|
||||
// would otherwise re-mirror each other's rows forever.
|
||||
catalog.id !== "beam" && mirror.catalogs.includes(catalog.id),
|
||||
);
|
||||
if (
|
||||
!warnedProcessHomeIsolation &&
|
||||
catalogs.some((catalog) => !catalog.processHomeFallbackAllowed)
|
||||
) {
|
||||
warnedProcessHomeIsolation = true;
|
||||
params.logger.warn(
|
||||
"beam mirror process-HOME fallback disabled: isolated state; only explicit catalog roots can be mirrored",
|
||||
);
|
||||
}
|
||||
const catalogById = new Map(catalogs.map((catalog) => [catalog.id, catalog]));
|
||||
const candidates: BeamMirrorCandidate[] = [];
|
||||
for (const catalog of catalogs) {
|
||||
|
||||
@@ -17,6 +17,7 @@ const hoisted = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: () => hoisted.activeRegistry,
|
||||
getActivePluginSessionExtensionRegistry: () => hoisted.activeRegistry,
|
||||
requireActivePluginRegistry: () => hoisted.activeRegistry,
|
||||
}));
|
||||
vi.mock("../../config/sessions/session-accessor.js", async (importOriginal) => ({
|
||||
@@ -25,6 +26,7 @@ vi.mock("../../config/sessions/session-accessor.js", async (importOriginal) => (
|
||||
}));
|
||||
|
||||
const { sessionCatalogHandlers } = await import("./session-catalog.js");
|
||||
const { listActiveSessionCatalogs } = await import("../../plugins/session-catalog-active.js");
|
||||
|
||||
function provider(
|
||||
id: string,
|
||||
@@ -121,6 +123,52 @@ describe("session catalog Gateway HOME isolation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("binds HOME isolation into the internal read-only catalog facade", async () => {
|
||||
const localHost = {
|
||||
hostId: "gateway:local",
|
||||
label: "Local",
|
||||
kind: "gateway" as const,
|
||||
connected: true,
|
||||
sessions: [],
|
||||
};
|
||||
const list = vi.fn(async (request: { allowProcessHomeFallback?: boolean }) =>
|
||||
request.allowProcessHomeFallback === false ? [] : [localHost],
|
||||
);
|
||||
const read = vi.fn(async (request: Parameters<SessionCatalogProvider["read"]>[0]) => {
|
||||
if (request.allowProcessHomeFallback === false) {
|
||||
throw new Error("local Test sessions are unavailable in isolated state");
|
||||
}
|
||||
return { hostId: request.hostId, threadId: request.threadId, items: [] };
|
||||
});
|
||||
hoisted.activeRegistry.sessionCatalogs = [{ provider: provider("test", { list, read }) }];
|
||||
|
||||
await withProfile(undefined, async () => {
|
||||
const [catalog] = listActiveSessionCatalogs();
|
||||
expect(catalog?.processHomeFallbackAllowed).toBe(true);
|
||||
await expect(catalog?.list({})).resolves.toEqual([localHost]);
|
||||
await expect(
|
||||
catalog?.read({ hostId: "gateway:local", threadId: "known-thread" }),
|
||||
).resolves.toMatchObject({ threadId: "known-thread" });
|
||||
});
|
||||
await withProfile("dev", async () => {
|
||||
const [catalog] = listActiveSessionCatalogs();
|
||||
expect(catalog?.processHomeFallbackAllowed).toBe(false);
|
||||
await expect(catalog?.list({})).resolves.toEqual([]);
|
||||
await expect(
|
||||
catalog?.read({ hostId: "gateway:local", threadId: "known-thread" }),
|
||||
).rejects.toThrow("local Test sessions are unavailable in isolated state");
|
||||
});
|
||||
|
||||
expect(list.mock.calls.map(([request]) => request.allowProcessHomeFallback)).toEqual([
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
expect(read.mock.calls.map(([request]) => request.allowProcessHomeFallback)).toEqual([
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["continue", "continueSession", {}],
|
||||
["archive", "archive", { confirmNoOtherRunner: true }],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { allowsProcessHomeSessionScan } from "../config/paths.js";
|
||||
import { getActivePluginSessionExtensionRegistry } from "./runtime.js";
|
||||
import type { SessionCatalogProvider } from "./session-catalog.js";
|
||||
|
||||
@@ -5,6 +6,7 @@ export type ActiveSessionCatalog = {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
processHomeFallbackAllowed: boolean;
|
||||
list: SessionCatalogProvider["list"];
|
||||
read: SessionCatalogProvider["read"];
|
||||
};
|
||||
@@ -16,13 +18,17 @@ export type ActiveSessionCatalog = {
|
||||
*/
|
||||
export function listActiveSessionCatalogs(): ActiveSessionCatalog[] {
|
||||
const registrations = getActivePluginSessionExtensionRegistry()?.sessionCatalogs ?? [];
|
||||
const allowProcessHomeFallback = allowsProcessHomeSessionScan();
|
||||
return registrations
|
||||
.map(({ pluginId, provider }) => ({
|
||||
pluginId,
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
list: provider.list.bind(provider),
|
||||
read: provider.read.bind(provider),
|
||||
processHomeFallbackAllowed: allowProcessHomeFallback,
|
||||
list: (params: Parameters<SessionCatalogProvider["list"]>[0]) =>
|
||||
provider.list({ ...params, allowProcessHomeFallback }),
|
||||
read: (params: Parameters<SessionCatalogProvider["read"]>[0]) =>
|
||||
provider.read({ ...params, allowProcessHomeFallback }),
|
||||
}))
|
||||
.toSorted((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user