fix(memory): keep migration host APIs private

This commit is contained in:
Galin Iliev
2026-08-10 21:12:02 -07:00
parent 95ef18ce8c
commit 02a08c73f6
10 changed files with 119 additions and 47 deletions
+3 -1
View File
@@ -8,6 +8,7 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildMemoryFlushPlan } from "./src/flush-plan.js";
import type { MemoryCoreRuntimeHost } from "./src/memory/runtime-host.js";
import { builtinScopedMemoryConformanceAdapter } from "./src/memory/scoped-memory-policy.js";
import { buildPromptSection } from "./src/prompt-section.js";
const closeMemorySearchManagerMock = vi.hoisted(() => vi.fn(async () => {}));
@@ -236,10 +237,11 @@ describe("memory-core plugin runtime registration", () => {
expect(closeMemorySearchManagerMock).toHaveBeenCalledWith({ cfg, agentId: "main" });
});
it("declares the selected lazy capability as legacy-only without synthesizing runtime authority", () => {
it("registers the tested scoped-policy adapter while keeping legacy reads unenforced", () => {
const capability = registerMemoryCoreCapability();
expect(capability.authorization).toEqual(LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES);
expect(capability.authorizationConformance).toBe(builtinScopedMemoryConformanceAdapter);
expect("authorization" in (capability.runtime ?? {})).toBe(false);
});
+3
View File
@@ -21,6 +21,7 @@ import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
import { buildMemoryFlushPlan } from "./src/flush-plan.js";
import type { MemoryCoreAcquireLocalService } from "./src/memory/embedding-local-service.js";
import type { MemoryCoreRuntimeHost } from "./src/memory/runtime-host.js";
import { builtinScopedMemoryConformanceAdapter } from "./src/memory/scoped-memory-policy.js";
import { buildPromptSection } from "./src/prompt-section.js";
import { registerSessionBackfillGatewayMethods } from "./src/session-backfill-gateway.js";
@@ -290,6 +291,8 @@ export default definePluginEntry({
registerSessionBackfillGatewayMethods(api);
api.registerMemoryCapability({
authorization: MEMORY_CORE_AUTHORIZATION_CAPABILITIES,
// Phase 1B publishes the tested policy adapter; Phase 1C owns admitting it for reads.
authorizationConformance: builtinScopedMemoryConformanceAdapter,
promptBuilder: buildPromptSection,
flushPlanResolver: buildMemoryFlushPlan,
runtime: memoryRuntime,
@@ -3,7 +3,6 @@ import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { isValidAgentId } from "openclaw/plugin-sdk/routing";
import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
type PreviewKind = "curated" | "memory" | "transcript" | "quarantine";
@@ -96,33 +95,10 @@ async function buildPreview(params: {
config: OpenClawConfig;
stateDir: string;
}): Promise<string[] | null> {
const { listAgentIds, readAgentRosterProperty, resolveAgentWorkspaceDir } =
await import("openclaw/plugin-sdk/memory-host-core");
const roster = readAgentRosterProperty(params.config);
const rawAgentIds =
roster?.kind === "entries" && roster.value && typeof roster.value === "object"
? Object.keys(roster.value)
: roster?.kind === "list" && Array.isArray(roster.value)
? roster.value.flatMap((entry) => {
const id =
entry && typeof entry === "object" ? (entry as { id?: unknown }).id : undefined;
return typeof id === "string" ? [id] : [];
})
: [];
if (rawAgentIds.some((agentId) => !isValidAgentId(agentId))) {
return previewLines({
items: [],
dmScope: 0,
backend: 1,
filesystem: 0,
sandbox: 0,
invalidAgent: 1,
});
}
let agentIds: readonly string[];
try {
agentIds = listAgentIds(params.config);
} catch {
const { resolveMemoryMigrationAgentWorkspaces } =
await import("openclaw/plugin-sdk/memory-migration-runtime");
const agentWorkspaces = resolveMemoryMigrationAgentWorkspaces(params.config);
if (agentWorkspaces.kind === "invalid-agent") {
return previewLines({
items: [],
dmScope: 0,
@@ -135,33 +111,31 @@ async function buildPreview(params: {
const items: PreviewItem[] = [];
let filesystem = 0;
let sandbox = 0;
for (const agentId of agentIds.toSorted()) {
const workspace = resolveAgentWorkspaceDir(params.config, agentId);
for (const agent of agentWorkspaces.agents.toSorted((left, right) =>
left.agentId.localeCompare(right.agentId),
)) {
const curated = await scanRegularFiles({
directory: workspace,
agentId,
directory: agent.workspaceDir,
agentId: agent.agentId,
kind: "curated",
extension: ".md",
});
const memory = await scanRegularFiles({
directory: path.join(workspace, "memory"),
agentId,
directory: path.join(agent.workspaceDir, "memory"),
agentId: agent.agentId,
kind: "memory",
});
// Transcripts are direct JSONL files. sessions.json is metadata, never memory content.
const transcripts = await scanRegularFiles({
directory: path.join(params.stateDir, "agents", agentId, "sessions"),
agentId,
directory: path.join(params.stateDir, "agents", agent.agentId, "sessions"),
agentId: agent.agentId,
kind: "transcript",
extension: ".jsonl",
});
items.push(...curated.items, ...memory.items, ...transcripts.items);
filesystem +=
curated.filesystemBlockers + memory.filesystemBlockers + transcripts.filesystemBlockers;
const entry =
params.config.agents?.entries?.[agentId] ??
params.config.agents?.list?.find((candidate) => candidate?.id === agentId);
if (entry?.sandbox?.mode === "all") {
if (agent.sandboxed) {
sandbox += 1;
}
}
+4
View File
@@ -123,6 +123,7 @@
"!dist/plugin-sdk/memory-host-events.d.ts",
"!dist/plugin-sdk/memory-host-markdown.d.ts",
"!dist/plugin-sdk/memory-host-search.d.ts",
"!dist/plugin-sdk/memory-migration-runtime.d.ts",
"!dist/plugin-sdk/message-tool-delivery-hints.d.ts",
"!dist/plugin-sdk/migration.d.ts",
"!dist/plugin-sdk/migration-runtime.d.ts",
@@ -848,6 +849,9 @@
"./plugin-sdk/migration": {
"default": "./dist/plugin-sdk/migration.js"
},
"./plugin-sdk/memory-migration-runtime": {
"default": "./dist/plugin-sdk/memory-migration-runtime.js"
},
"./plugin-sdk/migration-runtime": {
"default": "./dist/plugin-sdk/migration-runtime.js"
},
+1
View File
@@ -147,6 +147,7 @@
"time-runtime",
"logging-core",
"migration",
"memory-migration-runtime",
"migration-runtime",
"plugin-state-runtime",
"plugin-state-test-runtime",
@@ -80,6 +80,7 @@
"memory-host-events",
"memory-host-markdown",
"memory-host-search",
"memory-migration-runtime",
"message-tool-delivery-hints",
"migration",
"migration-runtime",
-5
View File
@@ -221,11 +221,6 @@ export type {
MemoryPromptSectionBuilder,
} from "../plugins/memory-state.js";
export { resolveDefaultAgentId } from "../agents/agent-scope-config.js";
export {
listAgentIds,
readAgentRosterProperty,
resolveAgentWorkspaceDir,
} from "../agents/agent-scope-config.js";
export { resolveSessionAgentId } from "../agents/agent-scope.js";
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import { resolveMemoryMigrationAgentWorkspaces } from "./memory-migration-runtime.js";
describe("resolveMemoryMigrationAgentWorkspaces", () => {
it("uses canonical entries precedence and returns their resolved workspace policy", () => {
const result = resolveMemoryMigrationAgentWorkspaces({
agents: {
entries: {
main: { sandbox: { mode: "all" }, workspace: "/canonical-workspace" },
},
list: [{ id: "..", workspace: "/ignored-workspace" }],
},
} as OpenClawConfig);
expect(result).toEqual({
kind: "resolved",
agents: [
{
agentId: "main",
sandboxed: true,
workspaceDir: "/canonical-workspace",
},
],
});
});
it("rejects invalid roster identities before resolving agent-owned paths", () => {
const result = resolveMemoryMigrationAgentWorkspaces({
agents: { list: [{ id: "../outside", workspace: "/outside" }] },
} as OpenClawConfig);
expect(result).toEqual({ kind: "invalid-agent" });
});
});
@@ -0,0 +1,58 @@
import {
listAgentIds,
readAgentRosterProperty,
resolveAgentConfig,
resolveAgentWorkspaceDir,
} from "../agents/agent-scope-config.js";
// Private runtime facade for the Memory Core doctor migration preview.
import type { OpenClawConfig } from "../config/types.js";
import { isValidAgentId } from "../routing/session-key.js";
export type MemoryMigrationAgentWorkspace = Readonly<{
agentId: string;
sandboxed: boolean;
workspaceDir: string;
}>;
export type MemoryMigrationAgentWorkspaces =
| Readonly<{ kind: "invalid-agent" }>
| Readonly<{ kind: "resolved"; agents: readonly MemoryMigrationAgentWorkspace[] }>;
function rawRosterAgentIds(config: OpenClawConfig): string[] {
const roster = readAgentRosterProperty(config);
if (roster?.kind === "entries" && roster.value && typeof roster.value === "object") {
return Object.keys(roster.value);
}
if (roster?.kind !== "list" || !Array.isArray(roster.value)) {
return [];
}
return roster.value.flatMap((entry) => {
const agentId = entry && typeof entry === "object" ? (entry as { id?: unknown }).id : undefined;
return typeof agentId === "string" ? [agentId] : [];
});
}
/**
* Resolves the whole roster before a migration preview touches agent-owned paths.
* Keep this private because roster precedence and workspace layout are host policy,
* not a general plugin contract.
*/
export function resolveMemoryMigrationAgentWorkspaces(
config: OpenClawConfig,
): MemoryMigrationAgentWorkspaces {
if (rawRosterAgentIds(config).some((agentId) => !isValidAgentId(agentId))) {
return { kind: "invalid-agent" };
}
try {
return {
kind: "resolved",
agents: listAgentIds(config).map((agentId) => ({
agentId,
sandboxed: resolveAgentConfig(config, agentId)?.sandbox?.mode === "all",
workspaceDir: resolveAgentWorkspaceDir(config, agentId),
})),
};
} catch {
return { kind: "invalid-agent" };
}
}
-1
View File
@@ -19,7 +19,6 @@ export {
isAcpSessionKey,
isIncognitoSessionKey,
isSubagentSessionKey,
isValidAgentId,
normalizeAccountId,
normalizeAgentId,
normalizeAgentIdStrict,