mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(memory): keep migration host APIs private
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ export {
|
||||
isAcpSessionKey,
|
||||
isIncognitoSessionKey,
|
||||
isSubagentSessionKey,
|
||||
isValidAgentId,
|
||||
normalizeAccountId,
|
||||
normalizeAgentId,
|
||||
normalizeAgentIdStrict,
|
||||
|
||||
Reference in New Issue
Block a user