mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
feat(memory): wire authorization shadow interfaces
This commit is contained in:
@@ -6,17 +6,21 @@ import {
|
||||
} from "./project-memory-bootstrap.js";
|
||||
|
||||
const runtimeMocks = vi.hoisted(() => ({
|
||||
getSelectedRuntime: vi.fn(),
|
||||
getManager: vi.fn(),
|
||||
listCurated: vi.fn(),
|
||||
search: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/memory-state.js", () => ({
|
||||
getMemoryRuntime: () => ({ getMemorySearchManager: runtimeMocks.getManager }),
|
||||
vi.mock("../plugins/memory-runtime.js", () => ({
|
||||
getSelectedMemoryRuntime: runtimeMocks.getSelectedRuntime,
|
||||
}));
|
||||
|
||||
describe("project memory bootstrap", () => {
|
||||
beforeEach(() => {
|
||||
runtimeMocks.getSelectedRuntime
|
||||
.mockReset()
|
||||
.mockReturnValue({ getMemorySearchManager: runtimeMocks.getManager });
|
||||
runtimeMocks.getManager.mockReset();
|
||||
runtimeMocks.listCurated.mockReset();
|
||||
runtimeMocks.search.mockReset();
|
||||
@@ -97,10 +101,18 @@ describe("project memory bootstrap", () => {
|
||||
|
||||
it("keeps sessions without an active repository unchanged", async () => {
|
||||
await expect(prepareEntries(entries, [])).resolves.toEqual([]);
|
||||
expect(runtimeMocks.getSelectedRuntime).not.toHaveBeenCalled();
|
||||
expect(runtimeMocks.getManager).not.toHaveBeenCalled();
|
||||
expect(buildProjectMemoryWriteInstruction(undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("acquires project bootstrap memory through the selected-runtime seam", async () => {
|
||||
await prepareEntries(entries);
|
||||
|
||||
expect(runtimeMocks.getSelectedRuntime).toHaveBeenCalledOnce();
|
||||
expect(runtimeMocks.getManager).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("filters tagged raw entries fail-closed with the all-keys rule", () => {
|
||||
const contextFiles = [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../../packages/memory-host-sdk/src/engine-storage.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { MemorySearchResult } from "../memory-host-sdk/host/types.js";
|
||||
import { getMemoryRuntime } from "../plugins/memory-state.js";
|
||||
import { getSelectedMemoryRuntime } from "../plugins/memory-runtime.js";
|
||||
import type { EmbeddedContextFile } from "./embedded-agent-helpers.js";
|
||||
|
||||
const PROJECT_MEMORY_BOOTSTRAP_MAX_CHARS = 2_000;
|
||||
@@ -123,7 +123,7 @@ export async function prepareProjectMemoryBootstrap(params: {
|
||||
if (params.activeProjectKeys.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const runtime = getMemoryRuntime();
|
||||
const runtime = getSelectedMemoryRuntime();
|
||||
if (!runtime) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listGitTrackedFiles } from "../test-utils/repo-files.js";
|
||||
import {
|
||||
MEMORY_AUTHORIZATION_PATH_DISPOSITIONS,
|
||||
MEMORY_AUTHORIZATION_PATH_INVENTORY,
|
||||
type MemoryAuthorizationPathInventoryEntry,
|
||||
} from "./memory-authorization-path-inventory.js";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
const REQUIRED_PHASE_0_PATH_IDS = [
|
||||
"selected-runtime-manager-acquisition",
|
||||
"selected-runtime-backend-resolution",
|
||||
"selected-runtime-startup-warmup-and-sync",
|
||||
"bootstrap-memory-and-user-files",
|
||||
"startup-recent-memory-context",
|
||||
"memory-search-tool",
|
||||
"memory-get-tool",
|
||||
"session-transcript-search",
|
||||
"active-memory-trigger-recall",
|
||||
"memory-wiki-prompt-supplement-registration",
|
||||
"memory-wiki-prompt-preparation-registration",
|
||||
"memory-wiki-corpus-supplement-registration",
|
||||
"memory-wiki-agent-query-read",
|
||||
"memory-wiki-agent-mutation",
|
||||
"memory-wiki-operator-query-read",
|
||||
"memory-wiki-operator-mutation",
|
||||
"memory-wiki-bridge-artifact-read",
|
||||
"memory-wiki-bridge-artifact-import",
|
||||
"memory-wiki-status",
|
||||
"lancedb-tool-recall",
|
||||
"lancedb-tool-store",
|
||||
"lancedb-tool-forget",
|
||||
"lancedb-auto-recall",
|
||||
"lancedb-auto-capture",
|
||||
"lancedb-cli-read",
|
||||
"memory-prompt-supplements",
|
||||
"memory-prompt-preparations",
|
||||
"memory-corpus-supplements",
|
||||
"talk-fast-context",
|
||||
"project-memory-bootstrap",
|
||||
"memory-status-command",
|
||||
"memory-cli-search-and-get",
|
||||
"memory-cli-sync-and-reindex",
|
||||
"memory-doctor-inspection-and-repair",
|
||||
"gateway-memory-search",
|
||||
"memory-import",
|
||||
"memory-migration-import",
|
||||
"memory-export",
|
||||
"memory-public-artifact-provider-list",
|
||||
"generic-file-read",
|
||||
"generic-file-write",
|
||||
"generic-file-edit",
|
||||
"generic-file-apply-patch",
|
||||
"sandbox-workspace-mounts",
|
||||
"unsandboxed-exec",
|
||||
"post-compaction-session-memory-sync",
|
||||
"memory-flush",
|
||||
"transcript-event-write",
|
||||
"transcript-history-and-replay",
|
||||
"compaction-summary",
|
||||
"compaction-checkpoint",
|
||||
"compaction-checkpoint-operator-branch-and-restore",
|
||||
"dreaming-source-recall",
|
||||
"dreaming-derived-artifacts",
|
||||
"profile-and-short-term-promotion",
|
||||
"child-agent-delegation",
|
||||
"child-agent-completion-handoff",
|
||||
"cron-triggered-run",
|
||||
"heartbeat-triggered-run",
|
||||
"webhook-triggered-run",
|
||||
"system-triggered-run",
|
||||
"final-reply-delivery",
|
||||
"message-tool-delivery",
|
||||
"session-send-delivery",
|
||||
"plugin-and-mcp-outbound-actions",
|
||||
] as const;
|
||||
|
||||
const MEMORY_MANAGER_CALL_NAMES = new Set([
|
||||
"getActiveMemorySearchManager",
|
||||
"getMemorySearchManager",
|
||||
]);
|
||||
|
||||
const SUPPLEMENTAL_PATH_DIRECTIONS = {
|
||||
"memory-wiki-prompt-supplement-registration": "egress",
|
||||
"memory-wiki-prompt-preparation-registration": "egress",
|
||||
"memory-wiki-corpus-supplement-registration": "egress",
|
||||
"memory-wiki-agent-query-read": "egress",
|
||||
"memory-wiki-agent-mutation": "ingress",
|
||||
"memory-wiki-operator-query-read": "egress",
|
||||
"memory-wiki-operator-mutation": "ingress",
|
||||
"memory-wiki-bridge-artifact-read": "egress",
|
||||
"memory-wiki-bridge-artifact-import": "ingress",
|
||||
"lancedb-tool-recall": "egress",
|
||||
"lancedb-tool-store": "ingress",
|
||||
"lancedb-tool-forget": "ingress",
|
||||
"lancedb-auto-recall": "egress",
|
||||
"lancedb-auto-capture": "ingress",
|
||||
"lancedb-cli-read": "egress",
|
||||
} as const;
|
||||
|
||||
const MEMORY_MIGRATION_IMPORT_ROUTE_SURFACES = [
|
||||
"src/cli/program/register.migrate.ts",
|
||||
"src/commands/migrate.ts",
|
||||
"src/commands/migrate/memory-import.ts",
|
||||
"src/commands/migrate/apply.ts",
|
||||
"extensions/migrate-claude/provider.ts",
|
||||
"extensions/migrate-claude/plan.ts",
|
||||
"extensions/migrate-claude/memory.ts",
|
||||
"extensions/migrate-claude/apply.ts",
|
||||
"extensions/migrate-hermes/provider.ts",
|
||||
"extensions/migrate-hermes/plan.ts",
|
||||
"extensions/migrate-hermes/memory.ts",
|
||||
"extensions/migrate-hermes/apply.ts",
|
||||
"extensions/codex/src/migration/provider.ts",
|
||||
"extensions/codex/src/migration/plan.ts",
|
||||
"extensions/codex/src/migration/apply.ts",
|
||||
] as const;
|
||||
|
||||
const MEMORY_MIGRATION_IMPORT_ROOTS = [
|
||||
"src/cli/program/register.migrate.ts",
|
||||
"src/commands/migrate.ts",
|
||||
"src/commands/migrate",
|
||||
"extensions/migrate-claude",
|
||||
"extensions/migrate-hermes",
|
||||
"extensions/codex/src/migration",
|
||||
] as const;
|
||||
|
||||
function isProductionTypeScript(file: string): boolean {
|
||||
return (
|
||||
file.endsWith(".ts") &&
|
||||
!file.endsWith(".d.ts") &&
|
||||
!file.includes(".test.") &&
|
||||
!file.includes(".spec.") &&
|
||||
!file.includes(".test-") &&
|
||||
!/(^|\/)test[-.]/u.test(file) &&
|
||||
!/(^|\/)tests?\//u.test(file)
|
||||
);
|
||||
}
|
||||
|
||||
function listContextFreeMemoryManagerCalls(file: string, sourceText: string): string[] {
|
||||
const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true);
|
||||
const callableBindings = new Set(MEMORY_MANAGER_CALL_NAMES);
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isImportDeclaration(statement)) {
|
||||
continue;
|
||||
}
|
||||
const bindings = statement.importClause?.namedBindings;
|
||||
if (!bindings || statement.importClause?.isTypeOnly || ts.isNamespaceImport(bindings)) {
|
||||
continue;
|
||||
}
|
||||
for (const element of bindings.elements) {
|
||||
const imported = element.propertyName?.text ?? element.name.text;
|
||||
if (!element.isTypeOnly && MEMORY_MANAGER_CALL_NAMES.has(imported)) {
|
||||
callableBindings.add(element.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
const calls: string[] = [];
|
||||
const visit = (node: ts.Node) => {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const expression = node.expression;
|
||||
if (
|
||||
(ts.isIdentifier(expression) && callableBindings.has(expression.text)) ||
|
||||
(ts.isPropertyAccessExpression(expression) &&
|
||||
MEMORY_MANAGER_CALL_NAMES.has(expression.name.text)) ||
|
||||
(ts.isElementAccessExpression(expression) &&
|
||||
expression.argumentExpression !== undefined &&
|
||||
ts.isStringLiteral(expression.argumentExpression) &&
|
||||
MEMORY_MANAGER_CALL_NAMES.has(expression.argumentExpression.text))
|
||||
) {
|
||||
calls.push(expression.getText(source));
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
ts.forEachChild(source, visit);
|
||||
return calls;
|
||||
}
|
||||
|
||||
function listImportedCallBindings(source: ts.SourceFile, importedName: string): Set<string> {
|
||||
const bindings = new Set([importedName]);
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isImportDeclaration(statement)) {
|
||||
continue;
|
||||
}
|
||||
const namedBindings = statement.importClause?.namedBindings;
|
||||
if (
|
||||
!namedBindings ||
|
||||
statement.importClause?.isTypeOnly ||
|
||||
ts.isNamespaceImport(namedBindings)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const element of namedBindings.elements) {
|
||||
const imported = element.propertyName?.text ?? element.name.text;
|
||||
if (!element.isTypeOnly && imported === importedName) {
|
||||
bindings.add(element.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
function isNamedCall(
|
||||
expression: ts.LeftHandSideExpression,
|
||||
bindings: ReadonlySet<string>,
|
||||
): boolean {
|
||||
return (
|
||||
(ts.isIdentifier(expression) && bindings.has(expression.text)) ||
|
||||
(ts.isPropertyAccessExpression(expression) && bindings.has(expression.name.text))
|
||||
);
|
||||
}
|
||||
|
||||
function isMigrationProviderApplyCall(expression: ts.LeftHandSideExpression): boolean {
|
||||
if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== "apply") {
|
||||
return false;
|
||||
}
|
||||
const receiver = expression.expression;
|
||||
return (
|
||||
(ts.isIdentifier(receiver) && receiver.text === "provider") ||
|
||||
(ts.isPropertyAccessExpression(receiver) && receiver.name.text === "provider")
|
||||
);
|
||||
}
|
||||
|
||||
function containsMemoryStringLiteral(node: ts.Node): boolean {
|
||||
let found = false;
|
||||
const visit = (child: ts.Node) => {
|
||||
if (ts.isStringLiteral(child) && child.text === "memory") {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(child, visit);
|
||||
};
|
||||
visit(node);
|
||||
return found;
|
||||
}
|
||||
|
||||
function isMemoryKindPropertyOfMigrationItem(
|
||||
node: ts.PropertyAssignment,
|
||||
createItemBindings: ReadonlySet<string>,
|
||||
): boolean {
|
||||
if (
|
||||
!ts.isIdentifier(node.name) ||
|
||||
node.name.text !== "kind" ||
|
||||
!containsMemoryStringLiteral(node.initializer)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const object = node.parent;
|
||||
const call = object.parent;
|
||||
return ts.isObjectLiteralExpression(object) && ts.isCallExpression(call)
|
||||
? isNamedCall(call.expression, createItemBindings)
|
||||
: false;
|
||||
}
|
||||
|
||||
function listMemoryMigrationIngressMarkers(file: string, sourceText: string): string[] {
|
||||
const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true);
|
||||
const createItemBindings = listImportedCallBindings(source, "createMigrationItem");
|
||||
const copyMemoryBindings = listImportedCallBindings(source, "copyMemoryMigrationFileItem");
|
||||
const defaultCommandBindings = listImportedCallBindings(source, "migrateDefaultCommand");
|
||||
const applyCommandBindings = listImportedCallBindings(source, "migrateApplyCommand");
|
||||
const applyBindings = listImportedCallBindings(source, "runMigrationApply");
|
||||
const markers: string[] = [];
|
||||
const visit = (node: ts.Node) => {
|
||||
if (
|
||||
ts.isPropertyAssignment(node) &&
|
||||
isMemoryKindPropertyOfMigrationItem(node, createItemBindings)
|
||||
) {
|
||||
markers.push(node.getText(source));
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
if (isNamedCall(node.expression, copyMemoryBindings)) {
|
||||
markers.push(node.getText(source));
|
||||
}
|
||||
if (isNamedCall(node.expression, defaultCommandBindings)) {
|
||||
markers.push("migration-default-command-dispatch");
|
||||
}
|
||||
if (isNamedCall(node.expression, applyCommandBindings)) {
|
||||
markers.push("migration-apply-command-dispatch");
|
||||
}
|
||||
if (isNamedCall(node.expression, applyBindings)) {
|
||||
// Generic migration application admits provider plans containing memory items, so every
|
||||
// caller is an ingress route until an owning Phase records its authorization boundary.
|
||||
markers.push("migration-plan-apply");
|
||||
}
|
||||
if (isMigrationProviderApplyCall(node.expression)) {
|
||||
markers.push("migration-provider-apply");
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
return markers;
|
||||
}
|
||||
|
||||
describe("memory authorization path inventory", () => {
|
||||
const inventory: readonly MemoryAuthorizationPathInventoryEntry[] =
|
||||
MEMORY_AUTHORIZATION_PATH_INVENTORY;
|
||||
|
||||
it("records every required ingress and egress path with one owner and disposition", () => {
|
||||
const ids = inventory.map((entry) => entry.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(ids).toEqual(expect.arrayContaining([...REQUIRED_PHASE_0_PATH_IDS]));
|
||||
expect(inventory.length).toBeGreaterThanOrEqual(55);
|
||||
for (const item of inventory) {
|
||||
expect(item.owner.length).toBeGreaterThan(0);
|
||||
expect(MEMORY_AUTHORIZATION_PATH_DISPOSITIONS).toContain(item.disposition);
|
||||
expect(item.surfaces.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Phase 0 shadow-only and explicitly fails enforced bypasses closed", () => {
|
||||
expect(inventory.filter((item) => item.disposition === "authorized")).toEqual([]);
|
||||
expect(inventory.filter((item) => item.disposition === "operator-only-authenticated")).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(inventory.some((item) => item.disposition === "legacy-only")).toBe(true);
|
||||
expect(inventory.some((item) => item.disposition === "blocked-in-enforced-mode")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps supplemental reads and mutations as distinct enforced-mode paths", () => {
|
||||
const entriesById = new Map(inventory.map((item) => [item.id, item]));
|
||||
for (const [id, direction] of Object.entries(SUPPLEMENTAL_PATH_DIRECTIONS)) {
|
||||
expect(entriesById.get(id)).toMatchObject({
|
||||
direction,
|
||||
disposition: "blocked-in-enforced-mode",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps post-compaction session sync separate from memory-flush derivation", () => {
|
||||
const entriesById = new Map(inventory.map((item) => [item.id, item]));
|
||||
|
||||
expect(entriesById.get("post-compaction-session-memory-sync")).toMatchObject({
|
||||
direction: "ingress",
|
||||
owner: "core-agent-runtime",
|
||||
disposition: "blocked-in-enforced-mode",
|
||||
surfaces: expect.arrayContaining([
|
||||
"src/agents/embedded-agent-runner/compaction-hooks.ts",
|
||||
"src/agents/embedded-agent-runner/compaction-session-execution.ts",
|
||||
"src/agents/embedded-agent-runner/compact.queued.ts",
|
||||
"src/agents/embedded-agent-runner/run/timeout-context-recovery.ts",
|
||||
]),
|
||||
});
|
||||
expect(entriesById.get("memory-flush")).toMatchObject({
|
||||
direction: "derive",
|
||||
});
|
||||
});
|
||||
|
||||
it("records the complete operator migration memory-import route", () => {
|
||||
const migrationImport = inventory.find((item) => item.id === "memory-migration-import");
|
||||
|
||||
expect(migrationImport).toMatchObject({
|
||||
direction: "ingress",
|
||||
owner: "operator-memory-host",
|
||||
disposition: "blocked-in-enforced-mode",
|
||||
surfaces: expect.arrayContaining([...MEMORY_MIGRATION_IMPORT_ROUTE_SURFACES]),
|
||||
});
|
||||
});
|
||||
|
||||
it("names only existing production surfaces", () => {
|
||||
const missing = inventory.flatMap((item) =>
|
||||
item.surfaces
|
||||
.filter((surface) => !fs.existsSync(path.join(REPO_ROOT, surface)))
|
||||
.map((surface) => `${item.id}:${surface}`),
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("finds direct and aliased context-free manager acquisition calls", () => {
|
||||
expect(
|
||||
listContextFreeMemoryManagerCalls(
|
||||
"fixture.ts",
|
||||
`
|
||||
import { getActiveMemorySearchManager as active } from "./memory-runtime.js";
|
||||
import { getMemorySearchManager as manager } from "./manager.js";
|
||||
import * as runtime from "./memory-runtime.js";
|
||||
import type { getMemorySearchManager as TypeOnly } from "./manager.js";
|
||||
interface Runtime { getMemorySearchManager(params: unknown): Promise<unknown>; }
|
||||
async function acquire(value: Runtime) {
|
||||
await active({}); await manager({}); await runtime.getActiveMemorySearchManager({});
|
||||
await value.getMemorySearchManager({}); await value["getMemorySearchManager"]({});
|
||||
}
|
||||
`,
|
||||
),
|
||||
).toEqual([
|
||||
"active",
|
||||
"manager",
|
||||
"runtime.getActiveMemorySearchManager",
|
||||
"value.getMemorySearchManager",
|
||||
'value["getMemorySearchManager"]',
|
||||
]);
|
||||
});
|
||||
|
||||
it("finds the generic migration provider apply consumer without matching unrelated apply calls", () => {
|
||||
expect(
|
||||
listMemoryMigrationIngressMarkers(
|
||||
"fixture.ts",
|
||||
`
|
||||
async function apply(params: { provider: { apply: Function } }, ctx: unknown) {
|
||||
await params.provider.apply(ctx, {});
|
||||
await other.apply(ctx, {});
|
||||
}
|
||||
`,
|
||||
),
|
||||
).toEqual(["migration-provider-apply"]);
|
||||
});
|
||||
|
||||
it("finds generic migration plan application consumers that can apply memory items", () => {
|
||||
expect(
|
||||
listMemoryMigrationIngressMarkers(
|
||||
"fixture.ts",
|
||||
`
|
||||
import { runMigrationApply as apply } from "./apply.js";
|
||||
async function migrate() {
|
||||
await apply({});
|
||||
await unrelated({});
|
||||
}
|
||||
`,
|
||||
),
|
||||
).toEqual(["migration-plan-apply"]);
|
||||
});
|
||||
|
||||
it("recognizes the generic CLI migration apply ingress", () => {
|
||||
const command = "src/cli/program/register.migrate.ts";
|
||||
const source = fs.readFileSync(path.join(REPO_ROOT, command), "utf8");
|
||||
|
||||
expect(listMemoryMigrationIngressMarkers(command, source)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"migration-default-command-dispatch",
|
||||
"migration-apply-command-dispatch",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat test-only source helpers as production manager paths", () => {
|
||||
expect(
|
||||
isProductionTypeScript("extensions/memory-core/src/memory/test-manager-helpers.ts"),
|
||||
).toBe(false);
|
||||
expect(isProductionTypeScript("extensions/memory-core/src/memory/search-manager.ts")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not allow an unrecorded production context-free manager acquisition", () => {
|
||||
const tracked = listGitTrackedFiles({
|
||||
repoRoot: REPO_ROOT,
|
||||
pathspecs: ["src", "extensions", "packages"],
|
||||
});
|
||||
if (!tracked) {
|
||||
throw new Error("could not list tracked files for the authorization-path inventory");
|
||||
}
|
||||
const inventoried = new Set(inventory.flatMap((item) => item.surfaces));
|
||||
const missing = tracked
|
||||
.filter(isProductionTypeScript)
|
||||
.filter(
|
||||
(file) =>
|
||||
listContextFreeMemoryManagerCalls(
|
||||
file,
|
||||
fs.readFileSync(path.join(REPO_ROOT, file), "utf8"),
|
||||
).length > 0,
|
||||
)
|
||||
.filter((file) => !inventoried.has(file));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not allow an unrecorded production migration memory-import producer or consumer", () => {
|
||||
const tracked = listGitTrackedFiles({
|
||||
repoRoot: REPO_ROOT,
|
||||
pathspecs: [...MEMORY_MIGRATION_IMPORT_ROOTS],
|
||||
});
|
||||
if (!tracked) {
|
||||
throw new Error(
|
||||
"could not list tracked files for the migration authorization-path inventory",
|
||||
);
|
||||
}
|
||||
const inventoried = new Set(inventory.flatMap((item) => item.surfaces));
|
||||
const missing = tracked
|
||||
.filter(isProductionTypeScript)
|
||||
.filter(
|
||||
(file) =>
|
||||
listMemoryMigrationIngressMarkers(
|
||||
file,
|
||||
fs.readFileSync(path.join(REPO_ROOT, file), "utf8"),
|
||||
).length > 0,
|
||||
)
|
||||
.filter((file) => !inventoried.has(file));
|
||||
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,575 @@
|
||||
/** Phase-0 inventory of every known path that can ingest, expose, or derive memory. */
|
||||
export const MEMORY_AUTHORIZATION_PATH_DISPOSITIONS = [
|
||||
"authorized",
|
||||
"blocked-in-enforced-mode",
|
||||
"legacy-only",
|
||||
"operator-only-authenticated",
|
||||
] as const;
|
||||
|
||||
export type MemoryAuthorizationPathDisposition =
|
||||
(typeof MEMORY_AUTHORIZATION_PATH_DISPOSITIONS)[number];
|
||||
export type MemoryAuthorizationPathDirection = "control" | "ingress" | "egress" | "derive";
|
||||
export type MemoryAuthorizationPathOwner =
|
||||
| "core-access-host"
|
||||
| "core-agent-runtime"
|
||||
| "core-session-runtime"
|
||||
| "core-tool-runtime"
|
||||
| "operator-memory-host"
|
||||
| "selected-memory-plugin"
|
||||
| "supplemental-memory-plugin"
|
||||
| "transport-egress-host"
|
||||
| "autonomous-run-host";
|
||||
|
||||
export type MemoryAuthorizationPathInventoryEntry = Readonly<{
|
||||
id: string;
|
||||
direction: MemoryAuthorizationPathDirection;
|
||||
owner: MemoryAuthorizationPathOwner;
|
||||
disposition: MemoryAuthorizationPathDisposition;
|
||||
surfaces: readonly [string, ...string[]];
|
||||
}>;
|
||||
|
||||
function entry(
|
||||
id: string,
|
||||
direction: MemoryAuthorizationPathDirection,
|
||||
owner: MemoryAuthorizationPathOwner,
|
||||
disposition: MemoryAuthorizationPathDisposition,
|
||||
...surfaces: [string, ...string[]]
|
||||
): MemoryAuthorizationPathInventoryEntry {
|
||||
return Object.freeze({ id, direction, owner, disposition, surfaces: Object.freeze(surfaces) });
|
||||
}
|
||||
|
||||
/**
|
||||
* `authorized` is intentionally absent in Phase 0: the rollout is shadow-only. Every path keeps
|
||||
* its current legacy behavior until its owning phase converts it, and enforced mode then blocks
|
||||
* the explicitly listed bypasses rather than silently falling through to context-free access.
|
||||
*/
|
||||
export const MEMORY_AUTHORIZATION_PATH_INVENTORY = Object.freeze([
|
||||
entry(
|
||||
"selected-runtime-manager-acquisition",
|
||||
"control",
|
||||
"core-access-host",
|
||||
"legacy-only",
|
||||
"src/plugins/memory-runtime.ts",
|
||||
"src/plugins/memory-state.ts",
|
||||
"src/plugin-sdk/memory-host-search.ts",
|
||||
"extensions/memory-core/index.ts",
|
||||
"extensions/memory-core/src/runtime-provider.ts",
|
||||
"extensions/memory-core/src/memory/search-manager.ts",
|
||||
),
|
||||
entry(
|
||||
"selected-runtime-backend-resolution",
|
||||
"control",
|
||||
"core-access-host",
|
||||
"legacy-only",
|
||||
"src/plugins/memory-runtime.ts",
|
||||
),
|
||||
entry(
|
||||
"selected-runtime-startup-warmup-and-sync",
|
||||
"control",
|
||||
"selected-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/index.ts",
|
||||
"src/gateway/server-startup-memory.ts",
|
||||
),
|
||||
entry(
|
||||
"bootstrap-memory-and-user-files",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"legacy-only",
|
||||
"src/agents/bootstrap-files.ts",
|
||||
"src/agents/workspace-bootstrap-read.ts",
|
||||
),
|
||||
entry(
|
||||
"startup-recent-memory-context",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"legacy-only",
|
||||
"src/auto-reply/reply/startup-context.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-search-tool",
|
||||
"egress",
|
||||
"selected-memory-plugin",
|
||||
"legacy-only",
|
||||
"extensions/memory-core/src/tools.ts",
|
||||
"extensions/memory-core/src/tools.shared.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-get-tool",
|
||||
"egress",
|
||||
"selected-memory-plugin",
|
||||
"legacy-only",
|
||||
"extensions/memory-core/src/tools.ts",
|
||||
),
|
||||
entry(
|
||||
"session-transcript-search",
|
||||
"egress",
|
||||
"selected-memory-plugin",
|
||||
"legacy-only",
|
||||
"extensions/memory-core/src/session-search-visibility.ts",
|
||||
"extensions/memory-core/src/tools.ts",
|
||||
),
|
||||
entry(
|
||||
"active-memory-trigger-recall",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/active-memory/trigger-recall.ts",
|
||||
),
|
||||
entry(
|
||||
"active-memory-session-recall",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/active-memory/recall.ts",
|
||||
"extensions/active-memory/recall-run.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-prompt-supplement-registration",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/index.ts",
|
||||
"extensions/memory-wiki/src/prompt-section.ts",
|
||||
"src/plugins/memory-state.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-prompt-preparation-registration",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/index.ts",
|
||||
"extensions/memory-wiki/src/prompt-section.ts",
|
||||
"src/plugins/memory-state.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-corpus-supplement-registration",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/index.ts",
|
||||
"extensions/memory-wiki/src/corpus-supplement.ts",
|
||||
"extensions/memory-core/src/tools.shared.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-agent-query-read",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/index.ts",
|
||||
"extensions/memory-wiki/src/tool.ts",
|
||||
"extensions/memory-wiki/src/query.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-agent-mutation",
|
||||
"ingress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/index.ts",
|
||||
"extensions/memory-wiki/src/tool.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-operator-query-read",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/src/gateway.ts",
|
||||
"extensions/memory-wiki/src/cli.ts",
|
||||
"extensions/memory-wiki/src/query.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-operator-mutation",
|
||||
"ingress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/src/gateway.ts",
|
||||
"extensions/memory-wiki/src/cli.ts",
|
||||
"extensions/memory-wiki/src/ingest.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-bridge-artifact-read",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/src/bridge.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-bridge-artifact-import",
|
||||
"ingress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/src/bridge.ts",
|
||||
"extensions/memory-wiki/src/source-page-shared.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-wiki-status",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-wiki/src/status.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-tool-recall",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-tool-store",
|
||||
"ingress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-tool-forget",
|
||||
"ingress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-auto-recall",
|
||||
"egress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-auto-capture",
|
||||
"ingress",
|
||||
"supplemental-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"lancedb-cli-read",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
"extensions/memory-lancedb/memory-cli.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-prompt-supplements",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/plugins/memory-state.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-prompt-preparations",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/plugins/memory-state.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-corpus-supplements",
|
||||
"egress",
|
||||
"core-access-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/plugins/memory-state.ts",
|
||||
"extensions/memory-core/src/tools.shared.ts",
|
||||
),
|
||||
entry(
|
||||
"talk-fast-context",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/talk/fast-context-runtime.ts",
|
||||
),
|
||||
entry(
|
||||
"project-memory-bootstrap",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/project-memory-bootstrap.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-status-command",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/cli-status.runtime.ts",
|
||||
"src/commands/status.scan-memory.ts",
|
||||
"src/commands/status.scan.deps.runtime.ts",
|
||||
"src/commands/status.scan.shared.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-cli-search-and-get",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/cli-index-search.runtime.ts",
|
||||
"extensions/memory-core/src/cli-runtime-common.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-cli-sync-and-reindex",
|
||||
"control",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/cli-index-search.runtime.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-doctor-inspection-and-repair",
|
||||
"control",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/commands/doctor-memory-search.ts",
|
||||
"src/gateway/server-methods/doctor.ts",
|
||||
),
|
||||
entry(
|
||||
"gateway-memory-search",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/gateway/server-methods/memory-search.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-import",
|
||||
"ingress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/cli-rem.runtime.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-migration-import",
|
||||
"ingress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/cli/program/register.migrate.ts",
|
||||
"src/commands/migrate.ts",
|
||||
"src/commands/migrate/memory-import.ts",
|
||||
"src/commands/migrate/apply.ts",
|
||||
"extensions/migrate-claude/provider.ts",
|
||||
"extensions/migrate-claude/plan.ts",
|
||||
"extensions/migrate-claude/memory.ts",
|
||||
"extensions/migrate-claude/apply.ts",
|
||||
"extensions/migrate-hermes/provider.ts",
|
||||
"extensions/migrate-hermes/plan.ts",
|
||||
"extensions/migrate-hermes/memory.ts",
|
||||
"extensions/migrate-hermes/apply.ts",
|
||||
"extensions/codex/src/migration/provider.ts",
|
||||
"extensions/codex/src/migration/plan.ts",
|
||||
"extensions/codex/src/migration/apply.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-export",
|
||||
"egress",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/cli-index-search.runtime.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-public-artifact-provider-list",
|
||||
"egress",
|
||||
"selected-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/plugins/memory-state.ts",
|
||||
"extensions/memory-core/index.ts",
|
||||
"extensions/memory-lancedb/index.ts",
|
||||
),
|
||||
entry(
|
||||
"generic-file-read",
|
||||
"egress",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/agent-tools.read.ts",
|
||||
"src/agents/tool-fs-policy.ts",
|
||||
),
|
||||
entry(
|
||||
"generic-file-write",
|
||||
"ingress",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/agent-tools.read.ts",
|
||||
"src/agents/tool-fs-policy.ts",
|
||||
),
|
||||
entry(
|
||||
"generic-file-edit",
|
||||
"ingress",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/agent-tools.read.ts",
|
||||
"src/agents/tool-fs-policy.ts",
|
||||
),
|
||||
entry(
|
||||
"generic-file-apply-patch",
|
||||
"ingress",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/apply-patch.ts",
|
||||
"src/agents/tool-fs-policy.ts",
|
||||
),
|
||||
entry(
|
||||
"sandbox-workspace-mounts",
|
||||
"control",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/sandbox/workspace-mounts.ts",
|
||||
),
|
||||
entry(
|
||||
"unsandboxed-exec",
|
||||
"egress",
|
||||
"core-tool-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/bash-tools.ts",
|
||||
"src/agents/tool-fs-policy.ts",
|
||||
),
|
||||
entry(
|
||||
"post-compaction-session-memory-sync",
|
||||
"ingress",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/embedded-agent-runner/compaction-hooks.ts",
|
||||
"src/agents/embedded-agent-runner/compaction-session-execution.ts",
|
||||
"src/agents/embedded-agent-runner/compact.queued.ts",
|
||||
"src/agents/embedded-agent-runner/run/timeout-context-recovery.ts",
|
||||
),
|
||||
entry(
|
||||
"memory-flush",
|
||||
"derive",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/auto-reply/reply/agent-runner-memory.ts",
|
||||
"src/agents/embedded-agent-runner/compaction-hooks.ts",
|
||||
"extensions/memory-core/src/flush-plan.ts",
|
||||
),
|
||||
entry(
|
||||
"transcript-event-write",
|
||||
"ingress",
|
||||
"core-session-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/sessions/session-manager-persistence.ts",
|
||||
"src/config/sessions/session-accessor.sqlite-transcript-write.ts",
|
||||
"src/config/sessions/transcript-write-context.ts",
|
||||
),
|
||||
entry(
|
||||
"transcript-history-and-replay",
|
||||
"egress",
|
||||
"core-session-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/gateway/session-transcript-readers.ts",
|
||||
"src/agents/embedded-agent-runner/transcript-rewrite.ts",
|
||||
),
|
||||
entry(
|
||||
"compaction-summary",
|
||||
"derive",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/compaction.ts",
|
||||
"src/agents/embedded-agent-runner/compaction-session-execution.ts",
|
||||
),
|
||||
entry(
|
||||
"compaction-checkpoint",
|
||||
"derive",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/embedded-agent-runner/compaction-checkpoint.ts",
|
||||
"src/agents/main-session-restart-recovery-checkpoint.ts",
|
||||
"src/gateway/session-compaction-checkpoints.ts",
|
||||
),
|
||||
entry(
|
||||
"compaction-checkpoint-operator-branch-and-restore",
|
||||
"control",
|
||||
"operator-memory-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/gateway/server-methods/sessions-compaction-checkpoints.ts",
|
||||
),
|
||||
entry(
|
||||
"dreaming-source-recall",
|
||||
"egress",
|
||||
"selected-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/dreaming-phases.ts",
|
||||
),
|
||||
entry(
|
||||
"dreaming-derived-artifacts",
|
||||
"derive",
|
||||
"selected-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/dreaming-narrative.ts",
|
||||
"extensions/memory-core/src/dreaming-markdown.ts",
|
||||
),
|
||||
entry(
|
||||
"profile-and-short-term-promotion",
|
||||
"derive",
|
||||
"selected-memory-plugin",
|
||||
"blocked-in-enforced-mode",
|
||||
"extensions/memory-core/src/short-term-promotion-apply.ts",
|
||||
),
|
||||
entry(
|
||||
"child-agent-delegation",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/subagent-registry.ts",
|
||||
"src/agents/openclaw-tools.ts",
|
||||
),
|
||||
entry(
|
||||
"child-agent-completion-handoff",
|
||||
"egress",
|
||||
"core-agent-runtime",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/subagent-announce-delivery.ts",
|
||||
),
|
||||
entry(
|
||||
"cron-triggered-run",
|
||||
"control",
|
||||
"autonomous-run-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/cron/service/timer-execution.ts",
|
||||
),
|
||||
entry(
|
||||
"heartbeat-triggered-run",
|
||||
"control",
|
||||
"autonomous-run-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/infra/heartbeat-runner.ts",
|
||||
),
|
||||
entry(
|
||||
"webhook-triggered-run",
|
||||
"control",
|
||||
"autonomous-run-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/gateway/server/hooks-request-handler.ts",
|
||||
),
|
||||
entry(
|
||||
"system-triggered-run",
|
||||
"control",
|
||||
"autonomous-run-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/gateway/server-methods/system-agent.ts",
|
||||
),
|
||||
entry(
|
||||
"final-reply-delivery",
|
||||
"egress",
|
||||
"transport-egress-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/auto-reply/reply/reply-delivery.ts",
|
||||
),
|
||||
entry(
|
||||
"message-tool-delivery",
|
||||
"egress",
|
||||
"transport-egress-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/tools/message-tool.ts",
|
||||
),
|
||||
entry(
|
||||
"session-send-delivery",
|
||||
"egress",
|
||||
"transport-egress-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/agents/tools/sessions-send-tool.ts",
|
||||
),
|
||||
entry(
|
||||
"plugin-and-mcp-outbound-actions",
|
||||
"egress",
|
||||
"transport-egress-host",
|
||||
"blocked-in-enforced-mode",
|
||||
"src/plugins/tools.ts",
|
||||
"src/agents/mcp-transport.ts",
|
||||
),
|
||||
] satisfies readonly MemoryAuthorizationPathInventoryEntry[]);
|
||||
@@ -0,0 +1,258 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES,
|
||||
LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES,
|
||||
MEMORY_AUTHORIZATION_CAPABILITY_NAMES,
|
||||
} from "../memory-host-sdk/host/authorization.js";
|
||||
import { inspectMemoryAuthorizationRuntime } from "./memory-authorization-runtime.js";
|
||||
import { observeMemoryAuthorizationShadowSurface } from "./memory-authorization-shadow.js";
|
||||
|
||||
const AUTHORIZED_METHOD_NAMES = [
|
||||
"authorize",
|
||||
"searchAuthorized",
|
||||
"readAuthorized",
|
||||
"writeAuthorized",
|
||||
"importAuthorized",
|
||||
"syncAuthorized",
|
||||
"exportAuthorized",
|
||||
"statusAuthorized",
|
||||
] as const;
|
||||
|
||||
function createRuntime(capabilities = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES) {
|
||||
const notCalled = vi.fn(() => {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
});
|
||||
return {
|
||||
authorization: capabilities,
|
||||
authorize: notCalled,
|
||||
searchAuthorized: notCalled,
|
||||
readAuthorized: notCalled,
|
||||
writeAuthorized: notCalled,
|
||||
importAuthorized: notCalled,
|
||||
syncAuthorized: notCalled,
|
||||
exportAuthorized: notCalled,
|
||||
statusAuthorized: notCalled,
|
||||
legacyManager: { search: notCalled },
|
||||
};
|
||||
}
|
||||
|
||||
class PrototypeRuntime {
|
||||
readonly authorization = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES;
|
||||
|
||||
authorize() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
searchAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
readAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
writeAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
importAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
syncAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
exportAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
|
||||
statusAuthorized() {
|
||||
throw new Error("runtime methods must not execute in shadow mode");
|
||||
}
|
||||
}
|
||||
|
||||
describe("memory authorization runtime inspection", () => {
|
||||
it("reports a complete declared surface without calling an authorized or legacy method", () => {
|
||||
const runtime = createRuntime();
|
||||
const inspection = inspectMemoryAuthorizationRuntime(runtime);
|
||||
|
||||
expect(inspection).toMatchObject({
|
||||
version: 1,
|
||||
capabilityDeclaration: "complete",
|
||||
declaredCapabilityCount: MEMORY_AUTHORIZATION_CAPABILITY_NAMES.length,
|
||||
implementedMethodCount: AUTHORIZED_METHOD_NAMES.length,
|
||||
surfaceComplete: true,
|
||||
reasonCode: "surface-complete",
|
||||
});
|
||||
expect(inspection.missingCapabilities).toEqual([]);
|
||||
expect(inspection.missingMethods).toEqual([]);
|
||||
expect(Object.isFrozen(inspection)).toBe(true);
|
||||
expect(Object.isFrozen(inspection.missingMethods)).toBe(true);
|
||||
expect(runtime.authorize).not.toHaveBeenCalled();
|
||||
expect(runtime.legacyManager.search).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports all-false and incomplete declarations as nonconforming", () => {
|
||||
const legacy = inspectMemoryAuthorizationRuntime(
|
||||
createRuntime(LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES),
|
||||
);
|
||||
const incomplete = inspectMemoryAuthorizationRuntime({
|
||||
authorization: { ...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, scopedSync: false },
|
||||
...Object.fromEntries(AUTHORIZED_METHOD_NAMES.map((name) => [name, () => undefined])),
|
||||
});
|
||||
|
||||
expect(legacy).toMatchObject({
|
||||
capabilityDeclaration: "partial",
|
||||
declaredCapabilityCount: 0,
|
||||
surfaceComplete: false,
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(legacy.missingCapabilities).toEqual(MEMORY_AUTHORIZATION_CAPABILITY_NAMES);
|
||||
expect(incomplete).toMatchObject({
|
||||
capabilityDeclaration: "partial",
|
||||
declaredCapabilityCount: MEMORY_AUTHORIZATION_CAPABILITY_NAMES.length - 1,
|
||||
surfaceComplete: false,
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(incomplete.missingCapabilities).toEqual(["scopedSync"]);
|
||||
});
|
||||
|
||||
it("uses the SDK's exact capability-declaration rules for shadow reporting", () => {
|
||||
const unexpectedCapability = inspectMemoryAuthorizationRuntime({
|
||||
...createRuntime(),
|
||||
authorization: { ...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, unexpected: true },
|
||||
});
|
||||
const symbolicCapability = inspectMemoryAuthorizationRuntime({
|
||||
...createRuntime(),
|
||||
authorization: {
|
||||
...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES,
|
||||
[Symbol("plugin-framework-metadata")]: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(unexpectedCapability).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
declaredCapabilityCount: 0,
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(symbolicCapability).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
declaredCapabilityCount: 0,
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts data descriptors from class prototypes and ignores unrelated symbols", () => {
|
||||
const runtime = new PrototypeRuntime();
|
||||
Object.defineProperty(runtime, Symbol("plugin-framework-metadata"), {
|
||||
value: "not an authorization capability",
|
||||
});
|
||||
|
||||
expect(inspectMemoryAuthorizationRuntime(runtime)).toMatchObject({
|
||||
capabilityDeclaration: "complete",
|
||||
implementedMethodCount: AUTHORIZED_METHOD_NAMES.length,
|
||||
surfaceComplete: true,
|
||||
reasonCode: "surface-complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed on accessor and proxy surfaces without evaluating their getters or methods", () => {
|
||||
let getterCalls = 0;
|
||||
const accessorRuntime = Object.create(null) as Record<string, unknown>;
|
||||
Object.defineProperty(accessorRuntime, "authorization", {
|
||||
enumerable: true,
|
||||
get() {
|
||||
getterCalls += 1;
|
||||
throw new Error("must not read authorization getter");
|
||||
},
|
||||
});
|
||||
for (const name of AUTHORIZED_METHOD_NAMES) {
|
||||
Object.defineProperty(accessorRuntime, name, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
getterCalls += 1;
|
||||
throw new Error("must not read method getter");
|
||||
},
|
||||
});
|
||||
}
|
||||
const proxyRuntime = new Proxy(
|
||||
{},
|
||||
{
|
||||
getOwnPropertyDescriptor() {
|
||||
throw new Error("hostile proxy");
|
||||
},
|
||||
},
|
||||
);
|
||||
const declarationProxyRuntime = {
|
||||
authorization: new Proxy(
|
||||
{},
|
||||
{
|
||||
getPrototypeOf() {
|
||||
throw new Error("hostile capability declaration");
|
||||
},
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const accessor = inspectMemoryAuthorizationRuntime(accessorRuntime);
|
||||
const undefinedDeclaration = inspectMemoryAuthorizationRuntime({ authorization: undefined });
|
||||
const proxy = inspectMemoryAuthorizationRuntime(proxyRuntime);
|
||||
const declarationProxy = inspectMemoryAuthorizationRuntime(declarationProxyRuntime);
|
||||
|
||||
expect(getterCalls).toBe(0);
|
||||
expect(accessor).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(undefinedDeclaration).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(proxy).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(declarationProxy).toMatchObject({
|
||||
capabilityDeclaration: "malformed",
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory authorization shadow inspection", () => {
|
||||
it("returns one bounded, content-free observation per selected runtime", () => {
|
||||
const runtime = Object.assign(createRuntime(LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES), {
|
||||
content: "private content sentinel",
|
||||
prompt: "private prompt sentinel",
|
||||
query: "private query sentinel",
|
||||
principalId: "private principal sentinel",
|
||||
});
|
||||
const first = observeMemoryAuthorizationShadowSurface(runtime);
|
||||
const second = observeMemoryAuthorizationShadowSurface(runtime);
|
||||
|
||||
expect(first).toMatchObject({
|
||||
mode: "shadow",
|
||||
capabilityDeclaration: "partial",
|
||||
reasonCode: "backend-nonconforming",
|
||||
});
|
||||
expect(second).toBeUndefined();
|
||||
expect(JSON.stringify(first)).not.toMatch(/private|content|prompt|query|principal/u);
|
||||
});
|
||||
|
||||
it("does not let a hostile proxy change a selected runtime path", () => {
|
||||
const runtime = new Proxy(
|
||||
{},
|
||||
{
|
||||
getOwnPropertyDescriptor() {
|
||||
throw new Error("hostile proxy");
|
||||
},
|
||||
},
|
||||
);
|
||||
const observation = observeMemoryAuthorizationShadowSurface(runtime);
|
||||
|
||||
expect(observation).toEqual(
|
||||
expect.objectContaining({ reasonCode: "backend-nonconforming", surfaceComplete: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
MEMORY_AUTHORIZATION_CAPABILITY_NAMES,
|
||||
isMemoryAuthorizationCapabilities,
|
||||
listMissingMemoryAuthorizationCapabilities,
|
||||
type AuthorizedMemoryRuntime,
|
||||
type MemoryAuthorizationCapabilityName,
|
||||
} from "../memory-host-sdk/host/authorization.js";
|
||||
|
||||
const AUTHORIZED_MEMORY_RUNTIME_METHODS = [
|
||||
"authorize",
|
||||
"searchAuthorized",
|
||||
"readAuthorized",
|
||||
"writeAuthorized",
|
||||
"importAuthorized",
|
||||
"syncAuthorized",
|
||||
"exportAuthorized",
|
||||
"statusAuthorized",
|
||||
] as const satisfies readonly (keyof AuthorizedMemoryRuntime)[];
|
||||
|
||||
type AuthorizedMemoryRuntimeMethodName = (typeof AUTHORIZED_MEMORY_RUNTIME_METHODS)[number];
|
||||
|
||||
export type MemoryAuthorizationRuntimeInspection = Readonly<{
|
||||
version: 1;
|
||||
capabilityDeclaration: "missing" | "malformed" | "partial" | "complete";
|
||||
declaredCapabilityCount: number;
|
||||
requiredCapabilityCount: number;
|
||||
implementedMethodCount: number;
|
||||
requiredMethodCount: number;
|
||||
missingCapabilities: readonly MemoryAuthorizationCapabilityName[];
|
||||
missingMethods: readonly AuthorizedMemoryRuntimeMethodName[];
|
||||
surfaceComplete: boolean;
|
||||
reasonCode: "surface-complete" | "backend-nonconforming";
|
||||
}>;
|
||||
|
||||
// Runtime interfaces are shallow; the bound prevents hostile prototype chains from extending a
|
||||
// shadow-only inspection beyond its fixed metadata budget.
|
||||
const MAXIMUM_RUNTIME_PROTOTYPE_DEPTH = 8;
|
||||
|
||||
function isObjectReference(value: unknown): value is object {
|
||||
return (typeof value === "object" && value !== null) || typeof value === "function";
|
||||
}
|
||||
|
||||
type DataPropertyLookup =
|
||||
| { kind: "data"; value: unknown }
|
||||
| { kind: "missing" | "accessor" | "unavailable" };
|
||||
|
||||
/**
|
||||
* Reads a data descriptor without evaluating the corresponding property. Runtime interfaces may
|
||||
* use class methods, so the bounded prototype walk accepts data descriptors there too. A getter
|
||||
* or hostile reflection failure remains nonconforming without touching an authorized or legacy
|
||||
* runtime method.
|
||||
*/
|
||||
function readDataProperty(value: unknown, key: string): DataPropertyLookup {
|
||||
if (!isObjectReference(value)) {
|
||||
return { kind: "missing" };
|
||||
}
|
||||
try {
|
||||
let current: object | null = value;
|
||||
for (let depth = 0; current && depth < MAXIMUM_RUNTIME_PROTOTYPE_DEPTH; depth += 1) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
||||
if (descriptor) {
|
||||
return "value" in descriptor
|
||||
? { kind: "data", value: descriptor.value }
|
||||
: { kind: "accessor" };
|
||||
}
|
||||
current = Object.getPrototypeOf(current);
|
||||
}
|
||||
} catch {
|
||||
// A Proxy can reject reflection. Treat it as a nonconforming declaration.
|
||||
return { kind: "unavailable" };
|
||||
}
|
||||
return { kind: "missing" };
|
||||
}
|
||||
|
||||
/**
|
||||
* The SDK validator deliberately enforces exact descriptor shape. A plugin runtime can still be
|
||||
* a hostile Proxy, so shadow inspection turns any reflection failure into a nonconforming result.
|
||||
*/
|
||||
function inspectCapabilityDeclaration(value: unknown): {
|
||||
hasWellFormedDeclaration: boolean;
|
||||
missingCapabilities: readonly MemoryAuthorizationCapabilityName[];
|
||||
} {
|
||||
try {
|
||||
if (!isMemoryAuthorizationCapabilities(value)) {
|
||||
return {
|
||||
hasWellFormedDeclaration: false,
|
||||
missingCapabilities: MEMORY_AUTHORIZATION_CAPABILITY_NAMES,
|
||||
};
|
||||
}
|
||||
return {
|
||||
hasWellFormedDeclaration: true,
|
||||
missingCapabilities: listMissingMemoryAuthorizationCapabilities(value),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
hasWellFormedDeclaration: false,
|
||||
missingCapabilities: MEMORY_AUTHORIZATION_CAPABILITY_NAMES,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function freezeInspection(params: Omit<MemoryAuthorizationRuntimeInspection, "version">) {
|
||||
return Object.freeze({
|
||||
version: 1 as const,
|
||||
...params,
|
||||
missingCapabilities: Object.freeze([...params.missingCapabilities]),
|
||||
missingMethods: Object.freeze([...params.missingMethods]),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces content-free shape metadata only. It is intentionally not an admission decision and
|
||||
* does not retain, invoke, or wrap the inspected runtime.
|
||||
*/
|
||||
export function inspectMemoryAuthorizationRuntime(
|
||||
runtime: unknown,
|
||||
): MemoryAuthorizationRuntimeInspection {
|
||||
const authorization = readDataProperty(runtime, "authorization");
|
||||
const hasDeclaration = authorization.kind === "data";
|
||||
// Use the shared contract validator so shadow reporting and enforced-mode admission agree on
|
||||
// the exact declaration shape. This boundary catches reflection traps as malformed.
|
||||
const { hasWellFormedDeclaration, missingCapabilities } = inspectCapabilityDeclaration(
|
||||
hasDeclaration ? authorization.value : undefined,
|
||||
);
|
||||
const declaredCapabilityCount =
|
||||
MEMORY_AUTHORIZATION_CAPABILITY_NAMES.length - missingCapabilities.length;
|
||||
const methods = AUTHORIZED_MEMORY_RUNTIME_METHODS.map((name) => ({
|
||||
name,
|
||||
property: readDataProperty(runtime, name),
|
||||
}));
|
||||
const implementedMethodCount = methods.filter(
|
||||
({ property }) => property.kind === "data" && typeof property.value === "function",
|
||||
).length;
|
||||
const missingMethods = methods
|
||||
.filter(({ property }) => property.kind !== "data" || typeof property.value !== "function")
|
||||
.map(({ name }) => name);
|
||||
const capabilityDeclaration = !hasDeclaration
|
||||
? authorization.kind === "missing"
|
||||
? "missing"
|
||||
: "malformed"
|
||||
: !hasWellFormedDeclaration
|
||||
? "malformed"
|
||||
: missingCapabilities.length === 0
|
||||
? "complete"
|
||||
: "partial";
|
||||
const surfaceComplete = capabilityDeclaration === "complete" && missingMethods.length === 0;
|
||||
|
||||
return freezeInspection({
|
||||
capabilityDeclaration,
|
||||
declaredCapabilityCount,
|
||||
requiredCapabilityCount: MEMORY_AUTHORIZATION_CAPABILITY_NAMES.length,
|
||||
implementedMethodCount,
|
||||
requiredMethodCount: AUTHORIZED_MEMORY_RUNTIME_METHODS.length,
|
||||
missingCapabilities,
|
||||
missingMethods,
|
||||
surfaceComplete,
|
||||
reasonCode: surfaceComplete ? "surface-complete" : "backend-nonconforming",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { MEMORY_AUTHORIZATION_CONTRACT_VERSION } from "../memory-host-sdk/host/authorization.js";
|
||||
import { inspectMemoryAuthorizationRuntime } from "./memory-authorization-runtime.js";
|
||||
|
||||
const inspectedRuntimes = new WeakSet<object>();
|
||||
|
||||
export type MemoryAuthorizationShadowMetadata = Readonly<{
|
||||
event: "memory-authorization-backend-surface";
|
||||
mode: "shadow";
|
||||
contractVersion: 1;
|
||||
capabilityDeclaration: "missing" | "malformed" | "partial" | "complete";
|
||||
declaredCapabilityCount: number;
|
||||
requiredCapabilityCount: number;
|
||||
implementedMethodCount: number;
|
||||
requiredMethodCount: number;
|
||||
surfaceComplete: boolean;
|
||||
reasonCode: "surface-complete" | "backend-nonconforming";
|
||||
}>;
|
||||
|
||||
function isObjectReference(value: unknown): value is object {
|
||||
return (typeof value === "object" && value !== null) || typeof value === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shadow mode returns bounded surface counts once per selected runtime. Reflection failures are
|
||||
* nonconforming observations; logging remains with the runtime owner and cannot change selection.
|
||||
*/
|
||||
export function observeMemoryAuthorizationShadowSurface(
|
||||
runtime: unknown,
|
||||
): MemoryAuthorizationShadowMetadata | undefined {
|
||||
if (!isObjectReference(runtime)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
if (inspectedRuntimes.has(runtime)) {
|
||||
return undefined;
|
||||
}
|
||||
inspectedRuntimes.add(runtime);
|
||||
const inspection = inspectMemoryAuthorizationRuntime(runtime);
|
||||
const metadata = Object.freeze({
|
||||
event: "memory-authorization-backend-surface" as const,
|
||||
mode: "shadow" as const,
|
||||
contractVersion: MEMORY_AUTHORIZATION_CONTRACT_VERSION,
|
||||
capabilityDeclaration: inspection.capabilityDeclaration,
|
||||
declaredCapabilityCount: inspection.declaredCapabilityCount,
|
||||
requiredCapabilityCount: inspection.requiredCapabilityCount,
|
||||
implementedMethodCount: inspection.implementedMethodCount,
|
||||
requiredMethodCount: inspection.requiredMethodCount,
|
||||
surfaceComplete: inspection.surfaceComplete,
|
||||
reasonCode: inspection.reasonCode,
|
||||
});
|
||||
return metadata;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ type AuthorizeSearchHits = NonNullable<MemoryPluginRuntime["authorizeSearchHits"
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getMemoryRuntime: vi.fn(),
|
||||
loadPluginRegistryHandle: vi.fn(),
|
||||
logDebug: vi.fn(),
|
||||
observeMemoryAuthorizationShadowSurface: vi.fn(),
|
||||
resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)),
|
||||
resolveAgentWorkspaceDir: vi.fn(),
|
||||
}));
|
||||
@@ -23,6 +25,14 @@ vi.mock("./loader.js", () => ({
|
||||
resolvePluginRegistryLoadCacheKey: mocks.resolvePluginRegistryLoadCacheKey,
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
createSubsystemLogger: vi.fn(() => ({ debug: mocks.logDebug })),
|
||||
}));
|
||||
|
||||
vi.mock("./memory-authorization-shadow.js", () => ({
|
||||
observeMemoryAuthorizationShadowSurface: mocks.observeMemoryAuthorizationShadowSurface,
|
||||
}));
|
||||
|
||||
vi.mock("./memory-state.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./memory-state.js")>();
|
||||
return { ...actual, getMemoryRuntime: mocks.getMemoryRuntime };
|
||||
@@ -33,6 +43,7 @@ import {
|
||||
closeActiveMemorySearchManagerCore,
|
||||
closeActiveMemorySearchManagersCore,
|
||||
getActiveMemorySearchManagerCore,
|
||||
getSelectedMemoryRuntime,
|
||||
resolveActiveMemoryBackendConfig,
|
||||
} from "./memory-runtime.js";
|
||||
import { resetStandaloneMemoryRegistrySlot } from "./memory-runtime.test-support.js";
|
||||
@@ -72,6 +83,8 @@ describe("memory runtime handles", () => {
|
||||
resetStandaloneMemoryRegistrySlot();
|
||||
mocks.getMemoryRuntime.mockReset().mockReturnValue(undefined);
|
||||
mocks.loadPluginRegistryHandle.mockReset();
|
||||
mocks.logDebug.mockReset();
|
||||
mocks.observeMemoryAuthorizationShadowSurface.mockReset();
|
||||
mocks.resolvePluginRegistryLoadCacheKey.mockClear();
|
||||
mocks.resolveAgentWorkspaceDir
|
||||
.mockReset()
|
||||
@@ -227,6 +240,45 @@ describe("memory runtime handles", () => {
|
||||
expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inspects direct selected-runtime acquisition through the canonical seam", () => {
|
||||
const runtime = createRuntime();
|
||||
mocks.getMemoryRuntime.mockReturnValue(runtime);
|
||||
|
||||
expect(getSelectedMemoryRuntime()).toBe(runtime);
|
||||
expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledOnce();
|
||||
expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith(runtime);
|
||||
});
|
||||
|
||||
it("wires each legacy resolution through shadow inspection without changing legacy resolution", () => {
|
||||
const { registry, runtime } = createRegistry();
|
||||
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
||||
|
||||
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
||||
backend: "builtin",
|
||||
});
|
||||
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
||||
backend: "builtin",
|
||||
});
|
||||
|
||||
expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith(runtime);
|
||||
expect(runtime.resolveMemoryBackendConfig).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps legacy resolution when shadow logging fails", () => {
|
||||
const { registry, runtime } = createRegistry();
|
||||
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
||||
mocks.observeMemoryAuthorizationShadowSurface.mockReturnValue({ mode: "shadow" });
|
||||
mocks.logDebug.mockImplementation(() => {
|
||||
throw new Error("logger unavailable");
|
||||
});
|
||||
|
||||
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
||||
backend: "builtin",
|
||||
});
|
||||
expect(runtime.resolveMemoryBackendConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("authorizes raw hits inside the selected plugin runtime scope", async () => {
|
||||
const { registry, runtime } = createRegistry();
|
||||
runtime.authorizeSearchHits.mockImplementationOnce(async ({ hits }) => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Runtime bridge for plugin-owned memory hooks and state.
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { loadPluginRegistryHandle, resolvePluginRegistryLoadCacheKey } from "./loader.js";
|
||||
import { observeMemoryAuthorizationShadowSurface } from "./memory-authorization-shadow.js";
|
||||
import {
|
||||
getMemoryRuntime,
|
||||
resolveSelectedMemoryCapabilityRegistration,
|
||||
@@ -20,6 +22,7 @@ type MemorySearchAuthorization = Parameters<
|
||||
NonNullable<MemoryPluginRuntime["authorizeSearchHits"]>
|
||||
>[0];
|
||||
type MemoryRuntimeOwner = { runtime: MemoryRuntime; registry?: PluginRegistry };
|
||||
const log = createSubsystemLogger("plugins/memory-authorization");
|
||||
let standaloneMemoryRegistrySlot:
|
||||
| { key: string; registry: PluginRegistry; retiredRuntimes: Map<MemoryRuntime, PluginRegistry> }
|
||||
| undefined;
|
||||
@@ -54,7 +57,7 @@ function resolveMemoryRuntimeFromRegistry(registry: PluginRegistry) {
|
||||
}
|
||||
|
||||
function listCurrentMemoryRuntimeOwners(): MemoryRuntimeOwner[] {
|
||||
const current = getMemoryRuntime();
|
||||
const current = getSelectedMemoryRuntime();
|
||||
const owners = new Map<MemoryRuntime, MemoryRuntimeOwner>();
|
||||
for (const [runtime, registry] of standaloneMemoryRegistrySlot?.retiredRuntimes ?? []) {
|
||||
owners.set(runtime, { runtime, registry });
|
||||
@@ -78,11 +81,39 @@ function withMemoryRuntimeOwner<T>(
|
||||
return withPluginRuntimeRegistryScope(owner.registry, () => run(owner.runtime));
|
||||
}
|
||||
|
||||
function inspectSelectedMemoryRuntime(runtime: MemoryRuntime): MemoryRuntime {
|
||||
// The inspection has no result-path effect: it only emits bounded shadow metadata once per
|
||||
// selected runtime object and deliberately tolerates malformed/plugin-hostile surfaces.
|
||||
const metadata = observeMemoryAuthorizationShadowSurface(runtime);
|
||||
if (metadata) {
|
||||
try {
|
||||
log.debug("memory authorization backend surface evaluated", metadata);
|
||||
} catch {
|
||||
// Shadow logging must not change selected runtime resolution or a legacy result path.
|
||||
}
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/** Reads the selected registered runtime through the canonical shadow-inspected seam. */
|
||||
export function getSelectedMemoryRuntime(): MemoryRuntime | undefined {
|
||||
const runtime = getMemoryRuntime();
|
||||
return runtime ? inspectSelectedMemoryRuntime(runtime) : undefined;
|
||||
}
|
||||
|
||||
function toMemoryRuntimeOwner(
|
||||
runtime: MemoryRuntime,
|
||||
registry?: PluginRegistry,
|
||||
): MemoryRuntimeOwner {
|
||||
inspectSelectedMemoryRuntime(runtime);
|
||||
return registry ? { runtime, registry } : { runtime };
|
||||
}
|
||||
|
||||
function ensureMemoryRuntime(params?: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
}): MemoryRuntimeOwner | undefined {
|
||||
const current = getMemoryRuntime();
|
||||
const current = getSelectedMemoryRuntime();
|
||||
if (current || !params) {
|
||||
return current ? { runtime: current } : undefined;
|
||||
}
|
||||
@@ -100,7 +131,9 @@ function ensureMemoryRuntime(params?: {
|
||||
const key = resolvePluginRegistryLoadCacheKey(loadOptions);
|
||||
if (standaloneMemoryRegistrySlot?.key === key) {
|
||||
const runtime = resolveMemoryRuntimeFromRegistry(standaloneMemoryRegistrySlot.registry);
|
||||
return runtime ? { runtime, registry: standaloneMemoryRegistrySlot.registry } : undefined;
|
||||
return runtime
|
||||
? toMemoryRuntimeOwner(runtime, standaloneMemoryRegistrySlot.registry)
|
||||
: undefined;
|
||||
}
|
||||
const registry = loadPluginRegistryHandle(loadOptions);
|
||||
if (!registry) {
|
||||
@@ -116,7 +149,7 @@ function ensureMemoryRuntime(params?: {
|
||||
retiredRuntimes.set(previousRuntime, previousSlot.registry);
|
||||
}
|
||||
standaloneMemoryRegistrySlot = { key, registry, retiredRuntimes };
|
||||
return runtime ? { runtime, registry } : undefined;
|
||||
return runtime ? toMemoryRuntimeOwner(runtime, registry) : undefined;
|
||||
}
|
||||
|
||||
/** Returns the active plugin-backed memory search manager for an agent. */
|
||||
|
||||
Reference in New Issue
Block a user